elasticgraph-schema_definition 0.18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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