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.
- checksums.yaml +4 -4
- data/README.md +0 -4
- data/lib/elastic_graph/schema_definition/api.rb +20 -1
- data/lib/elastic_graph/schema_definition/factory.rb +5 -5
- data/lib/elastic_graph/schema_definition/indexing/derived_fields/min_or_max_value.rb +1 -1
- data/lib/elastic_graph/schema_definition/indexing/event_envelope.rb +5 -2
- data/lib/elastic_graph/schema_definition/indexing/index.rb +21 -3
- data/lib/elastic_graph/schema_definition/indexing/json_schema_with_metadata.rb +8 -8
- data/lib/elastic_graph/schema_definition/indexing/relationship_resolver.rb +2 -2
- data/lib/elastic_graph/schema_definition/indexing/update_target_resolver.rb +1 -1
- data/lib/elastic_graph/schema_definition/jruby_patches.rb +59 -0
- data/lib/elastic_graph/schema_definition/mixins/has_indices.rb +122 -18
- data/lib/elastic_graph/schema_definition/mixins/has_subtypes.rb +21 -12
- data/lib/elastic_graph/schema_definition/mixins/implements_interfaces.rb +32 -1
- data/lib/elastic_graph/schema_definition/mixins/supports_default_value.rb +1 -14
- data/lib/elastic_graph/schema_definition/mixins/supports_filtering_and_aggregation.rb +16 -5
- data/lib/elastic_graph/schema_definition/rake_tasks.rb +13 -11
- data/lib/elastic_graph/schema_definition/results.rb +26 -28
- data/lib/elastic_graph/schema_definition/schema_artifact_manager.rb +3 -2
- data/lib/elastic_graph/schema_definition/schema_elements/built_in_types.rb +13 -8
- data/lib/elastic_graph/schema_definition/schema_elements/enum_type.rb +0 -5
- data/lib/elastic_graph/schema_definition/schema_elements/{enums_for_indexed_types.rb → enums_for_directly_queryable_types.rb} +10 -10
- data/lib/elastic_graph/schema_definition/schema_elements/field.rb +22 -5
- data/lib/elastic_graph/schema_definition/schema_elements/object_type.rb +11 -1
- data/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb +0 -5
- data/lib/elastic_graph/schema_definition/schema_elements/sub_aggregation_path.rb +1 -1
- data/lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb +4 -5
- data/lib/elastic_graph/schema_definition/schema_elements/union_type.rb +12 -0
- data/lib/elastic_graph/schema_definition/state.rb +17 -5
- metadata +29 -48
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2d7df929fabb0ebf9ed6bbee5aa2c981b0baf94fd43c6df151ff84b02345a347
|
|
4
|
+
data.tar.gz: 5b1501454d93320f48648c43087b85594a6aae6391333fdd269fda890a5304f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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/
|
|
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
|
|
110
|
-
@@
|
|
109
|
+
def new_enums_for_directly_queryable_types
|
|
110
|
+
@@enums_for_directly_queryable_types_new.call(@state)
|
|
111
111
|
end
|
|
112
|
-
@@
|
|
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&.
|
|
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 (
|
|
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
|
-
"
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
type.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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.
|
|
38
|
-
[nil, "#{relationship_error_prefix} references a type which is not
|
|
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.
|
|
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
|
-
|
|
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
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
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 @
|
|
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 `#{@
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
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? || !
|
|
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
|
|
38
|
-
super ||
|
|
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
|
|
94
|
-
|
|
95
|
-
[subtype.name, subtype.
|
|
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
|
-
|
|
105
|
+
uniq_root_document_type_vals = root_document_type_by_subtype_name.values.uniq
|
|
99
106
|
|
|
100
|
-
if
|
|
101
|
-
descriptions =
|
|
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}:
|
|
110
|
+
"#{name}: root_document_type? = #{value}"
|
|
104
111
|
end
|
|
105
112
|
|
|
106
113
|
raise Errors::SchemaError,
|
|
107
|
-
"The #{self.class.name} #{name} has some
|
|
108
|
-
"All subtypes must be
|
|
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
|
-
!!
|
|
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
|
-
|
|
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
|
|
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
|