elasticgraph-schema_definition 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +0 -4
  3. data/lib/elastic_graph/schema_definition/api.rb +20 -1
  4. data/lib/elastic_graph/schema_definition/factory.rb +5 -5
  5. data/lib/elastic_graph/schema_definition/indexing/derived_fields/min_or_max_value.rb +1 -1
  6. data/lib/elastic_graph/schema_definition/indexing/event_envelope.rb +5 -2
  7. data/lib/elastic_graph/schema_definition/indexing/index.rb +21 -3
  8. data/lib/elastic_graph/schema_definition/indexing/json_schema_with_metadata.rb +8 -8
  9. data/lib/elastic_graph/schema_definition/indexing/relationship_resolver.rb +2 -2
  10. data/lib/elastic_graph/schema_definition/indexing/update_target_resolver.rb +1 -1
  11. data/lib/elastic_graph/schema_definition/jruby_patches.rb +59 -0
  12. data/lib/elastic_graph/schema_definition/mixins/has_indices.rb +122 -18
  13. data/lib/elastic_graph/schema_definition/mixins/has_subtypes.rb +21 -12
  14. data/lib/elastic_graph/schema_definition/mixins/implements_interfaces.rb +32 -1
  15. data/lib/elastic_graph/schema_definition/mixins/supports_default_value.rb +1 -14
  16. data/lib/elastic_graph/schema_definition/mixins/supports_filtering_and_aggregation.rb +16 -5
  17. data/lib/elastic_graph/schema_definition/rake_tasks.rb +13 -11
  18. data/lib/elastic_graph/schema_definition/results.rb +26 -28
  19. data/lib/elastic_graph/schema_definition/schema_artifact_manager.rb +3 -2
  20. data/lib/elastic_graph/schema_definition/schema_elements/built_in_types.rb +13 -8
  21. data/lib/elastic_graph/schema_definition/schema_elements/enum_type.rb +0 -5
  22. data/lib/elastic_graph/schema_definition/schema_elements/{enums_for_indexed_types.rb → enums_for_directly_queryable_types.rb} +10 -10
  23. data/lib/elastic_graph/schema_definition/schema_elements/field.rb +22 -5
  24. data/lib/elastic_graph/schema_definition/schema_elements/object_type.rb +11 -1
  25. data/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb +0 -5
  26. data/lib/elastic_graph/schema_definition/schema_elements/sub_aggregation_path.rb +1 -1
  27. data/lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb +4 -5
  28. data/lib/elastic_graph/schema_definition/schema_elements/union_type.rb +12 -0
  29. data/lib/elastic_graph/schema_definition/state.rb +17 -5
  30. metadata +29 -48
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b581d94dc4105d042776cd08fc1a0b4968840a723a06de483f51276eaaa4d2f
4
- data.tar.gz: d00543cabfa2cb9bbdbc225de3224cd16adee14fe50a559da63299deff50b658
3
+ metadata.gz: 2d7df929fabb0ebf9ed6bbee5aa2c981b0baf94fd43c6df151ff84b02345a347
4
+ data.tar.gz: 5b1501454d93320f48648c43087b85594a6aae6391333fdd269fda890a5304f3
5
5
  SHA512:
6
- metadata.gz: b93e0ae17db9cb462e2e11a590965a6a4fe7a3e240eee8cf3042fc174ed8d933641e6f3adc5618ca816d9cba88fa944ff9f5541e97d5f7b3a4b68fa832e365d9
7
- data.tar.gz: eefd1fa67fdf00e33eddb7b128953a0bf301d57894367be7b0dd525b67237e439ff239e40373e8a958d02dcc3b417c1a5f080ac72796146e1a0c29da136ad422
6
+ metadata.gz: 8ded763f8bb207d76500090a965206fbdb729c6ed70fe9b83d56e09f8d983f21f523da0e80af54164a7891c2c29bff482f1ca56f9f89fa694f9f865fcd7691ce
7
+ data.tar.gz: 8d95ee2401637317cccd9f84937f19e229eb7d2e01e63b44682308c3032b302a7caf21c2c0f1971746535e04354fdbf48c2050b5879a59a2efe0217f96c8a1a8
data/README.md CHANGED
@@ -30,9 +30,6 @@ graph LR;
30
30
  graphql["graphql"];
31
31
  elasticgraph-schema_definition --> graphql;
32
32
  class graphql externalGemStyle;
33
- graphql-c_parser["graphql-c_parser"];
34
- elasticgraph-schema_definition --> graphql-c_parser;
35
- class graphql-c_parser externalGemStyle;
36
33
  rake["rake"];
37
34
  elasticgraph-schema_definition --> rake;
38
35
  class rake externalGemStyle;
@@ -40,7 +37,6 @@ graph LR;
40
37
  elasticgraph-local --> elasticgraph-schema_definition;
41
38
  class elasticgraph-local otherEgGemStyle;
42
39
  click graphql href "https://rubygems.org/gems/graphql" "Open on RubyGems.org" _blank;
43
- click graphql-c_parser href "https://rubygems.org/gems/graphql-c_parser" "Open on RubyGems.org" _blank;
44
40
  click rake href "https://rubygems.org/gems/rake" "Open on RubyGems.org" _blank;
45
41
  ```
46
42
 
@@ -13,6 +13,10 @@ require "elastic_graph/schema_definition/mixins/has_readable_to_s_and_inspect"
13
13
  require "elastic_graph/schema_definition/results"
14
14
  require "elastic_graph/schema_definition/state"
15
15
 
16
+ # :nocov: -- only loaded on JRuby
17
+ require "elastic_graph/schema_definition/jruby_patches" if RUBY_ENGINE == "jruby"
18
+ # :nocov:
19
+
16
20
  module ElasticGraph
17
21
  # The main entry point for schema definition from ElasticGraph applications.
18
22
  #
@@ -135,6 +139,10 @@ module ElasticGraph
135
139
  # one or more fields that concrete implementations of the interface must also define. Each implementation can be an
136
140
  # {SchemaElements::ObjectType} or {SchemaElements::InterfaceType}.
137
141
  #
142
+ # @note An interface type can declare an index with {Mixins::HasIndices#index}. This creates a _mixed-type index_ in
143
+ # the datastore where concrete types that implement the interface coexist. A subtype may opt out of this shared
144
+ # index inheritance and use a dedicated index by declaring its own with {Mixins::HasIndices#index}.
145
+ #
138
146
  # @param name [String] name of the interface
139
147
  # @yield [SchemaElements::InterfaceType] interface type object
140
148
  # @return [void]
@@ -199,6 +207,10 @@ module ElasticGraph
199
207
  # Defines a [GraphQL union type](https://graphql.org/learn/schema/#union-types). Use it to define an abstract supertype with one or
200
208
  # more concrete subtypes. Each subtype must be an {SchemaElements::ObjectType}, but they do not have to share any fields in common.
201
209
  #
210
+ # @note A union type can declare an index with {Mixins::HasIndices#index}. This creates a _mixed-type index_ in
211
+ # the datastore where the union members coexist. A subtype may opt out of this shared index inheritance and use
212
+ # a dedicated index by declaring its own with {Mixins::HasIndices#index}.
213
+ #
202
214
  # @param name [String] name of the union type
203
215
  # @yield [SchemaElements::UnionType] union type object
204
216
  # @return [void]
@@ -298,6 +310,8 @@ module ElasticGraph
298
310
  # @param name [Symbol] unique name of the resolver
299
311
  # @param klass [Class] resolver class
300
312
  # @param defined_at [String] the `require` path of the resolver
313
+ # @param built_in [bool] Whether this resolver is built-in to ElasticGraph or one of its extensions.
314
+ # Built-in resolvers that are unused in a schema will not trigger a warning.
301
315
  # @param resolver_config [Hash<Symbol, Object>] configuration options for the resolver, to support parameterized resolvers
302
316
  # @return [void]
303
317
  # @see Mixins::HasIndices#resolve_fields_with
@@ -365,7 +379,7 @@ module ElasticGraph
365
379
  # end
366
380
  # end
367
381
  # end
368
- def register_graphql_resolver(name, klass, defined_at:, **resolver_config)
382
+ def register_graphql_resolver(name, klass, defined_at:, built_in: false, **resolver_config)
369
383
  extension = SchemaArtifacts::RuntimeMetadata::Extension.new(klass, defined_at, resolver_config)
370
384
 
371
385
  needs_lookahead =
@@ -382,6 +396,11 @@ module ElasticGraph
382
396
  )
383
397
 
384
398
  @state.graphql_resolvers_by_name[name] = resolver
399
+ if built_in
400
+ @state.built_in_graphql_resolvers << name
401
+ else
402
+ @state.built_in_graphql_resolvers.delete(name)
403
+ end
385
404
  nil
386
405
  end
387
406
 
@@ -17,7 +17,7 @@ require "elastic_graph/schema_definition/schema_elements/deprecated_element"
17
17
  require "elastic_graph/schema_definition/schema_elements/directive"
18
18
  require "elastic_graph/schema_definition/schema_elements/enum_type"
19
19
  require "elastic_graph/schema_definition/schema_elements/enum_value"
20
- require "elastic_graph/schema_definition/schema_elements/enums_for_indexed_types"
20
+ require "elastic_graph/schema_definition/schema_elements/enums_for_directly_queryable_types"
21
21
  require "elastic_graph/schema_definition/schema_elements/field"
22
22
  require "elastic_graph/schema_definition/schema_elements/field_source"
23
23
  require "elastic_graph/schema_definition/schema_elements/graphql_sdl_enumerator"
@@ -106,10 +106,10 @@ module ElasticGraph
106
106
  end
107
107
  @@enum_value_new = prevent_non_factory_instantiation_of(SchemaElements::EnumValue)
108
108
 
109
- def new_enums_for_indexed_types
110
- @@enums_for_indexed_types_new.call(@state)
109
+ def new_enums_for_directly_queryable_types
110
+ @@enums_for_directly_queryable_types_new.call(@state)
111
111
  end
112
- @@enums_for_indexed_types_new = prevent_non_factory_instantiation_of(SchemaElements::EnumsForIndexedTypes)
112
+ @@enums_for_directly_queryable_types_new = prevent_non_factory_instantiation_of(SchemaElements::EnumsForDirectlyQueryableTypes)
113
113
 
114
114
  # Hard to type check this.
115
115
  # @dynamic new_field
@@ -489,7 +489,7 @@ module ElasticGraph
489
489
  EOS
490
490
  end
491
491
 
492
- if object_type&.indexed?
492
+ if object_type&.root_document_type?
493
493
  t.field @state.schema_elements.all_highlights, "[SearchHighlight!]!" do |f|
494
494
  f.documentation "All search highlights for this `#{type_name}`, indicating where in the indexed document the query matched."
495
495
  end
@@ -62,7 +62,7 @@ module ElasticGraph
62
62
  coercedCurrentFieldValue = (currentFieldValue instanceof Number) ? ((Number)currentFieldValue).longValue() : currentFieldValue;
63
63
  }
64
64
 
65
- if (coercedCurrentFieldValue == null || (#{min_or_max}NewValue != null && #{min_or_max}NewValue.compareTo(coercedCurrentFieldValue) #{operator} 0)) {
65
+ if (#{min_or_max}NewValue != null && (coercedCurrentFieldValue == null || #{min_or_max}NewValue.compareTo(coercedCurrentFieldValue) #{operator} 0)) {
66
66
  parentObject[fieldName] = #{min_or_max}NewValue;
67
67
  return true;
68
68
  }
@@ -54,8 +54,11 @@ module ElasticGraph
54
54
  "type" => "object",
55
55
  "additionalProperties" => false,
56
56
  "patternProperties" => {
57
- "description" => "A timestamp from which ElasticGraph will measure indexing latency. The timestamp name must end in `_at`.",
58
- "^\\w+_at$" => {"type" => "string", "format" => "date-time"}
57
+ "^\\w+_at$" => {
58
+ "description" => "A timestamp from which ElasticGraph will measure indexing latency. The timestamp name must end in `_at`.",
59
+ "type" => "string",
60
+ "format" => "date-time"
61
+ }
59
62
  }
60
63
  },
61
64
  JSON_SCHEMA_VERSION_KEY => {
@@ -63,7 +63,9 @@ module ElasticGraph
63
63
  # By using it here, it will cause queries to pass a `routing` parameter when
64
64
  # searching with id filtering on an index that does not use custom shard routing, giving
65
65
  # us a nice efficiency boost.
66
- self.routing_field_path = public_field_path("id", explanation: "indexed types must have an `id` field")
66
+ id_field_path = public_field_path("id", explanation: "indexed types must have an `id` field")
67
+ self.routing_field_path = id_field_path
68
+ id_field_path.last_part.json_schema nullable: false
67
69
  end
68
70
 
69
71
  yield self if block_given?
@@ -295,7 +297,7 @@ module ElasticGraph
295
297
  .except("type") # `type` is invalid at the mapping root because it always has to be an object.
296
298
  .then { |mapping| ListCountsMapping.merged_into(mapping, for_type: indexed_type) }
297
299
  .then do |fm|
298
- Support::HashUtil.deep_merge(fm, {"properties" => {
300
+ internal_fields = {
299
301
  "__sources" => {"type" => "keyword"},
300
302
  "__versions" => {
301
303
  "type" => "object",
@@ -315,7 +317,17 @@ module ElasticGraph
315
317
  # a boolean.
316
318
  "dynamic" => "false"
317
319
  }
318
- }})
320
+ }
321
+
322
+ # We add __typename for concrete types so they can be matched by __typename filters, which are
323
+ # applied when querying abstract types that span multiple indices. Since every document in a
324
+ # concrete type's index has the same value, we use constant_keyword here. It stores __typename
325
+ # once at the index level with zero per-document overhead.
326
+ unless indexed_type.abstract?
327
+ internal_fields["__typename"] = {"type" => "constant_keyword", "value" => indexed_type.name}
328
+ end
329
+
330
+ Support::HashUtil.deep_merge(fm, {"properties" => internal_fields})
319
331
  end
320
332
 
321
333
  {"dynamic" => "strict"}.merge(field_mappings).tap do |hash|
@@ -324,6 +336,12 @@ module ElasticGraph
324
336
  # made against the wrong shard.
325
337
  hash["_routing"] = {"required" => true} if uses_custom_routing?
326
338
  hash["_size"] = {"enabled" => true} if schema_def_state.index_document_sizes?
339
+
340
+ # Exclude non-returnable fields from `_source` to save storage. These fields are still
341
+ # indexed (in the inverted index and/or doc_values) for filtering, sorting, and aggregation,
342
+ # but their values are not stored in the compressed `_source` blob.
343
+ source_excludes = indexed_type.source_excludes_paths
344
+ hash["_source"] = {"excludes" => source_excludes} if source_excludes.any?
327
345
  end
328
346
  end
329
347
 
@@ -156,15 +156,15 @@ module ElasticGraph
156
156
  json_schema_resolver = JSONSchemaResolver.new(@state, json_schema, old_type_name_by_current_name)
157
157
  version = json_schema.fetch(JSON_SCHEMA_VERSION_KEY)
158
158
 
159
- types_to_check = @state.object_types_by_name.values.select do |type|
160
- type.indexed? && !@derived_indexing_type_names.include?(type.name)
161
- end
162
-
163
- types_to_check.filter_map do |object_type|
164
- if (index_def = object_type.index_def)
165
- identify_missing_necessary_fields_for_index_def(object_type, index_def, json_schema_resolver, version)
159
+ @state.object_types_by_name.values
160
+ .select { |type| type.has_own_index_def? && !@derived_indexing_type_names.include?(type.name) }
161
+ .flat_map do |object_type|
162
+ identify_missing_necessary_fields_for_index_def(
163
+ object_type,
164
+ object_type.own_index_def, # : Indexing::Index
165
+ json_schema_resolver, version
166
+ )
166
167
  end
167
- end.flatten
168
168
  end
169
169
 
170
170
  def identify_missing_necessary_fields_for_index_def(object_type, index_def, json_schema_resolver, json_schema_version)
@@ -34,8 +34,8 @@ module ElasticGraph
34
34
  end
35
35
 
36
36
  [nil, "#{relationship_error_prefix} #{issue}"]
37
- elsif !related_type.indexed?
38
- [nil, "#{relationship_error_prefix} references a type which is not indexed: `#{related_type.name}`. Only indexed types can be used in relations."]
37
+ elsif !related_type.root_document_type?
38
+ [nil, "#{relationship_error_prefix} references a type which is not a root document type: `#{related_type.name}`. Only root document types can be used in relations."]
39
39
  else
40
40
  relation_metadata = relation_field.runtime_metadata_graphql_field.relation # : SchemaArtifacts::RuntimeMetadata::Relation
41
41
  foreign_key_parent_type = (relation_metadata.direction == :in) ? related_type : object_type
@@ -137,7 +137,7 @@ module ElasticGraph
137
137
  #
138
138
  # Returns a tuple of the resolved source (if successful) and an error (if invalid).
139
139
  def resolve_field_source(adapter)
140
- index_def = object_type.index_def # : Index
140
+ index_def = object_type.own_index_def # : Index
141
141
 
142
142
  field_source_graphql_path_string = adapter.get_field_source(resolved_relationship.relationship, index_def) do |local_need|
143
143
  relationship_name = resolved_relationship.relationship_name
@@ -0,0 +1,59 @@
1
+ # Copyright 2024 - 2026 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ # Central location for JRuby workarounds in the schema_definition gem.
10
+ # Each patch should reference the upstream fix and specify when it can be removed.
11
+
12
+ module ElasticGraph
13
+ module SchemaDefinition
14
+ # @private
15
+ module JRubyPatches
16
+ # Bug: `Thread::Backtrace::Location#absolute_path` returns a relative path (same as `#path`)
17
+ # when the source file was loaded via `load` with a bare relative path (e.g. `load "schema.rb"`).
18
+ # On MRI, `absolute_path` correctly resolves to the full absolute path in this case.
19
+ # Workaround: override `absolute_path` to expand relative paths.
20
+ # Reported upstream: https://github.com/jruby/jruby/issues/9245
21
+ # TODO: remove once JRuby fixes this upstream.
22
+ # @private
23
+ module BacktraceLocationAbsolutePathPatch
24
+ def absolute_path
25
+ result = super
26
+ return result if result.nil? || result.start_with?("/")
27
+ ::File.expand_path(result)
28
+ end
29
+ end
30
+
31
+ ::Thread::Backtrace::Location.class_exec do
32
+ prepend BacktraceLocationAbsolutePathPatch
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ require "elastic_graph/schema_definition/mixins/verifies_graphql_name"
39
+ require "elastic_graph/schema_definition/mixins/has_indices"
40
+
41
+ # Bug: `def initialize(...)` + `super(...)` (or `super(*args, **kwargs)`) in a module
42
+ # prepended/included on a Struct subclass incorrectly warns about keyword arguments
43
+ # (3+ members) or crashes with ClassCastException (1 member).
44
+ # Workaround: use `ruby2_keywords` to avoid separate `**kwargs` forwarding.
45
+ # Reported upstream: https://github.com/jruby/jruby/issues/9242
46
+ # TODO: remove once JRuby fixes this upstream.
47
+ ElasticGraph::SchemaDefinition::Mixins::VerifiesGraphQLName.class_exec do
48
+ ruby2_keywords def initialize(*args, &block)
49
+ super(*args, &block)
50
+ ::ElasticGraph::SchemaDefinition::Mixins::VerifiesGraphQLName.verify_name!(name)
51
+ end
52
+ end
53
+
54
+ ElasticGraph::SchemaDefinition::Mixins::HasIndices.class_exec do
55
+ ruby2_keywords def initialize(*args, &block)
56
+ super(*args, &block)
57
+ initialize_has_indices { yield self }
58
+ end
59
+ end
@@ -26,19 +26,18 @@ module ElasticGraph
26
26
  # @private
27
27
  def initialize(*args, **options)
28
28
  super(*args, **options)
29
- @runtime_metadata_overrides = {}
30
- @can_configure_index = true
31
- resolve_fields_with :get_record_field_value
32
- yield self
33
- @can_configure_index = false
29
+ initialize_has_indices { yield self }
34
30
  end
35
31
 
36
- # Converts the current type from being an _embedded_ type (that is, a type that is embedded within another indexed type) to an
37
- # _indexed_ type that resides in the named index definition. Indexed types are directly indexed into the datastore, and will be
38
- # queryable from the root `Query` type.
32
+ # Declares a datastore index for the current type, converting it from an _embedded_ type to an _indexed_ type
33
+ # that is directly queryable from the root `Query` type. When called on an abstract `interface_type` or
34
+ # `union_type`, concrete subtypes inherit the index by default — they share the same datastore index without
35
+ # needing to call `t.index` themselves. A subtype can opt out of this shared index inheritance by calling
36
+ # `t.index` with a different name to use a dedicated index instead.
39
37
  #
40
38
  # @note Use {#root_query_fields} on indexed types to name the field that will be exposed on `Query`.
41
39
  # @note Indexed types must also define an `id` field, which ElasticGraph will use as the primary key.
40
+ # When an abstract type declares the index, each concrete subtype must also define `id`.
42
41
  # @note Datastore index settings can also be defined (or overridden) in an environment-specific settings YAML file. Index settings
43
42
  # that you want to configure differently for different environments (such as `index.number_of_shards`—-production and staging
44
43
  # will probably need different numbers!) should be configured in the per-environment YAML configuration files rather than here.
@@ -50,7 +49,7 @@ module ElasticGraph
50
49
  # @yield [Indexing::Index] the index, so it can be customized further
51
50
  # @return [void]
52
51
  #
53
- # @example Define a `campaigns` index
52
+ # @example Define a `campaigns` index on a concrete type
54
53
  # ElasticGraph.define_schema do |schema|
55
54
  # schema.object_type "Campaign" do |t|
56
55
  # t.field "id", "ID"
@@ -66,18 +65,45 @@ module ElasticGraph
66
65
  # end
67
66
  # end
68
67
  # end
68
+ #
69
+ # @example Declare a shared index on an interface
70
+ # ElasticGraph.define_schema do |schema|
71
+ # schema.interface_type "Vehicle" do |t|
72
+ # t.field "id", "ID"
73
+ # t.field "make", "String"
74
+ # t.index "vehicles"
75
+ # end
76
+ #
77
+ # schema.object_type "Car" do |t|
78
+ # t.implements "Vehicle"
79
+ # t.field "id", "ID"
80
+ # t.field "make", "String"
81
+ # t.field "numDoors", "Int"
82
+ # # Inherits the `vehicles` index — no need to call `t.index`.
83
+ # end
84
+ #
85
+ # schema.object_type "Motorcycle" do |t|
86
+ # t.implements "Vehicle"
87
+ # t.field "id", "ID"
88
+ # t.field "make", "String"
89
+ # t.field "engineCC", "Int"
90
+ # # Opts out of the shared index and gets its own dedicated index instead.
91
+ # t.index "motorcycles"
92
+ # end
93
+ # end
69
94
  def index(name, **settings, &block)
70
95
  unless @can_configure_index
71
96
  raise Errors::SchemaError, "Cannot define an index on `#{self.name}` after initialization is complete. " \
72
97
  "Indices must be configured during initial type definition."
73
98
  end
74
99
 
75
- if @index_def
100
+ if @own_index_def
76
101
  raise Errors::SchemaError, "Cannot define multiple indices on `#{self.name}`. " \
77
- "Only one index per type is supported. An index named `#{@index_def.name}` has already been defined."
102
+ "Only one index per type is supported. An index named `#{@own_index_def.name}` has already been defined."
78
103
  end
79
104
 
80
- @index_def = schema_def_state.factory.new_index(name, settings, self, &block)
105
+ schema_def_state.register_index(name, self)
106
+ @own_index_def = schema_def_state.factory.new_index(name, settings, self, &block)
81
107
  end
82
108
 
83
109
  # Configures the default GraphQL resolver that will be used to resolve the fields of this type. Individual fields
@@ -93,14 +119,54 @@ module ElasticGraph
93
119
  end
94
120
  end
95
121
 
96
- # @return [Indexing::Index, nil] the defined index for this type, or nil if no index is defined
122
+ # @return [Indexing::Index, nil] the index definition directly defined on this type, or nil if no index is defined directly.
123
+ # This will be nil when a type is inheriting an index definition from an abstract parent type.
124
+ def own_index_def
125
+ @own_index_def
126
+ end
127
+
128
+ # @return [Boolean] true if this type has its own index definition (not inherited from an abstract parent)
129
+ def has_own_index_def?
130
+ !@own_index_def.nil?
131
+ end
132
+
133
+ # Resolves this type's index definition. This will be one of:
134
+ # - This type's own_index_def (if it directly defines an index)
135
+ # - An inherited index from an abstract supertype (union/interface) that has an index
136
+ #
137
+ # This type can be a subtype of multiple abstract types (e.g., implements multiple interfaces), but unless it
138
+ # defines its own index, at most one of its supertypes may have an index. If multiple parent types are indexed,
139
+ # this method raises an error to prevent ambiguity about which index to inherit.
140
+ #
141
+ # @return [Indexing::Index, nil] the index definition, or nil if this type has no index
142
+ # @raise [Errors::SchemaError] if this type is a subtype of multiple indexed abstract types
97
143
  def index_def
98
- @index_def
144
+ return own_index_def if has_own_index_def?
145
+
146
+ indexed_supertypes = recursively_resolve_supertypes.select(&:has_own_index_def?)
147
+
148
+ if indexed_supertypes.size > 1
149
+ parent_names = indexed_supertypes.map { |p| p.own_index_def.name }.join(", ")
150
+ raise Errors::SchemaError,
151
+ "The `#{name}` type is a subtype of multiple indexed abstract types (#{parent_names}). " \
152
+ "If a concrete type does not define an index, it may not be a member of multiple indexed abstract types."
153
+ end
154
+
155
+ indexed_supertypes.first&.own_index_def
156
+ end
157
+
158
+ # @return [Boolean] true if this type is a root document type that lives at a document root in the datastore (is indexed).
159
+ # This returns true for types with their own index definition or types that inherit an index from a supertype.
160
+ def root_document_type?
161
+ !index_def.nil?
99
162
  end
100
163
 
101
- # @return [Boolean] true if this type has an index
102
- def indexed?
103
- !@index_def.nil?
164
+ # @return [Boolean] true if this type is directly queryable via a type-specific field on the root `Query` type.
165
+ # @note A concrete subtype that inherits an index from an abstract parent is NOT directly queryable on its own —
166
+ # only the abstract type that declared the index is. Use {#root_document_type?} to check whether a type
167
+ # is stored at the root of any index (own or inherited).
168
+ def directly_queryable?
169
+ has_own_index_def?
104
170
  end
105
171
 
106
172
  # Abstract types are rare, so return false. This can be overridden in the host class.
@@ -259,10 +325,48 @@ module ElasticGraph
259
325
  indexing_fields_by_name_in_index.values.reject { |f| f.source.nil? }
260
326
  end
261
327
 
328
+ # Returns the list of `_source.excludes` paths for non-returnable, non-highlightable fields.
329
+ #
330
+ # Hidden highlightable fields must remain in `_source` so the datastore can still
331
+ # produce search highlight snippets for them.
332
+ #
333
+ # Uses `indexing_fields_by_name_in_index` for traversal (same as
334
+ # `index_field_runtime_metadata_tuples`) to avoid infinite recursion
335
+ # through interface/union subtype cycles.
336
+ #
337
+ # @private
338
+ def source_excludes_paths(path_prefix = "", under_non_returnable_parent = false)
339
+ indexing_fields_by_name_in_index.flat_map do |name, field|
340
+ path = path_prefix + name
341
+ object_type = field.type.fully_unwrapped.as_object_type
342
+ non_returnable = under_non_returnable_parent || !field.returnable?
343
+
344
+ if object_type
345
+ if non_returnable && !field.highlightable?
346
+ ["#{path}.*"]
347
+ else
348
+ object_type.source_excludes_paths("#{path}.", non_returnable)
349
+ end
350
+ elsif non_returnable && !field.highlightable?
351
+ [path]
352
+ else
353
+ []
354
+ end
355
+ end
356
+ end
357
+
262
358
  private
263
359
 
360
+ def initialize_has_indices
361
+ @runtime_metadata_overrides = {}
362
+ @can_configure_index = true
363
+ resolve_fields_with :get_record_field_value
364
+ yield
365
+ @can_configure_index = false
366
+ end
367
+
264
368
  def self_update_target
265
- return nil if abstract? || !indexed?
369
+ return nil if abstract? || !root_document_type?
266
370
 
267
371
  # We exclude `id` from `data_params` because `Indexer::Operator::Update` automatically includes
268
372
  # `params.id` so we don't want it duplicated at `params.data.id` alongside other data params.
@@ -34,8 +34,15 @@ module ElasticGraph
34
34
  .merge("__typename" => schema_def_state.factory.new_field(name: "__typename", type: "String", parent_type: _ = self))
35
35
  end
36
36
 
37
- def indexed?
38
- super || subtypes_indexed?
37
+ def root_document_type?
38
+ super || subtypes_are_root_document_types?
39
+ end
40
+
41
+ # An abstract type is queryable if all of its subtypes are root document types (via a direct or inherited index)
42
+ # even if those subtypes aren't themselves directly queryable. This is why this doesn't delegate to a
43
+ # subtypes_are_directly_queryable helper.
44
+ def directly_queryable?
45
+ super || subtypes_are_root_document_types?
39
46
  end
40
47
 
41
48
  def recursively_resolve_subtypes
@@ -90,26 +97,28 @@ module ElasticGraph
90
97
  end
91
98
  end
92
99
 
93
- def subtypes_indexed?
94
- indexed_by_subtype_name = resolve_subtypes.to_h do |subtype, acc|
95
- [subtype.name, subtype.indexed?]
100
+ def subtypes_are_root_document_types?
101
+ root_document_type_by_subtype_name = resolve_subtypes.to_h do |subtype, acc|
102
+ [subtype.name, subtype.root_document_type?]
96
103
  end
97
104
 
98
- uniq_indexed = indexed_by_subtype_name.values.uniq
105
+ uniq_root_document_type_vals = root_document_type_by_subtype_name.values.uniq
99
106
 
100
- if uniq_indexed.size > 1
101
- descriptions = indexed_by_subtype_name.map do |name_value|
107
+ if uniq_root_document_type_vals.size > 1
108
+ descriptions = root_document_type_by_subtype_name.map do |name_value|
102
109
  name, value = name_value
103
- "#{name}: indexed? = #{value}"
110
+ "#{name}: root_document_type? = #{value}"
104
111
  end
105
112
 
106
113
  raise Errors::SchemaError,
107
- "The #{self.class.name} #{name} has some indexed subtypes, and some non-indexed subtypes. " \
108
- "All subtypes must be indexed or all must NOT be indexed. Subtypes:\n" \
114
+ "The #{self.class.name} #{name} has some subtypes that are root document types, and some that are not. " \
115
+ "All subtypes must be root document types or all must NOT be root document types. " \
116
+ "(A type is a root document type when it has an index definition or, for abstract types, when its subtypes have index definitions.) " \
117
+ "Subtypes:\n" \
109
118
  "#{descriptions.join("\n")}"
110
119
  end
111
120
 
112
- !!uniq_indexed.first
121
+ !!uniq_root_document_type_vals.first
113
122
  end
114
123
  end
115
124
  end
@@ -16,6 +16,10 @@ module ElasticGraph
16
16
  # Declares that the current type implements the specified interface, making the current type a subtype of the interface. The
17
17
  # current type must define all of the fields of the named interface, with the exact same field types.
18
18
  #
19
+ # @note If the named interface has declared an index (via {Mixins::HasIndices#index}), calling `implements`
20
+ # causes this type to automatically inherit that index — it will be stored in the same datastore index as all other
21
+ # implementations of the named interface. To use a dedicated index instead, call {Mixins::HasIndices#index} on this type.
22
+ #
19
23
  # @param interface_names [Array<String>] names of interface types implemented by this type
20
24
  # @return [void]
21
25
  #
@@ -112,11 +116,38 @@ module ElasticGraph
112
116
  if implemented_interfaces.empty?
113
117
  name
114
118
  else
115
- "#{name} implements #{implemented_interfaces.join(" & ")}"
119
+ # Include all ancestor interfaces in SDL
120
+ all_interfaces = recursively_resolve_supertypes.grep(SchemaElements::InterfaceType)
121
+ "#{name} implements #{all_interfaces.map(&:name).sort.join(" & ")}"
116
122
  end
117
123
 
118
124
  generate_sdl(name_section: name_section, &field_arg_selector)
119
125
  end
126
+
127
+ # Returns all supertypes of this type, including union memberships and interface ancestors.
128
+ #
129
+ # @return [Set<UnionType, InterfaceType>] set of supertypes
130
+ # @private
131
+ def recursively_resolve_supertypes
132
+ union_memberships = schema_def_state.union_types_by_member_ref[type_ref] # : ::Set[abstractType]
133
+ union_memberships | recursively_resolve_interface_supertypes
134
+ end
135
+
136
+ private
137
+
138
+ def recursively_resolve_interface_supertypes(ancestors: Set.new)
139
+ implemented_interfaces.flat_map do |interface_ref|
140
+ interface = interface_ref.resolved # : SchemaElements::InterfaceType
141
+
142
+ if ancestors.include?(interface)
143
+ raise Errors::SchemaError, "Your schema has self-referential types, which are not allowed, since " \
144
+ "it prevents the datastore mapping and GraphQL schema generation from terminating:\n" \
145
+ "- There is a circular reference chain involving #{(ancestors.map(&:name) + [interface_ref.name]).sort.inspect}."
146
+ end
147
+
148
+ [interface] + interface.send(:recursively_resolve_interface_supertypes, ancestors: ancestors | [interface])
149
+ end
150
+ end
120
151
  end
121
152
  end
122
153
  end
@@ -12,14 +12,7 @@ module ElasticGraph
12
12
  module SchemaDefinition
13
13
  module Mixins
14
14
  # A mixin designed to be included in a schema element class that supports default values.
15
- # Designed to be `prepended` so that it can hook into `initialize`.
16
15
  module SupportsDefaultValue
17
- # @private
18
- def initialize(...)
19
- __skip__ = super # steep can't type this.
20
- @default_value = NO_DEFAULT_PROVIDED
21
- end
22
-
23
16
  # Used to specify the default value for this field or argument.
24
17
  #
25
18
  # @param default_value [Object] default value for this field or argument
@@ -32,15 +25,9 @@ module ElasticGraph
32
25
  #
33
26
  # @return [String]
34
27
  def default_value_sdl
35
- return nil if @default_value == NO_DEFAULT_PROVIDED
28
+ return nil unless instance_variable_defined?(:@default_value)
36
29
  " = #{Support::GraphQLFormatter.serialize(@default_value)}"
37
30
  end
38
-
39
- private
40
-
41
- # A sentinel value that we can use to detect when a default has been provided.
42
- # We can't use `nil` to detect if a default has been provided because `nil` is a valid default value!
43
- NO_DEFAULT_PROVIDED = Module.new
44
31
  end
45
32
  end
46
33
  end