elasticgraph-schema_definition 0.18.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +7 -0
  4. data/elasticgraph-schema_definition.gemspec +26 -0
  5. data/lib/elastic_graph/schema_definition/api.rb +359 -0
  6. data/lib/elastic_graph/schema_definition/factory.rb +506 -0
  7. data/lib/elastic_graph/schema_definition/indexing/derived_fields/append_only_set.rb +79 -0
  8. data/lib/elastic_graph/schema_definition/indexing/derived_fields/field_initializer_support.rb +59 -0
  9. data/lib/elastic_graph/schema_definition/indexing/derived_fields/immutable_value.rb +99 -0
  10. data/lib/elastic_graph/schema_definition/indexing/derived_fields/min_or_max_value.rb +62 -0
  11. data/lib/elastic_graph/schema_definition/indexing/derived_indexed_type.rb +346 -0
  12. data/lib/elastic_graph/schema_definition/indexing/event_envelope.rb +74 -0
  13. data/lib/elastic_graph/schema_definition/indexing/field.rb +181 -0
  14. data/lib/elastic_graph/schema_definition/indexing/field_reference.rb +51 -0
  15. data/lib/elastic_graph/schema_definition/indexing/field_type/enum.rb +65 -0
  16. data/lib/elastic_graph/schema_definition/indexing/field_type/object.rb +113 -0
  17. data/lib/elastic_graph/schema_definition/indexing/field_type/scalar.rb +51 -0
  18. data/lib/elastic_graph/schema_definition/indexing/field_type/union.rb +70 -0
  19. data/lib/elastic_graph/schema_definition/indexing/index.rb +318 -0
  20. data/lib/elastic_graph/schema_definition/indexing/json_schema_field_metadata.rb +34 -0
  21. data/lib/elastic_graph/schema_definition/indexing/json_schema_with_metadata.rb +234 -0
  22. data/lib/elastic_graph/schema_definition/indexing/list_counts_mapping.rb +53 -0
  23. data/lib/elastic_graph/schema_definition/indexing/relationship_resolver.rb +96 -0
  24. data/lib/elastic_graph/schema_definition/indexing/rollover_config.rb +25 -0
  25. data/lib/elastic_graph/schema_definition/indexing/update_target_factory.rb +54 -0
  26. data/lib/elastic_graph/schema_definition/indexing/update_target_resolver.rb +195 -0
  27. data/lib/elastic_graph/schema_definition/json_schema_pruner.rb +61 -0
  28. data/lib/elastic_graph/schema_definition/mixins/can_be_graphql_only.rb +31 -0
  29. data/lib/elastic_graph/schema_definition/mixins/has_derived_graphql_type_customizations.rb +119 -0
  30. data/lib/elastic_graph/schema_definition/mixins/has_directives.rb +65 -0
  31. data/lib/elastic_graph/schema_definition/mixins/has_documentation.rb +74 -0
  32. data/lib/elastic_graph/schema_definition/mixins/has_indices.rb +281 -0
  33. data/lib/elastic_graph/schema_definition/mixins/has_readable_to_s_and_inspect.rb +46 -0
  34. data/lib/elastic_graph/schema_definition/mixins/has_subtypes.rb +116 -0
  35. data/lib/elastic_graph/schema_definition/mixins/has_type_info.rb +181 -0
  36. data/lib/elastic_graph/schema_definition/mixins/implements_interfaces.rb +122 -0
  37. data/lib/elastic_graph/schema_definition/mixins/supports_default_value.rb +47 -0
  38. data/lib/elastic_graph/schema_definition/mixins/supports_filtering_and_aggregation.rb +267 -0
  39. data/lib/elastic_graph/schema_definition/mixins/verifies_graphql_name.rb +38 -0
  40. data/lib/elastic_graph/schema_definition/rake_tasks.rb +190 -0
  41. data/lib/elastic_graph/schema_definition/results.rb +404 -0
  42. data/lib/elastic_graph/schema_definition/schema_artifact_manager.rb +482 -0
  43. data/lib/elastic_graph/schema_definition/schema_elements/argument.rb +56 -0
  44. data/lib/elastic_graph/schema_definition/schema_elements/built_in_types.rb +1541 -0
  45. data/lib/elastic_graph/schema_definition/schema_elements/deprecated_element.rb +21 -0
  46. data/lib/elastic_graph/schema_definition/schema_elements/directive.rb +40 -0
  47. data/lib/elastic_graph/schema_definition/schema_elements/enum_type.rb +189 -0
  48. data/lib/elastic_graph/schema_definition/schema_elements/enum_value.rb +73 -0
  49. data/lib/elastic_graph/schema_definition/schema_elements/enum_value_namer.rb +89 -0
  50. data/lib/elastic_graph/schema_definition/schema_elements/enums_for_indexed_types.rb +82 -0
  51. data/lib/elastic_graph/schema_definition/schema_elements/field.rb +1085 -0
  52. data/lib/elastic_graph/schema_definition/schema_elements/field_path.rb +112 -0
  53. data/lib/elastic_graph/schema_definition/schema_elements/field_source.rb +16 -0
  54. data/lib/elastic_graph/schema_definition/schema_elements/graphql_sdl_enumerator.rb +113 -0
  55. data/lib/elastic_graph/schema_definition/schema_elements/input_field.rb +31 -0
  56. data/lib/elastic_graph/schema_definition/schema_elements/input_type.rb +60 -0
  57. data/lib/elastic_graph/schema_definition/schema_elements/interface_type.rb +72 -0
  58. data/lib/elastic_graph/schema_definition/schema_elements/list_counts_state.rb +40 -0
  59. data/lib/elastic_graph/schema_definition/schema_elements/object_type.rb +53 -0
  60. data/lib/elastic_graph/schema_definition/schema_elements/relationship.rb +218 -0
  61. data/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb +310 -0
  62. data/lib/elastic_graph/schema_definition/schema_elements/sort_order_enum_value.rb +36 -0
  63. data/lib/elastic_graph/schema_definition/schema_elements/sub_aggregation_path.rb +66 -0
  64. data/lib/elastic_graph/schema_definition/schema_elements/type_namer.rb +237 -0
  65. data/lib/elastic_graph/schema_definition/schema_elements/type_reference.rb +353 -0
  66. data/lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb +579 -0
  67. data/lib/elastic_graph/schema_definition/schema_elements/union_type.rb +157 -0
  68. data/lib/elastic_graph/schema_definition/scripting/file_system_repository.rb +77 -0
  69. data/lib/elastic_graph/schema_definition/scripting/script.rb +48 -0
  70. data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_day_of_week.painless +24 -0
  71. data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_time_of_day.painless +41 -0
  72. data/lib/elastic_graph/schema_definition/scripting/scripts/filter/by_time_of_day.painless +22 -0
  73. data/lib/elastic_graph/schema_definition/scripting/scripts/update/index_data.painless +93 -0
  74. data/lib/elastic_graph/schema_definition/state.rb +212 -0
  75. data/lib/elastic_graph/schema_definition/test_support.rb +113 -0
  76. metadata +513 -0
@@ -0,0 +1,579 @@
1
+ # Copyright 2024 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ require "elastic_graph/error"
10
+ require "elastic_graph/schema_artifacts/runtime_metadata/relation"
11
+ require "elastic_graph/schema_definition/indexing/field"
12
+ require "elastic_graph/schema_definition/indexing/field_type/object"
13
+ require "elastic_graph/schema_definition/mixins/can_be_graphql_only"
14
+ require "elastic_graph/schema_definition/mixins/has_derived_graphql_type_customizations"
15
+ require "elastic_graph/schema_definition/mixins/has_directives"
16
+ require "elastic_graph/schema_definition/mixins/has_documentation"
17
+ require "elastic_graph/schema_definition/mixins/has_type_info"
18
+ require "elastic_graph/schema_definition/mixins/supports_filtering_and_aggregation"
19
+ require "elastic_graph/schema_definition/mixins/verifies_graphql_name"
20
+ require "elastic_graph/schema_definition/schema_elements/list_counts_state"
21
+
22
+ module ElasticGraph
23
+ module SchemaDefinition
24
+ module SchemaElements
25
+ # Defines common functionality for all GraphQL types that have subfields:
26
+ #
27
+ # - {InputType}
28
+ # - {InterfaceType}
29
+ # - {ObjectType}
30
+ #
31
+ # @abstract
32
+ #
33
+ # @!attribute [rw] schema_kind
34
+ # @private
35
+ # @!attribute [rw] schema_def_state
36
+ # @private
37
+ # @!attribute [rw] type_ref
38
+ # @private
39
+ # @!attribute [rw] reserved_field_names
40
+ # @private
41
+ # @!attribute [rw] graphql_fields_by_name
42
+ # @private
43
+ # @!attribute [rw] indexing_fields_by_name_in_index
44
+ # @private
45
+ # @!attribute [rw] field_factory
46
+ # @private
47
+ # @!attribute [rw] wrapping_type
48
+ # @private
49
+ # @!attribute [rw] relay_pagination_type
50
+ # @private
51
+ class TypeWithSubfields < Struct.new(
52
+ :schema_kind, :schema_def_state, :type_ref, :reserved_field_names,
53
+ :graphql_fields_by_name, :indexing_fields_by_name_in_index, :field_factory,
54
+ :wrapping_type, :relay_pagination_type
55
+ )
56
+ prepend Mixins::VerifiesGraphQLName
57
+ include Mixins::CanBeGraphQLOnly
58
+ include Mixins::HasDocumentation
59
+ include Mixins::HasDirectives
60
+ include Mixins::HasDerivedGraphQLTypeCustomizations
61
+ include Mixins::HasTypeInfo
62
+
63
+ # The following methods are provided by `Struct.new`:
64
+ # @dynamic type_ref
65
+
66
+ # The following methods are provided by `SupportsFilteringAndAggregation`:
67
+ # @dynamic derived_graphql_types
68
+
69
+ # The following methods are provided by `CanBeGraphQLOnly`:
70
+ # @dynamic graphql_only?
71
+
72
+ # @private
73
+ def initialize(schema_kind, schema_def_state, name, wrapping_type:, field_factory:)
74
+ # `any_satisfy`, `any_of`/`all_of`, and `not` are "reserved" field names. They are reserved for usage by
75
+ # ElasticGraph itself in the `*FilterInput` types it generates. If we allow them to be used as field
76
+ # names, we'll run into conflicts when we later generate the `*FilterInput` type.
77
+ #
78
+ # Note that we don't have the same kind of conflict for the other filtering operators (e.g.
79
+ # `equal_to_any_of`, `gt`, etc) because on the generated filter structure, those are leaf
80
+ # nodes. They never exist alongside document field names on a filter type, but these do,
81
+ # so we have to guard against them here.
82
+ reserved_field_names = [
83
+ schema_def_state.schema_elements.all_of,
84
+ schema_def_state.schema_elements.any_of,
85
+ schema_def_state.schema_elements.any_satisfy,
86
+ schema_def_state.schema_elements.not
87
+ ].to_set
88
+
89
+ # @type var graphql_fields_by_name: ::Hash[::String, Field]
90
+ graphql_fields_by_name = {}
91
+ # @type var indexing_fields_by_name_in_index: ::Hash[::String, Field]
92
+ indexing_fields_by_name_in_index = {}
93
+
94
+ super(
95
+ schema_kind,
96
+ schema_def_state,
97
+ schema_def_state.type_ref(name).to_final_form,
98
+ reserved_field_names,
99
+ graphql_fields_by_name,
100
+ indexing_fields_by_name_in_index,
101
+ field_factory,
102
+ wrapping_type,
103
+ false
104
+ )
105
+
106
+ yield self
107
+ end
108
+
109
+ # @return [String] the name of this GraphQL type
110
+ def name
111
+ type_ref.name
112
+ end
113
+
114
+ # Defines a [GraphQL field](https://spec.graphql.org/October2021/#sec-Language.Fields) on this type.
115
+ #
116
+ # @param name [String] name of the field
117
+ # @param type [String] type of the field as a [type reference](https://spec.graphql.org/October2021/#sec-Type-References). The named type must be
118
+ # one of {BuiltInTypes ElasticGraph's built-in types} or a type that has been defined in your schema.
119
+ # @param graphql_only [Boolean] if `true`, ElasticGraph will define the field as a GraphQL field but omit it from the indexing
120
+ # artifacts (`json_schemas.yaml` and `datastore_config.yaml`). This can be used along with `name_in_index` to support careful
121
+ # schema evolution.
122
+ # @param indexing_only [Boolean] if `true`, ElasticGraph will define the field for indexing (in the `json_schemas.yaml` and
123
+ # `datastore_config.yaml` schema artifact) but will omit it from the GraphQL schema. This can be useful to begin indexing a field
124
+ # before you expose it in GraphQL so that you can fully backfill it first.
125
+ # @option options [String] name_in_index the name of the field in the datastore index. Can be used to back a GraphQL field with a
126
+ # differently named field in the index.
127
+ # @option options [String] singular can be used on a list field (e.g. `t.field "tags", "[String!]!", singular: "tag"`) to tell
128
+ # ElasticGraph what the singular form of a field's name is. When provided, ElasticGraph will define a `groupedBy` field (using the
129
+ # singular form) allowing clients to group by individual values from the field.
130
+ # @option options [Boolean] aggregatable force-enables or disables the ability for aggregation queries to aggregate over this field.
131
+ # When not provided, ElasticGraph will infer field aggregatability based on the field's GraphQL type and mapping type.
132
+ # @option options [Boolean] filterable force-enables or disables the ability for queries to filter by this field. When not provided,
133
+ # ElasticGraph will infer field filterability based on the field's GraphQL type and mapping type.
134
+ # @option options [Boolean] groupable force-enables or disables the ability for aggregation queries to group by this field. When
135
+ # not provided, ElasticGraph will infer field groupability based on the field's GraphQL type and mapping type.
136
+ # @option options [Boolean] sortable force-enables or disables the ability for queries to sort by this field. When not provided,
137
+ # ElasticGraph will infer field sortability based on the field's GraphQL type and mapping type.
138
+ # @yield [Field] the field for further customization
139
+ # @return [void]
140
+ #
141
+ # @see #paginated_collection_field
142
+ # @see #relates_to_many
143
+ # @see #relates_to_one
144
+ #
145
+ # @note Be careful about defining non-nullable fields. Changing a field’s type from non-nullable (e.g. `Int!`) to nullable (e.g.
146
+ # `Int`) is a breaking change for clients. Making a field non-nullable may also prevent you from applying permissioning to a field
147
+ # via an AuthZ layer (as such a layer would have no way to force a field value to `null` when for a client denied field access).
148
+ # Therefore, we recommend limiting your use of `!` to only a few situations such as defining a type’s primary key (e.g.
149
+ # `t.field "id", "ID!"`) or defining a list field (e.g. `t.field "authors", "[String!]!"`) since empty lists already provide a
150
+ # "no data" representation. You can still configure the ElasticGraph indexer to require a non-null value for a field using
151
+ # `f.json_schema nullable: false`.
152
+ #
153
+ # @note ElasticGraph’s understanding of datastore capabilities may override your configured
154
+ # `aggregatable`/`filterable`/`groupable`/`sortable` options. For example, a field indexed as `text` for full text search will
155
+ # not be sortable or groupable even if you pass `sortable: true, groupable: true` when defining the field, because [text fields
156
+ # cannot be efficiently sorted by or grouped on](https://www.elastic.co/guide/en/elasticsearch/reference/8.15/text.html#text).
157
+ #
158
+ # @example Define a field with documentation
159
+ # ElasticGraph.define_schema do |schema|
160
+ # schema.object_type "Campaign" do |t|
161
+ # t.field "id", "ID" do |f|
162
+ # f.documentation "The Campaign's identifier."
163
+ # end
164
+ # end
165
+ # end
166
+ #
167
+ # @example Omit a new field from the GraphQL schema until its data has been backfilled
168
+ # ElasticGraph.define_schema do |schema|
169
+ # schema.object_type "Campaign" do |t|
170
+ # t.field "id", "ID"
171
+ #
172
+ # # TODO: remove `indexing_only: true` once the data for this field has been fully backfilled
173
+ # t.field "endDate", "Date", indexing_only: true
174
+ # end
175
+ # end
176
+ #
177
+ # @example Use `graphql_only` to introduce a new name for an existing field
178
+ # ElasticGraph.define_schema do |schema|
179
+ # schema.object_type "Campaign" do |t|
180
+ # t.field "id", "ID"
181
+ #
182
+ # t.field "endOn", "Date" do |f|
183
+ # f.directive "deprecated", reason: "Use `endDate` instead."
184
+ # end
185
+ #
186
+ # # We've decided we want to call the field `endDate` instead of `endOn`, but the data
187
+ # # for this field is currently indexed in `endOn`, so we can use `graphql_only` and
188
+ # # `name_in_index` to expose the existing data under a new field name.
189
+ # t.field "endDate", "Date", name_in_index: "endOn", graphql_only: true
190
+ # end
191
+ # end
192
+ def field(name, type, graphql_only: false, indexing_only: false, **options)
193
+ if reserved_field_names.include?(name)
194
+ raise SchemaError, "Invalid field name: `#{self.name}.#{name}`. `#{name}` is reserved for use by " \
195
+ "ElasticGraph as a filtering operator. To use it for a field name, add " \
196
+ "the `schema_element_name_overrides` option (on `ElasticGraph::SchemaDefinition::RakeTasks.new`) to " \
197
+ "configure an alternate name for the `#{name}` operator."
198
+ end
199
+
200
+ options = {name_in_index: nil}.merge(options) if graphql_only
201
+
202
+ field_factory.call(
203
+ name: name,
204
+ type: type,
205
+ graphql_only: graphql_only,
206
+ parent_type: wrapping_type,
207
+ **options
208
+ ) do |field|
209
+ yield field if block_given?
210
+
211
+ unless indexing_only
212
+ register_field(field.name, field, graphql_fields_by_name, "GraphQL", :indexing_only)
213
+ end
214
+
215
+ unless graphql_only
216
+ register_field(field.name_in_index, field, indexing_fields_by_name_in_index, "indexing", :graphql_only) do |f|
217
+ f.to_indexing_field_reference
218
+ end
219
+ end
220
+ end
221
+ end
222
+
223
+ # Registers the name of a field that existed in a prior version of the schema but has been deleted.
224
+ #
225
+ # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
226
+ # or {Field#renamed_from}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
227
+ # the call to this method can be removed.
228
+ #
229
+ # @param field_name [String] name of field that used to exist but has been deleted
230
+ # @return [void]
231
+ #
232
+ # @example Indicate that `Widget.description` has been deleted
233
+ # ElasticGraph.define_schema do |schema|
234
+ # schema.object_type "Widget" do |t|
235
+ # t.deleted_field "description"
236
+ # end
237
+ # end
238
+ def deleted_field(field_name)
239
+ schema_def_state.register_deleted_field(
240
+ name,
241
+ field_name,
242
+ defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
243
+ defined_via: %(type.deleted_field "#{field_name}")
244
+ )
245
+ end
246
+
247
+ # Registers an old name that this type used to have in a prior version of the schema.
248
+ #
249
+ # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
250
+ # or {API#deleted_type}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
251
+ # the call to this method can be removed.
252
+ #
253
+ # @param old_name [String] old name this field used to have in a prior version of the schema
254
+ # @return [void]
255
+ #
256
+ # @example Indicate that `Widget` used to be called `Component`.
257
+ # ElasticGraph.define_schema do |schema|
258
+ # schema.object_type "Widget" do |t|
259
+ # t.renamed_from "Component"
260
+ # end
261
+ # end
262
+ def renamed_from(old_name)
263
+ schema_def_state.register_renamed_type(
264
+ name,
265
+ from: old_name,
266
+ defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
267
+ defined_via: %(type.renamed_from "#{old_name}")
268
+ )
269
+ end
270
+
271
+ # An alternative to {#field} for when you have a list field that you want exposed as a [paginated Relay
272
+ # connection](https://relay.dev/graphql/connections.htm) rather than as a simple list.
273
+ #
274
+ # @note Bear in mind that pagination does not have much efficiency benefit in this case: all elements of the collection will be
275
+ # retrieved when fetching this field from the datastore. The pagination implementation will just trim down the collection before
276
+ # returning it.
277
+ #
278
+ # @param name [String] name of the field
279
+ # @param element_type [String] name of the type of element in the collection
280
+ # @param name_in_index [String] the name of the field in the datastore index. Can be used to back a GraphQL field with a
281
+ # differently named field in the index.
282
+ # @param singular [String] indicates what the singular form of a field's name is. When provided, ElasticGraph will define a
283
+ # `groupedBy` field (using the singular form) allowing clients to group by individual values from the field.
284
+ # @yield [Field] the field for further customization
285
+ # @return [void]
286
+ #
287
+ # @see #field
288
+ # @see #relates_to_many
289
+ # @see #relates_to_one
290
+ #
291
+ # @example Define `Author.books` as a paginated collection field
292
+ # ElasticGraph.define_schema do |schema|
293
+ # schema.object_type "Author" do |t|
294
+ # t.field "id", "ID"
295
+ # t.field "name", "String"
296
+ # t.paginated_collection_field "books", "String"
297
+ # t.index "authors"
298
+ # end
299
+ # end
300
+ def paginated_collection_field(name, element_type, name_in_index: name, singular: nil, &block)
301
+ element_type_ref = schema_def_state.type_ref(element_type).to_final_form
302
+ element_type = element_type_ref.name
303
+
304
+ schema_def_state.paginated_collection_element_types << element_type
305
+
306
+ backing_indexing_field = field(name, "[#{element_type}!]!", indexing_only: true, name_in_index: name_in_index, &block)
307
+
308
+ field(
309
+ name,
310
+ element_type_ref.as_connection.name,
311
+ name_in_index: name_in_index,
312
+ type_for_derived_types: "[#{element_type}]",
313
+ groupable: !!singular,
314
+ sortable: false,
315
+ graphql_only: true,
316
+ singular: singular,
317
+ backing_indexing_field: backing_indexing_field
318
+ ) do |f|
319
+ f.define_relay_pagination_arguments!
320
+ block&.call(f)
321
+ end
322
+ end
323
+
324
+ # Defines a "has one" relationship between the current indexed type and another indexed type by defining a field clients
325
+ # can use to navigate across indexed types in a single GraphQL query.
326
+ #
327
+ # @param field_name [String] name of the relationship field
328
+ # @param type [String] name of the related type
329
+ # @param via [String] name of the foreign key field
330
+ # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
331
+ # references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type and references the `id` of
332
+ # the related type.
333
+ # @yield [Relationship] the generated relationship fields, for further customization
334
+ # @return [void]
335
+ #
336
+ # @see #field
337
+ # @see #relates_to_many
338
+ #
339
+ # @example Use `relates_to_one` to define `Player.team`
340
+ # ElasticGraph.define_schema do |schema|
341
+ # schema.object_type "Team" do |t|
342
+ # t.field "id", "ID"
343
+ # t.field "name", "String"
344
+ # t.field "homeCity", "String"
345
+ # t.index "teams"
346
+ # end
347
+ #
348
+ # schema.object_type "Player" do |t|
349
+ # t.field "id", "ID"
350
+ # t.field "name", "String"
351
+ # t.relates_to_one "team", "Team", via: "teamId", dir: :out
352
+ # t.index "players"
353
+ # end
354
+ # end
355
+ def relates_to_one(field_name, type, via:, dir:, &block)
356
+ foreign_key_type = schema_def_state.type_ref(type).non_null? ? "ID!" : "ID"
357
+ relates_to(field_name, type, via: via, dir: dir, foreign_key_type: foreign_key_type, cardinality: :one, related_type: type, &block)
358
+ end
359
+
360
+ # Defines a "has many" relationship between the current indexed type and another indexed type by defining a pair of fields clients
361
+ # can use to navigate across indexed types in a single GraphQL query. The pair of generated fields will be [Relay Connection
362
+ # types](https://relay.dev/graphql/connections.htm#sec-Connection-Types) allowing you to filter, sort, paginate, and aggregated the
363
+ # related data.
364
+ #
365
+ # @param field_name [String] name of the relationship field
366
+ # @param type [String] name of the related type
367
+ # @param via [String] name of the foreign key field
368
+ # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
369
+ # references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type and references the `id` of
370
+ # the related type.
371
+ # @param singular [String] singular form of the `field_name`; will be used (along with an `Aggregations` suffix) for the name of
372
+ # the generated aggregations field
373
+ # @yield [Relationship] the generated relationship fields, for further customization
374
+ # @return [void]
375
+ #
376
+ # @see #field
377
+ # @see #paginated_collection_field
378
+ # @see #relates_to_one
379
+ #
380
+ # @example Use `relates_to_many` to define `Team.players` and `Team.playerAggregations`
381
+ # ElasticGraph.define_schema do |schema|
382
+ # schema.object_type "Team" do |t|
383
+ # t.field "id", "ID"
384
+ # t.field "name", "String"
385
+ # t.field "homeCity", "String"
386
+ # t.relates_to_many "players", "Player", via: "teamId", dir: :in, singular: "player"
387
+ # t.index "teams"
388
+ # end
389
+ #
390
+ # schema.object_type "Player" do |t|
391
+ # t.field "id", "ID"
392
+ # t.field "name", "String"
393
+ # t.field "teamId", "ID"
394
+ # t.index "players"
395
+ # end
396
+ # end
397
+ def relates_to_many(field_name, type, via:, dir:, singular:)
398
+ foreign_key_type = (dir == :out) ? "[ID!]!" : "ID"
399
+ type_ref = schema_def_state.type_ref(type).to_final_form
400
+
401
+ relates_to(field_name, type_ref.as_connection.name, via: via, dir: dir, foreign_key_type: foreign_key_type, cardinality: :many, related_type: type) do |f|
402
+ f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
403
+ a.documentation "Used to filter the returned `#{field_name}` based on the provided criteria."
404
+ end
405
+
406
+ f.argument schema_def_state.schema_elements.order_by, "[#{type_ref.as_sort_order.name}!]" do |a|
407
+ a.documentation "Used to specify how the returned `#{field_name}` should be sorted."
408
+ end
409
+
410
+ f.define_relay_pagination_arguments!
411
+
412
+ yield f if block_given?
413
+ end
414
+
415
+ aggregations_name = schema_def_state.schema_elements.normalize_case("#{singular}_aggregations")
416
+ relates_to(aggregations_name, type_ref.as_aggregation.as_connection.name, via: via, dir: dir, foreign_key_type: foreign_key_type, cardinality: :many, related_type: type) do |f|
417
+ f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
418
+ a.documentation "Used to filter the `#{type}` documents that get aggregated over based on the provided criteria."
419
+ end
420
+
421
+ f.define_relay_pagination_arguments!
422
+
423
+ yield f if block_given?
424
+
425
+ f.documentation f.derived_documentation("Aggregations over the `#{field_name}` data")
426
+ end
427
+ end
428
+
429
+ # Converts the type to GraphQL SDL syntax.
430
+ #
431
+ # @private
432
+ def to_sdl(&field_arg_selector)
433
+ generate_sdl(name_section: name, &field_arg_selector)
434
+ end
435
+
436
+ # @private
437
+ def generate_sdl(name_section:, &field_arg_selector)
438
+ <<~SDL
439
+ #{formatted_documentation}#{schema_kind} #{name_section} #{directives_sdl(suffix_with: " ")}{
440
+ #{fields_sdl(&field_arg_selector)}
441
+ }
442
+ SDL
443
+ end
444
+
445
+ # @private
446
+ def aggregated_values_type
447
+ schema_def_state.type_ref("NonNumeric").as_aggregated_values
448
+ end
449
+
450
+ # @private
451
+ def indexed?
452
+ false
453
+ end
454
+
455
+ # @private
456
+ def to_indexing_field_type
457
+ Indexing::FieldType::Object.new(
458
+ type_name: name,
459
+ subfields: indexing_fields_by_name_in_index.values.map(&:to_indexing_field).compact,
460
+ mapping_options: mapping_options,
461
+ json_schema_options: json_schema_options
462
+ )
463
+ end
464
+
465
+ # @private
466
+ def current_sources
467
+ indexing_fields_by_name_in_index.values.flat_map do |field|
468
+ child_field_sources = field.type.fully_unwrapped.as_object_type&.current_sources || []
469
+ [field.source&.relationship_name || SELF_RELATIONSHIP_NAME] + child_field_sources
470
+ end
471
+ end
472
+
473
+ # @private
474
+ def index_field_runtime_metadata_tuples(
475
+ # path from the overall document root
476
+ path_prefix: "",
477
+ # the source of the parent field
478
+ parent_source: SELF_RELATIONSHIP_NAME,
479
+ # tracks the state of the list counts field
480
+ list_counts_state: ListCountsState::INITIAL
481
+ )
482
+ indexing_fields_by_name_in_index.flat_map do |name, field|
483
+ path = path_prefix + name
484
+ source = field.source&.relationship_name || parent_source
485
+ index_field = SchemaArtifacts::RuntimeMetadata::IndexField.new(source: source)
486
+
487
+ list_count_field_tuples = field.paths_to_lists_for_count_indexing.map do |subpath|
488
+ [list_counts_state.path_to_count_subfield(subpath), index_field] # : [::String, SchemaArtifacts::RuntimeMetadata::IndexField]
489
+ end
490
+
491
+ if (object_type = field.type.fully_unwrapped.as_object_type)
492
+ new_list_counts_state =
493
+ if field.type.list? && field.nested?
494
+ ListCountsState.new_list_counts_field(at: "#{path}.#{LIST_COUNTS_FIELD}")
495
+ else
496
+ list_counts_state[name]
497
+ end
498
+
499
+ object_type.index_field_runtime_metadata_tuples(
500
+ path_prefix: "#{path}.",
501
+ parent_source: source,
502
+ list_counts_state: new_list_counts_state
503
+ )
504
+ else
505
+ [[path, index_field]] # : ::Array[[::String, SchemaArtifacts::RuntimeMetadata::IndexField]]
506
+ end + list_count_field_tuples
507
+ end
508
+ end
509
+
510
+ private
511
+
512
+ def fields_sdl(&arg_selector)
513
+ graphql_fields_by_name.values
514
+ .map { |f| f.to_sdl(&arg_selector) }
515
+ .flat_map { |sdl| sdl.split("\n") }
516
+ .join("\n ")
517
+ end
518
+
519
+ def register_field(name, field, registry, registry_type, only_option_to_fix, &to_comparable)
520
+ if (existing_field = registry[name])
521
+ field = Field.pick_most_accurate_from(field, existing_field, to_comparable: to_comparable || ->(f) { f }) do
522
+ raise SchemaError, "Duplicate #{registry_type} field on Type #{self.name}: #{name}. " \
523
+ "To resolve this, set `#{only_option_to_fix}: true` on one of the fields."
524
+ end
525
+ end
526
+
527
+ registry[name] = field
528
+ end
529
+
530
+ def relates_to(field_name, type, via:, dir:, foreign_key_type:, cardinality:, related_type:)
531
+ field(field_name, type, sortable: false, filterable: false, groupable: false, graphql_only: true) do |field|
532
+ relationship = schema_def_state.factory.new_relationship(
533
+ field,
534
+ cardinality: cardinality,
535
+ related_type: schema_def_state.type_ref(related_type).to_final_form,
536
+ foreign_key: via,
537
+ direction: dir
538
+ )
539
+
540
+ yield relationship if block_given?
541
+
542
+ field.relationship = relationship
543
+ field.runtime_metadata_graphql_field = field.runtime_metadata_graphql_field.with(relation: relationship.runtime_metadata)
544
+
545
+ if dir == :out
546
+ register_inferred_foreign_key_fields(from_type: [via, foreign_key_type], to_other: ["id", "ID!"], related_type: relationship.related_type)
547
+ else
548
+ register_inferred_foreign_key_fields(from_type: ["id", "ID!"], to_other: [via, foreign_key_type], related_type: relationship.related_type)
549
+ end
550
+ end
551
+ end
552
+
553
+ def register_inferred_foreign_key_fields(from_type:, to_other:, related_type:)
554
+ # The root `Query` object shouldn't have inferred foreign key fields (it's not indexed).
555
+ return if name.to_s == "Query"
556
+
557
+ from_field_name, from_type_name = from_type
558
+ field(from_field_name, from_type_name, indexing_only: true, accuracy_confidence: :medium)
559
+
560
+ # If it's a self-referential, we also should add a foreign key field for the other end of the relation.
561
+ if name == related_type.unwrap_non_null.name
562
+ # This must be `:low` confidence for cases where we have a self-referential type that goes both
563
+ # directions, such as:
564
+ #
565
+ # s.object_type "MyTypeBothDirections" do |t|
566
+ # t.relates_to_one "parent", "MyTypeBothDirections!", via: "children_ids", dir: :in
567
+ # t.relates_to_many "children", "MyTypeBothDirections", via: "children_ids", dir: :out
568
+ # end
569
+ #
570
+ # In such a circumstance, the `from_type` side may be more accurate (and will be defined on the `field`
571
+ # call above) and we want it preferred over this definition here.
572
+ to_field_name, to_type_name = to_other
573
+ field(to_field_name, to_type_name, indexing_only: true, accuracy_confidence: :low)
574
+ end
575
+ end
576
+ end
577
+ end
578
+ end
579
+ end