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_directly_queryable_types_new =
prevent_non_factory_instantiation_of(SchemaElements::EnumsForDirectlyQueryableTypes)
@@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)
@@index_new =
prevent_non_factory_instantiation_of(Indexing::Index)
@@results_new =
prevent_non_factory_instantiation_of(Results)
@@schema_artifact_manager_new =
prevent_non_factory_instantiation_of(SchemaArtifactManager)

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(state) ⇒ Factory

Returns a new instance of Factory.



61
62
63
# File 'lib/elastic_graph/schema_definition/factory.rb', line 61

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.



69
70
71
72
73
# File 'lib/elastic_graph/schema_definition/factory.rb', line 69

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



213
214
215
216
217
218
# File 'lib/elastic_graph/schema_definition/factory.rb', line 213

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.



192
193
194
195
196
197
198
# File 'lib/elastic_graph/schema_definition/factory.rb', line 192

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.



205
206
207
208
209
210
211
# File 'lib/elastic_graph/schema_definition/factory.rb', line 205

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.



315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/elastic_graph/schema_definition/factory.rb', line 315

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.resolve_fields_with :object_with_lookahead
    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



80
81
82
83
84
# File 'lib/elastic_graph/schema_definition/factory.rb', line 80

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



87
88
89
# File 'lib/elastic_graph/schema_definition/factory.rb', line 87

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

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



75
76
77
# File 'lib/elastic_graph/schema_definition/factory.rb', line 75

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



92
93
94
# File 'lib/elastic_graph/schema_definition/factory.rb', line 92

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

#new_enum_type(name, &block) ⇒ Object



97
98
99
# File 'lib/elastic_graph/schema_definition/factory.rb', line 97

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

#new_enum_value(name, original_name) ⇒ Object



102
103
104
105
106
# File 'lib/elastic_graph/schema_definition/factory.rb', line 102

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_directly_queryable_typesObject



109
110
111
# File 'lib/elastic_graph/schema_definition/factory.rb', line 109

def new_enums_for_directly_queryable_types
  @@enums_for_directly_queryable_types_new.call(@state)
end

#new_field_source(relationship_name:, field_path:) ⇒ Object



265
266
267
# File 'lib/elastic_graph/schema_definition/factory.rb', line 265

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



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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/elastic_graph/schema_definition/factory.rb', line 142

def new_filter_input_type(source_type, name_prefix: source_type, category: :filter_input)
  all_of = @state.schema_elements.all_of
  any_of = @state.schema_elements.any_of
  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 match all documents 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.

        When `null` is passed, matches all documents.
        When an empty list is passed, this part of the filter matches no documents.
      EOS
    end

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

        Note: multiple filters are automatically ANDed together. This is only needed when you have multiple filters that can't
        be provided on a single `#{t.name}` input because of collisions between key names. For example, if you want to AND multiple
        OR'd sub-filters (the equivalent of (A OR B) AND (C OR D)), you could do #{all_of}: [{#{any_of}: [...]}, {#{any_of}: [...]}].

        When `null` or an empty list is passed, matches all documents.
      EOS
    end

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

        When `null` or an empty object is passed, matches no documents.
      EOS
    end

    yield t
  end
end

#new_graphql_sdl_enumerator(all_types) ⇒ Object



121
122
123
# File 'lib/elastic_graph/schema_definition/factory.rb', line 121

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

#new_index(name, settings, type, &block) ⇒ Object



281
282
283
# File 'lib/elastic_graph/schema_definition/factory.rb', line 281

def new_index(name, settings, type, &block)
  @@index_new.call(name, settings, @state, type, &block)
end

#new_input_type(name) ⇒ Object



135
136
137
138
139
# File 'lib/elastic_graph/schema_definition/factory.rb', line 135

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

#new_interface_type(name) ⇒ Object



220
221
222
223
224
# File 'lib/elastic_graph/schema_definition/factory.rb', line 220

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



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

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



270
271
272
273
274
275
276
277
278
# File 'lib/elastic_graph/schema_definition/factory.rb', line 270

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_resultsObject



286
287
288
# File 'lib/elastic_graph/schema_definition/factory.rb', line 286

def new_results
  @@results_new.call(@state)
end

#new_scalar_type(name) ⇒ Object



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

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

#new_schema_artifact_manager(schema_definition_results:, schema_artifacts_directory:, enforce_json_schema_version:, output:, max_diff_lines: 50) ⇒ Object



291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/elastic_graph/schema_definition/factory.rb', line 291

def new_schema_artifact_manager(
  schema_definition_results:,
  schema_artifacts_directory:,
  enforce_json_schema_version:,
  output:,
  max_diff_lines: 50
)
  @@schema_artifact_manager_new.call(
    schema_definition_results:,
    schema_artifacts_directory:,
    enforce_json_schema_version:,
    output:,
    max_diff_lines:
  )
end

#new_sort_order_enum_value(enum_value, sort_order_field_path) ⇒ Object



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

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



246
247
248
# File 'lib/elastic_graph/schema_definition/factory.rb', line 246

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

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



251
252
253
254
255
# File 'lib/elastic_graph/schema_definition/factory.rb', line 251

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



258
259
260
261
262
# File 'lib/elastic_graph/schema_definition/factory.rb', line 258

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