Class: ElasticGraph::SchemaDefinition::SchemaElements::TypeWithSubfields Abstract

Inherits:
Struct
  • Object
show all
Includes:
Mixins::CanBeGraphQLOnly, Mixins::HasDerivedGraphQLTypeCustomizations, Mixins::HasDirectives, Mixins::HasDocumentation, Mixins::HasTypeInfo, Mixins::VerifiesGraphQLName
Defined in:
lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb

Overview

This class is abstract.

Defines common functionality for all GraphQL types that have subfields:

Direct Known Subclasses

InputType, InterfaceType, ObjectType

Constant Summary

Constants included from Mixins::HasTypeInfo

Mixins::HasTypeInfo::CUSTOMIZABLE_DATASTORE_PARAMS

Instance Attribute Summary collapse

Attributes included from Mixins::HasDocumentation

#doc_comment

Instance Method Summary collapse

Methods included from Mixins::HasTypeInfo

#json_schema, #json_schema_options, #mapping, #mapping_options

Methods included from Mixins::HasDerivedGraphQLTypeCustomizations

#customize_derived_type_fields, #customize_derived_types, #derived_field_customizations_by_name_for_type, #derived_field_customizations_by_type_and_field_name, #derived_type_customizations_by_name, #derived_type_customizations_for_type

Methods included from Mixins::HasDirectives

#directive, #directives, #directives_sdl

Methods included from Mixins::HasDocumentation

#append_to_documentation, #derived_documentation, #documentation, #formatted_documentation

Methods included from Mixins::CanBeGraphQLOnly

#graphql_only, #graphql_only?

Methods included from Mixins::VerifiesGraphQLName

verify_name!

Constructor Details

#initialize(schema_kind, schema_def_state, name, wrapping_type:, field_factory:) {|_self| ... } ⇒ TypeWithSubfields

Returns a new instance of TypeWithSubfields.

Yields:

  • (_self)

Yield Parameters:



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 73

def initialize(schema_kind, schema_def_state, name, wrapping_type:, field_factory:)
  # `any_satisfy`, `any_of`/`all_of`, and `not` are "reserved" field names. They are reserved for usage by
  # ElasticGraph itself in the `*FilterInput` types it generates. If we allow them to be used as field
  # names, we'll run into conflicts when we later generate the `*FilterInput` type.
  #
  # Note that we don't have the same kind of conflict for the other filtering operators (e.g.
  # `equal_to_any_of`, `gt`, etc) because on the generated filter structure, those are leaf
  # nodes. They never exist alongside document field names on a filter type, but these do,
  # so we have to guard against them here.
  reserved_field_names = [
    schema_def_state.schema_elements.all_of,
    schema_def_state.schema_elements.any_of,
    schema_def_state.schema_elements.any_satisfy,
    schema_def_state.schema_elements.not
  ].to_set

  # @type var graphql_fields_by_name: ::Hash[::String, Field]
  graphql_fields_by_name = {}
  # @type var indexing_fields_by_name_in_index: ::Hash[::String, Field]
  indexing_fields_by_name_in_index = {}

  super(
    schema_kind,
    schema_def_state,
    schema_def_state.type_ref(name).to_final_form,
    reserved_field_names,
    graphql_fields_by_name,
    indexing_fields_by_name_in_index,
    field_factory,
    wrapping_type,
    false
  )

  yield self
end

Instance Attribute Details

#field_factoryObject



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 51

class TypeWithSubfields < Struct.new(
  :schema_kind, :schema_def_state, :type_ref, :reserved_field_names,
  :graphql_fields_by_name, :indexing_fields_by_name_in_index, :field_factory,
  :wrapping_type, :relay_pagination_type
)
  prepend Mixins::VerifiesGraphQLName
  include Mixins::CanBeGraphQLOnly
  include Mixins::HasDocumentation
  include Mixins::HasDirectives
  include Mixins::HasDerivedGraphQLTypeCustomizations
  include Mixins::HasTypeInfo

  # The following methods are provided by `Struct.new`:
  # @dynamic type_ref

  # The following methods are provided by `SupportsFilteringAndAggregation`:
  # @dynamic derived_graphql_types

  # The following methods are provided by `CanBeGraphQLOnly`:
  # @dynamic graphql_only?

  # @private
  def initialize(schema_kind, schema_def_state, name, wrapping_type:, field_factory:)
    # `any_satisfy`, `any_of`/`all_of`, and `not` are "reserved" field names. They are reserved for usage by
    # ElasticGraph itself in the `*FilterInput` types it generates. If we allow them to be used as field
    # names, we'll run into conflicts when we later generate the `*FilterInput` type.
    #
    # Note that we don't have the same kind of conflict for the other filtering operators (e.g.
    # `equal_to_any_of`, `gt`, etc) because on the generated filter structure, those are leaf
    # nodes. They never exist alongside document field names on a filter type, but these do,
    # so we have to guard against them here.
    reserved_field_names = [
      schema_def_state.schema_elements.all_of,
      schema_def_state.schema_elements.any_of,
      schema_def_state.schema_elements.any_satisfy,
      schema_def_state.schema_elements.not
    ].to_set

    # @type var graphql_fields_by_name: ::Hash[::String, Field]
    graphql_fields_by_name = {}
    # @type var indexing_fields_by_name_in_index: ::Hash[::String, Field]
    indexing_fields_by_name_in_index = {}

    super(
      schema_kind,
      schema_def_state,
      schema_def_state.type_ref(name).to_final_form,
      reserved_field_names,
      graphql_fields_by_name,
      indexing_fields_by_name_in_index,
      field_factory,
      wrapping_type,
      false
    )

    yield self
  end

  # @return [String] the name of this GraphQL type
  def name
    type_ref.name
  end

  # Defines a [GraphQL field](https://spec.graphql.org/October2021/#sec-Language.Fields) on this type.
  #
  # @param name [String] name of the field
  # @param type [String] type of the field as a [type reference](https://spec.graphql.org/October2021/#sec-Type-References). The named type must be
  #   one of {BuiltInTypes ElasticGraph's built-in types} or a type that has been defined in your schema.
  # @param graphql_only [Boolean] if `true`, ElasticGraph will define the field as a GraphQL field but omit it from the indexing
  #   artifacts (`json_schemas.yaml` and `datastore_config.yaml`). This can be used along with `name_in_index` to support careful
  #   schema evolution.
  # @param indexing_only [Boolean] if `true`, ElasticGraph will define the field for indexing (in the `json_schemas.yaml` and
  #   `datastore_config.yaml` schema artifact) but will omit it from the GraphQL schema. This can be useful to begin indexing a field
  #   before you expose it in GraphQL so that you can fully backfill it first.
  # @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
  #   differently named field in the index.
  # @option options [String] singular can be used on a list field (e.g. `t.field "tags", "[String!]!", singular: "tag"`) to tell
  #   ElasticGraph what the singular form of a field's name is. When provided, ElasticGraph will define a `groupedBy` field (using the
  #   singular form) allowing clients to group by individual values from the field.
  # @option options [Boolean] aggregatable force-enables or disables the ability for aggregation queries to aggregate over this field.
  #   When not provided, ElasticGraph will infer field aggregatability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] filterable force-enables or disables the ability for queries to filter by this field. When not provided,
  #   ElasticGraph will infer field filterability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] groupable force-enables or disables the ability for aggregation queries to group by this field. When
  #   not provided, ElasticGraph will infer field groupability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] sortable force-enables or disables the ability for queries to sort by this field. When not provided,
  #   ElasticGraph will infer field sortability based on the field's GraphQL type and mapping type.
  # @yield [Field] the field for further customization
  # @return [void]
  #
  # @see #paginated_collection_field
  # @see #relates_to_many
  # @see #relates_to_one
  #
  # @note Be careful about defining non-nullable fields. Changing a field’s type from non-nullable (e.g. `Int!`) to nullable (e.g.
  #   `Int`) is a breaking change for clients. Making a field non-nullable may also prevent you from applying permissioning to a field
  #   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).
  #   Therefore, we recommend limiting your use of `!` to only a few situations such as defining a type’s primary key (e.g.
  #   `t.field "id", "ID!"`) or defining a list field (e.g. `t.field "authors", "[String!]!"`) since empty lists already provide a
  #   "no data" representation. You can still configure the ElasticGraph indexer to require a non-null value for a field using
  #   `f.json_schema nullable: false`.
  #
  # @note ElasticGraph’s understanding of datastore capabilities may override your configured
  #   `aggregatable`/`filterable`/`groupable`/`sortable` options. For example, a field indexed as `text` for full text search will
  #   not be sortable or groupable even if you pass `sortable: true, groupable: true` when defining the field, because [text fields
  #   cannot be efficiently sorted by or grouped on](https://www.elastic.co/guide/en/elasticsearch/reference/8.15/text.html#text).
  #
  # @example Define a field with documentation
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID" do |f|
  #         f.documentation "The Campaign's identifier."
  #       end
  #     end
  #   end
  #
  # @example Omit a new field from the GraphQL schema until its data has been backfilled
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID"
  #
  #       # TODO: remove `indexing_only: true` once the data for this field has been fully backfilled
  #       t.field "endDate", "Date", indexing_only: true
  #     end
  #   end
  #
  # @example Use `graphql_only` to introduce a new name for an existing field
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID"
  #
  #       t.field "endOn", "Date" do |f|
  #         f.directive "deprecated", reason: "Use `endDate` instead."
  #       end
  #
  #       # We've decided we want to call the field `endDate` instead of `endOn`, but the data
  #       # for this field is currently indexed in `endOn`, so we can use `graphql_only` and
  #       # `name_in_index` to expose the existing data under a new field name.
  #       t.field "endDate", "Date", name_in_index: "endOn", graphql_only: true
  #     end
  #   end
  def field(name, type, graphql_only: false, indexing_only: false, **options)
    if reserved_field_names.include?(name)
      raise Errors::SchemaError, "Invalid field name: `#{self.name}.#{name}`. `#{name}` is reserved for use by " \
        "ElasticGraph as a filtering operator. To use it for a field name, add " \
        "the `schema_element_name_overrides` option (on `ElasticGraph::SchemaDefinition::RakeTasks.new`) to " \
        "configure an alternate name for the `#{name}` operator."
    end

    options = {name_in_index: nil}.merge(options) if graphql_only

    field_factory.call(
      name: name,
      type: type,
      graphql_only: graphql_only,
      parent_type: wrapping_type,
      **options
    ) do |field|
      yield field if block_given?

      unless indexing_only
        register_field(field.name, field, graphql_fields_by_name, "GraphQL", :indexing_only)
      end

      unless graphql_only
        register_field(field.name_in_index, field, indexing_fields_by_name_in_index, "indexing", :graphql_only) do |f|
          f.to_indexing_field_reference
        end
      end
    end
  end

  # Registers the name of a field that existed in a prior version of the schema but has been deleted.
  #
  # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
  #   or {Field#renamed_from}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
  #   the call to this method can be removed.
  #
  # @param field_name [String] name of field that used to exist but has been deleted
  # @return [void]
  #
  # @example Indicate that `Widget.description` has been deleted
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Widget" do |t|
  #       t.deleted_field "description"
  #     end
  #   end
  def deleted_field(field_name)
    schema_def_state.register_deleted_field(
      name,
      field_name,
      defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
      defined_via: %(type.deleted_field "#{field_name}")
    )
  end

  # Registers an old name that this type used to have in a prior version of the schema.
  #
  # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
  #   or {API#deleted_type}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
  #   the call to this method can be removed.
  #
  # @param old_name [String] old name this field used to have in a prior version of the schema
  # @return [void]
  #
  # @example Indicate that `Widget` used to be called `Component`.
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Widget" do |t|
  #       t.renamed_from "Component"
  #     end
  #   end
  def renamed_from(old_name)
    schema_def_state.register_renamed_type(
      name,
      from: old_name,
      defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
      defined_via: %(type.renamed_from "#{old_name}")
    )
  end

  # An alternative to {#field} for when you have a list field that you want exposed as a [paginated Relay
  # connection](https://relay.dev/graphql/connections.htm) rather than as a simple list.
  #
  # @note Bear in mind that pagination does not have much efficiency benefit in this case: all elements of the collection will be
  #   retrieved when fetching this field from the datastore. The pagination implementation will just trim down the collection before
  #   returning it.
  #
  # @param name [String] name of the field
  # @param element_type [String] name of the type of element in the collection
  # @param name_in_index [String] the name of the field in the datastore index. Can be used to back a GraphQL field with a
  #   differently named field in the index.
  # @param singular [String] indicates what the singular form of a field's name is. When provided, ElasticGraph will define a
  #   `groupedBy` field (using the singular form) allowing clients to group by individual values from the field.
  # @yield [Field] the field for further customization
  # @return [void]
  #
  # @see #field
  # @see #relates_to_many
  # @see #relates_to_one
  #
  # @example Define `Author.books` as a paginated collection field
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Author" do |t|
  #       t.field "id", "ID"
  #       t.field "name", "String"
  #       t.paginated_collection_field "books", "String"
  #       t.index "authors"
  #     end
  #   end
  def paginated_collection_field(name, element_type, name_in_index: name, singular: nil, &block)
    element_type_ref = schema_def_state.type_ref(element_type).to_final_form
    element_type = element_type_ref.name

    schema_def_state.paginated_collection_element_types << element_type

    backing_indexing_field = field(name, "[#{element_type}!]!", indexing_only: true, name_in_index: name_in_index, &block)

    field(
      name,
      element_type_ref.as_connection.name,
      name_in_index: name_in_index,
      type_for_derived_types: "[#{element_type}]",
      groupable: !!singular,
      sortable: false,
      graphql_only: true,
      singular: singular,
      backing_indexing_field: backing_indexing_field
    ) do |f|
      f.define_relay_pagination_arguments!
      block&.call(f)
    end
  end

  # Defines a "has one" relationship between the current indexed type and another indexed type by defining a field clients
  # can use to navigate across indexed types in a single GraphQL query.
  #
  # @param field_name [String] name of the relationship field
  # @param type [String] name of the related type
  # @param via [String] name of the foreign key field
  # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
  #   references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type  and references the `id` of
  #   the related type.
  # @yield [Relationship] the generated relationship fields, for further customization
  # @return [void]
  #
  # @see #field
  # @see #relates_to_many
  #
  # @example Use `relates_to_one` to define `Player.team`
  #  ElasticGraph.define_schema do |schema|
  #    schema.object_type "Team" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "homeCity", "String"
  #      t.index "teams"
  #    end
  #
  #    schema.object_type "Player" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.relates_to_one "team", "Team", via: "teamId", dir: :out
  #      t.index "players"
  #    end
  #  end
  def relates_to_one(field_name, type, via:, dir:, &block)
    foreign_key_type = schema_def_state.type_ref(type).non_null? ? "ID!" : "ID"
    relates_to(field_name, type, via: via, dir: dir, foreign_key_type: foreign_key_type, cardinality: :one, related_type: type, &block)
  end

  # Defines a "has many" relationship between the current indexed type and another indexed type by defining a pair of fields clients
  # can use to navigate across indexed types in a single GraphQL query. The pair of generated fields will be [Relay Connection
  # types](https://relay.dev/graphql/connections.htm#sec-Connection-Types) allowing you to filter, sort, paginate, and aggregated the
  # related data.
  #
  # @param field_name [String] name of the relationship field
  # @param type [String] name of the related type
  # @param via [String] name of the foreign key field
  # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
  #   references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type  and references the `id` of
  #   the related type.
  # @param singular [String] singular form of the `field_name`; will be used (along with an `Aggregations` suffix) for the name of
  #   the generated aggregations field
  # @yield [Relationship] the generated relationship fields, for further customization
  # @return [void]
  #
  # @see #field
  # @see #paginated_collection_field
  # @see #relates_to_one
  #
  # @example Use `relates_to_many` to define `Team.players` and `Team.playerAggregations`
  #  ElasticGraph.define_schema do |schema|
  #    schema.object_type "Team" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "homeCity", "String"
  #      t.relates_to_many "players", "Player", via: "teamId", dir: :in, singular: "player"
  #      t.index "teams"
  #    end
  #
  #    schema.object_type "Player" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "teamId", "ID"
  #      t.index "players"
  #    end
  #  end
  def relates_to_many(field_name, type, via:, dir:, singular:)
    foreign_key_type = (dir == :out) ? "[ID!]!" : "ID"
    type_ref = schema_def_state.type_ref(type).to_final_form

    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|
      f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
        a.documentation "Used to filter the returned `#{field_name}` based on the provided criteria."
      end

      f.argument schema_def_state.schema_elements.order_by, "[#{type_ref.as_sort_order.name}!]" do |a|
        a.documentation "Used to specify how the returned `#{field_name}` should be sorted."
      end

      f.define_relay_pagination_arguments!

      yield f if block_given?
    end

    aggregations_name = schema_def_state.schema_elements.normalize_case("#{singular}_aggregations")
    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|
      f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
        a.documentation "Used to filter the `#{type}` documents that get aggregated over based on the provided criteria."
      end

      f.define_relay_pagination_arguments!

      yield f if block_given?

      f.documentation f.derived_documentation("Aggregations over the `#{field_name}` data")
    end
  end

  # Converts the type to GraphQL SDL syntax.
  #
  # @private
  def to_sdl(&field_arg_selector)
    generate_sdl(name_section: name, &field_arg_selector)
  end

  # @private
  def generate_sdl(name_section:, &field_arg_selector)
    <<~SDL
      #{formatted_documentation}#{schema_kind} #{name_section} #{directives_sdl(suffix_with: " ")}{
        #{fields_sdl(&field_arg_selector)}
      }
    SDL
  end

  # @private
  def aggregated_values_type
    schema_def_state.type_ref("NonNumeric").as_aggregated_values
  end

  # @private
  def indexed?
    false
  end

  # @private
  def to_indexing_field_type
    Indexing::FieldType::Object.new(
      type_name: name,
      subfields: indexing_fields_by_name_in_index.values.map(&:to_indexing_field).compact,
      mapping_options: mapping_options,
      json_schema_options: json_schema_options
    )
  end

  # @private
  def current_sources
    indexing_fields_by_name_in_index.values.flat_map do |field|
      child_field_sources = field.type.fully_unwrapped.as_object_type&.current_sources || []
      [field.source&.relationship_name || SELF_RELATIONSHIP_NAME] + child_field_sources
    end
  end

  # @private
  def (
    # path from the overall document root
    path_prefix: "",
    # the source of the parent field
    parent_source: SELF_RELATIONSHIP_NAME,
    # tracks the state of the list counts field
    list_counts_state: ListCountsState::INITIAL
  )
    indexing_fields_by_name_in_index.flat_map do |name, field|
      path = path_prefix + name
      source = field.source&.relationship_name || parent_source
      index_field = SchemaArtifacts::RuntimeMetadata::IndexField.new(source: source)

      list_count_field_tuples = field.paths_to_lists_for_count_indexing.map do |subpath|
        [list_counts_state.path_to_count_subfield(subpath), index_field] # : [::String, SchemaArtifacts::RuntimeMetadata::IndexField]
      end

      if (object_type = field.type.fully_unwrapped.as_object_type)
        new_list_counts_state =
          if field.type.list? && field.nested?
            ListCountsState.new_list_counts_field(at: "#{path}.#{LIST_COUNTS_FIELD}")
          else
            list_counts_state[name]
          end

        object_type.(
          path_prefix: "#{path}.",
          parent_source: source,
          list_counts_state: new_list_counts_state
        )
      else
        [[path, index_field]] # : ::Array[[::String, SchemaArtifacts::RuntimeMetadata::IndexField]]
      end + list_count_field_tuples
    end
  end

  private

  def fields_sdl(&arg_selector)
    graphql_fields_by_name.values
      .map { |f| f.to_sdl(&arg_selector) }
      .flat_map { |sdl| sdl.split("\n") }
      .join("\n  ")
  end

  def register_field(name, field, registry, registry_type, only_option_to_fix, &to_comparable)
    if (existing_field = registry[name])
      field = Field.pick_most_accurate_from(field, existing_field, to_comparable: to_comparable || ->(f) { f }) do
        raise Errors::SchemaError, "Duplicate #{registry_type} field on Type #{self.name}: #{name}. " \
          "To resolve this, set `#{only_option_to_fix}: true` on one of the fields."
      end
    end

    registry[name] = field
  end

  def relates_to(field_name, type, via:, dir:, foreign_key_type:, cardinality:, related_type:)
    field(field_name, type, sortable: false, filterable: false, groupable: false, graphql_only: true) do |field|
      relationship = schema_def_state.factory.new_relationship(
        field,
        cardinality: cardinality,
        related_type: schema_def_state.type_ref(related_type).to_final_form,
        foreign_key: via,
        direction: dir
      )

      yield relationship if block_given?

      field.relationship = relationship

      if dir == :out
        register_inferred_foreign_key_fields(from_type: [via, foreign_key_type], to_other: ["id", "ID!"], related_type: relationship.related_type)
      else
        register_inferred_foreign_key_fields(from_type: ["id", "ID!"], to_other: [via, foreign_key_type], related_type: relationship.related_type)
      end
    end
  end

  def register_inferred_foreign_key_fields(from_type:, to_other:, related_type:)
    # The root `Query` object shouldn't have inferred foreign key fields (it's not indexed).
    return if name.to_s == "Query"

    from_field_name, from_type_name = from_type
    field(from_field_name, from_type_name, indexing_only: true, accuracy_confidence: :medium)

    # If it's a self-referential, we also should add a foreign key field for the other end of the relation.
    if name == related_type.unwrap_non_null.name
      # This must be `:low` confidence for cases where we have a self-referential type that goes both
      # directions, such as:
      #
      # s.object_type "MyTypeBothDirections" do |t|
      #   t.relates_to_one "parent", "MyTypeBothDirections!", via: "children_ids", dir: :in
      #   t.relates_to_many "children", "MyTypeBothDirections", via: "children_ids", dir: :out
      # end
      #
      # In such a circumstance, the `from_type` side may be more accurate (and will be defined on the `field`
      # call above) and we want it preferred over this definition here.
      to_field_name, to_type_name = to_other
      field(to_field_name, to_type_name, indexing_only: true, accuracy_confidence: :low)
    end
  end
end

#graphql_fields_by_nameObject



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 51

class TypeWithSubfields < Struct.new(
  :schema_kind, :schema_def_state, :type_ref, :reserved_field_names,
  :graphql_fields_by_name, :indexing_fields_by_name_in_index, :field_factory,
  :wrapping_type, :relay_pagination_type
)
  prepend Mixins::VerifiesGraphQLName
  include Mixins::CanBeGraphQLOnly
  include Mixins::HasDocumentation
  include Mixins::HasDirectives
  include Mixins::HasDerivedGraphQLTypeCustomizations
  include Mixins::HasTypeInfo

  # The following methods are provided by `Struct.new`:
  # @dynamic type_ref

  # The following methods are provided by `SupportsFilteringAndAggregation`:
  # @dynamic derived_graphql_types

  # The following methods are provided by `CanBeGraphQLOnly`:
  # @dynamic graphql_only?

  # @private
  def initialize(schema_kind, schema_def_state, name, wrapping_type:, field_factory:)
    # `any_satisfy`, `any_of`/`all_of`, and `not` are "reserved" field names. They are reserved for usage by
    # ElasticGraph itself in the `*FilterInput` types it generates. If we allow them to be used as field
    # names, we'll run into conflicts when we later generate the `*FilterInput` type.
    #
    # Note that we don't have the same kind of conflict for the other filtering operators (e.g.
    # `equal_to_any_of`, `gt`, etc) because on the generated filter structure, those are leaf
    # nodes. They never exist alongside document field names on a filter type, but these do,
    # so we have to guard against them here.
    reserved_field_names = [
      schema_def_state.schema_elements.all_of,
      schema_def_state.schema_elements.any_of,
      schema_def_state.schema_elements.any_satisfy,
      schema_def_state.schema_elements.not
    ].to_set

    # @type var graphql_fields_by_name: ::Hash[::String, Field]
    graphql_fields_by_name = {}
    # @type var indexing_fields_by_name_in_index: ::Hash[::String, Field]
    indexing_fields_by_name_in_index = {}

    super(
      schema_kind,
      schema_def_state,
      schema_def_state.type_ref(name).to_final_form,
      reserved_field_names,
      graphql_fields_by_name,
      indexing_fields_by_name_in_index,
      field_factory,
      wrapping_type,
      false
    )

    yield self
  end

  # @return [String] the name of this GraphQL type
  def name
    type_ref.name
  end

  # Defines a [GraphQL field](https://spec.graphql.org/October2021/#sec-Language.Fields) on this type.
  #
  # @param name [String] name of the field
  # @param type [String] type of the field as a [type reference](https://spec.graphql.org/October2021/#sec-Type-References). The named type must be
  #   one of {BuiltInTypes ElasticGraph's built-in types} or a type that has been defined in your schema.
  # @param graphql_only [Boolean] if `true`, ElasticGraph will define the field as a GraphQL field but omit it from the indexing
  #   artifacts (`json_schemas.yaml` and `datastore_config.yaml`). This can be used along with `name_in_index` to support careful
  #   schema evolution.
  # @param indexing_only [Boolean] if `true`, ElasticGraph will define the field for indexing (in the `json_schemas.yaml` and
  #   `datastore_config.yaml` schema artifact) but will omit it from the GraphQL schema. This can be useful to begin indexing a field
  #   before you expose it in GraphQL so that you can fully backfill it first.
  # @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
  #   differently named field in the index.
  # @option options [String] singular can be used on a list field (e.g. `t.field "tags", "[String!]!", singular: "tag"`) to tell
  #   ElasticGraph what the singular form of a field's name is. When provided, ElasticGraph will define a `groupedBy` field (using the
  #   singular form) allowing clients to group by individual values from the field.
  # @option options [Boolean] aggregatable force-enables or disables the ability for aggregation queries to aggregate over this field.
  #   When not provided, ElasticGraph will infer field aggregatability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] filterable force-enables or disables the ability for queries to filter by this field. When not provided,
  #   ElasticGraph will infer field filterability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] groupable force-enables or disables the ability for aggregation queries to group by this field. When
  #   not provided, ElasticGraph will infer field groupability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] sortable force-enables or disables the ability for queries to sort by this field. When not provided,
  #   ElasticGraph will infer field sortability based on the field's GraphQL type and mapping type.
  # @yield [Field] the field for further customization
  # @return [void]
  #
  # @see #paginated_collection_field
  # @see #relates_to_many
  # @see #relates_to_one
  #
  # @note Be careful about defining non-nullable fields. Changing a field’s type from non-nullable (e.g. `Int!`) to nullable (e.g.
  #   `Int`) is a breaking change for clients. Making a field non-nullable may also prevent you from applying permissioning to a field
  #   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).
  #   Therefore, we recommend limiting your use of `!` to only a few situations such as defining a type’s primary key (e.g.
  #   `t.field "id", "ID!"`) or defining a list field (e.g. `t.field "authors", "[String!]!"`) since empty lists already provide a
  #   "no data" representation. You can still configure the ElasticGraph indexer to require a non-null value for a field using
  #   `f.json_schema nullable: false`.
  #
  # @note ElasticGraph’s understanding of datastore capabilities may override your configured
  #   `aggregatable`/`filterable`/`groupable`/`sortable` options. For example, a field indexed as `text` for full text search will
  #   not be sortable or groupable even if you pass `sortable: true, groupable: true` when defining the field, because [text fields
  #   cannot be efficiently sorted by or grouped on](https://www.elastic.co/guide/en/elasticsearch/reference/8.15/text.html#text).
  #
  # @example Define a field with documentation
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID" do |f|
  #         f.documentation "The Campaign's identifier."
  #       end
  #     end
  #   end
  #
  # @example Omit a new field from the GraphQL schema until its data has been backfilled
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID"
  #
  #       # TODO: remove `indexing_only: true` once the data for this field has been fully backfilled
  #       t.field "endDate", "Date", indexing_only: true
  #     end
  #   end
  #
  # @example Use `graphql_only` to introduce a new name for an existing field
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID"
  #
  #       t.field "endOn", "Date" do |f|
  #         f.directive "deprecated", reason: "Use `endDate` instead."
  #       end
  #
  #       # We've decided we want to call the field `endDate` instead of `endOn`, but the data
  #       # for this field is currently indexed in `endOn`, so we can use `graphql_only` and
  #       # `name_in_index` to expose the existing data under a new field name.
  #       t.field "endDate", "Date", name_in_index: "endOn", graphql_only: true
  #     end
  #   end
  def field(name, type, graphql_only: false, indexing_only: false, **options)
    if reserved_field_names.include?(name)
      raise Errors::SchemaError, "Invalid field name: `#{self.name}.#{name}`. `#{name}` is reserved for use by " \
        "ElasticGraph as a filtering operator. To use it for a field name, add " \
        "the `schema_element_name_overrides` option (on `ElasticGraph::SchemaDefinition::RakeTasks.new`) to " \
        "configure an alternate name for the `#{name}` operator."
    end

    options = {name_in_index: nil}.merge(options) if graphql_only

    field_factory.call(
      name: name,
      type: type,
      graphql_only: graphql_only,
      parent_type: wrapping_type,
      **options
    ) do |field|
      yield field if block_given?

      unless indexing_only
        register_field(field.name, field, graphql_fields_by_name, "GraphQL", :indexing_only)
      end

      unless graphql_only
        register_field(field.name_in_index, field, indexing_fields_by_name_in_index, "indexing", :graphql_only) do |f|
          f.to_indexing_field_reference
        end
      end
    end
  end

  # Registers the name of a field that existed in a prior version of the schema but has been deleted.
  #
  # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
  #   or {Field#renamed_from}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
  #   the call to this method can be removed.
  #
  # @param field_name [String] name of field that used to exist but has been deleted
  # @return [void]
  #
  # @example Indicate that `Widget.description` has been deleted
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Widget" do |t|
  #       t.deleted_field "description"
  #     end
  #   end
  def deleted_field(field_name)
    schema_def_state.register_deleted_field(
      name,
      field_name,
      defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
      defined_via: %(type.deleted_field "#{field_name}")
    )
  end

  # Registers an old name that this type used to have in a prior version of the schema.
  #
  # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
  #   or {API#deleted_type}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
  #   the call to this method can be removed.
  #
  # @param old_name [String] old name this field used to have in a prior version of the schema
  # @return [void]
  #
  # @example Indicate that `Widget` used to be called `Component`.
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Widget" do |t|
  #       t.renamed_from "Component"
  #     end
  #   end
  def renamed_from(old_name)
    schema_def_state.register_renamed_type(
      name,
      from: old_name,
      defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
      defined_via: %(type.renamed_from "#{old_name}")
    )
  end

  # An alternative to {#field} for when you have a list field that you want exposed as a [paginated Relay
  # connection](https://relay.dev/graphql/connections.htm) rather than as a simple list.
  #
  # @note Bear in mind that pagination does not have much efficiency benefit in this case: all elements of the collection will be
  #   retrieved when fetching this field from the datastore. The pagination implementation will just trim down the collection before
  #   returning it.
  #
  # @param name [String] name of the field
  # @param element_type [String] name of the type of element in the collection
  # @param name_in_index [String] the name of the field in the datastore index. Can be used to back a GraphQL field with a
  #   differently named field in the index.
  # @param singular [String] indicates what the singular form of a field's name is. When provided, ElasticGraph will define a
  #   `groupedBy` field (using the singular form) allowing clients to group by individual values from the field.
  # @yield [Field] the field for further customization
  # @return [void]
  #
  # @see #field
  # @see #relates_to_many
  # @see #relates_to_one
  #
  # @example Define `Author.books` as a paginated collection field
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Author" do |t|
  #       t.field "id", "ID"
  #       t.field "name", "String"
  #       t.paginated_collection_field "books", "String"
  #       t.index "authors"
  #     end
  #   end
  def paginated_collection_field(name, element_type, name_in_index: name, singular: nil, &block)
    element_type_ref = schema_def_state.type_ref(element_type).to_final_form
    element_type = element_type_ref.name

    schema_def_state.paginated_collection_element_types << element_type

    backing_indexing_field = field(name, "[#{element_type}!]!", indexing_only: true, name_in_index: name_in_index, &block)

    field(
      name,
      element_type_ref.as_connection.name,
      name_in_index: name_in_index,
      type_for_derived_types: "[#{element_type}]",
      groupable: !!singular,
      sortable: false,
      graphql_only: true,
      singular: singular,
      backing_indexing_field: backing_indexing_field
    ) do |f|
      f.define_relay_pagination_arguments!
      block&.call(f)
    end
  end

  # Defines a "has one" relationship between the current indexed type and another indexed type by defining a field clients
  # can use to navigate across indexed types in a single GraphQL query.
  #
  # @param field_name [String] name of the relationship field
  # @param type [String] name of the related type
  # @param via [String] name of the foreign key field
  # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
  #   references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type  and references the `id` of
  #   the related type.
  # @yield [Relationship] the generated relationship fields, for further customization
  # @return [void]
  #
  # @see #field
  # @see #relates_to_many
  #
  # @example Use `relates_to_one` to define `Player.team`
  #  ElasticGraph.define_schema do |schema|
  #    schema.object_type "Team" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "homeCity", "String"
  #      t.index "teams"
  #    end
  #
  #    schema.object_type "Player" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.relates_to_one "team", "Team", via: "teamId", dir: :out
  #      t.index "players"
  #    end
  #  end
  def relates_to_one(field_name, type, via:, dir:, &block)
    foreign_key_type = schema_def_state.type_ref(type).non_null? ? "ID!" : "ID"
    relates_to(field_name, type, via: via, dir: dir, foreign_key_type: foreign_key_type, cardinality: :one, related_type: type, &block)
  end

  # Defines a "has many" relationship between the current indexed type and another indexed type by defining a pair of fields clients
  # can use to navigate across indexed types in a single GraphQL query. The pair of generated fields will be [Relay Connection
  # types](https://relay.dev/graphql/connections.htm#sec-Connection-Types) allowing you to filter, sort, paginate, and aggregated the
  # related data.
  #
  # @param field_name [String] name of the relationship field
  # @param type [String] name of the related type
  # @param via [String] name of the foreign key field
  # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
  #   references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type  and references the `id` of
  #   the related type.
  # @param singular [String] singular form of the `field_name`; will be used (along with an `Aggregations` suffix) for the name of
  #   the generated aggregations field
  # @yield [Relationship] the generated relationship fields, for further customization
  # @return [void]
  #
  # @see #field
  # @see #paginated_collection_field
  # @see #relates_to_one
  #
  # @example Use `relates_to_many` to define `Team.players` and `Team.playerAggregations`
  #  ElasticGraph.define_schema do |schema|
  #    schema.object_type "Team" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "homeCity", "String"
  #      t.relates_to_many "players", "Player", via: "teamId", dir: :in, singular: "player"
  #      t.index "teams"
  #    end
  #
  #    schema.object_type "Player" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "teamId", "ID"
  #      t.index "players"
  #    end
  #  end
  def relates_to_many(field_name, type, via:, dir:, singular:)
    foreign_key_type = (dir == :out) ? "[ID!]!" : "ID"
    type_ref = schema_def_state.type_ref(type).to_final_form

    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|
      f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
        a.documentation "Used to filter the returned `#{field_name}` based on the provided criteria."
      end

      f.argument schema_def_state.schema_elements.order_by, "[#{type_ref.as_sort_order.name}!]" do |a|
        a.documentation "Used to specify how the returned `#{field_name}` should be sorted."
      end

      f.define_relay_pagination_arguments!

      yield f if block_given?
    end

    aggregations_name = schema_def_state.schema_elements.normalize_case("#{singular}_aggregations")
    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|
      f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
        a.documentation "Used to filter the `#{type}` documents that get aggregated over based on the provided criteria."
      end

      f.define_relay_pagination_arguments!

      yield f if block_given?

      f.documentation f.derived_documentation("Aggregations over the `#{field_name}` data")
    end
  end

  # Converts the type to GraphQL SDL syntax.
  #
  # @private
  def to_sdl(&field_arg_selector)
    generate_sdl(name_section: name, &field_arg_selector)
  end

  # @private
  def generate_sdl(name_section:, &field_arg_selector)
    <<~SDL
      #{formatted_documentation}#{schema_kind} #{name_section} #{directives_sdl(suffix_with: " ")}{
        #{fields_sdl(&field_arg_selector)}
      }
    SDL
  end

  # @private
  def aggregated_values_type
    schema_def_state.type_ref("NonNumeric").as_aggregated_values
  end

  # @private
  def indexed?
    false
  end

  # @private
  def to_indexing_field_type
    Indexing::FieldType::Object.new(
      type_name: name,
      subfields: indexing_fields_by_name_in_index.values.map(&:to_indexing_field).compact,
      mapping_options: mapping_options,
      json_schema_options: json_schema_options
    )
  end

  # @private
  def current_sources
    indexing_fields_by_name_in_index.values.flat_map do |field|
      child_field_sources = field.type.fully_unwrapped.as_object_type&.current_sources || []
      [field.source&.relationship_name || SELF_RELATIONSHIP_NAME] + child_field_sources
    end
  end

  # @private
  def (
    # path from the overall document root
    path_prefix: "",
    # the source of the parent field
    parent_source: SELF_RELATIONSHIP_NAME,
    # tracks the state of the list counts field
    list_counts_state: ListCountsState::INITIAL
  )
    indexing_fields_by_name_in_index.flat_map do |name, field|
      path = path_prefix + name
      source = field.source&.relationship_name || parent_source
      index_field = SchemaArtifacts::RuntimeMetadata::IndexField.new(source: source)

      list_count_field_tuples = field.paths_to_lists_for_count_indexing.map do |subpath|
        [list_counts_state.path_to_count_subfield(subpath), index_field] # : [::String, SchemaArtifacts::RuntimeMetadata::IndexField]
      end

      if (object_type = field.type.fully_unwrapped.as_object_type)
        new_list_counts_state =
          if field.type.list? && field.nested?
            ListCountsState.new_list_counts_field(at: "#{path}.#{LIST_COUNTS_FIELD}")
          else
            list_counts_state[name]
          end

        object_type.(
          path_prefix: "#{path}.",
          parent_source: source,
          list_counts_state: new_list_counts_state
        )
      else
        [[path, index_field]] # : ::Array[[::String, SchemaArtifacts::RuntimeMetadata::IndexField]]
      end + list_count_field_tuples
    end
  end

  private

  def fields_sdl(&arg_selector)
    graphql_fields_by_name.values
      .map { |f| f.to_sdl(&arg_selector) }
      .flat_map { |sdl| sdl.split("\n") }
      .join("\n  ")
  end

  def register_field(name, field, registry, registry_type, only_option_to_fix, &to_comparable)
    if (existing_field = registry[name])
      field = Field.pick_most_accurate_from(field, existing_field, to_comparable: to_comparable || ->(f) { f }) do
        raise Errors::SchemaError, "Duplicate #{registry_type} field on Type #{self.name}: #{name}. " \
          "To resolve this, set `#{only_option_to_fix}: true` on one of the fields."
      end
    end

    registry[name] = field
  end

  def relates_to(field_name, type, via:, dir:, foreign_key_type:, cardinality:, related_type:)
    field(field_name, type, sortable: false, filterable: false, groupable: false, graphql_only: true) do |field|
      relationship = schema_def_state.factory.new_relationship(
        field,
        cardinality: cardinality,
        related_type: schema_def_state.type_ref(related_type).to_final_form,
        foreign_key: via,
        direction: dir
      )

      yield relationship if block_given?

      field.relationship = relationship

      if dir == :out
        register_inferred_foreign_key_fields(from_type: [via, foreign_key_type], to_other: ["id", "ID!"], related_type: relationship.related_type)
      else
        register_inferred_foreign_key_fields(from_type: ["id", "ID!"], to_other: [via, foreign_key_type], related_type: relationship.related_type)
      end
    end
  end

  def register_inferred_foreign_key_fields(from_type:, to_other:, related_type:)
    # The root `Query` object shouldn't have inferred foreign key fields (it's not indexed).
    return if name.to_s == "Query"

    from_field_name, from_type_name = from_type
    field(from_field_name, from_type_name, indexing_only: true, accuracy_confidence: :medium)

    # If it's a self-referential, we also should add a foreign key field for the other end of the relation.
    if name == related_type.unwrap_non_null.name
      # This must be `:low` confidence for cases where we have a self-referential type that goes both
      # directions, such as:
      #
      # s.object_type "MyTypeBothDirections" do |t|
      #   t.relates_to_one "parent", "MyTypeBothDirections!", via: "children_ids", dir: :in
      #   t.relates_to_many "children", "MyTypeBothDirections", via: "children_ids", dir: :out
      # end
      #
      # In such a circumstance, the `from_type` side may be more accurate (and will be defined on the `field`
      # call above) and we want it preferred over this definition here.
      to_field_name, to_type_name = to_other
      field(to_field_name, to_type_name, indexing_only: true, accuracy_confidence: :low)
    end
  end
end

#indexing_fields_by_name_in_indexObject



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 51

class TypeWithSubfields < Struct.new(
  :schema_kind, :schema_def_state, :type_ref, :reserved_field_names,
  :graphql_fields_by_name, :indexing_fields_by_name_in_index, :field_factory,
  :wrapping_type, :relay_pagination_type
)
  prepend Mixins::VerifiesGraphQLName
  include Mixins::CanBeGraphQLOnly
  include Mixins::HasDocumentation
  include Mixins::HasDirectives
  include Mixins::HasDerivedGraphQLTypeCustomizations
  include Mixins::HasTypeInfo

  # The following methods are provided by `Struct.new`:
  # @dynamic type_ref

  # The following methods are provided by `SupportsFilteringAndAggregation`:
  # @dynamic derived_graphql_types

  # The following methods are provided by `CanBeGraphQLOnly`:
  # @dynamic graphql_only?

  # @private
  def initialize(schema_kind, schema_def_state, name, wrapping_type:, field_factory:)
    # `any_satisfy`, `any_of`/`all_of`, and `not` are "reserved" field names. They are reserved for usage by
    # ElasticGraph itself in the `*FilterInput` types it generates. If we allow them to be used as field
    # names, we'll run into conflicts when we later generate the `*FilterInput` type.
    #
    # Note that we don't have the same kind of conflict for the other filtering operators (e.g.
    # `equal_to_any_of`, `gt`, etc) because on the generated filter structure, those are leaf
    # nodes. They never exist alongside document field names on a filter type, but these do,
    # so we have to guard against them here.
    reserved_field_names = [
      schema_def_state.schema_elements.all_of,
      schema_def_state.schema_elements.any_of,
      schema_def_state.schema_elements.any_satisfy,
      schema_def_state.schema_elements.not
    ].to_set

    # @type var graphql_fields_by_name: ::Hash[::String, Field]
    graphql_fields_by_name = {}
    # @type var indexing_fields_by_name_in_index: ::Hash[::String, Field]
    indexing_fields_by_name_in_index = {}

    super(
      schema_kind,
      schema_def_state,
      schema_def_state.type_ref(name).to_final_form,
      reserved_field_names,
      graphql_fields_by_name,
      indexing_fields_by_name_in_index,
      field_factory,
      wrapping_type,
      false
    )

    yield self
  end

  # @return [String] the name of this GraphQL type
  def name
    type_ref.name
  end

  # Defines a [GraphQL field](https://spec.graphql.org/October2021/#sec-Language.Fields) on this type.
  #
  # @param name [String] name of the field
  # @param type [String] type of the field as a [type reference](https://spec.graphql.org/October2021/#sec-Type-References). The named type must be
  #   one of {BuiltInTypes ElasticGraph's built-in types} or a type that has been defined in your schema.
  # @param graphql_only [Boolean] if `true`, ElasticGraph will define the field as a GraphQL field but omit it from the indexing
  #   artifacts (`json_schemas.yaml` and `datastore_config.yaml`). This can be used along with `name_in_index` to support careful
  #   schema evolution.
  # @param indexing_only [Boolean] if `true`, ElasticGraph will define the field for indexing (in the `json_schemas.yaml` and
  #   `datastore_config.yaml` schema artifact) but will omit it from the GraphQL schema. This can be useful to begin indexing a field
  #   before you expose it in GraphQL so that you can fully backfill it first.
  # @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
  #   differently named field in the index.
  # @option options [String] singular can be used on a list field (e.g. `t.field "tags", "[String!]!", singular: "tag"`) to tell
  #   ElasticGraph what the singular form of a field's name is. When provided, ElasticGraph will define a `groupedBy` field (using the
  #   singular form) allowing clients to group by individual values from the field.
  # @option options [Boolean] aggregatable force-enables or disables the ability for aggregation queries to aggregate over this field.
  #   When not provided, ElasticGraph will infer field aggregatability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] filterable force-enables or disables the ability for queries to filter by this field. When not provided,
  #   ElasticGraph will infer field filterability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] groupable force-enables or disables the ability for aggregation queries to group by this field. When
  #   not provided, ElasticGraph will infer field groupability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] sortable force-enables or disables the ability for queries to sort by this field. When not provided,
  #   ElasticGraph will infer field sortability based on the field's GraphQL type and mapping type.
  # @yield [Field] the field for further customization
  # @return [void]
  #
  # @see #paginated_collection_field
  # @see #relates_to_many
  # @see #relates_to_one
  #
  # @note Be careful about defining non-nullable fields. Changing a field’s type from non-nullable (e.g. `Int!`) to nullable (e.g.
  #   `Int`) is a breaking change for clients. Making a field non-nullable may also prevent you from applying permissioning to a field
  #   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).
  #   Therefore, we recommend limiting your use of `!` to only a few situations such as defining a type’s primary key (e.g.
  #   `t.field "id", "ID!"`) or defining a list field (e.g. `t.field "authors", "[String!]!"`) since empty lists already provide a
  #   "no data" representation. You can still configure the ElasticGraph indexer to require a non-null value for a field using
  #   `f.json_schema nullable: false`.
  #
  # @note ElasticGraph’s understanding of datastore capabilities may override your configured
  #   `aggregatable`/`filterable`/`groupable`/`sortable` options. For example, a field indexed as `text` for full text search will
  #   not be sortable or groupable even if you pass `sortable: true, groupable: true` when defining the field, because [text fields
  #   cannot be efficiently sorted by or grouped on](https://www.elastic.co/guide/en/elasticsearch/reference/8.15/text.html#text).
  #
  # @example Define a field with documentation
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID" do |f|
  #         f.documentation "The Campaign's identifier."
  #       end
  #     end
  #   end
  #
  # @example Omit a new field from the GraphQL schema until its data has been backfilled
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID"
  #
  #       # TODO: remove `indexing_only: true` once the data for this field has been fully backfilled
  #       t.field "endDate", "Date", indexing_only: true
  #     end
  #   end
  #
  # @example Use `graphql_only` to introduce a new name for an existing field
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID"
  #
  #       t.field "endOn", "Date" do |f|
  #         f.directive "deprecated", reason: "Use `endDate` instead."
  #       end
  #
  #       # We've decided we want to call the field `endDate` instead of `endOn`, but the data
  #       # for this field is currently indexed in `endOn`, so we can use `graphql_only` and
  #       # `name_in_index` to expose the existing data under a new field name.
  #       t.field "endDate", "Date", name_in_index: "endOn", graphql_only: true
  #     end
  #   end
  def field(name, type, graphql_only: false, indexing_only: false, **options)
    if reserved_field_names.include?(name)
      raise Errors::SchemaError, "Invalid field name: `#{self.name}.#{name}`. `#{name}` is reserved for use by " \
        "ElasticGraph as a filtering operator. To use it for a field name, add " \
        "the `schema_element_name_overrides` option (on `ElasticGraph::SchemaDefinition::RakeTasks.new`) to " \
        "configure an alternate name for the `#{name}` operator."
    end

    options = {name_in_index: nil}.merge(options) if graphql_only

    field_factory.call(
      name: name,
      type: type,
      graphql_only: graphql_only,
      parent_type: wrapping_type,
      **options
    ) do |field|
      yield field if block_given?

      unless indexing_only
        register_field(field.name, field, graphql_fields_by_name, "GraphQL", :indexing_only)
      end

      unless graphql_only
        register_field(field.name_in_index, field, indexing_fields_by_name_in_index, "indexing", :graphql_only) do |f|
          f.to_indexing_field_reference
        end
      end
    end
  end

  # Registers the name of a field that existed in a prior version of the schema but has been deleted.
  #
  # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
  #   or {Field#renamed_from}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
  #   the call to this method can be removed.
  #
  # @param field_name [String] name of field that used to exist but has been deleted
  # @return [void]
  #
  # @example Indicate that `Widget.description` has been deleted
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Widget" do |t|
  #       t.deleted_field "description"
  #     end
  #   end
  def deleted_field(field_name)
    schema_def_state.register_deleted_field(
      name,
      field_name,
      defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
      defined_via: %(type.deleted_field "#{field_name}")
    )
  end

  # Registers an old name that this type used to have in a prior version of the schema.
  #
  # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
  #   or {API#deleted_type}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
  #   the call to this method can be removed.
  #
  # @param old_name [String] old name this field used to have in a prior version of the schema
  # @return [void]
  #
  # @example Indicate that `Widget` used to be called `Component`.
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Widget" do |t|
  #       t.renamed_from "Component"
  #     end
  #   end
  def renamed_from(old_name)
    schema_def_state.register_renamed_type(
      name,
      from: old_name,
      defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
      defined_via: %(type.renamed_from "#{old_name}")
    )
  end

  # An alternative to {#field} for when you have a list field that you want exposed as a [paginated Relay
  # connection](https://relay.dev/graphql/connections.htm) rather than as a simple list.
  #
  # @note Bear in mind that pagination does not have much efficiency benefit in this case: all elements of the collection will be
  #   retrieved when fetching this field from the datastore. The pagination implementation will just trim down the collection before
  #   returning it.
  #
  # @param name [String] name of the field
  # @param element_type [String] name of the type of element in the collection
  # @param name_in_index [String] the name of the field in the datastore index. Can be used to back a GraphQL field with a
  #   differently named field in the index.
  # @param singular [String] indicates what the singular form of a field's name is. When provided, ElasticGraph will define a
  #   `groupedBy` field (using the singular form) allowing clients to group by individual values from the field.
  # @yield [Field] the field for further customization
  # @return [void]
  #
  # @see #field
  # @see #relates_to_many
  # @see #relates_to_one
  #
  # @example Define `Author.books` as a paginated collection field
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Author" do |t|
  #       t.field "id", "ID"
  #       t.field "name", "String"
  #       t.paginated_collection_field "books", "String"
  #       t.index "authors"
  #     end
  #   end
  def paginated_collection_field(name, element_type, name_in_index: name, singular: nil, &block)
    element_type_ref = schema_def_state.type_ref(element_type).to_final_form
    element_type = element_type_ref.name

    schema_def_state.paginated_collection_element_types << element_type

    backing_indexing_field = field(name, "[#{element_type}!]!", indexing_only: true, name_in_index: name_in_index, &block)

    field(
      name,
      element_type_ref.as_connection.name,
      name_in_index: name_in_index,
      type_for_derived_types: "[#{element_type}]",
      groupable: !!singular,
      sortable: false,
      graphql_only: true,
      singular: singular,
      backing_indexing_field: backing_indexing_field
    ) do |f|
      f.define_relay_pagination_arguments!
      block&.call(f)
    end
  end

  # Defines a "has one" relationship between the current indexed type and another indexed type by defining a field clients
  # can use to navigate across indexed types in a single GraphQL query.
  #
  # @param field_name [String] name of the relationship field
  # @param type [String] name of the related type
  # @param via [String] name of the foreign key field
  # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
  #   references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type  and references the `id` of
  #   the related type.
  # @yield [Relationship] the generated relationship fields, for further customization
  # @return [void]
  #
  # @see #field
  # @see #relates_to_many
  #
  # @example Use `relates_to_one` to define `Player.team`
  #  ElasticGraph.define_schema do |schema|
  #    schema.object_type "Team" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "homeCity", "String"
  #      t.index "teams"
  #    end
  #
  #    schema.object_type "Player" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.relates_to_one "team", "Team", via: "teamId", dir: :out
  #      t.index "players"
  #    end
  #  end
  def relates_to_one(field_name, type, via:, dir:, &block)
    foreign_key_type = schema_def_state.type_ref(type).non_null? ? "ID!" : "ID"
    relates_to(field_name, type, via: via, dir: dir, foreign_key_type: foreign_key_type, cardinality: :one, related_type: type, &block)
  end

  # Defines a "has many" relationship between the current indexed type and another indexed type by defining a pair of fields clients
  # can use to navigate across indexed types in a single GraphQL query. The pair of generated fields will be [Relay Connection
  # types](https://relay.dev/graphql/connections.htm#sec-Connection-Types) allowing you to filter, sort, paginate, and aggregated the
  # related data.
  #
  # @param field_name [String] name of the relationship field
  # @param type [String] name of the related type
  # @param via [String] name of the foreign key field
  # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
  #   references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type  and references the `id` of
  #   the related type.
  # @param singular [String] singular form of the `field_name`; will be used (along with an `Aggregations` suffix) for the name of
  #   the generated aggregations field
  # @yield [Relationship] the generated relationship fields, for further customization
  # @return [void]
  #
  # @see #field
  # @see #paginated_collection_field
  # @see #relates_to_one
  #
  # @example Use `relates_to_many` to define `Team.players` and `Team.playerAggregations`
  #  ElasticGraph.define_schema do |schema|
  #    schema.object_type "Team" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "homeCity", "String"
  #      t.relates_to_many "players", "Player", via: "teamId", dir: :in, singular: "player"
  #      t.index "teams"
  #    end
  #
  #    schema.object_type "Player" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "teamId", "ID"
  #      t.index "players"
  #    end
  #  end
  def relates_to_many(field_name, type, via:, dir:, singular:)
    foreign_key_type = (dir == :out) ? "[ID!]!" : "ID"
    type_ref = schema_def_state.type_ref(type).to_final_form

    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|
      f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
        a.documentation "Used to filter the returned `#{field_name}` based on the provided criteria."
      end

      f.argument schema_def_state.schema_elements.order_by, "[#{type_ref.as_sort_order.name}!]" do |a|
        a.documentation "Used to specify how the returned `#{field_name}` should be sorted."
      end

      f.define_relay_pagination_arguments!

      yield f if block_given?
    end

    aggregations_name = schema_def_state.schema_elements.normalize_case("#{singular}_aggregations")
    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|
      f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
        a.documentation "Used to filter the `#{type}` documents that get aggregated over based on the provided criteria."
      end

      f.define_relay_pagination_arguments!

      yield f if block_given?

      f.documentation f.derived_documentation("Aggregations over the `#{field_name}` data")
    end
  end

  # Converts the type to GraphQL SDL syntax.
  #
  # @private
  def to_sdl(&field_arg_selector)
    generate_sdl(name_section: name, &field_arg_selector)
  end

  # @private
  def generate_sdl(name_section:, &field_arg_selector)
    <<~SDL
      #{formatted_documentation}#{schema_kind} #{name_section} #{directives_sdl(suffix_with: " ")}{
        #{fields_sdl(&field_arg_selector)}
      }
    SDL
  end

  # @private
  def aggregated_values_type
    schema_def_state.type_ref("NonNumeric").as_aggregated_values
  end

  # @private
  def indexed?
    false
  end

  # @private
  def to_indexing_field_type
    Indexing::FieldType::Object.new(
      type_name: name,
      subfields: indexing_fields_by_name_in_index.values.map(&:to_indexing_field).compact,
      mapping_options: mapping_options,
      json_schema_options: json_schema_options
    )
  end

  # @private
  def current_sources
    indexing_fields_by_name_in_index.values.flat_map do |field|
      child_field_sources = field.type.fully_unwrapped.as_object_type&.current_sources || []
      [field.source&.relationship_name || SELF_RELATIONSHIP_NAME] + child_field_sources
    end
  end

  # @private
  def (
    # path from the overall document root
    path_prefix: "",
    # the source of the parent field
    parent_source: SELF_RELATIONSHIP_NAME,
    # tracks the state of the list counts field
    list_counts_state: ListCountsState::INITIAL
  )
    indexing_fields_by_name_in_index.flat_map do |name, field|
      path = path_prefix + name
      source = field.source&.relationship_name || parent_source
      index_field = SchemaArtifacts::RuntimeMetadata::IndexField.new(source: source)

      list_count_field_tuples = field.paths_to_lists_for_count_indexing.map do |subpath|
        [list_counts_state.path_to_count_subfield(subpath), index_field] # : [::String, SchemaArtifacts::RuntimeMetadata::IndexField]
      end

      if (object_type = field.type.fully_unwrapped.as_object_type)
        new_list_counts_state =
          if field.type.list? && field.nested?
            ListCountsState.new_list_counts_field(at: "#{path}.#{LIST_COUNTS_FIELD}")
          else
            list_counts_state[name]
          end

        object_type.(
          path_prefix: "#{path}.",
          parent_source: source,
          list_counts_state: new_list_counts_state
        )
      else
        [[path, index_field]] # : ::Array[[::String, SchemaArtifacts::RuntimeMetadata::IndexField]]
      end + list_count_field_tuples
    end
  end

  private

  def fields_sdl(&arg_selector)
    graphql_fields_by_name.values
      .map { |f| f.to_sdl(&arg_selector) }
      .flat_map { |sdl| sdl.split("\n") }
      .join("\n  ")
  end

  def register_field(name, field, registry, registry_type, only_option_to_fix, &to_comparable)
    if (existing_field = registry[name])
      field = Field.pick_most_accurate_from(field, existing_field, to_comparable: to_comparable || ->(f) { f }) do
        raise Errors::SchemaError, "Duplicate #{registry_type} field on Type #{self.name}: #{name}. " \
          "To resolve this, set `#{only_option_to_fix}: true` on one of the fields."
      end
    end

    registry[name] = field
  end

  def relates_to(field_name, type, via:, dir:, foreign_key_type:, cardinality:, related_type:)
    field(field_name, type, sortable: false, filterable: false, groupable: false, graphql_only: true) do |field|
      relationship = schema_def_state.factory.new_relationship(
        field,
        cardinality: cardinality,
        related_type: schema_def_state.type_ref(related_type).to_final_form,
        foreign_key: via,
        direction: dir
      )

      yield relationship if block_given?

      field.relationship = relationship

      if dir == :out
        register_inferred_foreign_key_fields(from_type: [via, foreign_key_type], to_other: ["id", "ID!"], related_type: relationship.related_type)
      else
        register_inferred_foreign_key_fields(from_type: ["id", "ID!"], to_other: [via, foreign_key_type], related_type: relationship.related_type)
      end
    end
  end

  def register_inferred_foreign_key_fields(from_type:, to_other:, related_type:)
    # The root `Query` object shouldn't have inferred foreign key fields (it's not indexed).
    return if name.to_s == "Query"

    from_field_name, from_type_name = from_type
    field(from_field_name, from_type_name, indexing_only: true, accuracy_confidence: :medium)

    # If it's a self-referential, we also should add a foreign key field for the other end of the relation.
    if name == related_type.unwrap_non_null.name
      # This must be `:low` confidence for cases where we have a self-referential type that goes both
      # directions, such as:
      #
      # s.object_type "MyTypeBothDirections" do |t|
      #   t.relates_to_one "parent", "MyTypeBothDirections!", via: "children_ids", dir: :in
      #   t.relates_to_many "children", "MyTypeBothDirections", via: "children_ids", dir: :out
      # end
      #
      # In such a circumstance, the `from_type` side may be more accurate (and will be defined on the `field`
      # call above) and we want it preferred over this definition here.
      to_field_name, to_type_name = to_other
      field(to_field_name, to_type_name, indexing_only: true, accuracy_confidence: :low)
    end
  end
end

#relay_pagination_typeObject



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 51

class TypeWithSubfields < Struct.new(
  :schema_kind, :schema_def_state, :type_ref, :reserved_field_names,
  :graphql_fields_by_name, :indexing_fields_by_name_in_index, :field_factory,
  :wrapping_type, :relay_pagination_type
)
  prepend Mixins::VerifiesGraphQLName
  include Mixins::CanBeGraphQLOnly
  include Mixins::HasDocumentation
  include Mixins::HasDirectives
  include Mixins::HasDerivedGraphQLTypeCustomizations
  include Mixins::HasTypeInfo

  # The following methods are provided by `Struct.new`:
  # @dynamic type_ref

  # The following methods are provided by `SupportsFilteringAndAggregation`:
  # @dynamic derived_graphql_types

  # The following methods are provided by `CanBeGraphQLOnly`:
  # @dynamic graphql_only?

  # @private
  def initialize(schema_kind, schema_def_state, name, wrapping_type:, field_factory:)
    # `any_satisfy`, `any_of`/`all_of`, and `not` are "reserved" field names. They are reserved for usage by
    # ElasticGraph itself in the `*FilterInput` types it generates. If we allow them to be used as field
    # names, we'll run into conflicts when we later generate the `*FilterInput` type.
    #
    # Note that we don't have the same kind of conflict for the other filtering operators (e.g.
    # `equal_to_any_of`, `gt`, etc) because on the generated filter structure, those are leaf
    # nodes. They never exist alongside document field names on a filter type, but these do,
    # so we have to guard against them here.
    reserved_field_names = [
      schema_def_state.schema_elements.all_of,
      schema_def_state.schema_elements.any_of,
      schema_def_state.schema_elements.any_satisfy,
      schema_def_state.schema_elements.not
    ].to_set

    # @type var graphql_fields_by_name: ::Hash[::String, Field]
    graphql_fields_by_name = {}
    # @type var indexing_fields_by_name_in_index: ::Hash[::String, Field]
    indexing_fields_by_name_in_index = {}

    super(
      schema_kind,
      schema_def_state,
      schema_def_state.type_ref(name).to_final_form,
      reserved_field_names,
      graphql_fields_by_name,
      indexing_fields_by_name_in_index,
      field_factory,
      wrapping_type,
      false
    )

    yield self
  end

  # @return [String] the name of this GraphQL type
  def name
    type_ref.name
  end

  # Defines a [GraphQL field](https://spec.graphql.org/October2021/#sec-Language.Fields) on this type.
  #
  # @param name [String] name of the field
  # @param type [String] type of the field as a [type reference](https://spec.graphql.org/October2021/#sec-Type-References). The named type must be
  #   one of {BuiltInTypes ElasticGraph's built-in types} or a type that has been defined in your schema.
  # @param graphql_only [Boolean] if `true`, ElasticGraph will define the field as a GraphQL field but omit it from the indexing
  #   artifacts (`json_schemas.yaml` and `datastore_config.yaml`). This can be used along with `name_in_index` to support careful
  #   schema evolution.
  # @param indexing_only [Boolean] if `true`, ElasticGraph will define the field for indexing (in the `json_schemas.yaml` and
  #   `datastore_config.yaml` schema artifact) but will omit it from the GraphQL schema. This can be useful to begin indexing a field
  #   before you expose it in GraphQL so that you can fully backfill it first.
  # @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
  #   differently named field in the index.
  # @option options [String] singular can be used on a list field (e.g. `t.field "tags", "[String!]!", singular: "tag"`) to tell
  #   ElasticGraph what the singular form of a field's name is. When provided, ElasticGraph will define a `groupedBy` field (using the
  #   singular form) allowing clients to group by individual values from the field.
  # @option options [Boolean] aggregatable force-enables or disables the ability for aggregation queries to aggregate over this field.
  #   When not provided, ElasticGraph will infer field aggregatability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] filterable force-enables or disables the ability for queries to filter by this field. When not provided,
  #   ElasticGraph will infer field filterability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] groupable force-enables or disables the ability for aggregation queries to group by this field. When
  #   not provided, ElasticGraph will infer field groupability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] sortable force-enables or disables the ability for queries to sort by this field. When not provided,
  #   ElasticGraph will infer field sortability based on the field's GraphQL type and mapping type.
  # @yield [Field] the field for further customization
  # @return [void]
  #
  # @see #paginated_collection_field
  # @see #relates_to_many
  # @see #relates_to_one
  #
  # @note Be careful about defining non-nullable fields. Changing a field’s type from non-nullable (e.g. `Int!`) to nullable (e.g.
  #   `Int`) is a breaking change for clients. Making a field non-nullable may also prevent you from applying permissioning to a field
  #   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).
  #   Therefore, we recommend limiting your use of `!` to only a few situations such as defining a type’s primary key (e.g.
  #   `t.field "id", "ID!"`) or defining a list field (e.g. `t.field "authors", "[String!]!"`) since empty lists already provide a
  #   "no data" representation. You can still configure the ElasticGraph indexer to require a non-null value for a field using
  #   `f.json_schema nullable: false`.
  #
  # @note ElasticGraph’s understanding of datastore capabilities may override your configured
  #   `aggregatable`/`filterable`/`groupable`/`sortable` options. For example, a field indexed as `text` for full text search will
  #   not be sortable or groupable even if you pass `sortable: true, groupable: true` when defining the field, because [text fields
  #   cannot be efficiently sorted by or grouped on](https://www.elastic.co/guide/en/elasticsearch/reference/8.15/text.html#text).
  #
  # @example Define a field with documentation
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID" do |f|
  #         f.documentation "The Campaign's identifier."
  #       end
  #     end
  #   end
  #
  # @example Omit a new field from the GraphQL schema until its data has been backfilled
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID"
  #
  #       # TODO: remove `indexing_only: true` once the data for this field has been fully backfilled
  #       t.field "endDate", "Date", indexing_only: true
  #     end
  #   end
  #
  # @example Use `graphql_only` to introduce a new name for an existing field
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID"
  #
  #       t.field "endOn", "Date" do |f|
  #         f.directive "deprecated", reason: "Use `endDate` instead."
  #       end
  #
  #       # We've decided we want to call the field `endDate` instead of `endOn`, but the data
  #       # for this field is currently indexed in `endOn`, so we can use `graphql_only` and
  #       # `name_in_index` to expose the existing data under a new field name.
  #       t.field "endDate", "Date", name_in_index: "endOn", graphql_only: true
  #     end
  #   end
  def field(name, type, graphql_only: false, indexing_only: false, **options)
    if reserved_field_names.include?(name)
      raise Errors::SchemaError, "Invalid field name: `#{self.name}.#{name}`. `#{name}` is reserved for use by " \
        "ElasticGraph as a filtering operator. To use it for a field name, add " \
        "the `schema_element_name_overrides` option (on `ElasticGraph::SchemaDefinition::RakeTasks.new`) to " \
        "configure an alternate name for the `#{name}` operator."
    end

    options = {name_in_index: nil}.merge(options) if graphql_only

    field_factory.call(
      name: name,
      type: type,
      graphql_only: graphql_only,
      parent_type: wrapping_type,
      **options
    ) do |field|
      yield field if block_given?

      unless indexing_only
        register_field(field.name, field, graphql_fields_by_name, "GraphQL", :indexing_only)
      end

      unless graphql_only
        register_field(field.name_in_index, field, indexing_fields_by_name_in_index, "indexing", :graphql_only) do |f|
          f.to_indexing_field_reference
        end
      end
    end
  end

  # Registers the name of a field that existed in a prior version of the schema but has been deleted.
  #
  # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
  #   or {Field#renamed_from}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
  #   the call to this method can be removed.
  #
  # @param field_name [String] name of field that used to exist but has been deleted
  # @return [void]
  #
  # @example Indicate that `Widget.description` has been deleted
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Widget" do |t|
  #       t.deleted_field "description"
  #     end
  #   end
  def deleted_field(field_name)
    schema_def_state.register_deleted_field(
      name,
      field_name,
      defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
      defined_via: %(type.deleted_field "#{field_name}")
    )
  end

  # Registers an old name that this type used to have in a prior version of the schema.
  #
  # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
  #   or {API#deleted_type}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
  #   the call to this method can be removed.
  #
  # @param old_name [String] old name this field used to have in a prior version of the schema
  # @return [void]
  #
  # @example Indicate that `Widget` used to be called `Component`.
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Widget" do |t|
  #       t.renamed_from "Component"
  #     end
  #   end
  def renamed_from(old_name)
    schema_def_state.register_renamed_type(
      name,
      from: old_name,
      defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
      defined_via: %(type.renamed_from "#{old_name}")
    )
  end

  # An alternative to {#field} for when you have a list field that you want exposed as a [paginated Relay
  # connection](https://relay.dev/graphql/connections.htm) rather than as a simple list.
  #
  # @note Bear in mind that pagination does not have much efficiency benefit in this case: all elements of the collection will be
  #   retrieved when fetching this field from the datastore. The pagination implementation will just trim down the collection before
  #   returning it.
  #
  # @param name [String] name of the field
  # @param element_type [String] name of the type of element in the collection
  # @param name_in_index [String] the name of the field in the datastore index. Can be used to back a GraphQL field with a
  #   differently named field in the index.
  # @param singular [String] indicates what the singular form of a field's name is. When provided, ElasticGraph will define a
  #   `groupedBy` field (using the singular form) allowing clients to group by individual values from the field.
  # @yield [Field] the field for further customization
  # @return [void]
  #
  # @see #field
  # @see #relates_to_many
  # @see #relates_to_one
  #
  # @example Define `Author.books` as a paginated collection field
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Author" do |t|
  #       t.field "id", "ID"
  #       t.field "name", "String"
  #       t.paginated_collection_field "books", "String"
  #       t.index "authors"
  #     end
  #   end
  def paginated_collection_field(name, element_type, name_in_index: name, singular: nil, &block)
    element_type_ref = schema_def_state.type_ref(element_type).to_final_form
    element_type = element_type_ref.name

    schema_def_state.paginated_collection_element_types << element_type

    backing_indexing_field = field(name, "[#{element_type}!]!", indexing_only: true, name_in_index: name_in_index, &block)

    field(
      name,
      element_type_ref.as_connection.name,
      name_in_index: name_in_index,
      type_for_derived_types: "[#{element_type}]",
      groupable: !!singular,
      sortable: false,
      graphql_only: true,
      singular: singular,
      backing_indexing_field: backing_indexing_field
    ) do |f|
      f.define_relay_pagination_arguments!
      block&.call(f)
    end
  end

  # Defines a "has one" relationship between the current indexed type and another indexed type by defining a field clients
  # can use to navigate across indexed types in a single GraphQL query.
  #
  # @param field_name [String] name of the relationship field
  # @param type [String] name of the related type
  # @param via [String] name of the foreign key field
  # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
  #   references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type  and references the `id` of
  #   the related type.
  # @yield [Relationship] the generated relationship fields, for further customization
  # @return [void]
  #
  # @see #field
  # @see #relates_to_many
  #
  # @example Use `relates_to_one` to define `Player.team`
  #  ElasticGraph.define_schema do |schema|
  #    schema.object_type "Team" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "homeCity", "String"
  #      t.index "teams"
  #    end
  #
  #    schema.object_type "Player" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.relates_to_one "team", "Team", via: "teamId", dir: :out
  #      t.index "players"
  #    end
  #  end
  def relates_to_one(field_name, type, via:, dir:, &block)
    foreign_key_type = schema_def_state.type_ref(type).non_null? ? "ID!" : "ID"
    relates_to(field_name, type, via: via, dir: dir, foreign_key_type: foreign_key_type, cardinality: :one, related_type: type, &block)
  end

  # Defines a "has many" relationship between the current indexed type and another indexed type by defining a pair of fields clients
  # can use to navigate across indexed types in a single GraphQL query. The pair of generated fields will be [Relay Connection
  # types](https://relay.dev/graphql/connections.htm#sec-Connection-Types) allowing you to filter, sort, paginate, and aggregated the
  # related data.
  #
  # @param field_name [String] name of the relationship field
  # @param type [String] name of the related type
  # @param via [String] name of the foreign key field
  # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
  #   references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type  and references the `id` of
  #   the related type.
  # @param singular [String] singular form of the `field_name`; will be used (along with an `Aggregations` suffix) for the name of
  #   the generated aggregations field
  # @yield [Relationship] the generated relationship fields, for further customization
  # @return [void]
  #
  # @see #field
  # @see #paginated_collection_field
  # @see #relates_to_one
  #
  # @example Use `relates_to_many` to define `Team.players` and `Team.playerAggregations`
  #  ElasticGraph.define_schema do |schema|
  #    schema.object_type "Team" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "homeCity", "String"
  #      t.relates_to_many "players", "Player", via: "teamId", dir: :in, singular: "player"
  #      t.index "teams"
  #    end
  #
  #    schema.object_type "Player" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "teamId", "ID"
  #      t.index "players"
  #    end
  #  end
  def relates_to_many(field_name, type, via:, dir:, singular:)
    foreign_key_type = (dir == :out) ? "[ID!]!" : "ID"
    type_ref = schema_def_state.type_ref(type).to_final_form

    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|
      f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
        a.documentation "Used to filter the returned `#{field_name}` based on the provided criteria."
      end

      f.argument schema_def_state.schema_elements.order_by, "[#{type_ref.as_sort_order.name}!]" do |a|
        a.documentation "Used to specify how the returned `#{field_name}` should be sorted."
      end

      f.define_relay_pagination_arguments!

      yield f if block_given?
    end

    aggregations_name = schema_def_state.schema_elements.normalize_case("#{singular}_aggregations")
    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|
      f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
        a.documentation "Used to filter the `#{type}` documents that get aggregated over based on the provided criteria."
      end

      f.define_relay_pagination_arguments!

      yield f if block_given?

      f.documentation f.derived_documentation("Aggregations over the `#{field_name}` data")
    end
  end

  # Converts the type to GraphQL SDL syntax.
  #
  # @private
  def to_sdl(&field_arg_selector)
    generate_sdl(name_section: name, &field_arg_selector)
  end

  # @private
  def generate_sdl(name_section:, &field_arg_selector)
    <<~SDL
      #{formatted_documentation}#{schema_kind} #{name_section} #{directives_sdl(suffix_with: " ")}{
        #{fields_sdl(&field_arg_selector)}
      }
    SDL
  end

  # @private
  def aggregated_values_type
    schema_def_state.type_ref("NonNumeric").as_aggregated_values
  end

  # @private
  def indexed?
    false
  end

  # @private
  def to_indexing_field_type
    Indexing::FieldType::Object.new(
      type_name: name,
      subfields: indexing_fields_by_name_in_index.values.map(&:to_indexing_field).compact,
      mapping_options: mapping_options,
      json_schema_options: json_schema_options
    )
  end

  # @private
  def current_sources
    indexing_fields_by_name_in_index.values.flat_map do |field|
      child_field_sources = field.type.fully_unwrapped.as_object_type&.current_sources || []
      [field.source&.relationship_name || SELF_RELATIONSHIP_NAME] + child_field_sources
    end
  end

  # @private
  def (
    # path from the overall document root
    path_prefix: "",
    # the source of the parent field
    parent_source: SELF_RELATIONSHIP_NAME,
    # tracks the state of the list counts field
    list_counts_state: ListCountsState::INITIAL
  )
    indexing_fields_by_name_in_index.flat_map do |name, field|
      path = path_prefix + name
      source = field.source&.relationship_name || parent_source
      index_field = SchemaArtifacts::RuntimeMetadata::IndexField.new(source: source)

      list_count_field_tuples = field.paths_to_lists_for_count_indexing.map do |subpath|
        [list_counts_state.path_to_count_subfield(subpath), index_field] # : [::String, SchemaArtifacts::RuntimeMetadata::IndexField]
      end

      if (object_type = field.type.fully_unwrapped.as_object_type)
        new_list_counts_state =
          if field.type.list? && field.nested?
            ListCountsState.new_list_counts_field(at: "#{path}.#{LIST_COUNTS_FIELD}")
          else
            list_counts_state[name]
          end

        object_type.(
          path_prefix: "#{path}.",
          parent_source: source,
          list_counts_state: new_list_counts_state
        )
      else
        [[path, index_field]] # : ::Array[[::String, SchemaArtifacts::RuntimeMetadata::IndexField]]
      end + list_count_field_tuples
    end
  end

  private

  def fields_sdl(&arg_selector)
    graphql_fields_by_name.values
      .map { |f| f.to_sdl(&arg_selector) }
      .flat_map { |sdl| sdl.split("\n") }
      .join("\n  ")
  end

  def register_field(name, field, registry, registry_type, only_option_to_fix, &to_comparable)
    if (existing_field = registry[name])
      field = Field.pick_most_accurate_from(field, existing_field, to_comparable: to_comparable || ->(f) { f }) do
        raise Errors::SchemaError, "Duplicate #{registry_type} field on Type #{self.name}: #{name}. " \
          "To resolve this, set `#{only_option_to_fix}: true` on one of the fields."
      end
    end

    registry[name] = field
  end

  def relates_to(field_name, type, via:, dir:, foreign_key_type:, cardinality:, related_type:)
    field(field_name, type, sortable: false, filterable: false, groupable: false, graphql_only: true) do |field|
      relationship = schema_def_state.factory.new_relationship(
        field,
        cardinality: cardinality,
        related_type: schema_def_state.type_ref(related_type).to_final_form,
        foreign_key: via,
        direction: dir
      )

      yield relationship if block_given?

      field.relationship = relationship

      if dir == :out
        register_inferred_foreign_key_fields(from_type: [via, foreign_key_type], to_other: ["id", "ID!"], related_type: relationship.related_type)
      else
        register_inferred_foreign_key_fields(from_type: ["id", "ID!"], to_other: [via, foreign_key_type], related_type: relationship.related_type)
      end
    end
  end

  def register_inferred_foreign_key_fields(from_type:, to_other:, related_type:)
    # The root `Query` object shouldn't have inferred foreign key fields (it's not indexed).
    return if name.to_s == "Query"

    from_field_name, from_type_name = from_type
    field(from_field_name, from_type_name, indexing_only: true, accuracy_confidence: :medium)

    # If it's a self-referential, we also should add a foreign key field for the other end of the relation.
    if name == related_type.unwrap_non_null.name
      # This must be `:low` confidence for cases where we have a self-referential type that goes both
      # directions, such as:
      #
      # s.object_type "MyTypeBothDirections" do |t|
      #   t.relates_to_one "parent", "MyTypeBothDirections!", via: "children_ids", dir: :in
      #   t.relates_to_many "children", "MyTypeBothDirections", via: "children_ids", dir: :out
      # end
      #
      # In such a circumstance, the `from_type` side may be more accurate (and will be defined on the `field`
      # call above) and we want it preferred over this definition here.
      to_field_name, to_type_name = to_other
      field(to_field_name, to_type_name, indexing_only: true, accuracy_confidence: :low)
    end
  end
end

#reserved_field_namesObject



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 51

class TypeWithSubfields < Struct.new(
  :schema_kind, :schema_def_state, :type_ref, :reserved_field_names,
  :graphql_fields_by_name, :indexing_fields_by_name_in_index, :field_factory,
  :wrapping_type, :relay_pagination_type
)
  prepend Mixins::VerifiesGraphQLName
  include Mixins::CanBeGraphQLOnly
  include Mixins::HasDocumentation
  include Mixins::HasDirectives
  include Mixins::HasDerivedGraphQLTypeCustomizations
  include Mixins::HasTypeInfo

  # The following methods are provided by `Struct.new`:
  # @dynamic type_ref

  # The following methods are provided by `SupportsFilteringAndAggregation`:
  # @dynamic derived_graphql_types

  # The following methods are provided by `CanBeGraphQLOnly`:
  # @dynamic graphql_only?

  # @private
  def initialize(schema_kind, schema_def_state, name, wrapping_type:, field_factory:)
    # `any_satisfy`, `any_of`/`all_of`, and `not` are "reserved" field names. They are reserved for usage by
    # ElasticGraph itself in the `*FilterInput` types it generates. If we allow them to be used as field
    # names, we'll run into conflicts when we later generate the `*FilterInput` type.
    #
    # Note that we don't have the same kind of conflict for the other filtering operators (e.g.
    # `equal_to_any_of`, `gt`, etc) because on the generated filter structure, those are leaf
    # nodes. They never exist alongside document field names on a filter type, but these do,
    # so we have to guard against them here.
    reserved_field_names = [
      schema_def_state.schema_elements.all_of,
      schema_def_state.schema_elements.any_of,
      schema_def_state.schema_elements.any_satisfy,
      schema_def_state.schema_elements.not
    ].to_set

    # @type var graphql_fields_by_name: ::Hash[::String, Field]
    graphql_fields_by_name = {}
    # @type var indexing_fields_by_name_in_index: ::Hash[::String, Field]
    indexing_fields_by_name_in_index = {}

    super(
      schema_kind,
      schema_def_state,
      schema_def_state.type_ref(name).to_final_form,
      reserved_field_names,
      graphql_fields_by_name,
      indexing_fields_by_name_in_index,
      field_factory,
      wrapping_type,
      false
    )

    yield self
  end

  # @return [String] the name of this GraphQL type
  def name
    type_ref.name
  end

  # Defines a [GraphQL field](https://spec.graphql.org/October2021/#sec-Language.Fields) on this type.
  #
  # @param name [String] name of the field
  # @param type [String] type of the field as a [type reference](https://spec.graphql.org/October2021/#sec-Type-References). The named type must be
  #   one of {BuiltInTypes ElasticGraph's built-in types} or a type that has been defined in your schema.
  # @param graphql_only [Boolean] if `true`, ElasticGraph will define the field as a GraphQL field but omit it from the indexing
  #   artifacts (`json_schemas.yaml` and `datastore_config.yaml`). This can be used along with `name_in_index` to support careful
  #   schema evolution.
  # @param indexing_only [Boolean] if `true`, ElasticGraph will define the field for indexing (in the `json_schemas.yaml` and
  #   `datastore_config.yaml` schema artifact) but will omit it from the GraphQL schema. This can be useful to begin indexing a field
  #   before you expose it in GraphQL so that you can fully backfill it first.
  # @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
  #   differently named field in the index.
  # @option options [String] singular can be used on a list field (e.g. `t.field "tags", "[String!]!", singular: "tag"`) to tell
  #   ElasticGraph what the singular form of a field's name is. When provided, ElasticGraph will define a `groupedBy` field (using the
  #   singular form) allowing clients to group by individual values from the field.
  # @option options [Boolean] aggregatable force-enables or disables the ability for aggregation queries to aggregate over this field.
  #   When not provided, ElasticGraph will infer field aggregatability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] filterable force-enables or disables the ability for queries to filter by this field. When not provided,
  #   ElasticGraph will infer field filterability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] groupable force-enables or disables the ability for aggregation queries to group by this field. When
  #   not provided, ElasticGraph will infer field groupability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] sortable force-enables or disables the ability for queries to sort by this field. When not provided,
  #   ElasticGraph will infer field sortability based on the field's GraphQL type and mapping type.
  # @yield [Field] the field for further customization
  # @return [void]
  #
  # @see #paginated_collection_field
  # @see #relates_to_many
  # @see #relates_to_one
  #
  # @note Be careful about defining non-nullable fields. Changing a field’s type from non-nullable (e.g. `Int!`) to nullable (e.g.
  #   `Int`) is a breaking change for clients. Making a field non-nullable may also prevent you from applying permissioning to a field
  #   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).
  #   Therefore, we recommend limiting your use of `!` to only a few situations such as defining a type’s primary key (e.g.
  #   `t.field "id", "ID!"`) or defining a list field (e.g. `t.field "authors", "[String!]!"`) since empty lists already provide a
  #   "no data" representation. You can still configure the ElasticGraph indexer to require a non-null value for a field using
  #   `f.json_schema nullable: false`.
  #
  # @note ElasticGraph’s understanding of datastore capabilities may override your configured
  #   `aggregatable`/`filterable`/`groupable`/`sortable` options. For example, a field indexed as `text` for full text search will
  #   not be sortable or groupable even if you pass `sortable: true, groupable: true` when defining the field, because [text fields
  #   cannot be efficiently sorted by or grouped on](https://www.elastic.co/guide/en/elasticsearch/reference/8.15/text.html#text).
  #
  # @example Define a field with documentation
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID" do |f|
  #         f.documentation "The Campaign's identifier."
  #       end
  #     end
  #   end
  #
  # @example Omit a new field from the GraphQL schema until its data has been backfilled
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID"
  #
  #       # TODO: remove `indexing_only: true` once the data for this field has been fully backfilled
  #       t.field "endDate", "Date", indexing_only: true
  #     end
  #   end
  #
  # @example Use `graphql_only` to introduce a new name for an existing field
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID"
  #
  #       t.field "endOn", "Date" do |f|
  #         f.directive "deprecated", reason: "Use `endDate` instead."
  #       end
  #
  #       # We've decided we want to call the field `endDate` instead of `endOn`, but the data
  #       # for this field is currently indexed in `endOn`, so we can use `graphql_only` and
  #       # `name_in_index` to expose the existing data under a new field name.
  #       t.field "endDate", "Date", name_in_index: "endOn", graphql_only: true
  #     end
  #   end
  def field(name, type, graphql_only: false, indexing_only: false, **options)
    if reserved_field_names.include?(name)
      raise Errors::SchemaError, "Invalid field name: `#{self.name}.#{name}`. `#{name}` is reserved for use by " \
        "ElasticGraph as a filtering operator. To use it for a field name, add " \
        "the `schema_element_name_overrides` option (on `ElasticGraph::SchemaDefinition::RakeTasks.new`) to " \
        "configure an alternate name for the `#{name}` operator."
    end

    options = {name_in_index: nil}.merge(options) if graphql_only

    field_factory.call(
      name: name,
      type: type,
      graphql_only: graphql_only,
      parent_type: wrapping_type,
      **options
    ) do |field|
      yield field if block_given?

      unless indexing_only
        register_field(field.name, field, graphql_fields_by_name, "GraphQL", :indexing_only)
      end

      unless graphql_only
        register_field(field.name_in_index, field, indexing_fields_by_name_in_index, "indexing", :graphql_only) do |f|
          f.to_indexing_field_reference
        end
      end
    end
  end

  # Registers the name of a field that existed in a prior version of the schema but has been deleted.
  #
  # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
  #   or {Field#renamed_from}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
  #   the call to this method can be removed.
  #
  # @param field_name [String] name of field that used to exist but has been deleted
  # @return [void]
  #
  # @example Indicate that `Widget.description` has been deleted
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Widget" do |t|
  #       t.deleted_field "description"
  #     end
  #   end
  def deleted_field(field_name)
    schema_def_state.register_deleted_field(
      name,
      field_name,
      defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
      defined_via: %(type.deleted_field "#{field_name}")
    )
  end

  # Registers an old name that this type used to have in a prior version of the schema.
  #
  # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
  #   or {API#deleted_type}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
  #   the call to this method can be removed.
  #
  # @param old_name [String] old name this field used to have in a prior version of the schema
  # @return [void]
  #
  # @example Indicate that `Widget` used to be called `Component`.
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Widget" do |t|
  #       t.renamed_from "Component"
  #     end
  #   end
  def renamed_from(old_name)
    schema_def_state.register_renamed_type(
      name,
      from: old_name,
      defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
      defined_via: %(type.renamed_from "#{old_name}")
    )
  end

  # An alternative to {#field} for when you have a list field that you want exposed as a [paginated Relay
  # connection](https://relay.dev/graphql/connections.htm) rather than as a simple list.
  #
  # @note Bear in mind that pagination does not have much efficiency benefit in this case: all elements of the collection will be
  #   retrieved when fetching this field from the datastore. The pagination implementation will just trim down the collection before
  #   returning it.
  #
  # @param name [String] name of the field
  # @param element_type [String] name of the type of element in the collection
  # @param name_in_index [String] the name of the field in the datastore index. Can be used to back a GraphQL field with a
  #   differently named field in the index.
  # @param singular [String] indicates what the singular form of a field's name is. When provided, ElasticGraph will define a
  #   `groupedBy` field (using the singular form) allowing clients to group by individual values from the field.
  # @yield [Field] the field for further customization
  # @return [void]
  #
  # @see #field
  # @see #relates_to_many
  # @see #relates_to_one
  #
  # @example Define `Author.books` as a paginated collection field
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Author" do |t|
  #       t.field "id", "ID"
  #       t.field "name", "String"
  #       t.paginated_collection_field "books", "String"
  #       t.index "authors"
  #     end
  #   end
  def paginated_collection_field(name, element_type, name_in_index: name, singular: nil, &block)
    element_type_ref = schema_def_state.type_ref(element_type).to_final_form
    element_type = element_type_ref.name

    schema_def_state.paginated_collection_element_types << element_type

    backing_indexing_field = field(name, "[#{element_type}!]!", indexing_only: true, name_in_index: name_in_index, &block)

    field(
      name,
      element_type_ref.as_connection.name,
      name_in_index: name_in_index,
      type_for_derived_types: "[#{element_type}]",
      groupable: !!singular,
      sortable: false,
      graphql_only: true,
      singular: singular,
      backing_indexing_field: backing_indexing_field
    ) do |f|
      f.define_relay_pagination_arguments!
      block&.call(f)
    end
  end

  # Defines a "has one" relationship between the current indexed type and another indexed type by defining a field clients
  # can use to navigate across indexed types in a single GraphQL query.
  #
  # @param field_name [String] name of the relationship field
  # @param type [String] name of the related type
  # @param via [String] name of the foreign key field
  # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
  #   references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type  and references the `id` of
  #   the related type.
  # @yield [Relationship] the generated relationship fields, for further customization
  # @return [void]
  #
  # @see #field
  # @see #relates_to_many
  #
  # @example Use `relates_to_one` to define `Player.team`
  #  ElasticGraph.define_schema do |schema|
  #    schema.object_type "Team" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "homeCity", "String"
  #      t.index "teams"
  #    end
  #
  #    schema.object_type "Player" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.relates_to_one "team", "Team", via: "teamId", dir: :out
  #      t.index "players"
  #    end
  #  end
  def relates_to_one(field_name, type, via:, dir:, &block)
    foreign_key_type = schema_def_state.type_ref(type).non_null? ? "ID!" : "ID"
    relates_to(field_name, type, via: via, dir: dir, foreign_key_type: foreign_key_type, cardinality: :one, related_type: type, &block)
  end

  # Defines a "has many" relationship between the current indexed type and another indexed type by defining a pair of fields clients
  # can use to navigate across indexed types in a single GraphQL query. The pair of generated fields will be [Relay Connection
  # types](https://relay.dev/graphql/connections.htm#sec-Connection-Types) allowing you to filter, sort, paginate, and aggregated the
  # related data.
  #
  # @param field_name [String] name of the relationship field
  # @param type [String] name of the related type
  # @param via [String] name of the foreign key field
  # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
  #   references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type  and references the `id` of
  #   the related type.
  # @param singular [String] singular form of the `field_name`; will be used (along with an `Aggregations` suffix) for the name of
  #   the generated aggregations field
  # @yield [Relationship] the generated relationship fields, for further customization
  # @return [void]
  #
  # @see #field
  # @see #paginated_collection_field
  # @see #relates_to_one
  #
  # @example Use `relates_to_many` to define `Team.players` and `Team.playerAggregations`
  #  ElasticGraph.define_schema do |schema|
  #    schema.object_type "Team" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "homeCity", "String"
  #      t.relates_to_many "players", "Player", via: "teamId", dir: :in, singular: "player"
  #      t.index "teams"
  #    end
  #
  #    schema.object_type "Player" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "teamId", "ID"
  #      t.index "players"
  #    end
  #  end
  def relates_to_many(field_name, type, via:, dir:, singular:)
    foreign_key_type = (dir == :out) ? "[ID!]!" : "ID"
    type_ref = schema_def_state.type_ref(type).to_final_form

    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|
      f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
        a.documentation "Used to filter the returned `#{field_name}` based on the provided criteria."
      end

      f.argument schema_def_state.schema_elements.order_by, "[#{type_ref.as_sort_order.name}!]" do |a|
        a.documentation "Used to specify how the returned `#{field_name}` should be sorted."
      end

      f.define_relay_pagination_arguments!

      yield f if block_given?
    end

    aggregations_name = schema_def_state.schema_elements.normalize_case("#{singular}_aggregations")
    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|
      f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
        a.documentation "Used to filter the `#{type}` documents that get aggregated over based on the provided criteria."
      end

      f.define_relay_pagination_arguments!

      yield f if block_given?

      f.documentation f.derived_documentation("Aggregations over the `#{field_name}` data")
    end
  end

  # Converts the type to GraphQL SDL syntax.
  #
  # @private
  def to_sdl(&field_arg_selector)
    generate_sdl(name_section: name, &field_arg_selector)
  end

  # @private
  def generate_sdl(name_section:, &field_arg_selector)
    <<~SDL
      #{formatted_documentation}#{schema_kind} #{name_section} #{directives_sdl(suffix_with: " ")}{
        #{fields_sdl(&field_arg_selector)}
      }
    SDL
  end

  # @private
  def aggregated_values_type
    schema_def_state.type_ref("NonNumeric").as_aggregated_values
  end

  # @private
  def indexed?
    false
  end

  # @private
  def to_indexing_field_type
    Indexing::FieldType::Object.new(
      type_name: name,
      subfields: indexing_fields_by_name_in_index.values.map(&:to_indexing_field).compact,
      mapping_options: mapping_options,
      json_schema_options: json_schema_options
    )
  end

  # @private
  def current_sources
    indexing_fields_by_name_in_index.values.flat_map do |field|
      child_field_sources = field.type.fully_unwrapped.as_object_type&.current_sources || []
      [field.source&.relationship_name || SELF_RELATIONSHIP_NAME] + child_field_sources
    end
  end

  # @private
  def (
    # path from the overall document root
    path_prefix: "",
    # the source of the parent field
    parent_source: SELF_RELATIONSHIP_NAME,
    # tracks the state of the list counts field
    list_counts_state: ListCountsState::INITIAL
  )
    indexing_fields_by_name_in_index.flat_map do |name, field|
      path = path_prefix + name
      source = field.source&.relationship_name || parent_source
      index_field = SchemaArtifacts::RuntimeMetadata::IndexField.new(source: source)

      list_count_field_tuples = field.paths_to_lists_for_count_indexing.map do |subpath|
        [list_counts_state.path_to_count_subfield(subpath), index_field] # : [::String, SchemaArtifacts::RuntimeMetadata::IndexField]
      end

      if (object_type = field.type.fully_unwrapped.as_object_type)
        new_list_counts_state =
          if field.type.list? && field.nested?
            ListCountsState.new_list_counts_field(at: "#{path}.#{LIST_COUNTS_FIELD}")
          else
            list_counts_state[name]
          end

        object_type.(
          path_prefix: "#{path}.",
          parent_source: source,
          list_counts_state: new_list_counts_state
        )
      else
        [[path, index_field]] # : ::Array[[::String, SchemaArtifacts::RuntimeMetadata::IndexField]]
      end + list_count_field_tuples
    end
  end

  private

  def fields_sdl(&arg_selector)
    graphql_fields_by_name.values
      .map { |f| f.to_sdl(&arg_selector) }
      .flat_map { |sdl| sdl.split("\n") }
      .join("\n  ")
  end

  def register_field(name, field, registry, registry_type, only_option_to_fix, &to_comparable)
    if (existing_field = registry[name])
      field = Field.pick_most_accurate_from(field, existing_field, to_comparable: to_comparable || ->(f) { f }) do
        raise Errors::SchemaError, "Duplicate #{registry_type} field on Type #{self.name}: #{name}. " \
          "To resolve this, set `#{only_option_to_fix}: true` on one of the fields."
      end
    end

    registry[name] = field
  end

  def relates_to(field_name, type, via:, dir:, foreign_key_type:, cardinality:, related_type:)
    field(field_name, type, sortable: false, filterable: false, groupable: false, graphql_only: true) do |field|
      relationship = schema_def_state.factory.new_relationship(
        field,
        cardinality: cardinality,
        related_type: schema_def_state.type_ref(related_type).to_final_form,
        foreign_key: via,
        direction: dir
      )

      yield relationship if block_given?

      field.relationship = relationship

      if dir == :out
        register_inferred_foreign_key_fields(from_type: [via, foreign_key_type], to_other: ["id", "ID!"], related_type: relationship.related_type)
      else
        register_inferred_foreign_key_fields(from_type: ["id", "ID!"], to_other: [via, foreign_key_type], related_type: relationship.related_type)
      end
    end
  end

  def register_inferred_foreign_key_fields(from_type:, to_other:, related_type:)
    # The root `Query` object shouldn't have inferred foreign key fields (it's not indexed).
    return if name.to_s == "Query"

    from_field_name, from_type_name = from_type
    field(from_field_name, from_type_name, indexing_only: true, accuracy_confidence: :medium)

    # If it's a self-referential, we also should add a foreign key field for the other end of the relation.
    if name == related_type.unwrap_non_null.name
      # This must be `:low` confidence for cases where we have a self-referential type that goes both
      # directions, such as:
      #
      # s.object_type "MyTypeBothDirections" do |t|
      #   t.relates_to_one "parent", "MyTypeBothDirections!", via: "children_ids", dir: :in
      #   t.relates_to_many "children", "MyTypeBothDirections", via: "children_ids", dir: :out
      # end
      #
      # In such a circumstance, the `from_type` side may be more accurate (and will be defined on the `field`
      # call above) and we want it preferred over this definition here.
      to_field_name, to_type_name = to_other
      field(to_field_name, to_type_name, indexing_only: true, accuracy_confidence: :low)
    end
  end
end

#schema_def_stateObject



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 51

class TypeWithSubfields < Struct.new(
  :schema_kind, :schema_def_state, :type_ref, :reserved_field_names,
  :graphql_fields_by_name, :indexing_fields_by_name_in_index, :field_factory,
  :wrapping_type, :relay_pagination_type
)
  prepend Mixins::VerifiesGraphQLName
  include Mixins::CanBeGraphQLOnly
  include Mixins::HasDocumentation
  include Mixins::HasDirectives
  include Mixins::HasDerivedGraphQLTypeCustomizations
  include Mixins::HasTypeInfo

  # The following methods are provided by `Struct.new`:
  # @dynamic type_ref

  # The following methods are provided by `SupportsFilteringAndAggregation`:
  # @dynamic derived_graphql_types

  # The following methods are provided by `CanBeGraphQLOnly`:
  # @dynamic graphql_only?

  # @private
  def initialize(schema_kind, schema_def_state, name, wrapping_type:, field_factory:)
    # `any_satisfy`, `any_of`/`all_of`, and `not` are "reserved" field names. They are reserved for usage by
    # ElasticGraph itself in the `*FilterInput` types it generates. If we allow them to be used as field
    # names, we'll run into conflicts when we later generate the `*FilterInput` type.
    #
    # Note that we don't have the same kind of conflict for the other filtering operators (e.g.
    # `equal_to_any_of`, `gt`, etc) because on the generated filter structure, those are leaf
    # nodes. They never exist alongside document field names on a filter type, but these do,
    # so we have to guard against them here.
    reserved_field_names = [
      schema_def_state.schema_elements.all_of,
      schema_def_state.schema_elements.any_of,
      schema_def_state.schema_elements.any_satisfy,
      schema_def_state.schema_elements.not
    ].to_set

    # @type var graphql_fields_by_name: ::Hash[::String, Field]
    graphql_fields_by_name = {}
    # @type var indexing_fields_by_name_in_index: ::Hash[::String, Field]
    indexing_fields_by_name_in_index = {}

    super(
      schema_kind,
      schema_def_state,
      schema_def_state.type_ref(name).to_final_form,
      reserved_field_names,
      graphql_fields_by_name,
      indexing_fields_by_name_in_index,
      field_factory,
      wrapping_type,
      false
    )

    yield self
  end

  # @return [String] the name of this GraphQL type
  def name
    type_ref.name
  end

  # Defines a [GraphQL field](https://spec.graphql.org/October2021/#sec-Language.Fields) on this type.
  #
  # @param name [String] name of the field
  # @param type [String] type of the field as a [type reference](https://spec.graphql.org/October2021/#sec-Type-References). The named type must be
  #   one of {BuiltInTypes ElasticGraph's built-in types} or a type that has been defined in your schema.
  # @param graphql_only [Boolean] if `true`, ElasticGraph will define the field as a GraphQL field but omit it from the indexing
  #   artifacts (`json_schemas.yaml` and `datastore_config.yaml`). This can be used along with `name_in_index` to support careful
  #   schema evolution.
  # @param indexing_only [Boolean] if `true`, ElasticGraph will define the field for indexing (in the `json_schemas.yaml` and
  #   `datastore_config.yaml` schema artifact) but will omit it from the GraphQL schema. This can be useful to begin indexing a field
  #   before you expose it in GraphQL so that you can fully backfill it first.
  # @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
  #   differently named field in the index.
  # @option options [String] singular can be used on a list field (e.g. `t.field "tags", "[String!]!", singular: "tag"`) to tell
  #   ElasticGraph what the singular form of a field's name is. When provided, ElasticGraph will define a `groupedBy` field (using the
  #   singular form) allowing clients to group by individual values from the field.
  # @option options [Boolean] aggregatable force-enables or disables the ability for aggregation queries to aggregate over this field.
  #   When not provided, ElasticGraph will infer field aggregatability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] filterable force-enables or disables the ability for queries to filter by this field. When not provided,
  #   ElasticGraph will infer field filterability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] groupable force-enables or disables the ability for aggregation queries to group by this field. When
  #   not provided, ElasticGraph will infer field groupability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] sortable force-enables or disables the ability for queries to sort by this field. When not provided,
  #   ElasticGraph will infer field sortability based on the field's GraphQL type and mapping type.
  # @yield [Field] the field for further customization
  # @return [void]
  #
  # @see #paginated_collection_field
  # @see #relates_to_many
  # @see #relates_to_one
  #
  # @note Be careful about defining non-nullable fields. Changing a field’s type from non-nullable (e.g. `Int!`) to nullable (e.g.
  #   `Int`) is a breaking change for clients. Making a field non-nullable may also prevent you from applying permissioning to a field
  #   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).
  #   Therefore, we recommend limiting your use of `!` to only a few situations such as defining a type’s primary key (e.g.
  #   `t.field "id", "ID!"`) or defining a list field (e.g. `t.field "authors", "[String!]!"`) since empty lists already provide a
  #   "no data" representation. You can still configure the ElasticGraph indexer to require a non-null value for a field using
  #   `f.json_schema nullable: false`.
  #
  # @note ElasticGraph’s understanding of datastore capabilities may override your configured
  #   `aggregatable`/`filterable`/`groupable`/`sortable` options. For example, a field indexed as `text` for full text search will
  #   not be sortable or groupable even if you pass `sortable: true, groupable: true` when defining the field, because [text fields
  #   cannot be efficiently sorted by or grouped on](https://www.elastic.co/guide/en/elasticsearch/reference/8.15/text.html#text).
  #
  # @example Define a field with documentation
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID" do |f|
  #         f.documentation "The Campaign's identifier."
  #       end
  #     end
  #   end
  #
  # @example Omit a new field from the GraphQL schema until its data has been backfilled
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID"
  #
  #       # TODO: remove `indexing_only: true` once the data for this field has been fully backfilled
  #       t.field "endDate", "Date", indexing_only: true
  #     end
  #   end
  #
  # @example Use `graphql_only` to introduce a new name for an existing field
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID"
  #
  #       t.field "endOn", "Date" do |f|
  #         f.directive "deprecated", reason: "Use `endDate` instead."
  #       end
  #
  #       # We've decided we want to call the field `endDate` instead of `endOn`, but the data
  #       # for this field is currently indexed in `endOn`, so we can use `graphql_only` and
  #       # `name_in_index` to expose the existing data under a new field name.
  #       t.field "endDate", "Date", name_in_index: "endOn", graphql_only: true
  #     end
  #   end
  def field(name, type, graphql_only: false, indexing_only: false, **options)
    if reserved_field_names.include?(name)
      raise Errors::SchemaError, "Invalid field name: `#{self.name}.#{name}`. `#{name}` is reserved for use by " \
        "ElasticGraph as a filtering operator. To use it for a field name, add " \
        "the `schema_element_name_overrides` option (on `ElasticGraph::SchemaDefinition::RakeTasks.new`) to " \
        "configure an alternate name for the `#{name}` operator."
    end

    options = {name_in_index: nil}.merge(options) if graphql_only

    field_factory.call(
      name: name,
      type: type,
      graphql_only: graphql_only,
      parent_type: wrapping_type,
      **options
    ) do |field|
      yield field if block_given?

      unless indexing_only
        register_field(field.name, field, graphql_fields_by_name, "GraphQL", :indexing_only)
      end

      unless graphql_only
        register_field(field.name_in_index, field, indexing_fields_by_name_in_index, "indexing", :graphql_only) do |f|
          f.to_indexing_field_reference
        end
      end
    end
  end

  # Registers the name of a field that existed in a prior version of the schema but has been deleted.
  #
  # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
  #   or {Field#renamed_from}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
  #   the call to this method can be removed.
  #
  # @param field_name [String] name of field that used to exist but has been deleted
  # @return [void]
  #
  # @example Indicate that `Widget.description` has been deleted
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Widget" do |t|
  #       t.deleted_field "description"
  #     end
  #   end
  def deleted_field(field_name)
    schema_def_state.register_deleted_field(
      name,
      field_name,
      defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
      defined_via: %(type.deleted_field "#{field_name}")
    )
  end

  # Registers an old name that this type used to have in a prior version of the schema.
  #
  # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
  #   or {API#deleted_type}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
  #   the call to this method can be removed.
  #
  # @param old_name [String] old name this field used to have in a prior version of the schema
  # @return [void]
  #
  # @example Indicate that `Widget` used to be called `Component`.
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Widget" do |t|
  #       t.renamed_from "Component"
  #     end
  #   end
  def renamed_from(old_name)
    schema_def_state.register_renamed_type(
      name,
      from: old_name,
      defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
      defined_via: %(type.renamed_from "#{old_name}")
    )
  end

  # An alternative to {#field} for when you have a list field that you want exposed as a [paginated Relay
  # connection](https://relay.dev/graphql/connections.htm) rather than as a simple list.
  #
  # @note Bear in mind that pagination does not have much efficiency benefit in this case: all elements of the collection will be
  #   retrieved when fetching this field from the datastore. The pagination implementation will just trim down the collection before
  #   returning it.
  #
  # @param name [String] name of the field
  # @param element_type [String] name of the type of element in the collection
  # @param name_in_index [String] the name of the field in the datastore index. Can be used to back a GraphQL field with a
  #   differently named field in the index.
  # @param singular [String] indicates what the singular form of a field's name is. When provided, ElasticGraph will define a
  #   `groupedBy` field (using the singular form) allowing clients to group by individual values from the field.
  # @yield [Field] the field for further customization
  # @return [void]
  #
  # @see #field
  # @see #relates_to_many
  # @see #relates_to_one
  #
  # @example Define `Author.books` as a paginated collection field
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Author" do |t|
  #       t.field "id", "ID"
  #       t.field "name", "String"
  #       t.paginated_collection_field "books", "String"
  #       t.index "authors"
  #     end
  #   end
  def paginated_collection_field(name, element_type, name_in_index: name, singular: nil, &block)
    element_type_ref = schema_def_state.type_ref(element_type).to_final_form
    element_type = element_type_ref.name

    schema_def_state.paginated_collection_element_types << element_type

    backing_indexing_field = field(name, "[#{element_type}!]!", indexing_only: true, name_in_index: name_in_index, &block)

    field(
      name,
      element_type_ref.as_connection.name,
      name_in_index: name_in_index,
      type_for_derived_types: "[#{element_type}]",
      groupable: !!singular,
      sortable: false,
      graphql_only: true,
      singular: singular,
      backing_indexing_field: backing_indexing_field
    ) do |f|
      f.define_relay_pagination_arguments!
      block&.call(f)
    end
  end

  # Defines a "has one" relationship between the current indexed type and another indexed type by defining a field clients
  # can use to navigate across indexed types in a single GraphQL query.
  #
  # @param field_name [String] name of the relationship field
  # @param type [String] name of the related type
  # @param via [String] name of the foreign key field
  # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
  #   references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type  and references the `id` of
  #   the related type.
  # @yield [Relationship] the generated relationship fields, for further customization
  # @return [void]
  #
  # @see #field
  # @see #relates_to_many
  #
  # @example Use `relates_to_one` to define `Player.team`
  #  ElasticGraph.define_schema do |schema|
  #    schema.object_type "Team" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "homeCity", "String"
  #      t.index "teams"
  #    end
  #
  #    schema.object_type "Player" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.relates_to_one "team", "Team", via: "teamId", dir: :out
  #      t.index "players"
  #    end
  #  end
  def relates_to_one(field_name, type, via:, dir:, &block)
    foreign_key_type = schema_def_state.type_ref(type).non_null? ? "ID!" : "ID"
    relates_to(field_name, type, via: via, dir: dir, foreign_key_type: foreign_key_type, cardinality: :one, related_type: type, &block)
  end

  # Defines a "has many" relationship between the current indexed type and another indexed type by defining a pair of fields clients
  # can use to navigate across indexed types in a single GraphQL query. The pair of generated fields will be [Relay Connection
  # types](https://relay.dev/graphql/connections.htm#sec-Connection-Types) allowing you to filter, sort, paginate, and aggregated the
  # related data.
  #
  # @param field_name [String] name of the relationship field
  # @param type [String] name of the related type
  # @param via [String] name of the foreign key field
  # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
  #   references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type  and references the `id` of
  #   the related type.
  # @param singular [String] singular form of the `field_name`; will be used (along with an `Aggregations` suffix) for the name of
  #   the generated aggregations field
  # @yield [Relationship] the generated relationship fields, for further customization
  # @return [void]
  #
  # @see #field
  # @see #paginated_collection_field
  # @see #relates_to_one
  #
  # @example Use `relates_to_many` to define `Team.players` and `Team.playerAggregations`
  #  ElasticGraph.define_schema do |schema|
  #    schema.object_type "Team" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "homeCity", "String"
  #      t.relates_to_many "players", "Player", via: "teamId", dir: :in, singular: "player"
  #      t.index "teams"
  #    end
  #
  #    schema.object_type "Player" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "teamId", "ID"
  #      t.index "players"
  #    end
  #  end
  def relates_to_many(field_name, type, via:, dir:, singular:)
    foreign_key_type = (dir == :out) ? "[ID!]!" : "ID"
    type_ref = schema_def_state.type_ref(type).to_final_form

    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|
      f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
        a.documentation "Used to filter the returned `#{field_name}` based on the provided criteria."
      end

      f.argument schema_def_state.schema_elements.order_by, "[#{type_ref.as_sort_order.name}!]" do |a|
        a.documentation "Used to specify how the returned `#{field_name}` should be sorted."
      end

      f.define_relay_pagination_arguments!

      yield f if block_given?
    end

    aggregations_name = schema_def_state.schema_elements.normalize_case("#{singular}_aggregations")
    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|
      f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
        a.documentation "Used to filter the `#{type}` documents that get aggregated over based on the provided criteria."
      end

      f.define_relay_pagination_arguments!

      yield f if block_given?

      f.documentation f.derived_documentation("Aggregations over the `#{field_name}` data")
    end
  end

  # Converts the type to GraphQL SDL syntax.
  #
  # @private
  def to_sdl(&field_arg_selector)
    generate_sdl(name_section: name, &field_arg_selector)
  end

  # @private
  def generate_sdl(name_section:, &field_arg_selector)
    <<~SDL
      #{formatted_documentation}#{schema_kind} #{name_section} #{directives_sdl(suffix_with: " ")}{
        #{fields_sdl(&field_arg_selector)}
      }
    SDL
  end

  # @private
  def aggregated_values_type
    schema_def_state.type_ref("NonNumeric").as_aggregated_values
  end

  # @private
  def indexed?
    false
  end

  # @private
  def to_indexing_field_type
    Indexing::FieldType::Object.new(
      type_name: name,
      subfields: indexing_fields_by_name_in_index.values.map(&:to_indexing_field).compact,
      mapping_options: mapping_options,
      json_schema_options: json_schema_options
    )
  end

  # @private
  def current_sources
    indexing_fields_by_name_in_index.values.flat_map do |field|
      child_field_sources = field.type.fully_unwrapped.as_object_type&.current_sources || []
      [field.source&.relationship_name || SELF_RELATIONSHIP_NAME] + child_field_sources
    end
  end

  # @private
  def (
    # path from the overall document root
    path_prefix: "",
    # the source of the parent field
    parent_source: SELF_RELATIONSHIP_NAME,
    # tracks the state of the list counts field
    list_counts_state: ListCountsState::INITIAL
  )
    indexing_fields_by_name_in_index.flat_map do |name, field|
      path = path_prefix + name
      source = field.source&.relationship_name || parent_source
      index_field = SchemaArtifacts::RuntimeMetadata::IndexField.new(source: source)

      list_count_field_tuples = field.paths_to_lists_for_count_indexing.map do |subpath|
        [list_counts_state.path_to_count_subfield(subpath), index_field] # : [::String, SchemaArtifacts::RuntimeMetadata::IndexField]
      end

      if (object_type = field.type.fully_unwrapped.as_object_type)
        new_list_counts_state =
          if field.type.list? && field.nested?
            ListCountsState.new_list_counts_field(at: "#{path}.#{LIST_COUNTS_FIELD}")
          else
            list_counts_state[name]
          end

        object_type.(
          path_prefix: "#{path}.",
          parent_source: source,
          list_counts_state: new_list_counts_state
        )
      else
        [[path, index_field]] # : ::Array[[::String, SchemaArtifacts::RuntimeMetadata::IndexField]]
      end + list_count_field_tuples
    end
  end

  private

  def fields_sdl(&arg_selector)
    graphql_fields_by_name.values
      .map { |f| f.to_sdl(&arg_selector) }
      .flat_map { |sdl| sdl.split("\n") }
      .join("\n  ")
  end

  def register_field(name, field, registry, registry_type, only_option_to_fix, &to_comparable)
    if (existing_field = registry[name])
      field = Field.pick_most_accurate_from(field, existing_field, to_comparable: to_comparable || ->(f) { f }) do
        raise Errors::SchemaError, "Duplicate #{registry_type} field on Type #{self.name}: #{name}. " \
          "To resolve this, set `#{only_option_to_fix}: true` on one of the fields."
      end
    end

    registry[name] = field
  end

  def relates_to(field_name, type, via:, dir:, foreign_key_type:, cardinality:, related_type:)
    field(field_name, type, sortable: false, filterable: false, groupable: false, graphql_only: true) do |field|
      relationship = schema_def_state.factory.new_relationship(
        field,
        cardinality: cardinality,
        related_type: schema_def_state.type_ref(related_type).to_final_form,
        foreign_key: via,
        direction: dir
      )

      yield relationship if block_given?

      field.relationship = relationship

      if dir == :out
        register_inferred_foreign_key_fields(from_type: [via, foreign_key_type], to_other: ["id", "ID!"], related_type: relationship.related_type)
      else
        register_inferred_foreign_key_fields(from_type: ["id", "ID!"], to_other: [via, foreign_key_type], related_type: relationship.related_type)
      end
    end
  end

  def register_inferred_foreign_key_fields(from_type:, to_other:, related_type:)
    # The root `Query` object shouldn't have inferred foreign key fields (it's not indexed).
    return if name.to_s == "Query"

    from_field_name, from_type_name = from_type
    field(from_field_name, from_type_name, indexing_only: true, accuracy_confidence: :medium)

    # If it's a self-referential, we also should add a foreign key field for the other end of the relation.
    if name == related_type.unwrap_non_null.name
      # This must be `:low` confidence for cases where we have a self-referential type that goes both
      # directions, such as:
      #
      # s.object_type "MyTypeBothDirections" do |t|
      #   t.relates_to_one "parent", "MyTypeBothDirections!", via: "children_ids", dir: :in
      #   t.relates_to_many "children", "MyTypeBothDirections", via: "children_ids", dir: :out
      # end
      #
      # In such a circumstance, the `from_type` side may be more accurate (and will be defined on the `field`
      # call above) and we want it preferred over this definition here.
      to_field_name, to_type_name = to_other
      field(to_field_name, to_type_name, indexing_only: true, accuracy_confidence: :low)
    end
  end
end

#schema_kindObject



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 51

class TypeWithSubfields < Struct.new(
  :schema_kind, :schema_def_state, :type_ref, :reserved_field_names,
  :graphql_fields_by_name, :indexing_fields_by_name_in_index, :field_factory,
  :wrapping_type, :relay_pagination_type
)
  prepend Mixins::VerifiesGraphQLName
  include Mixins::CanBeGraphQLOnly
  include Mixins::HasDocumentation
  include Mixins::HasDirectives
  include Mixins::HasDerivedGraphQLTypeCustomizations
  include Mixins::HasTypeInfo

  # The following methods are provided by `Struct.new`:
  # @dynamic type_ref

  # The following methods are provided by `SupportsFilteringAndAggregation`:
  # @dynamic derived_graphql_types

  # The following methods are provided by `CanBeGraphQLOnly`:
  # @dynamic graphql_only?

  # @private
  def initialize(schema_kind, schema_def_state, name, wrapping_type:, field_factory:)
    # `any_satisfy`, `any_of`/`all_of`, and `not` are "reserved" field names. They are reserved for usage by
    # ElasticGraph itself in the `*FilterInput` types it generates. If we allow them to be used as field
    # names, we'll run into conflicts when we later generate the `*FilterInput` type.
    #
    # Note that we don't have the same kind of conflict for the other filtering operators (e.g.
    # `equal_to_any_of`, `gt`, etc) because on the generated filter structure, those are leaf
    # nodes. They never exist alongside document field names on a filter type, but these do,
    # so we have to guard against them here.
    reserved_field_names = [
      schema_def_state.schema_elements.all_of,
      schema_def_state.schema_elements.any_of,
      schema_def_state.schema_elements.any_satisfy,
      schema_def_state.schema_elements.not
    ].to_set

    # @type var graphql_fields_by_name: ::Hash[::String, Field]
    graphql_fields_by_name = {}
    # @type var indexing_fields_by_name_in_index: ::Hash[::String, Field]
    indexing_fields_by_name_in_index = {}

    super(
      schema_kind,
      schema_def_state,
      schema_def_state.type_ref(name).to_final_form,
      reserved_field_names,
      graphql_fields_by_name,
      indexing_fields_by_name_in_index,
      field_factory,
      wrapping_type,
      false
    )

    yield self
  end

  # @return [String] the name of this GraphQL type
  def name
    type_ref.name
  end

  # Defines a [GraphQL field](https://spec.graphql.org/October2021/#sec-Language.Fields) on this type.
  #
  # @param name [String] name of the field
  # @param type [String] type of the field as a [type reference](https://spec.graphql.org/October2021/#sec-Type-References). The named type must be
  #   one of {BuiltInTypes ElasticGraph's built-in types} or a type that has been defined in your schema.
  # @param graphql_only [Boolean] if `true`, ElasticGraph will define the field as a GraphQL field but omit it from the indexing
  #   artifacts (`json_schemas.yaml` and `datastore_config.yaml`). This can be used along with `name_in_index` to support careful
  #   schema evolution.
  # @param indexing_only [Boolean] if `true`, ElasticGraph will define the field for indexing (in the `json_schemas.yaml` and
  #   `datastore_config.yaml` schema artifact) but will omit it from the GraphQL schema. This can be useful to begin indexing a field
  #   before you expose it in GraphQL so that you can fully backfill it first.
  # @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
  #   differently named field in the index.
  # @option options [String] singular can be used on a list field (e.g. `t.field "tags", "[String!]!", singular: "tag"`) to tell
  #   ElasticGraph what the singular form of a field's name is. When provided, ElasticGraph will define a `groupedBy` field (using the
  #   singular form) allowing clients to group by individual values from the field.
  # @option options [Boolean] aggregatable force-enables or disables the ability for aggregation queries to aggregate over this field.
  #   When not provided, ElasticGraph will infer field aggregatability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] filterable force-enables or disables the ability for queries to filter by this field. When not provided,
  #   ElasticGraph will infer field filterability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] groupable force-enables or disables the ability for aggregation queries to group by this field. When
  #   not provided, ElasticGraph will infer field groupability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] sortable force-enables or disables the ability for queries to sort by this field. When not provided,
  #   ElasticGraph will infer field sortability based on the field's GraphQL type and mapping type.
  # @yield [Field] the field for further customization
  # @return [void]
  #
  # @see #paginated_collection_field
  # @see #relates_to_many
  # @see #relates_to_one
  #
  # @note Be careful about defining non-nullable fields. Changing a field’s type from non-nullable (e.g. `Int!`) to nullable (e.g.
  #   `Int`) is a breaking change for clients. Making a field non-nullable may also prevent you from applying permissioning to a field
  #   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).
  #   Therefore, we recommend limiting your use of `!` to only a few situations such as defining a type’s primary key (e.g.
  #   `t.field "id", "ID!"`) or defining a list field (e.g. `t.field "authors", "[String!]!"`) since empty lists already provide a
  #   "no data" representation. You can still configure the ElasticGraph indexer to require a non-null value for a field using
  #   `f.json_schema nullable: false`.
  #
  # @note ElasticGraph’s understanding of datastore capabilities may override your configured
  #   `aggregatable`/`filterable`/`groupable`/`sortable` options. For example, a field indexed as `text` for full text search will
  #   not be sortable or groupable even if you pass `sortable: true, groupable: true` when defining the field, because [text fields
  #   cannot be efficiently sorted by or grouped on](https://www.elastic.co/guide/en/elasticsearch/reference/8.15/text.html#text).
  #
  # @example Define a field with documentation
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID" do |f|
  #         f.documentation "The Campaign's identifier."
  #       end
  #     end
  #   end
  #
  # @example Omit a new field from the GraphQL schema until its data has been backfilled
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID"
  #
  #       # TODO: remove `indexing_only: true` once the data for this field has been fully backfilled
  #       t.field "endDate", "Date", indexing_only: true
  #     end
  #   end
  #
  # @example Use `graphql_only` to introduce a new name for an existing field
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID"
  #
  #       t.field "endOn", "Date" do |f|
  #         f.directive "deprecated", reason: "Use `endDate` instead."
  #       end
  #
  #       # We've decided we want to call the field `endDate` instead of `endOn`, but the data
  #       # for this field is currently indexed in `endOn`, so we can use `graphql_only` and
  #       # `name_in_index` to expose the existing data under a new field name.
  #       t.field "endDate", "Date", name_in_index: "endOn", graphql_only: true
  #     end
  #   end
  def field(name, type, graphql_only: false, indexing_only: false, **options)
    if reserved_field_names.include?(name)
      raise Errors::SchemaError, "Invalid field name: `#{self.name}.#{name}`. `#{name}` is reserved for use by " \
        "ElasticGraph as a filtering operator. To use it for a field name, add " \
        "the `schema_element_name_overrides` option (on `ElasticGraph::SchemaDefinition::RakeTasks.new`) to " \
        "configure an alternate name for the `#{name}` operator."
    end

    options = {name_in_index: nil}.merge(options) if graphql_only

    field_factory.call(
      name: name,
      type: type,
      graphql_only: graphql_only,
      parent_type: wrapping_type,
      **options
    ) do |field|
      yield field if block_given?

      unless indexing_only
        register_field(field.name, field, graphql_fields_by_name, "GraphQL", :indexing_only)
      end

      unless graphql_only
        register_field(field.name_in_index, field, indexing_fields_by_name_in_index, "indexing", :graphql_only) do |f|
          f.to_indexing_field_reference
        end
      end
    end
  end

  # Registers the name of a field that existed in a prior version of the schema but has been deleted.
  #
  # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
  #   or {Field#renamed_from}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
  #   the call to this method can be removed.
  #
  # @param field_name [String] name of field that used to exist but has been deleted
  # @return [void]
  #
  # @example Indicate that `Widget.description` has been deleted
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Widget" do |t|
  #       t.deleted_field "description"
  #     end
  #   end
  def deleted_field(field_name)
    schema_def_state.register_deleted_field(
      name,
      field_name,
      defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
      defined_via: %(type.deleted_field "#{field_name}")
    )
  end

  # Registers an old name that this type used to have in a prior version of the schema.
  #
  # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
  #   or {API#deleted_type}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
  #   the call to this method can be removed.
  #
  # @param old_name [String] old name this field used to have in a prior version of the schema
  # @return [void]
  #
  # @example Indicate that `Widget` used to be called `Component`.
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Widget" do |t|
  #       t.renamed_from "Component"
  #     end
  #   end
  def renamed_from(old_name)
    schema_def_state.register_renamed_type(
      name,
      from: old_name,
      defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
      defined_via: %(type.renamed_from "#{old_name}")
    )
  end

  # An alternative to {#field} for when you have a list field that you want exposed as a [paginated Relay
  # connection](https://relay.dev/graphql/connections.htm) rather than as a simple list.
  #
  # @note Bear in mind that pagination does not have much efficiency benefit in this case: all elements of the collection will be
  #   retrieved when fetching this field from the datastore. The pagination implementation will just trim down the collection before
  #   returning it.
  #
  # @param name [String] name of the field
  # @param element_type [String] name of the type of element in the collection
  # @param name_in_index [String] the name of the field in the datastore index. Can be used to back a GraphQL field with a
  #   differently named field in the index.
  # @param singular [String] indicates what the singular form of a field's name is. When provided, ElasticGraph will define a
  #   `groupedBy` field (using the singular form) allowing clients to group by individual values from the field.
  # @yield [Field] the field for further customization
  # @return [void]
  #
  # @see #field
  # @see #relates_to_many
  # @see #relates_to_one
  #
  # @example Define `Author.books` as a paginated collection field
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Author" do |t|
  #       t.field "id", "ID"
  #       t.field "name", "String"
  #       t.paginated_collection_field "books", "String"
  #       t.index "authors"
  #     end
  #   end
  def paginated_collection_field(name, element_type, name_in_index: name, singular: nil, &block)
    element_type_ref = schema_def_state.type_ref(element_type).to_final_form
    element_type = element_type_ref.name

    schema_def_state.paginated_collection_element_types << element_type

    backing_indexing_field = field(name, "[#{element_type}!]!", indexing_only: true, name_in_index: name_in_index, &block)

    field(
      name,
      element_type_ref.as_connection.name,
      name_in_index: name_in_index,
      type_for_derived_types: "[#{element_type}]",
      groupable: !!singular,
      sortable: false,
      graphql_only: true,
      singular: singular,
      backing_indexing_field: backing_indexing_field
    ) do |f|
      f.define_relay_pagination_arguments!
      block&.call(f)
    end
  end

  # Defines a "has one" relationship between the current indexed type and another indexed type by defining a field clients
  # can use to navigate across indexed types in a single GraphQL query.
  #
  # @param field_name [String] name of the relationship field
  # @param type [String] name of the related type
  # @param via [String] name of the foreign key field
  # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
  #   references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type  and references the `id` of
  #   the related type.
  # @yield [Relationship] the generated relationship fields, for further customization
  # @return [void]
  #
  # @see #field
  # @see #relates_to_many
  #
  # @example Use `relates_to_one` to define `Player.team`
  #  ElasticGraph.define_schema do |schema|
  #    schema.object_type "Team" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "homeCity", "String"
  #      t.index "teams"
  #    end
  #
  #    schema.object_type "Player" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.relates_to_one "team", "Team", via: "teamId", dir: :out
  #      t.index "players"
  #    end
  #  end
  def relates_to_one(field_name, type, via:, dir:, &block)
    foreign_key_type = schema_def_state.type_ref(type).non_null? ? "ID!" : "ID"
    relates_to(field_name, type, via: via, dir: dir, foreign_key_type: foreign_key_type, cardinality: :one, related_type: type, &block)
  end

  # Defines a "has many" relationship between the current indexed type and another indexed type by defining a pair of fields clients
  # can use to navigate across indexed types in a single GraphQL query. The pair of generated fields will be [Relay Connection
  # types](https://relay.dev/graphql/connections.htm#sec-Connection-Types) allowing you to filter, sort, paginate, and aggregated the
  # related data.
  #
  # @param field_name [String] name of the relationship field
  # @param type [String] name of the related type
  # @param via [String] name of the foreign key field
  # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
  #   references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type  and references the `id` of
  #   the related type.
  # @param singular [String] singular form of the `field_name`; will be used (along with an `Aggregations` suffix) for the name of
  #   the generated aggregations field
  # @yield [Relationship] the generated relationship fields, for further customization
  # @return [void]
  #
  # @see #field
  # @see #paginated_collection_field
  # @see #relates_to_one
  #
  # @example Use `relates_to_many` to define `Team.players` and `Team.playerAggregations`
  #  ElasticGraph.define_schema do |schema|
  #    schema.object_type "Team" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "homeCity", "String"
  #      t.relates_to_many "players", "Player", via: "teamId", dir: :in, singular: "player"
  #      t.index "teams"
  #    end
  #
  #    schema.object_type "Player" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "teamId", "ID"
  #      t.index "players"
  #    end
  #  end
  def relates_to_many(field_name, type, via:, dir:, singular:)
    foreign_key_type = (dir == :out) ? "[ID!]!" : "ID"
    type_ref = schema_def_state.type_ref(type).to_final_form

    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|
      f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
        a.documentation "Used to filter the returned `#{field_name}` based on the provided criteria."
      end

      f.argument schema_def_state.schema_elements.order_by, "[#{type_ref.as_sort_order.name}!]" do |a|
        a.documentation "Used to specify how the returned `#{field_name}` should be sorted."
      end

      f.define_relay_pagination_arguments!

      yield f if block_given?
    end

    aggregations_name = schema_def_state.schema_elements.normalize_case("#{singular}_aggregations")
    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|
      f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
        a.documentation "Used to filter the `#{type}` documents that get aggregated over based on the provided criteria."
      end

      f.define_relay_pagination_arguments!

      yield f if block_given?

      f.documentation f.derived_documentation("Aggregations over the `#{field_name}` data")
    end
  end

  # Converts the type to GraphQL SDL syntax.
  #
  # @private
  def to_sdl(&field_arg_selector)
    generate_sdl(name_section: name, &field_arg_selector)
  end

  # @private
  def generate_sdl(name_section:, &field_arg_selector)
    <<~SDL
      #{formatted_documentation}#{schema_kind} #{name_section} #{directives_sdl(suffix_with: " ")}{
        #{fields_sdl(&field_arg_selector)}
      }
    SDL
  end

  # @private
  def aggregated_values_type
    schema_def_state.type_ref("NonNumeric").as_aggregated_values
  end

  # @private
  def indexed?
    false
  end

  # @private
  def to_indexing_field_type
    Indexing::FieldType::Object.new(
      type_name: name,
      subfields: indexing_fields_by_name_in_index.values.map(&:to_indexing_field).compact,
      mapping_options: mapping_options,
      json_schema_options: json_schema_options
    )
  end

  # @private
  def current_sources
    indexing_fields_by_name_in_index.values.flat_map do |field|
      child_field_sources = field.type.fully_unwrapped.as_object_type&.current_sources || []
      [field.source&.relationship_name || SELF_RELATIONSHIP_NAME] + child_field_sources
    end
  end

  # @private
  def (
    # path from the overall document root
    path_prefix: "",
    # the source of the parent field
    parent_source: SELF_RELATIONSHIP_NAME,
    # tracks the state of the list counts field
    list_counts_state: ListCountsState::INITIAL
  )
    indexing_fields_by_name_in_index.flat_map do |name, field|
      path = path_prefix + name
      source = field.source&.relationship_name || parent_source
      index_field = SchemaArtifacts::RuntimeMetadata::IndexField.new(source: source)

      list_count_field_tuples = field.paths_to_lists_for_count_indexing.map do |subpath|
        [list_counts_state.path_to_count_subfield(subpath), index_field] # : [::String, SchemaArtifacts::RuntimeMetadata::IndexField]
      end

      if (object_type = field.type.fully_unwrapped.as_object_type)
        new_list_counts_state =
          if field.type.list? && field.nested?
            ListCountsState.new_list_counts_field(at: "#{path}.#{LIST_COUNTS_FIELD}")
          else
            list_counts_state[name]
          end

        object_type.(
          path_prefix: "#{path}.",
          parent_source: source,
          list_counts_state: new_list_counts_state
        )
      else
        [[path, index_field]] # : ::Array[[::String, SchemaArtifacts::RuntimeMetadata::IndexField]]
      end + list_count_field_tuples
    end
  end

  private

  def fields_sdl(&arg_selector)
    graphql_fields_by_name.values
      .map { |f| f.to_sdl(&arg_selector) }
      .flat_map { |sdl| sdl.split("\n") }
      .join("\n  ")
  end

  def register_field(name, field, registry, registry_type, only_option_to_fix, &to_comparable)
    if (existing_field = registry[name])
      field = Field.pick_most_accurate_from(field, existing_field, to_comparable: to_comparable || ->(f) { f }) do
        raise Errors::SchemaError, "Duplicate #{registry_type} field on Type #{self.name}: #{name}. " \
          "To resolve this, set `#{only_option_to_fix}: true` on one of the fields."
      end
    end

    registry[name] = field
  end

  def relates_to(field_name, type, via:, dir:, foreign_key_type:, cardinality:, related_type:)
    field(field_name, type, sortable: false, filterable: false, groupable: false, graphql_only: true) do |field|
      relationship = schema_def_state.factory.new_relationship(
        field,
        cardinality: cardinality,
        related_type: schema_def_state.type_ref(related_type).to_final_form,
        foreign_key: via,
        direction: dir
      )

      yield relationship if block_given?

      field.relationship = relationship

      if dir == :out
        register_inferred_foreign_key_fields(from_type: [via, foreign_key_type], to_other: ["id", "ID!"], related_type: relationship.related_type)
      else
        register_inferred_foreign_key_fields(from_type: ["id", "ID!"], to_other: [via, foreign_key_type], related_type: relationship.related_type)
      end
    end
  end

  def register_inferred_foreign_key_fields(from_type:, to_other:, related_type:)
    # The root `Query` object shouldn't have inferred foreign key fields (it's not indexed).
    return if name.to_s == "Query"

    from_field_name, from_type_name = from_type
    field(from_field_name, from_type_name, indexing_only: true, accuracy_confidence: :medium)

    # If it's a self-referential, we also should add a foreign key field for the other end of the relation.
    if name == related_type.unwrap_non_null.name
      # This must be `:low` confidence for cases where we have a self-referential type that goes both
      # directions, such as:
      #
      # s.object_type "MyTypeBothDirections" do |t|
      #   t.relates_to_one "parent", "MyTypeBothDirections!", via: "children_ids", dir: :in
      #   t.relates_to_many "children", "MyTypeBothDirections", via: "children_ids", dir: :out
      # end
      #
      # In such a circumstance, the `from_type` side may be more accurate (and will be defined on the `field`
      # call above) and we want it preferred over this definition here.
      to_field_name, to_type_name = to_other
      field(to_field_name, to_type_name, indexing_only: true, accuracy_confidence: :low)
    end
  end
end

#type_refObject



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 51

class TypeWithSubfields < Struct.new(
  :schema_kind, :schema_def_state, :type_ref, :reserved_field_names,
  :graphql_fields_by_name, :indexing_fields_by_name_in_index, :field_factory,
  :wrapping_type, :relay_pagination_type
)
  prepend Mixins::VerifiesGraphQLName
  include Mixins::CanBeGraphQLOnly
  include Mixins::HasDocumentation
  include Mixins::HasDirectives
  include Mixins::HasDerivedGraphQLTypeCustomizations
  include Mixins::HasTypeInfo

  # The following methods are provided by `Struct.new`:
  # @dynamic type_ref

  # The following methods are provided by `SupportsFilteringAndAggregation`:
  # @dynamic derived_graphql_types

  # The following methods are provided by `CanBeGraphQLOnly`:
  # @dynamic graphql_only?

  # @private
  def initialize(schema_kind, schema_def_state, name, wrapping_type:, field_factory:)
    # `any_satisfy`, `any_of`/`all_of`, and `not` are "reserved" field names. They are reserved for usage by
    # ElasticGraph itself in the `*FilterInput` types it generates. If we allow them to be used as field
    # names, we'll run into conflicts when we later generate the `*FilterInput` type.
    #
    # Note that we don't have the same kind of conflict for the other filtering operators (e.g.
    # `equal_to_any_of`, `gt`, etc) because on the generated filter structure, those are leaf
    # nodes. They never exist alongside document field names on a filter type, but these do,
    # so we have to guard against them here.
    reserved_field_names = [
      schema_def_state.schema_elements.all_of,
      schema_def_state.schema_elements.any_of,
      schema_def_state.schema_elements.any_satisfy,
      schema_def_state.schema_elements.not
    ].to_set

    # @type var graphql_fields_by_name: ::Hash[::String, Field]
    graphql_fields_by_name = {}
    # @type var indexing_fields_by_name_in_index: ::Hash[::String, Field]
    indexing_fields_by_name_in_index = {}

    super(
      schema_kind,
      schema_def_state,
      schema_def_state.type_ref(name).to_final_form,
      reserved_field_names,
      graphql_fields_by_name,
      indexing_fields_by_name_in_index,
      field_factory,
      wrapping_type,
      false
    )

    yield self
  end

  # @return [String] the name of this GraphQL type
  def name
    type_ref.name
  end

  # Defines a [GraphQL field](https://spec.graphql.org/October2021/#sec-Language.Fields) on this type.
  #
  # @param name [String] name of the field
  # @param type [String] type of the field as a [type reference](https://spec.graphql.org/October2021/#sec-Type-References). The named type must be
  #   one of {BuiltInTypes ElasticGraph's built-in types} or a type that has been defined in your schema.
  # @param graphql_only [Boolean] if `true`, ElasticGraph will define the field as a GraphQL field but omit it from the indexing
  #   artifacts (`json_schemas.yaml` and `datastore_config.yaml`). This can be used along with `name_in_index` to support careful
  #   schema evolution.
  # @param indexing_only [Boolean] if `true`, ElasticGraph will define the field for indexing (in the `json_schemas.yaml` and
  #   `datastore_config.yaml` schema artifact) but will omit it from the GraphQL schema. This can be useful to begin indexing a field
  #   before you expose it in GraphQL so that you can fully backfill it first.
  # @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
  #   differently named field in the index.
  # @option options [String] singular can be used on a list field (e.g. `t.field "tags", "[String!]!", singular: "tag"`) to tell
  #   ElasticGraph what the singular form of a field's name is. When provided, ElasticGraph will define a `groupedBy` field (using the
  #   singular form) allowing clients to group by individual values from the field.
  # @option options [Boolean] aggregatable force-enables or disables the ability for aggregation queries to aggregate over this field.
  #   When not provided, ElasticGraph will infer field aggregatability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] filterable force-enables or disables the ability for queries to filter by this field. When not provided,
  #   ElasticGraph will infer field filterability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] groupable force-enables or disables the ability for aggregation queries to group by this field. When
  #   not provided, ElasticGraph will infer field groupability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] sortable force-enables or disables the ability for queries to sort by this field. When not provided,
  #   ElasticGraph will infer field sortability based on the field's GraphQL type and mapping type.
  # @yield [Field] the field for further customization
  # @return [void]
  #
  # @see #paginated_collection_field
  # @see #relates_to_many
  # @see #relates_to_one
  #
  # @note Be careful about defining non-nullable fields. Changing a field’s type from non-nullable (e.g. `Int!`) to nullable (e.g.
  #   `Int`) is a breaking change for clients. Making a field non-nullable may also prevent you from applying permissioning to a field
  #   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).
  #   Therefore, we recommend limiting your use of `!` to only a few situations such as defining a type’s primary key (e.g.
  #   `t.field "id", "ID!"`) or defining a list field (e.g. `t.field "authors", "[String!]!"`) since empty lists already provide a
  #   "no data" representation. You can still configure the ElasticGraph indexer to require a non-null value for a field using
  #   `f.json_schema nullable: false`.
  #
  # @note ElasticGraph’s understanding of datastore capabilities may override your configured
  #   `aggregatable`/`filterable`/`groupable`/`sortable` options. For example, a field indexed as `text` for full text search will
  #   not be sortable or groupable even if you pass `sortable: true, groupable: true` when defining the field, because [text fields
  #   cannot be efficiently sorted by or grouped on](https://www.elastic.co/guide/en/elasticsearch/reference/8.15/text.html#text).
  #
  # @example Define a field with documentation
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID" do |f|
  #         f.documentation "The Campaign's identifier."
  #       end
  #     end
  #   end
  #
  # @example Omit a new field from the GraphQL schema until its data has been backfilled
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID"
  #
  #       # TODO: remove `indexing_only: true` once the data for this field has been fully backfilled
  #       t.field "endDate", "Date", indexing_only: true
  #     end
  #   end
  #
  # @example Use `graphql_only` to introduce a new name for an existing field
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID"
  #
  #       t.field "endOn", "Date" do |f|
  #         f.directive "deprecated", reason: "Use `endDate` instead."
  #       end
  #
  #       # We've decided we want to call the field `endDate` instead of `endOn`, but the data
  #       # for this field is currently indexed in `endOn`, so we can use `graphql_only` and
  #       # `name_in_index` to expose the existing data under a new field name.
  #       t.field "endDate", "Date", name_in_index: "endOn", graphql_only: true
  #     end
  #   end
  def field(name, type, graphql_only: false, indexing_only: false, **options)
    if reserved_field_names.include?(name)
      raise Errors::SchemaError, "Invalid field name: `#{self.name}.#{name}`. `#{name}` is reserved for use by " \
        "ElasticGraph as a filtering operator. To use it for a field name, add " \
        "the `schema_element_name_overrides` option (on `ElasticGraph::SchemaDefinition::RakeTasks.new`) to " \
        "configure an alternate name for the `#{name}` operator."
    end

    options = {name_in_index: nil}.merge(options) if graphql_only

    field_factory.call(
      name: name,
      type: type,
      graphql_only: graphql_only,
      parent_type: wrapping_type,
      **options
    ) do |field|
      yield field if block_given?

      unless indexing_only
        register_field(field.name, field, graphql_fields_by_name, "GraphQL", :indexing_only)
      end

      unless graphql_only
        register_field(field.name_in_index, field, indexing_fields_by_name_in_index, "indexing", :graphql_only) do |f|
          f.to_indexing_field_reference
        end
      end
    end
  end

  # Registers the name of a field that existed in a prior version of the schema but has been deleted.
  #
  # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
  #   or {Field#renamed_from}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
  #   the call to this method can be removed.
  #
  # @param field_name [String] name of field that used to exist but has been deleted
  # @return [void]
  #
  # @example Indicate that `Widget.description` has been deleted
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Widget" do |t|
  #       t.deleted_field "description"
  #     end
  #   end
  def deleted_field(field_name)
    schema_def_state.register_deleted_field(
      name,
      field_name,
      defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
      defined_via: %(type.deleted_field "#{field_name}")
    )
  end

  # Registers an old name that this type used to have in a prior version of the schema.
  #
  # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
  #   or {API#deleted_type}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
  #   the call to this method can be removed.
  #
  # @param old_name [String] old name this field used to have in a prior version of the schema
  # @return [void]
  #
  # @example Indicate that `Widget` used to be called `Component`.
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Widget" do |t|
  #       t.renamed_from "Component"
  #     end
  #   end
  def renamed_from(old_name)
    schema_def_state.register_renamed_type(
      name,
      from: old_name,
      defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
      defined_via: %(type.renamed_from "#{old_name}")
    )
  end

  # An alternative to {#field} for when you have a list field that you want exposed as a [paginated Relay
  # connection](https://relay.dev/graphql/connections.htm) rather than as a simple list.
  #
  # @note Bear in mind that pagination does not have much efficiency benefit in this case: all elements of the collection will be
  #   retrieved when fetching this field from the datastore. The pagination implementation will just trim down the collection before
  #   returning it.
  #
  # @param name [String] name of the field
  # @param element_type [String] name of the type of element in the collection
  # @param name_in_index [String] the name of the field in the datastore index. Can be used to back a GraphQL field with a
  #   differently named field in the index.
  # @param singular [String] indicates what the singular form of a field's name is. When provided, ElasticGraph will define a
  #   `groupedBy` field (using the singular form) allowing clients to group by individual values from the field.
  # @yield [Field] the field for further customization
  # @return [void]
  #
  # @see #field
  # @see #relates_to_many
  # @see #relates_to_one
  #
  # @example Define `Author.books` as a paginated collection field
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Author" do |t|
  #       t.field "id", "ID"
  #       t.field "name", "String"
  #       t.paginated_collection_field "books", "String"
  #       t.index "authors"
  #     end
  #   end
  def paginated_collection_field(name, element_type, name_in_index: name, singular: nil, &block)
    element_type_ref = schema_def_state.type_ref(element_type).to_final_form
    element_type = element_type_ref.name

    schema_def_state.paginated_collection_element_types << element_type

    backing_indexing_field = field(name, "[#{element_type}!]!", indexing_only: true, name_in_index: name_in_index, &block)

    field(
      name,
      element_type_ref.as_connection.name,
      name_in_index: name_in_index,
      type_for_derived_types: "[#{element_type}]",
      groupable: !!singular,
      sortable: false,
      graphql_only: true,
      singular: singular,
      backing_indexing_field: backing_indexing_field
    ) do |f|
      f.define_relay_pagination_arguments!
      block&.call(f)
    end
  end

  # Defines a "has one" relationship between the current indexed type and another indexed type by defining a field clients
  # can use to navigate across indexed types in a single GraphQL query.
  #
  # @param field_name [String] name of the relationship field
  # @param type [String] name of the related type
  # @param via [String] name of the foreign key field
  # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
  #   references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type  and references the `id` of
  #   the related type.
  # @yield [Relationship] the generated relationship fields, for further customization
  # @return [void]
  #
  # @see #field
  # @see #relates_to_many
  #
  # @example Use `relates_to_one` to define `Player.team`
  #  ElasticGraph.define_schema do |schema|
  #    schema.object_type "Team" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "homeCity", "String"
  #      t.index "teams"
  #    end
  #
  #    schema.object_type "Player" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.relates_to_one "team", "Team", via: "teamId", dir: :out
  #      t.index "players"
  #    end
  #  end
  def relates_to_one(field_name, type, via:, dir:, &block)
    foreign_key_type = schema_def_state.type_ref(type).non_null? ? "ID!" : "ID"
    relates_to(field_name, type, via: via, dir: dir, foreign_key_type: foreign_key_type, cardinality: :one, related_type: type, &block)
  end

  # Defines a "has many" relationship between the current indexed type and another indexed type by defining a pair of fields clients
  # can use to navigate across indexed types in a single GraphQL query. The pair of generated fields will be [Relay Connection
  # types](https://relay.dev/graphql/connections.htm#sec-Connection-Types) allowing you to filter, sort, paginate, and aggregated the
  # related data.
  #
  # @param field_name [String] name of the relationship field
  # @param type [String] name of the related type
  # @param via [String] name of the foreign key field
  # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
  #   references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type  and references the `id` of
  #   the related type.
  # @param singular [String] singular form of the `field_name`; will be used (along with an `Aggregations` suffix) for the name of
  #   the generated aggregations field
  # @yield [Relationship] the generated relationship fields, for further customization
  # @return [void]
  #
  # @see #field
  # @see #paginated_collection_field
  # @see #relates_to_one
  #
  # @example Use `relates_to_many` to define `Team.players` and `Team.playerAggregations`
  #  ElasticGraph.define_schema do |schema|
  #    schema.object_type "Team" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "homeCity", "String"
  #      t.relates_to_many "players", "Player", via: "teamId", dir: :in, singular: "player"
  #      t.index "teams"
  #    end
  #
  #    schema.object_type "Player" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "teamId", "ID"
  #      t.index "players"
  #    end
  #  end
  def relates_to_many(field_name, type, via:, dir:, singular:)
    foreign_key_type = (dir == :out) ? "[ID!]!" : "ID"
    type_ref = schema_def_state.type_ref(type).to_final_form

    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|
      f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
        a.documentation "Used to filter the returned `#{field_name}` based on the provided criteria."
      end

      f.argument schema_def_state.schema_elements.order_by, "[#{type_ref.as_sort_order.name}!]" do |a|
        a.documentation "Used to specify how the returned `#{field_name}` should be sorted."
      end

      f.define_relay_pagination_arguments!

      yield f if block_given?
    end

    aggregations_name = schema_def_state.schema_elements.normalize_case("#{singular}_aggregations")
    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|
      f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
        a.documentation "Used to filter the `#{type}` documents that get aggregated over based on the provided criteria."
      end

      f.define_relay_pagination_arguments!

      yield f if block_given?

      f.documentation f.derived_documentation("Aggregations over the `#{field_name}` data")
    end
  end

  # Converts the type to GraphQL SDL syntax.
  #
  # @private
  def to_sdl(&field_arg_selector)
    generate_sdl(name_section: name, &field_arg_selector)
  end

  # @private
  def generate_sdl(name_section:, &field_arg_selector)
    <<~SDL
      #{formatted_documentation}#{schema_kind} #{name_section} #{directives_sdl(suffix_with: " ")}{
        #{fields_sdl(&field_arg_selector)}
      }
    SDL
  end

  # @private
  def aggregated_values_type
    schema_def_state.type_ref("NonNumeric").as_aggregated_values
  end

  # @private
  def indexed?
    false
  end

  # @private
  def to_indexing_field_type
    Indexing::FieldType::Object.new(
      type_name: name,
      subfields: indexing_fields_by_name_in_index.values.map(&:to_indexing_field).compact,
      mapping_options: mapping_options,
      json_schema_options: json_schema_options
    )
  end

  # @private
  def current_sources
    indexing_fields_by_name_in_index.values.flat_map do |field|
      child_field_sources = field.type.fully_unwrapped.as_object_type&.current_sources || []
      [field.source&.relationship_name || SELF_RELATIONSHIP_NAME] + child_field_sources
    end
  end

  # @private
  def (
    # path from the overall document root
    path_prefix: "",
    # the source of the parent field
    parent_source: SELF_RELATIONSHIP_NAME,
    # tracks the state of the list counts field
    list_counts_state: ListCountsState::INITIAL
  )
    indexing_fields_by_name_in_index.flat_map do |name, field|
      path = path_prefix + name
      source = field.source&.relationship_name || parent_source
      index_field = SchemaArtifacts::RuntimeMetadata::IndexField.new(source: source)

      list_count_field_tuples = field.paths_to_lists_for_count_indexing.map do |subpath|
        [list_counts_state.path_to_count_subfield(subpath), index_field] # : [::String, SchemaArtifacts::RuntimeMetadata::IndexField]
      end

      if (object_type = field.type.fully_unwrapped.as_object_type)
        new_list_counts_state =
          if field.type.list? && field.nested?
            ListCountsState.new_list_counts_field(at: "#{path}.#{LIST_COUNTS_FIELD}")
          else
            list_counts_state[name]
          end

        object_type.(
          path_prefix: "#{path}.",
          parent_source: source,
          list_counts_state: new_list_counts_state
        )
      else
        [[path, index_field]] # : ::Array[[::String, SchemaArtifacts::RuntimeMetadata::IndexField]]
      end + list_count_field_tuples
    end
  end

  private

  def fields_sdl(&arg_selector)
    graphql_fields_by_name.values
      .map { |f| f.to_sdl(&arg_selector) }
      .flat_map { |sdl| sdl.split("\n") }
      .join("\n  ")
  end

  def register_field(name, field, registry, registry_type, only_option_to_fix, &to_comparable)
    if (existing_field = registry[name])
      field = Field.pick_most_accurate_from(field, existing_field, to_comparable: to_comparable || ->(f) { f }) do
        raise Errors::SchemaError, "Duplicate #{registry_type} field on Type #{self.name}: #{name}. " \
          "To resolve this, set `#{only_option_to_fix}: true` on one of the fields."
      end
    end

    registry[name] = field
  end

  def relates_to(field_name, type, via:, dir:, foreign_key_type:, cardinality:, related_type:)
    field(field_name, type, sortable: false, filterable: false, groupable: false, graphql_only: true) do |field|
      relationship = schema_def_state.factory.new_relationship(
        field,
        cardinality: cardinality,
        related_type: schema_def_state.type_ref(related_type).to_final_form,
        foreign_key: via,
        direction: dir
      )

      yield relationship if block_given?

      field.relationship = relationship

      if dir == :out
        register_inferred_foreign_key_fields(from_type: [via, foreign_key_type], to_other: ["id", "ID!"], related_type: relationship.related_type)
      else
        register_inferred_foreign_key_fields(from_type: ["id", "ID!"], to_other: [via, foreign_key_type], related_type: relationship.related_type)
      end
    end
  end

  def register_inferred_foreign_key_fields(from_type:, to_other:, related_type:)
    # The root `Query` object shouldn't have inferred foreign key fields (it's not indexed).
    return if name.to_s == "Query"

    from_field_name, from_type_name = from_type
    field(from_field_name, from_type_name, indexing_only: true, accuracy_confidence: :medium)

    # If it's a self-referential, we also should add a foreign key field for the other end of the relation.
    if name == related_type.unwrap_non_null.name
      # This must be `:low` confidence for cases where we have a self-referential type that goes both
      # directions, such as:
      #
      # s.object_type "MyTypeBothDirections" do |t|
      #   t.relates_to_one "parent", "MyTypeBothDirections!", via: "children_ids", dir: :in
      #   t.relates_to_many "children", "MyTypeBothDirections", via: "children_ids", dir: :out
      # end
      #
      # In such a circumstance, the `from_type` side may be more accurate (and will be defined on the `field`
      # call above) and we want it preferred over this definition here.
      to_field_name, to_type_name = to_other
      field(to_field_name, to_type_name, indexing_only: true, accuracy_confidence: :low)
    end
  end
end

#wrapping_typeObject



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 51

class TypeWithSubfields < Struct.new(
  :schema_kind, :schema_def_state, :type_ref, :reserved_field_names,
  :graphql_fields_by_name, :indexing_fields_by_name_in_index, :field_factory,
  :wrapping_type, :relay_pagination_type
)
  prepend Mixins::VerifiesGraphQLName
  include Mixins::CanBeGraphQLOnly
  include Mixins::HasDocumentation
  include Mixins::HasDirectives
  include Mixins::HasDerivedGraphQLTypeCustomizations
  include Mixins::HasTypeInfo

  # The following methods are provided by `Struct.new`:
  # @dynamic type_ref

  # The following methods are provided by `SupportsFilteringAndAggregation`:
  # @dynamic derived_graphql_types

  # The following methods are provided by `CanBeGraphQLOnly`:
  # @dynamic graphql_only?

  # @private
  def initialize(schema_kind, schema_def_state, name, wrapping_type:, field_factory:)
    # `any_satisfy`, `any_of`/`all_of`, and `not` are "reserved" field names. They are reserved for usage by
    # ElasticGraph itself in the `*FilterInput` types it generates. If we allow them to be used as field
    # names, we'll run into conflicts when we later generate the `*FilterInput` type.
    #
    # Note that we don't have the same kind of conflict for the other filtering operators (e.g.
    # `equal_to_any_of`, `gt`, etc) because on the generated filter structure, those are leaf
    # nodes. They never exist alongside document field names on a filter type, but these do,
    # so we have to guard against them here.
    reserved_field_names = [
      schema_def_state.schema_elements.all_of,
      schema_def_state.schema_elements.any_of,
      schema_def_state.schema_elements.any_satisfy,
      schema_def_state.schema_elements.not
    ].to_set

    # @type var graphql_fields_by_name: ::Hash[::String, Field]
    graphql_fields_by_name = {}
    # @type var indexing_fields_by_name_in_index: ::Hash[::String, Field]
    indexing_fields_by_name_in_index = {}

    super(
      schema_kind,
      schema_def_state,
      schema_def_state.type_ref(name).to_final_form,
      reserved_field_names,
      graphql_fields_by_name,
      indexing_fields_by_name_in_index,
      field_factory,
      wrapping_type,
      false
    )

    yield self
  end

  # @return [String] the name of this GraphQL type
  def name
    type_ref.name
  end

  # Defines a [GraphQL field](https://spec.graphql.org/October2021/#sec-Language.Fields) on this type.
  #
  # @param name [String] name of the field
  # @param type [String] type of the field as a [type reference](https://spec.graphql.org/October2021/#sec-Type-References). The named type must be
  #   one of {BuiltInTypes ElasticGraph's built-in types} or a type that has been defined in your schema.
  # @param graphql_only [Boolean] if `true`, ElasticGraph will define the field as a GraphQL field but omit it from the indexing
  #   artifacts (`json_schemas.yaml` and `datastore_config.yaml`). This can be used along with `name_in_index` to support careful
  #   schema evolution.
  # @param indexing_only [Boolean] if `true`, ElasticGraph will define the field for indexing (in the `json_schemas.yaml` and
  #   `datastore_config.yaml` schema artifact) but will omit it from the GraphQL schema. This can be useful to begin indexing a field
  #   before you expose it in GraphQL so that you can fully backfill it first.
  # @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
  #   differently named field in the index.
  # @option options [String] singular can be used on a list field (e.g. `t.field "tags", "[String!]!", singular: "tag"`) to tell
  #   ElasticGraph what the singular form of a field's name is. When provided, ElasticGraph will define a `groupedBy` field (using the
  #   singular form) allowing clients to group by individual values from the field.
  # @option options [Boolean] aggregatable force-enables or disables the ability for aggregation queries to aggregate over this field.
  #   When not provided, ElasticGraph will infer field aggregatability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] filterable force-enables or disables the ability for queries to filter by this field. When not provided,
  #   ElasticGraph will infer field filterability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] groupable force-enables or disables the ability for aggregation queries to group by this field. When
  #   not provided, ElasticGraph will infer field groupability based on the field's GraphQL type and mapping type.
  # @option options [Boolean] sortable force-enables or disables the ability for queries to sort by this field. When not provided,
  #   ElasticGraph will infer field sortability based on the field's GraphQL type and mapping type.
  # @yield [Field] the field for further customization
  # @return [void]
  #
  # @see #paginated_collection_field
  # @see #relates_to_many
  # @see #relates_to_one
  #
  # @note Be careful about defining non-nullable fields. Changing a field’s type from non-nullable (e.g. `Int!`) to nullable (e.g.
  #   `Int`) is a breaking change for clients. Making a field non-nullable may also prevent you from applying permissioning to a field
  #   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).
  #   Therefore, we recommend limiting your use of `!` to only a few situations such as defining a type’s primary key (e.g.
  #   `t.field "id", "ID!"`) or defining a list field (e.g. `t.field "authors", "[String!]!"`) since empty lists already provide a
  #   "no data" representation. You can still configure the ElasticGraph indexer to require a non-null value for a field using
  #   `f.json_schema nullable: false`.
  #
  # @note ElasticGraph’s understanding of datastore capabilities may override your configured
  #   `aggregatable`/`filterable`/`groupable`/`sortable` options. For example, a field indexed as `text` for full text search will
  #   not be sortable or groupable even if you pass `sortable: true, groupable: true` when defining the field, because [text fields
  #   cannot be efficiently sorted by or grouped on](https://www.elastic.co/guide/en/elasticsearch/reference/8.15/text.html#text).
  #
  # @example Define a field with documentation
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID" do |f|
  #         f.documentation "The Campaign's identifier."
  #       end
  #     end
  #   end
  #
  # @example Omit a new field from the GraphQL schema until its data has been backfilled
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID"
  #
  #       # TODO: remove `indexing_only: true` once the data for this field has been fully backfilled
  #       t.field "endDate", "Date", indexing_only: true
  #     end
  #   end
  #
  # @example Use `graphql_only` to introduce a new name for an existing field
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Campaign" do |t|
  #       t.field "id", "ID"
  #
  #       t.field "endOn", "Date" do |f|
  #         f.directive "deprecated", reason: "Use `endDate` instead."
  #       end
  #
  #       # We've decided we want to call the field `endDate` instead of `endOn`, but the data
  #       # for this field is currently indexed in `endOn`, so we can use `graphql_only` and
  #       # `name_in_index` to expose the existing data under a new field name.
  #       t.field "endDate", "Date", name_in_index: "endOn", graphql_only: true
  #     end
  #   end
  def field(name, type, graphql_only: false, indexing_only: false, **options)
    if reserved_field_names.include?(name)
      raise Errors::SchemaError, "Invalid field name: `#{self.name}.#{name}`. `#{name}` is reserved for use by " \
        "ElasticGraph as a filtering operator. To use it for a field name, add " \
        "the `schema_element_name_overrides` option (on `ElasticGraph::SchemaDefinition::RakeTasks.new`) to " \
        "configure an alternate name for the `#{name}` operator."
    end

    options = {name_in_index: nil}.merge(options) if graphql_only

    field_factory.call(
      name: name,
      type: type,
      graphql_only: graphql_only,
      parent_type: wrapping_type,
      **options
    ) do |field|
      yield field if block_given?

      unless indexing_only
        register_field(field.name, field, graphql_fields_by_name, "GraphQL", :indexing_only)
      end

      unless graphql_only
        register_field(field.name_in_index, field, indexing_fields_by_name_in_index, "indexing", :graphql_only) do |f|
          f.to_indexing_field_reference
        end
      end
    end
  end

  # Registers the name of a field that existed in a prior version of the schema but has been deleted.
  #
  # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
  #   or {Field#renamed_from}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
  #   the call to this method can be removed.
  #
  # @param field_name [String] name of field that used to exist but has been deleted
  # @return [void]
  #
  # @example Indicate that `Widget.description` has been deleted
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Widget" do |t|
  #       t.deleted_field "description"
  #     end
  #   end
  def deleted_field(field_name)
    schema_def_state.register_deleted_field(
      name,
      field_name,
      defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
      defined_via: %(type.deleted_field "#{field_name}")
    )
  end

  # Registers an old name that this type used to have in a prior version of the schema.
  #
  # @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
  #   or {API#deleted_type}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
  #   the call to this method can be removed.
  #
  # @param old_name [String] old name this field used to have in a prior version of the schema
  # @return [void]
  #
  # @example Indicate that `Widget` used to be called `Component`.
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Widget" do |t|
  #       t.renamed_from "Component"
  #     end
  #   end
  def renamed_from(old_name)
    schema_def_state.register_renamed_type(
      name,
      from: old_name,
      defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
      defined_via: %(type.renamed_from "#{old_name}")
    )
  end

  # An alternative to {#field} for when you have a list field that you want exposed as a [paginated Relay
  # connection](https://relay.dev/graphql/connections.htm) rather than as a simple list.
  #
  # @note Bear in mind that pagination does not have much efficiency benefit in this case: all elements of the collection will be
  #   retrieved when fetching this field from the datastore. The pagination implementation will just trim down the collection before
  #   returning it.
  #
  # @param name [String] name of the field
  # @param element_type [String] name of the type of element in the collection
  # @param name_in_index [String] the name of the field in the datastore index. Can be used to back a GraphQL field with a
  #   differently named field in the index.
  # @param singular [String] indicates what the singular form of a field's name is. When provided, ElasticGraph will define a
  #   `groupedBy` field (using the singular form) allowing clients to group by individual values from the field.
  # @yield [Field] the field for further customization
  # @return [void]
  #
  # @see #field
  # @see #relates_to_many
  # @see #relates_to_one
  #
  # @example Define `Author.books` as a paginated collection field
  #   ElasticGraph.define_schema do |schema|
  #     schema.object_type "Author" do |t|
  #       t.field "id", "ID"
  #       t.field "name", "String"
  #       t.paginated_collection_field "books", "String"
  #       t.index "authors"
  #     end
  #   end
  def paginated_collection_field(name, element_type, name_in_index: name, singular: nil, &block)
    element_type_ref = schema_def_state.type_ref(element_type).to_final_form
    element_type = element_type_ref.name

    schema_def_state.paginated_collection_element_types << element_type

    backing_indexing_field = field(name, "[#{element_type}!]!", indexing_only: true, name_in_index: name_in_index, &block)

    field(
      name,
      element_type_ref.as_connection.name,
      name_in_index: name_in_index,
      type_for_derived_types: "[#{element_type}]",
      groupable: !!singular,
      sortable: false,
      graphql_only: true,
      singular: singular,
      backing_indexing_field: backing_indexing_field
    ) do |f|
      f.define_relay_pagination_arguments!
      block&.call(f)
    end
  end

  # Defines a "has one" relationship between the current indexed type and another indexed type by defining a field clients
  # can use to navigate across indexed types in a single GraphQL query.
  #
  # @param field_name [String] name of the relationship field
  # @param type [String] name of the related type
  # @param via [String] name of the foreign key field
  # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
  #   references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type  and references the `id` of
  #   the related type.
  # @yield [Relationship] the generated relationship fields, for further customization
  # @return [void]
  #
  # @see #field
  # @see #relates_to_many
  #
  # @example Use `relates_to_one` to define `Player.team`
  #  ElasticGraph.define_schema do |schema|
  #    schema.object_type "Team" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "homeCity", "String"
  #      t.index "teams"
  #    end
  #
  #    schema.object_type "Player" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.relates_to_one "team", "Team", via: "teamId", dir: :out
  #      t.index "players"
  #    end
  #  end
  def relates_to_one(field_name, type, via:, dir:, &block)
    foreign_key_type = schema_def_state.type_ref(type).non_null? ? "ID!" : "ID"
    relates_to(field_name, type, via: via, dir: dir, foreign_key_type: foreign_key_type, cardinality: :one, related_type: type, &block)
  end

  # Defines a "has many" relationship between the current indexed type and another indexed type by defining a pair of fields clients
  # can use to navigate across indexed types in a single GraphQL query. The pair of generated fields will be [Relay Connection
  # types](https://relay.dev/graphql/connections.htm#sec-Connection-Types) allowing you to filter, sort, paginate, and aggregated the
  # related data.
  #
  # @param field_name [String] name of the relationship field
  # @param type [String] name of the related type
  # @param via [String] name of the foreign key field
  # @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
  #   references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type  and references the `id` of
  #   the related type.
  # @param singular [String] singular form of the `field_name`; will be used (along with an `Aggregations` suffix) for the name of
  #   the generated aggregations field
  # @yield [Relationship] the generated relationship fields, for further customization
  # @return [void]
  #
  # @see #field
  # @see #paginated_collection_field
  # @see #relates_to_one
  #
  # @example Use `relates_to_many` to define `Team.players` and `Team.playerAggregations`
  #  ElasticGraph.define_schema do |schema|
  #    schema.object_type "Team" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "homeCity", "String"
  #      t.relates_to_many "players", "Player", via: "teamId", dir: :in, singular: "player"
  #      t.index "teams"
  #    end
  #
  #    schema.object_type "Player" do |t|
  #      t.field "id", "ID"
  #      t.field "name", "String"
  #      t.field "teamId", "ID"
  #      t.index "players"
  #    end
  #  end
  def relates_to_many(field_name, type, via:, dir:, singular:)
    foreign_key_type = (dir == :out) ? "[ID!]!" : "ID"
    type_ref = schema_def_state.type_ref(type).to_final_form

    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|
      f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
        a.documentation "Used to filter the returned `#{field_name}` based on the provided criteria."
      end

      f.argument schema_def_state.schema_elements.order_by, "[#{type_ref.as_sort_order.name}!]" do |a|
        a.documentation "Used to specify how the returned `#{field_name}` should be sorted."
      end

      f.define_relay_pagination_arguments!

      yield f if block_given?
    end

    aggregations_name = schema_def_state.schema_elements.normalize_case("#{singular}_aggregations")
    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|
      f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
        a.documentation "Used to filter the `#{type}` documents that get aggregated over based on the provided criteria."
      end

      f.define_relay_pagination_arguments!

      yield f if block_given?

      f.documentation f.derived_documentation("Aggregations over the `#{field_name}` data")
    end
  end

  # Converts the type to GraphQL SDL syntax.
  #
  # @private
  def to_sdl(&field_arg_selector)
    generate_sdl(name_section: name, &field_arg_selector)
  end

  # @private
  def generate_sdl(name_section:, &field_arg_selector)
    <<~SDL
      #{formatted_documentation}#{schema_kind} #{name_section} #{directives_sdl(suffix_with: " ")}{
        #{fields_sdl(&field_arg_selector)}
      }
    SDL
  end

  # @private
  def aggregated_values_type
    schema_def_state.type_ref("NonNumeric").as_aggregated_values
  end

  # @private
  def indexed?
    false
  end

  # @private
  def to_indexing_field_type
    Indexing::FieldType::Object.new(
      type_name: name,
      subfields: indexing_fields_by_name_in_index.values.map(&:to_indexing_field).compact,
      mapping_options: mapping_options,
      json_schema_options: json_schema_options
    )
  end

  # @private
  def current_sources
    indexing_fields_by_name_in_index.values.flat_map do |field|
      child_field_sources = field.type.fully_unwrapped.as_object_type&.current_sources || []
      [field.source&.relationship_name || SELF_RELATIONSHIP_NAME] + child_field_sources
    end
  end

  # @private
  def (
    # path from the overall document root
    path_prefix: "",
    # the source of the parent field
    parent_source: SELF_RELATIONSHIP_NAME,
    # tracks the state of the list counts field
    list_counts_state: ListCountsState::INITIAL
  )
    indexing_fields_by_name_in_index.flat_map do |name, field|
      path = path_prefix + name
      source = field.source&.relationship_name || parent_source
      index_field = SchemaArtifacts::RuntimeMetadata::IndexField.new(source: source)

      list_count_field_tuples = field.paths_to_lists_for_count_indexing.map do |subpath|
        [list_counts_state.path_to_count_subfield(subpath), index_field] # : [::String, SchemaArtifacts::RuntimeMetadata::IndexField]
      end

      if (object_type = field.type.fully_unwrapped.as_object_type)
        new_list_counts_state =
          if field.type.list? && field.nested?
            ListCountsState.new_list_counts_field(at: "#{path}.#{LIST_COUNTS_FIELD}")
          else
            list_counts_state[name]
          end

        object_type.(
          path_prefix: "#{path}.",
          parent_source: source,
          list_counts_state: new_list_counts_state
        )
      else
        [[path, index_field]] # : ::Array[[::String, SchemaArtifacts::RuntimeMetadata::IndexField]]
      end + list_count_field_tuples
    end
  end

  private

  def fields_sdl(&arg_selector)
    graphql_fields_by_name.values
      .map { |f| f.to_sdl(&arg_selector) }
      .flat_map { |sdl| sdl.split("\n") }
      .join("\n  ")
  end

  def register_field(name, field, registry, registry_type, only_option_to_fix, &to_comparable)
    if (existing_field = registry[name])
      field = Field.pick_most_accurate_from(field, existing_field, to_comparable: to_comparable || ->(f) { f }) do
        raise Errors::SchemaError, "Duplicate #{registry_type} field on Type #{self.name}: #{name}. " \
          "To resolve this, set `#{only_option_to_fix}: true` on one of the fields."
      end
    end

    registry[name] = field
  end

  def relates_to(field_name, type, via:, dir:, foreign_key_type:, cardinality:, related_type:)
    field(field_name, type, sortable: false, filterable: false, groupable: false, graphql_only: true) do |field|
      relationship = schema_def_state.factory.new_relationship(
        field,
        cardinality: cardinality,
        related_type: schema_def_state.type_ref(related_type).to_final_form,
        foreign_key: via,
        direction: dir
      )

      yield relationship if block_given?

      field.relationship = relationship

      if dir == :out
        register_inferred_foreign_key_fields(from_type: [via, foreign_key_type], to_other: ["id", "ID!"], related_type: relationship.related_type)
      else
        register_inferred_foreign_key_fields(from_type: ["id", "ID!"], to_other: [via, foreign_key_type], related_type: relationship.related_type)
      end
    end
  end

  def register_inferred_foreign_key_fields(from_type:, to_other:, related_type:)
    # The root `Query` object shouldn't have inferred foreign key fields (it's not indexed).
    return if name.to_s == "Query"

    from_field_name, from_type_name = from_type
    field(from_field_name, from_type_name, indexing_only: true, accuracy_confidence: :medium)

    # If it's a self-referential, we also should add a foreign key field for the other end of the relation.
    if name == related_type.unwrap_non_null.name
      # This must be `:low` confidence for cases where we have a self-referential type that goes both
      # directions, such as:
      #
      # s.object_type "MyTypeBothDirections" do |t|
      #   t.relates_to_one "parent", "MyTypeBothDirections!", via: "children_ids", dir: :in
      #   t.relates_to_many "children", "MyTypeBothDirections", via: "children_ids", dir: :out
      # end
      #
      # In such a circumstance, the `from_type` side may be more accurate (and will be defined on the `field`
      # call above) and we want it preferred over this definition here.
      to_field_name, to_type_name = to_other
      field(to_field_name, to_type_name, indexing_only: true, accuracy_confidence: :low)
    end
  end
end

Instance Method Details

#aggregated_values_typeObject



446
447
448
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 446

def aggregated_values_type
  schema_def_state.type_ref("NonNumeric").as_aggregated_values
end

#current_sourcesObject



466
467
468
469
470
471
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 466

def current_sources
  indexing_fields_by_name_in_index.values.flat_map do |field|
    child_field_sources = field.type.fully_unwrapped.as_object_type&.current_sources || []
    [field.source&.relationship_name || SELF_RELATIONSHIP_NAME] + child_field_sources
  end
end

#deleted_field(field_name) ⇒ void

Note:

In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API or Field#renamed_from. Likewise, when ElasticGraph no longer needs to know about this, it’ll give you a warning indicating the call to this method can be removed.

This method returns an undefined value.

Registers the name of a field that existed in a prior version of the schema but has been deleted.

Examples:

Indicate that ‘Widget.description` has been deleted

ElasticGraph.define_schema do |schema|
  schema.object_type "Widget" do |t|
    t.deleted_field "description"
  end
end

Parameters:

  • field_name (String)

    name of field that used to exist but has been deleted



238
239
240
241
242
243
244
245
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 238

def deleted_field(field_name)
  schema_def_state.register_deleted_field(
    name,
    field_name,
    defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
    defined_via: %(type.deleted_field "#{field_name}")
  )
end

#field(name, type, graphql_only: false, indexing_only: false, **options) {|Field| ... } ⇒ void

Note:

Be careful about defining non-nullable fields. Changing a field’s type from non-nullable (e.g. ‘Int!`) to nullable (e.g. `Int`) is a breaking change for clients. Making a field non-nullable may also prevent you from applying permissioning to a field 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). Therefore, we recommend limiting your use of `!` to only a few situations such as defining a type’s primary key (e.g. `t.field “id”, “ID!”`) or defining a list field (e.g. `t.field “authors”, “[String!]!”`) since empty lists already provide a “no data” representation. You can still configure the ElasticGraph indexer to require a non-null value for a field using `f.json_schema nullable: false`.

Note:

ElasticGraph’s understanding of datastore capabilities may override your configured ‘aggregatable`/`filterable`/`groupable`/`sortable` options. For example, a field indexed as `text` for full text search will not be sortable or groupable even if you pass `sortable: true, groupable: true` when defining the field, because [text fields cannot be efficiently sorted by or grouped on](www.elastic.co/guide/en/elasticsearch/reference/8.15/text.html#text).

This method returns an undefined value.

Defines a [GraphQL field](spec.graphql.org/October2021/#sec-Language.Fields) on this type.

Examples:

Define a field with documentation

ElasticGraph.define_schema do |schema|
  schema.object_type "Campaign" do |t|
    t.field "id", "ID" do |f|
      f.documentation "The Campaign's identifier."
    end
  end
end

Omit a new field from the GraphQL schema until its data has been backfilled

ElasticGraph.define_schema do |schema|
  schema.object_type "Campaign" do |t|
    t.field "id", "ID"

    # TODO: remove `indexing_only: true` once the data for this field has been fully backfilled
    t.field "endDate", "Date", indexing_only: true
  end
end

Use ‘graphql_only` to introduce a new name for an existing field

ElasticGraph.define_schema do |schema|
  schema.object_type "Campaign" do |t|
    t.field "id", "ID"

    t.field "endOn", "Date" do |f|
      f.directive "deprecated", reason: "Use `endDate` instead."
    end

    # We've decided we want to call the field `endDate` instead of `endOn`, but the data
    # for this field is currently indexed in `endOn`, so we can use `graphql_only` and
    # `name_in_index` to expose the existing data under a new field name.
    t.field "endDate", "Date", name_in_index: "endOn", graphql_only: true
  end
end

Parameters:

  • name (String)

    name of the field

  • type (String)

    type of the field as a [type reference](spec.graphql.org/October2021/#sec-Type-References). The named type must be one of ElasticGraph’s built-in types or a type that has been defined in your schema.

  • graphql_only (Boolean) (defaults to: false)

    if ‘true`, ElasticGraph will define the field as a GraphQL field but omit it from the indexing artifacts (`json_schemas.yaml` and `datastore_config.yaml`). This can be used along with `name_in_index` to support careful schema evolution.

  • indexing_only (Boolean) (defaults to: false)

    if ‘true`, ElasticGraph will define the field for indexing (in the `json_schemas.yaml` and `datastore_config.yaml` schema artifact) but will omit it from the GraphQL schema. This can be useful to begin indexing a field before you expose it in GraphQL so that you can fully backfill it first.

  • options (Hash)

    a customizable set of options

Options Hash (**options):

  • name_in_index (String)

    the name of the field in the datastore index. Can be used to back a GraphQL field with a differently named field in the index.

  • singular (String)

    can be used on a list field (e.g. ‘t.field “tags”, “[String!]!”, singular: “tag”`) to tell ElasticGraph what the singular form of a field’s name is. When provided, ElasticGraph will define a ‘groupedBy` field (using the singular form) allowing clients to group by individual values from the field.

  • aggregatable (Boolean)

    force-enables or disables the ability for aggregation queries to aggregate over this field. When not provided, ElasticGraph will infer field aggregatability based on the field’s GraphQL type and mapping type.

  • filterable (Boolean)

    force-enables or disables the ability for queries to filter by this field. When not provided, ElasticGraph will infer field filterability based on the field’s GraphQL type and mapping type.

  • groupable (Boolean)

    force-enables or disables the ability for aggregation queries to group by this field. When not provided, ElasticGraph will infer field groupability based on the field’s GraphQL type and mapping type.

  • sortable (Boolean)

    force-enables or disables the ability for queries to sort by this field. When not provided, ElasticGraph will infer field sortability based on the field’s GraphQL type and mapping type.

Yields:

  • (Field)

    the field for further customization

See Also:



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 192

def field(name, type, graphql_only: false, indexing_only: false, **options)
  if reserved_field_names.include?(name)
    raise Errors::SchemaError, "Invalid field name: `#{self.name}.#{name}`. `#{name}` is reserved for use by " \
      "ElasticGraph as a filtering operator. To use it for a field name, add " \
      "the `schema_element_name_overrides` option (on `ElasticGraph::SchemaDefinition::RakeTasks.new`) to " \
      "configure an alternate name for the `#{name}` operator."
  end

  options = {name_in_index: nil}.merge(options) if graphql_only

  field_factory.call(
    name: name,
    type: type,
    graphql_only: graphql_only,
    parent_type: wrapping_type,
    **options
  ) do |field|
    yield field if block_given?

    unless indexing_only
      register_field(field.name, field, graphql_fields_by_name, "GraphQL", :indexing_only)
    end

    unless graphql_only
      register_field(field.name_in_index, field, indexing_fields_by_name_in_index, "indexing", :graphql_only) do |f|
        f.to_indexing_field_reference
      end
    end
  end
end

#generate_sdl(name_section:, &field_arg_selector) ⇒ Object



437
438
439
440
441
442
443
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 437

def generate_sdl(name_section:, &field_arg_selector)
  <<~SDL
    #{formatted_documentation}#{schema_kind} #{name_section} #{directives_sdl(suffix_with: " ")}{
      #{fields_sdl(&field_arg_selector)}
    }
  SDL
end

#index_field_runtime_metadata_tuples(path_prefix: "", parent_source: SELF_RELATIONSHIP_NAME, list_counts_state: ListCountsState::INITIAL) ⇒ Object



474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 474

def (
  # path from the overall document root
  path_prefix: "",
  # the source of the parent field
  parent_source: SELF_RELATIONSHIP_NAME,
  # tracks the state of the list counts field
  list_counts_state: ListCountsState::INITIAL
)
  indexing_fields_by_name_in_index.flat_map do |name, field|
    path = path_prefix + name
    source = field.source&.relationship_name || parent_source
    index_field = SchemaArtifacts::RuntimeMetadata::IndexField.new(source: source)

    list_count_field_tuples = field.paths_to_lists_for_count_indexing.map do |subpath|
      [list_counts_state.path_to_count_subfield(subpath), index_field] # : [::String, SchemaArtifacts::RuntimeMetadata::IndexField]
    end

    if (object_type = field.type.fully_unwrapped.as_object_type)
      new_list_counts_state =
        if field.type.list? && field.nested?
          ListCountsState.new_list_counts_field(at: "#{path}.#{LIST_COUNTS_FIELD}")
        else
          list_counts_state[name]
        end

      object_type.(
        path_prefix: "#{path}.",
        parent_source: source,
        list_counts_state: new_list_counts_state
      )
    else
      [[path, index_field]] # : ::Array[[::String, SchemaArtifacts::RuntimeMetadata::IndexField]]
    end + list_count_field_tuples
  end
end

#indexed?Boolean

Returns:

  • (Boolean)


451
452
453
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 451

def indexed?
  false
end

#nameString

Returns the name of this GraphQL type.

Returns:

  • (String)

    the name of this GraphQL type



110
111
112
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 110

def name
  type_ref.name
end

#paginated_collection_field(name, element_type, name_in_index: name, singular: nil) {|Field| ... } ⇒ void

Note:

Bear in mind that pagination does not have much efficiency benefit in this case: all elements of the collection will be retrieved when fetching this field from the datastore. The pagination implementation will just trim down the collection before returning it.

This method returns an undefined value.

An alternative to #field for when you have a list field that you want exposed as a [paginated Relay connection](relay.dev/graphql/connections.htm) rather than as a simple list.

Examples:

Define ‘Author.books` as a paginated collection field

ElasticGraph.define_schema do |schema|
  schema.object_type "Author" do |t|
    t.field "id", "ID"
    t.field "name", "String"
    t.paginated_collection_field "books", "String"
    t.index "authors"
  end
end

Parameters:

  • name (String)

    name of the field

  • element_type (String)

    name of the type of element in the collection

  • name_in_index (String) (defaults to: name)

    the name of the field in the datastore index. Can be used to back a GraphQL field with a differently named field in the index.

  • singular (String) (defaults to: nil)

    indicates what the singular form of a field’s name is. When provided, ElasticGraph will define a ‘groupedBy` field (using the singular form) allowing clients to group by individual values from the field.

Yields:

  • (Field)

    the field for further customization

See Also:



300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 300

def paginated_collection_field(name, element_type, name_in_index: name, singular: nil, &block)
  element_type_ref = schema_def_state.type_ref(element_type).to_final_form
  element_type = element_type_ref.name

  schema_def_state.paginated_collection_element_types << element_type

  backing_indexing_field = field(name, "[#{element_type}!]!", indexing_only: true, name_in_index: name_in_index, &block)

  field(
    name,
    element_type_ref.as_connection.name,
    name_in_index: name_in_index,
    type_for_derived_types: "[#{element_type}]",
    groupable: !!singular,
    sortable: false,
    graphql_only: true,
    singular: singular,
    backing_indexing_field: backing_indexing_field
  ) do |f|
    f.define_relay_pagination_arguments!
    block&.call(f)
  end
end

#relates_to_many(field_name, type, via:, dir:, singular:) {|Relationship| ... } ⇒ void

This method returns an undefined value.

Defines a “has many” relationship between the current indexed type and another indexed type by defining a pair of fields clients can use to navigate across indexed types in a single GraphQL query. The pair of generated fields will be [Relay Connection types](relay.dev/graphql/connections.htm#sec-Connection-Types) allowing you to filter, sort, paginate, and aggregated the related data.

Examples:

Use ‘relates_to_many` to define `Team.players` and `Team.playerAggregations`

ElasticGraph.define_schema do |schema|
  schema.object_type "Team" do |t|
    t.field "id", "ID"
    t.field "name", "String"
    t.field "homeCity", "String"
    t.relates_to_many "players", "Player", via: "teamId", dir: :in, singular: "player"
    t.index "teams"
  end

  schema.object_type "Player" do |t|
    t.field "id", "ID"
    t.field "name", "String"
    t.field "teamId", "ID"
    t.index "players"
  end
end

Parameters:

  • field_name (String)

    name of the relationship field

  • type (String)

    name of the related type

  • via (String)

    name of the foreign key field

  • dir (:in, :out)

    direction of the foreign key. Use ‘:in` for an inbound foreign key that resides on the related type and references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type and references the `id` of the related type.

  • singular (String)

    singular form of the ‘field_name`; will be used (along with an `Aggregations` suffix) for the name of the generated aggregations field

Yields:

  • (Relationship)

    the generated relationship fields, for further customization

See Also:



397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 397

def relates_to_many(field_name, type, via:, dir:, singular:)
  foreign_key_type = (dir == :out) ? "[ID!]!" : "ID"
  type_ref = schema_def_state.type_ref(type).to_final_form

  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|
    f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
      a.documentation "Used to filter the returned `#{field_name}` based on the provided criteria."
    end

    f.argument schema_def_state.schema_elements.order_by, "[#{type_ref.as_sort_order.name}!]" do |a|
      a.documentation "Used to specify how the returned `#{field_name}` should be sorted."
    end

    f.define_relay_pagination_arguments!

    yield f if block_given?
  end

  aggregations_name = schema_def_state.schema_elements.normalize_case("#{singular}_aggregations")
  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|
    f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
      a.documentation "Used to filter the `#{type}` documents that get aggregated over based on the provided criteria."
    end

    f.define_relay_pagination_arguments!

    yield f if block_given?

    f.documentation f.derived_documentation("Aggregations over the `#{field_name}` data")
  end
end

#relates_to_one(field_name, type, via:, dir:) {|Relationship| ... } ⇒ void

This method returns an undefined value.

Defines a “has one” relationship between the current indexed type and another indexed type by defining a field clients can use to navigate across indexed types in a single GraphQL query.

Examples:

Use ‘relates_to_one` to define `Player.team`

ElasticGraph.define_schema do |schema|
  schema.object_type "Team" do |t|
    t.field "id", "ID"
    t.field "name", "String"
    t.field "homeCity", "String"
    t.index "teams"
  end

  schema.object_type "Player" do |t|
    t.field "id", "ID"
    t.field "name", "String"
    t.relates_to_one "team", "Team", via: "teamId", dir: :out
    t.index "players"
  end
end

Parameters:

  • field_name (String)

    name of the relationship field

  • type (String)

    name of the related type

  • via (String)

    name of the foreign key field

  • dir (:in, :out)

    direction of the foreign key. Use ‘:in` for an inbound foreign key that resides on the related type and references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type and references the `id` of the related type.

Yields:

  • (Relationship)

    the generated relationship fields, for further customization

See Also:



355
356
357
358
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 355

def relates_to_one(field_name, type, via:, dir:, &block)
  foreign_key_type = schema_def_state.type_ref(type).non_null? ? "ID!" : "ID"
  relates_to(field_name, type, via: via, dir: dir, foreign_key_type: foreign_key_type, cardinality: :one, related_type: type, &block)
end

#renamed_from(old_name) ⇒ void

Note:

In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API or API#deleted_type. Likewise, when ElasticGraph no longer needs to know about this, it’ll give you a warning indicating the call to this method can be removed.

This method returns an undefined value.

Registers an old name that this type used to have in a prior version of the schema.

Examples:

Indicate that ‘Widget` used to be called `Component`.

ElasticGraph.define_schema do |schema|
  schema.object_type "Widget" do |t|
    t.renamed_from "Component"
  end
end

Parameters:

  • old_name (String)

    old name this field used to have in a prior version of the schema



262
263
264
265
266
267
268
269
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 262

def renamed_from(old_name)
  schema_def_state.register_renamed_type(
    name,
    from: old_name,
    defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
    defined_via: %(type.renamed_from "#{old_name}")
  )
end

#to_indexing_field_typeObject



456
457
458
459
460
461
462
463
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 456

def to_indexing_field_type
  Indexing::FieldType::Object.new(
    type_name: name,
    subfields: indexing_fields_by_name_in_index.values.map(&:to_indexing_field).compact,
    mapping_options: mapping_options,
    json_schema_options: json_schema_options
  )
end

#to_sdl(&field_arg_selector) ⇒ Object

Converts the type to GraphQL SDL syntax.



432
433
434
# File 'lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb', line 432

def to_sdl(&field_arg_selector)
  generate_sdl(name_section: name, &field_arg_selector)
end