Class: ElasticGraph::SchemaDefinition::Factory

Inherits:
Object
  • Object
show all
Defined in:
lib/elastic_graph/schema_definition/factory.rb

Overview

A class responsible for instantiating all schema elements. We want all schema element instantiation to go through this one class to support extension libraries. ElasticGraph supports extension libraries that provide modules that get extended onto specific instances of ElasticGraph framework classes. We prefer this approach rather than having extension library modules applied via ‘include` or `prepend`, because they _permanently modify_ the host classes. ElasticGraph is designed to avoid all mutable global state, and that includes mutations to ElasticGraph class ancestor chains from extension libraries.

Concretely, if we included or prepended extension libraries modules, we’d have a hard time keeping our tests order-independent and deterministic while running all the ElasticGraph test suites in the same Ruby process. A test using an extension library could cause a core ElasticGraph class to get mutated in a way that impacts a test that runs in the same process later. Instead, we expect extension libraries to hook into ElasticGraph using ‘extend` on particular object instances.

But that creates a bit of a problem: how can an extension library extend a module onto every instance of a specific type of schema element while it is in use? The answer is this factory class:

- An extension library can extend a module onto `schema.factory`.
- That module can in turn override any of these factory methods and extend another module onto the schema
  element instances.

Constant Summary collapse

@@deprecated_element_new =
prevent_non_factory_instantiation_of(SchemaElements::DeprecatedElement)
@@argument_new =
prevent_non_factory_instantiation_of(SchemaElements::Argument)
@@built_in_types_new =
prevent_non_factory_instantiation_of(SchemaElements::BuiltInTypes)
@@directive_new =
prevent_non_factory_instantiation_of(SchemaElements::Directive)
@@enum_type_new =
prevent_non_factory_instantiation_of(SchemaElements::EnumType)
@@enum_value_new =
prevent_non_factory_instantiation_of(SchemaElements::EnumValue)
@@enums_for_indexed_types_new =
prevent_non_factory_instantiation_of(SchemaElements::EnumsForIndexedTypes)
@@field_new =
prevent_non_factory_instantiation_of(SchemaElements::Field)
@@graphql_sdl_enumerator_new =
prevent_non_factory_instantiation_of(SchemaElements::GraphQLSDLEnumerator)
@@input_field_new =
prevent_non_factory_instantiation_of(SchemaElements::InputField)
@@input_type_new =
prevent_non_factory_instantiation_of(SchemaElements::InputType)
@@interface_type_new =
prevent_non_factory_instantiation_of(SchemaElements::InterfaceType)
@@object_type_new =
prevent_non_factory_instantiation_of(SchemaElements::ObjectType)
@@scalar_type_new =
prevent_non_factory_instantiation_of(SchemaElements::ScalarType)
@@sort_order_enum_value_new =
prevent_non_factory_instantiation_of(SchemaElements::SortOrderEnumValue)
@@type_reference_new =
prevent_non_factory_instantiation_of(SchemaElements::TypeReference)
@@type_with_subfields_new =
prevent_non_factory_instantiation_of(SchemaElements::TypeWithSubfields)
@@union_type_new =
prevent_non_factory_instantiation_of(SchemaElements::UnionType)
@@field_source_new =
prevent_non_factory_instantiation_of(SchemaElements::FieldSource)
@@relationship_new =
prevent_non_factory_instantiation_of(SchemaElements::Relationship)

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(state) ⇒ Factory

Returns a new instance of Factory.



58
59
60
# File 'lib/elastic_graph/schema_definition/factory.rb', line 58

def initialize(state)
  @state = state
end

Class Method Details

.prevent_non_factory_instantiation_of(klass) ⇒ Object

Helper method to help enforce our desired invariant: we want every instantiation of these schema element classes to happen via this factory method provided here. To enforce that, this helper returns the ‘new` method (as a `Method` object) after removing it from the given class. That makes it impossible for `new` to be called by anyone except from the factory using the captured method object.



66
67
68
69
70
# File 'lib/elastic_graph/schema_definition/factory.rb', line 66

def self.prevent_non_factory_instantiation_of(klass)
  klass.method(:new).tap do
    klass.singleton_class.undef_method :new
  end
end

Instance Method Details

#build_relay_pagination_types(type_name, include_total_edge_count: false, derived_indexed_types: [], support_pagination: true, &customize_connection) ⇒ Object



196
197
198
199
200
201
# File 'lib/elastic_graph/schema_definition/factory.rb', line 196

def build_relay_pagination_types(type_name, include_total_edge_count: false, derived_indexed_types: [], support_pagination: true, &customize_connection)
  [
    (edge_type_for(type_name) if support_pagination),
    connection_type_for(type_name, include_total_edge_count, derived_indexed_types, support_pagination, &customize_connection)
  ].compact
end

#build_standard_filter_input_types_for_index_leaf_type(source_type, name_prefix: source_type, &define_filter_fields) ⇒ Object

Builds the standard set of filter input types for types which are indexing leaf types.

All GraphQL leaf types (enums and scalars) are indexing leaf types, but some GraphQL object types are as well. For example, ‘GeoLocation` is an object type in GraphQL (with separate lat/long fields) but is an indexing leaf type because we use the datastore `geo_point` type for it.



175
176
177
178
179
180
181
# File 'lib/elastic_graph/schema_definition/factory.rb', line 175

def build_standard_filter_input_types_for_index_leaf_type(source_type, name_prefix: source_type, &define_filter_fields)
  single_value_filter = new_filter_input_type(source_type, name_prefix: name_prefix, &define_filter_fields)
  list_filter = new_list_filter_input_type(source_type, name_prefix: name_prefix, any_satisfy_type_category: :list_element_filter_input)
  list_element_filter = new_list_element_filter_input_type(source_type, name_prefix: name_prefix, &define_filter_fields)

  [single_value_filter, list_filter, list_element_filter]
end

#build_standard_filter_input_types_for_index_object_type(source_type, name_prefix: source_type, &define_filter_fields) ⇒ Object

Builds the standard set of filter input types for types which are indexing object types.

Most GraphQL object types are indexing object types as well, but not all. For example, ‘GeoLocation` is an object type in GraphQL (with separate lat/long fields) but is an indexing leaf type because we use the datastore `geo_point` type for it.



188
189
190
191
192
193
194
# File 'lib/elastic_graph/schema_definition/factory.rb', line 188

def build_standard_filter_input_types_for_index_object_type(source_type, name_prefix: source_type, &define_filter_fields)
  single_value_filter = new_filter_input_type(source_type, name_prefix: name_prefix, &define_filter_fields)
  list_filter = new_list_filter_input_type(source_type, name_prefix: name_prefix, any_satisfy_type_category: :filter_input)
  fields_list_filter = new_fields_list_filter_input_type(source_type, name_prefix: name_prefix)

  [single_value_filter, list_filter, fields_list_filter]
end

#new_aggregated_values_type_for_index_leaf_type(index_leaf_type) ⇒ Object

Responsible for creating a new ‘*AggregatedValues` type for an index leaf type.

An index leaf type is a scalar, enum, object type that is backed by a single, indivisible field in the index. All scalar and enum types are index leaf types, and object types rarely (but sometimes) are. For example, the ‘GeoLocation` object type has two subfields (`latitude` and `longitude`) but is backed by a single `geo_point` field in the index, so it is an index leaf type.



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/elastic_graph/schema_definition/factory.rb', line 271

def new_aggregated_values_type_for_index_leaf_type(index_leaf_type)
  new_object_type @state.type_ref(index_leaf_type).as_aggregated_values.name do |type|
    type.graphql_only true
    type.documentation "A return type used from aggregations to provided aggregated values over `#{index_leaf_type}` fields."
    type. = {elasticgraph_category: :scalar_aggregated_values}

    type.field @state.schema_elements.approximate_distinct_value_count, "JsonSafeLong", graphql_only: true do |f|
      # Note: the 1-6% accuracy figure comes from the Elasticsearch docs:
      # https://www.elastic.co/guide/en/elasticsearch/reference/8.10/search-aggregations-metrics-cardinality-aggregation.html#_counts_are_approximate
      f.documentation <<~EOS
        An approximation of the number of unique values for this field within this grouping.

        The approximation uses the HyperLogLog++ algorithm from the [HyperLogLog in Practice](https://research.google.com/pubs/archive/40671.pdf)
        paper. The accuracy of the returned value varies based on the specific dataset, but
        it usually differs from the true distinct value count by less than 7%.
      EOS

      f. empty_bucket_value: 0, function: :cardinality
    end

    yield type
  end
end

#new_argument(field, name, value_type) ⇒ Object



77
78
79
80
81
# File 'lib/elastic_graph/schema_definition/factory.rb', line 77

def new_argument(field, name, value_type)
  @@argument_new.call(@state, field, name, value_type).tap do |argument|
    yield argument if block_given?
  end
end

#new_built_in_types(api) ⇒ Object



84
85
86
# File 'lib/elastic_graph/schema_definition/factory.rb', line 84

def new_built_in_types(api)
  @@built_in_types_new.call(api, @state)
end

#new_deprecated_element(name, defined_at:, defined_via:) ⇒ Object



72
73
74
# File 'lib/elastic_graph/schema_definition/factory.rb', line 72

def new_deprecated_element(name, defined_at:, defined_via:)
  @@deprecated_element_new.call(schema_def_state: @state, name: name, defined_at: defined_at, defined_via: defined_via)
end

#new_directive(name, arguments) ⇒ Object



89
90
91
# File 'lib/elastic_graph/schema_definition/factory.rb', line 89

def new_directive(name, arguments)
  @@directive_new.call(name, arguments)
end

#new_enum_type(name, &block) ⇒ Object



94
95
96
# File 'lib/elastic_graph/schema_definition/factory.rb', line 94

def new_enum_type(name, &block)
  @@enum_type_new.call(@state, name, &(_ = block))
end

#new_enum_value(name, original_name) ⇒ Object



99
100
101
102
103
# File 'lib/elastic_graph/schema_definition/factory.rb', line 99

def new_enum_value(name, original_name)
  @@enum_value_new.call(@state, name, original_name) do |enum_value|
    yield enum_value if block_given?
  end
end

#new_enums_for_indexed_typesObject



106
107
108
# File 'lib/elastic_graph/schema_definition/factory.rb', line 106

def new_enums_for_indexed_types
  @@enums_for_indexed_types_new.call(@state)
end

#new_field_source(relationship_name:, field_path:) ⇒ Object



248
249
250
# File 'lib/elastic_graph/schema_definition/factory.rb', line 248

def new_field_source(relationship_name:, field_path:)
  @@field_source_new.call(relationship_name, field_path)
end

#new_filter_input_type(source_type, name_prefix: source_type, category: :filter_input) ⇒ Object



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/elastic_graph/schema_definition/factory.rb', line 139

def new_filter_input_type(source_type, name_prefix: source_type, category: :filter_input)
  new_input_type(@state.type_ref(name_prefix).as_static_derived_type(category).name) do |t|
    t.documentation <<~EOS
      Input type used to specify filters on `#{source_type}` fields.

      Will be ignored if passed as an empty object (or as `null`).
    EOS

    t.field @state.schema_elements.any_of, "[#{t.name}!]" do |f|
      f.documentation <<~EOS
        Matches records where any of the provided sub-filters evaluate to true.
        This works just like an OR operator in SQL.

        Will be ignored when `null` is passed. When an empty list is passed, will cause this
        part of the filter to match no documents.
      EOS
    end

    t.field @state.schema_elements.not, t.name do |f|
      f.documentation <<~EOS
        Matches records where the provided sub-filter does not evaluate to true.
        This works just like a NOT operator in SQL.

        Will be ignored when `null` or an empty object is passed.
      EOS
    end

    yield t
  end
end

#new_graphql_sdl_enumerator(all_types_except_root_query_type) ⇒ Object



118
119
120
# File 'lib/elastic_graph/schema_definition/factory.rb', line 118

def new_graphql_sdl_enumerator(all_types_except_root_query_type)
  @@graphql_sdl_enumerator_new.call(@state, all_types_except_root_query_type)
end

#new_input_type(name) ⇒ Object



132
133
134
135
136
# File 'lib/elastic_graph/schema_definition/factory.rb', line 132

def new_input_type(name)
  @@input_type_new.call(@state, name) do |input_type|
    yield input_type
  end
end

#new_interface_type(name) ⇒ Object



203
204
205
206
207
# File 'lib/elastic_graph/schema_definition/factory.rb', line 203

def new_interface_type(name)
  @@interface_type_new.call(@state, name.to_s) do |interface_type|
    yield interface_type
  end
end

#new_object_type(name) ⇒ Object



210
211
212
213
214
# File 'lib/elastic_graph/schema_definition/factory.rb', line 210

def new_object_type(name)
  @@object_type_new.call(@state, name.to_s) do |object_type|
    yield object_type if block_given?
  end
end

#new_relationship(field, cardinality:, related_type:, foreign_key:, direction:) ⇒ Object



253
254
255
256
257
258
259
260
261
# File 'lib/elastic_graph/schema_definition/factory.rb', line 253

def new_relationship(field, cardinality:, related_type:, foreign_key:, direction:)
  @@relationship_new.call(
    field,
    cardinality: cardinality,
    related_type: related_type,
    foreign_key: foreign_key,
    direction: direction
  )
end

#new_scalar_type(name) ⇒ Object



217
218
219
220
221
# File 'lib/elastic_graph/schema_definition/factory.rb', line 217

def new_scalar_type(name)
  @@scalar_type_new.call(@state, name.to_s) do |scalar_type|
    yield scalar_type
  end
end

#new_sort_order_enum_value(enum_value, sort_order_field_path) ⇒ Object



224
225
226
# File 'lib/elastic_graph/schema_definition/factory.rb', line 224

def new_sort_order_enum_value(enum_value, sort_order_field_path)
  @@sort_order_enum_value_new.call(enum_value, sort_order_field_path)
end

#new_type_reference(name) ⇒ Object



229
230
231
# File 'lib/elastic_graph/schema_definition/factory.rb', line 229

def new_type_reference(name)
  @@type_reference_new.call(name, @state)
end

#new_type_with_subfields(schema_kind, name, wrapping_type:, field_factory:) ⇒ Object



234
235
236
237
238
# File 'lib/elastic_graph/schema_definition/factory.rb', line 234

def new_type_with_subfields(schema_kind, name, wrapping_type:, field_factory:)
  @@type_with_subfields_new.call(schema_kind, @state, name, wrapping_type: wrapping_type, field_factory: field_factory) do |type_with_subfields|
    yield type_with_subfields
  end
end

#new_union_type(name) ⇒ Object



241
242
243
244
245
# File 'lib/elastic_graph/schema_definition/factory.rb', line 241

def new_union_type(name)
  @@union_type_new.call(@state, name.to_s) do |union_type|
    yield union_type
  end
end