elasticgraph-schema_definition 0.18.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +7 -0
  4. data/elasticgraph-schema_definition.gemspec +26 -0
  5. data/lib/elastic_graph/schema_definition/api.rb +359 -0
  6. data/lib/elastic_graph/schema_definition/factory.rb +506 -0
  7. data/lib/elastic_graph/schema_definition/indexing/derived_fields/append_only_set.rb +79 -0
  8. data/lib/elastic_graph/schema_definition/indexing/derived_fields/field_initializer_support.rb +59 -0
  9. data/lib/elastic_graph/schema_definition/indexing/derived_fields/immutable_value.rb +99 -0
  10. data/lib/elastic_graph/schema_definition/indexing/derived_fields/min_or_max_value.rb +62 -0
  11. data/lib/elastic_graph/schema_definition/indexing/derived_indexed_type.rb +346 -0
  12. data/lib/elastic_graph/schema_definition/indexing/event_envelope.rb +74 -0
  13. data/lib/elastic_graph/schema_definition/indexing/field.rb +181 -0
  14. data/lib/elastic_graph/schema_definition/indexing/field_reference.rb +51 -0
  15. data/lib/elastic_graph/schema_definition/indexing/field_type/enum.rb +65 -0
  16. data/lib/elastic_graph/schema_definition/indexing/field_type/object.rb +113 -0
  17. data/lib/elastic_graph/schema_definition/indexing/field_type/scalar.rb +51 -0
  18. data/lib/elastic_graph/schema_definition/indexing/field_type/union.rb +70 -0
  19. data/lib/elastic_graph/schema_definition/indexing/index.rb +318 -0
  20. data/lib/elastic_graph/schema_definition/indexing/json_schema_field_metadata.rb +34 -0
  21. data/lib/elastic_graph/schema_definition/indexing/json_schema_with_metadata.rb +234 -0
  22. data/lib/elastic_graph/schema_definition/indexing/list_counts_mapping.rb +53 -0
  23. data/lib/elastic_graph/schema_definition/indexing/relationship_resolver.rb +96 -0
  24. data/lib/elastic_graph/schema_definition/indexing/rollover_config.rb +25 -0
  25. data/lib/elastic_graph/schema_definition/indexing/update_target_factory.rb +54 -0
  26. data/lib/elastic_graph/schema_definition/indexing/update_target_resolver.rb +195 -0
  27. data/lib/elastic_graph/schema_definition/json_schema_pruner.rb +61 -0
  28. data/lib/elastic_graph/schema_definition/mixins/can_be_graphql_only.rb +31 -0
  29. data/lib/elastic_graph/schema_definition/mixins/has_derived_graphql_type_customizations.rb +119 -0
  30. data/lib/elastic_graph/schema_definition/mixins/has_directives.rb +65 -0
  31. data/lib/elastic_graph/schema_definition/mixins/has_documentation.rb +74 -0
  32. data/lib/elastic_graph/schema_definition/mixins/has_indices.rb +281 -0
  33. data/lib/elastic_graph/schema_definition/mixins/has_readable_to_s_and_inspect.rb +46 -0
  34. data/lib/elastic_graph/schema_definition/mixins/has_subtypes.rb +116 -0
  35. data/lib/elastic_graph/schema_definition/mixins/has_type_info.rb +181 -0
  36. data/lib/elastic_graph/schema_definition/mixins/implements_interfaces.rb +122 -0
  37. data/lib/elastic_graph/schema_definition/mixins/supports_default_value.rb +47 -0
  38. data/lib/elastic_graph/schema_definition/mixins/supports_filtering_and_aggregation.rb +267 -0
  39. data/lib/elastic_graph/schema_definition/mixins/verifies_graphql_name.rb +38 -0
  40. data/lib/elastic_graph/schema_definition/rake_tasks.rb +190 -0
  41. data/lib/elastic_graph/schema_definition/results.rb +404 -0
  42. data/lib/elastic_graph/schema_definition/schema_artifact_manager.rb +482 -0
  43. data/lib/elastic_graph/schema_definition/schema_elements/argument.rb +56 -0
  44. data/lib/elastic_graph/schema_definition/schema_elements/built_in_types.rb +1541 -0
  45. data/lib/elastic_graph/schema_definition/schema_elements/deprecated_element.rb +21 -0
  46. data/lib/elastic_graph/schema_definition/schema_elements/directive.rb +40 -0
  47. data/lib/elastic_graph/schema_definition/schema_elements/enum_type.rb +189 -0
  48. data/lib/elastic_graph/schema_definition/schema_elements/enum_value.rb +73 -0
  49. data/lib/elastic_graph/schema_definition/schema_elements/enum_value_namer.rb +89 -0
  50. data/lib/elastic_graph/schema_definition/schema_elements/enums_for_indexed_types.rb +82 -0
  51. data/lib/elastic_graph/schema_definition/schema_elements/field.rb +1085 -0
  52. data/lib/elastic_graph/schema_definition/schema_elements/field_path.rb +112 -0
  53. data/lib/elastic_graph/schema_definition/schema_elements/field_source.rb +16 -0
  54. data/lib/elastic_graph/schema_definition/schema_elements/graphql_sdl_enumerator.rb +113 -0
  55. data/lib/elastic_graph/schema_definition/schema_elements/input_field.rb +31 -0
  56. data/lib/elastic_graph/schema_definition/schema_elements/input_type.rb +60 -0
  57. data/lib/elastic_graph/schema_definition/schema_elements/interface_type.rb +72 -0
  58. data/lib/elastic_graph/schema_definition/schema_elements/list_counts_state.rb +40 -0
  59. data/lib/elastic_graph/schema_definition/schema_elements/object_type.rb +53 -0
  60. data/lib/elastic_graph/schema_definition/schema_elements/relationship.rb +218 -0
  61. data/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb +310 -0
  62. data/lib/elastic_graph/schema_definition/schema_elements/sort_order_enum_value.rb +36 -0
  63. data/lib/elastic_graph/schema_definition/schema_elements/sub_aggregation_path.rb +66 -0
  64. data/lib/elastic_graph/schema_definition/schema_elements/type_namer.rb +237 -0
  65. data/lib/elastic_graph/schema_definition/schema_elements/type_reference.rb +353 -0
  66. data/lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb +579 -0
  67. data/lib/elastic_graph/schema_definition/schema_elements/union_type.rb +157 -0
  68. data/lib/elastic_graph/schema_definition/scripting/file_system_repository.rb +77 -0
  69. data/lib/elastic_graph/schema_definition/scripting/script.rb +48 -0
  70. data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_day_of_week.painless +24 -0
  71. data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_time_of_day.painless +41 -0
  72. data/lib/elastic_graph/schema_definition/scripting/scripts/filter/by_time_of_day.painless +22 -0
  73. data/lib/elastic_graph/schema_definition/scripting/scripts/update/index_data.painless +93 -0
  74. data/lib/elastic_graph/schema_definition/state.rb +212 -0
  75. data/lib/elastic_graph/schema_definition/test_support.rb +113 -0
  76. metadata +513 -0
@@ -0,0 +1,74 @@
1
+ # Copyright 2024 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ module ElasticGraph
10
+ module SchemaDefinition
11
+ module Mixins
12
+ # Supports GraphQL documentation.
13
+ module HasDocumentation
14
+ # @dynamic doc_comment, doc_comment=
15
+ # @!attribute doc_comment
16
+ # @return [String, nil] current documentation string for the schema element
17
+ attr_accessor :doc_comment
18
+
19
+ # Sets the documentation of the schema element.
20
+ #
21
+ # @param comment [String] the documentation string
22
+ # @return [void]
23
+ #
24
+ # @example Define documentation on an object type and on a field
25
+ # ElasticGraph.define_schema do |schema|
26
+ # schema.object_type "Campaign" do |t|
27
+ # t.documentation "A marketing campaign."
28
+ #
29
+ # t.field "id", "ID" do |f|
30
+ # f.documentation <<~EOS
31
+ # The identifier of the campaign.
32
+ #
33
+ # Note: this is randomly generated.
34
+ # EOS
35
+ # end
36
+ # end
37
+ # end
38
+ def documentation(comment)
39
+ self.doc_comment = comment
40
+ end
41
+
42
+ # Appends some additional documentation to the existing documentation string.
43
+ #
44
+ # @param comment [String] additional documentation
45
+ # @return [void]
46
+ def append_to_documentation(comment)
47
+ new_documentation = doc_comment ? "#{doc_comment}\n\n#{comment}" : comment
48
+ documentation(new_documentation)
49
+ end
50
+
51
+ # Formats the documentation using GraphQL SDL syntax.
52
+ #
53
+ # @return [String] formatted documentation string
54
+ def formatted_documentation
55
+ return nil unless (comment = doc_comment)
56
+ %("""\n#{comment.chomp}\n"""\n)
57
+ end
58
+
59
+ # Generates a documentation string that is derived from the schema elements existing documentation.
60
+ #
61
+ # @param intro [String] string that goes before the schema element's existing documentation
62
+ # @param outro [String, nil] string that goes after the schema element's existing documentation
63
+ # @return [String]
64
+ def derived_documentation(intro, outro = nil)
65
+ outro &&= "\n\n#{outro}."
66
+ return "#{intro}.#{outro}" unless doc_comment
67
+
68
+ quoted_doc = doc_comment.split("\n").map { |line| "> #{line}" }.join("\n")
69
+ "#{intro}:\n\n#{quoted_doc}#{outro}"
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,281 @@
1
+ # Copyright 2024 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ require "elastic_graph/constants"
10
+ require "elastic_graph/schema_definition/indexing/update_target_factory"
11
+
12
+ module ElasticGraph
13
+ module SchemaDefinition
14
+ module Mixins
15
+ # Provides APIs for defining datastore indices.
16
+ module HasIndices
17
+ # @dynamic runtime_metadata_overrides
18
+ # @private
19
+ attr_accessor :runtime_metadata_overrides
20
+
21
+ # @private
22
+ def initialize(*args, **options)
23
+ super(*args, **options)
24
+ self.runtime_metadata_overrides = {}
25
+ yield self
26
+
27
+ # Freeze `indices` so that the indexable status of a type does not change after instantiation.
28
+ # (That would cause problems.)
29
+ indices.freeze
30
+ end
31
+
32
+ # Converts the current type from being an _embedded_ type (that is, a type that is embedded within another indexed type) to an
33
+ # _indexed_ type that resides in the named index definition. Indexed types are directly indexed into the datastore, and will be
34
+ # queryable from the root `Query` type.
35
+ #
36
+ # @note Use {#root_query_fields} on indexed types to name the field that will be exposed on `Query`.
37
+ # @note Indexed types must also define an `id` field, which ElasticGraph will use as the primary key.
38
+ # @note Datastore index settings can also be defined (or overridden) in an environment-specific settings YAML file. Index settings
39
+ # that you want to configure differently for different environments (such as `index.number_of_shards`—-production and staging
40
+ # will probably need different numbers!) should be configured in the per-environment YAML configuration files rather than here.
41
+ #
42
+ # @param name [String] name of the index. See the [Elasticsearch docs](https://www.elastic.co/guide/en/elasticsearch/reference/8.15/indices-create-index.html#indices-create-api-path-params)
43
+ # for restrictions.
44
+ # @param settings [Hash<Symbol, Object>] datastore index settings you want applied to every environment. See the [Elasticsearch docs](https://www.elastic.co/guide/en/elasticsearch/reference/8.15/index-modules.html#index-modules-settings)
45
+ # for a list of valid settings, but be sure to omit the `index.` prefix here.
46
+ # @yield [Indexing::Index] the index, so it can be customized further
47
+ # @return [void]
48
+ #
49
+ # @example Define a `campaigns` index
50
+ # ElasticGraph.define_schema do |schema|
51
+ # schema.object_type "Campaign" do |t|
52
+ # t.field "id", "ID"
53
+ #
54
+ # t.index(
55
+ # "campaigns",
56
+ # # Configure `index.refresh_interval`.
57
+ # refresh_interval: "1s",
58
+ # # Use `index.search` to log warnings for any search query that take more than five seconds.
59
+ # search: {slowlog: {level: "WARN", threshold: {query: {warn: "5s"}}}}
60
+ # ) do |i|
61
+ # # The index can be customized further here.
62
+ # end
63
+ # end
64
+ # end
65
+ def index(name, **settings, &block)
66
+ indices.replace([Indexing::Index.new(name, settings, schema_def_state, self, &block)])
67
+ end
68
+
69
+ # List of indices. (Currently we only store one but we may support multiple in the future).
70
+ #
71
+ # @private
72
+ def indices
73
+ @indices ||= []
74
+ end
75
+
76
+ # @return [Boolean] true if this type has an index
77
+ def indexed?
78
+ indices.any?
79
+ end
80
+
81
+ # Abstract types are rare, so return false. This can be overridden in the host class.
82
+ #
83
+ # @private
84
+ def abstract?
85
+ false
86
+ end
87
+
88
+ # Configures the ElasticGraph indexer to derive another type from this indexed type, using the `from_id` field as
89
+ # the source of the `id` of the derived type, and the provided block for the definitions of the derived fields.
90
+ #
91
+ # @param name [String] name of the derived type
92
+ # @param from_id [String] path to the source type field with `id` values for the derived type
93
+ # @param route_with [String, nil] path to the source type field with values for shard routing on the derived type
94
+ # @param rollover_with [String, nil] path to the source type field with values for index rollover on the derived type
95
+ # @yield [Indexing::DerivedIndexedType] configuration object for field derivations
96
+ # @return [void]
97
+ #
98
+ # @example Derive a `Course` type from `StudentCourseEnrollment` events
99
+ # ElasticGraph.define_schema do |schema|
100
+ # # `StudentCourseEnrollment` is a directly indexed type.
101
+ # schema.object_type "StudentCourseEnrollment" do |t|
102
+ # t.field "id", "ID"
103
+ # t.field "courseId", "ID"
104
+ # t.field "courseName", "String"
105
+ # t.field "studentName", "String"
106
+ # t.field "courseStartDate", "Date"
107
+ #
108
+ # t.index "student_course_enrollments"
109
+ #
110
+ # # Here we define how the `Course` indexed type is derived when we index `StudentCourseEnrollment` events.
111
+ # t.derive_indexed_type_fields "Course", from_id: "courseId" do |derive|
112
+ # # `derive` is an instance of `DerivedIndexedType`.
113
+ # derive.immutable_value "name", from: "courseName"
114
+ # derive.append_only_set "students", from: "studentName"
115
+ # derive.min_value "firstOfferedDate", from: "courseStartDate"
116
+ # derive.max_value "mostRecentlyOfferedDate", from: "courseStartDate"
117
+ # end
118
+ # end
119
+ #
120
+ # # `Course` is an indexed type that is derived entirely from `StudentCourseEnrollment` events.
121
+ # schema.object_type "Course" do |t|
122
+ # t.field "id", "ID"
123
+ # t.field "name", "String"
124
+ # t.field "students", "[String!]!"
125
+ # t.field "firstOfferedDate", "Date"
126
+ # t.field "mostRecentlyOfferedDate", "Date"
127
+ #
128
+ # t.index "courses"
129
+ # end
130
+ # end
131
+ def derive_indexed_type_fields(
132
+ name,
133
+ from_id:,
134
+ route_with: nil,
135
+ rollover_with: nil,
136
+ &block
137
+ )
138
+ Indexing::DerivedIndexedType.new(
139
+ source_type: self,
140
+ destination_type_ref: schema_def_state.type_ref(name).to_final_form,
141
+ id_source: from_id,
142
+ routing_value_source: route_with,
143
+ rollover_timestamp_value_source: rollover_with,
144
+ &block
145
+ ).tap { |dit| derived_indexed_types << dit }
146
+ end
147
+
148
+ # @return [Array<Indexing::DerivedIndexedType>] list of derived types for this source type
149
+ def derived_indexed_types
150
+ @derived_indexed_types ||= []
151
+ end
152
+
153
+ # @private
154
+ def runtime_metadata(extra_update_targets)
155
+ SchemaArtifacts::RuntimeMetadata::ObjectType.new(
156
+ update_targets: derived_indexed_types.map(&:runtime_metadata_for_source_type) + [self_update_target].compact + extra_update_targets,
157
+ index_definition_names: indices.map(&:name),
158
+ graphql_fields_by_name: runtime_metadata_graphql_fields_by_name,
159
+ elasticgraph_category: nil,
160
+ source_type: nil,
161
+ graphql_only_return_type: graphql_only?
162
+ ).with(**runtime_metadata_overrides)
163
+ end
164
+
165
+ # Determines what the root `Query` fields will be to query this indexed type. In addition, this method accepts a block, which you
166
+ # can use to customize the root query field (such as adding a GraphQL directive to it).
167
+ #
168
+ # @param plural [String] the plural name of the entity; used for the root `Query` field that queries documents of this indexed type
169
+ # @param singular [String, nil] the singular name of the entity; used for the root `Query` field (with an `Aggregations` suffix) that
170
+ # queries aggregations of this indexed type. If not provided, will derive it from the type name (e.g. converting it to `camelCase`
171
+ # or `snake_case`, depending on configuration).
172
+ # @yield [SchemaElements::Field] field on the root `Query` type used to query this indexed type, to support customization
173
+ # @return [void]
174
+ #
175
+ # @example Set `plural` and `singular` names
176
+ # ElasticGraph.define_schema do |schema|
177
+ # schema.object_type "Person" do |t|
178
+ # t.field "id", "ID"
179
+ #
180
+ # # Results in `Query.people` and `Query.personAggregations`.
181
+ # t.root_query_fields plural: "people", singular: "person"
182
+ #
183
+ # t.index "people"
184
+ # end
185
+ # end
186
+ #
187
+ # @example Customize `Query` fields
188
+ # ElasticGraph.define_schema do |schema|
189
+ # schema.object_type "Person" do |t|
190
+ # t.field "id", "ID"
191
+ #
192
+ # t.root_query_fields plural: "people", singular: "person" do |f|
193
+ # # Marks `Query.people` and `Query.personAggregations` as deprecated.
194
+ # f.directive "deprecated"
195
+ # end
196
+ #
197
+ # t.index "people"
198
+ # end
199
+ # end
200
+ def root_query_fields(plural:, singular: nil, &customization_block)
201
+ @plural_root_query_field_name = plural
202
+ @singular_root_query_field_name = singular
203
+ @root_query_fields_customizations = customization_block
204
+ end
205
+
206
+ # @return [String] the plural name of the entity; used for the root `Query` field that queries documents of this indexed type
207
+ def plural_root_query_field_name
208
+ @plural_root_query_field_name || naively_pluralize_type_name(name)
209
+ end
210
+
211
+ # @return [String] the singular name of the entity; used for the root `Query` field (with an `Aggregations` suffix) that queries
212
+ # aggregations of this indexed type. If not provided, will derive it from the type name (e.g. converting it to `camelCase` or
213
+ # `snake_case`, depending on configuration).
214
+ def singular_root_query_field_name
215
+ @singular_root_query_field_name || to_field_name(name)
216
+ end
217
+
218
+ # @private
219
+ def root_query_fields_customizations
220
+ @root_query_fields_customizations
221
+ end
222
+
223
+ # @private
224
+ def fields_with_sources
225
+ indexing_fields_by_name_in_index.values.reject { |f| f.source.nil? }
226
+ end
227
+
228
+ private
229
+
230
+ def self_update_target
231
+ return nil if abstract? || !indexed?
232
+
233
+ # We exclude `id` from `data_params` because `Indexer::Operator::Update` automatically includes
234
+ # `params.id` so we don't want it duplicated at `params.data.id` alongside other data params.
235
+ #
236
+ # In addition, we exclude fields that have an alternate `source` -- those fields will get populated
237
+ # by a different event and we don't want to risk "stomping" their value via this update target.
238
+ data_params = indexing_fields_by_name_in_index.select { |name, field| name != "id" && field.source.nil? }.to_h do |field|
239
+ [field, SchemaArtifacts::RuntimeMetadata::DynamicParam.new(source_path: field, cardinality: :one)]
240
+ end
241
+
242
+ index_runtime_metadata = indices.first.runtime_metadata
243
+
244
+ Indexing::UpdateTargetFactory.new_normal_indexing_update_target(
245
+ type: name,
246
+ relationship: SELF_RELATIONSHIP_NAME,
247
+ id_source: "id",
248
+ data_params: data_params,
249
+ # Some day we may want to consider supporting multiple indices. If/when we add support for that,
250
+ # we'll need to change the runtime metadata here to have a map of these values, keyed by index
251
+ # name.
252
+ routing_value_source: index_runtime_metadata.route_with,
253
+ rollover_timestamp_value_source: index_runtime_metadata.rollover&.timestamp_field_path
254
+ )
255
+ end
256
+
257
+ def runtime_metadata_graphql_fields_by_name
258
+ graphql_fields_by_name.transform_values(&:runtime_metadata_graphql_field)
259
+ end
260
+
261
+ # Provides a "best effort" conversion of a type name to the plural form.
262
+ # In practice, schema definers should set `root_query_field` on their
263
+ # indexed types so we don't have to try to convert the type to its plural
264
+ # form. Still, this has value, particularly given our existing tests
265
+ # (where I don't want to require that we set this).
266
+ #
267
+ # Note: we could pull in ActiveSupport to pluralize more accurately, but I
268
+ # really don't want to pull in any part of Rails just for that :(.
269
+ def naively_pluralize_type_name(type_name)
270
+ normalized = to_field_name(type_name)
271
+ normalized + (normalized.end_with?("s") ? "es" : "s")
272
+ end
273
+
274
+ def to_field_name(type_name)
275
+ name_without_leading_uppercase = type_name.sub(/^([[:upper:]])/) { $1.downcase }
276
+ schema_def_state.schema_elements.normalize_case(name_without_leading_uppercase)
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,46 @@
1
+ # Copyright 2024 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ module ElasticGraph
10
+ module SchemaDefinition
11
+ module Mixins
12
+ # Dynamic mixin that provides readable output from `#to_s` and `#inspect`. The default
13
+ # output Ruby prints for these methods is quite unwieldy for all our schema definition
14
+ # types, because we have a large interconnected object graph and `Struct` classes print
15
+ # all their state. In fact, before implementing this, we observed output of more than
16
+ # 5 million characters long!
17
+ #
18
+ # To use this module include a new instance of it:
19
+ #
20
+ # include HasReadableToSAndInspect.new
21
+ #
22
+ # Optionally, provide a block that, given an instance of the class, returns a string description for
23
+ # inclusion in the output:
24
+ #
25
+ # include HasReadableToSAndInspect.new { |obj| obj.name }
26
+ #
27
+ # @private
28
+ class HasReadableToSAndInspect < ::Module
29
+ def initialize
30
+ if block_given?
31
+ define_method :to_s do
32
+ "#<#{self.class.name} #{yield self}>"
33
+ end
34
+ else
35
+ # When no block is given, we just want to use the stock `Object#to_s`, which renders the memory address.
36
+ define_method :to_s do
37
+ ::Object.instance_method(:to_s).bind_call(self)
38
+ end
39
+ end
40
+
41
+ alias_method :inspect, :to_s
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,116 @@
1
+ # Copyright 2024 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ require "elastic_graph/error"
10
+ require "elastic_graph/schema_definition/indexing/field_type/union"
11
+ require "elastic_graph/schema_definition/schema_elements/list_counts_state"
12
+
13
+ module ElasticGraph
14
+ module SchemaDefinition
15
+ module Mixins
16
+ # Provides common support for abstract GraphQL types that have subtypes (e.g. union and interface types).
17
+ #
18
+ # @private
19
+ module HasSubtypes
20
+ def to_indexing_field_type
21
+ subtypes_by_name = recursively_resolve_subtypes.to_h do |type|
22
+ [type.name, _ = type.to_indexing_field_type]
23
+ end
24
+
25
+ Indexing::FieldType::Union.new(subtypes_by_name)
26
+ end
27
+
28
+ def graphql_fields_by_name
29
+ merge_fields_by_name_from_subtypes(&:graphql_fields_by_name)
30
+ end
31
+
32
+ def indexing_fields_by_name_in_index
33
+ merge_fields_by_name_from_subtypes(&:indexing_fields_by_name_in_index)
34
+ .merge("__typename" => schema_def_state.factory.new_field(name: "__typename", type: "String", parent_type: _ = self))
35
+ end
36
+
37
+ def indexed?
38
+ super || subtypes_indexed?
39
+ end
40
+
41
+ def recursively_resolve_subtypes
42
+ resolve_subtypes.flat_map do |type|
43
+ type.is_a?(HasSubtypes) ? (_ = type).recursively_resolve_subtypes : [type]
44
+ end
45
+ end
46
+
47
+ def abstract?
48
+ true
49
+ end
50
+
51
+ def current_sources
52
+ resolve_subtypes.flat_map(&:current_sources)
53
+ end
54
+
55
+ def index_field_runtime_metadata_tuples(
56
+ path_prefix: "",
57
+ parent_source: SELF_RELATIONSHIP_NAME,
58
+ list_counts_state: SchemaElements::ListCountsState::INITIAL
59
+ )
60
+ resolve_subtypes.flat_map do |t|
61
+ t.index_field_runtime_metadata_tuples(
62
+ path_prefix: path_prefix,
63
+ parent_source: parent_source,
64
+ list_counts_state: list_counts_state
65
+ )
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def merge_fields_by_name_from_subtypes
72
+ resolved_subtypes = resolve_subtypes
73
+
74
+ resolved_subtypes.reduce(_ = {}) do |fields_by_name, subtype|
75
+ fields_by_name.merge(yield subtype) do |field_name, def1, def2|
76
+ if (def1.name_in_index == def2.name_in_index && def1.resolve_mapping != def2.resolve_mapping) || (def1.type.unwrap_non_null != def2.type.unwrap_non_null)
77
+ def_strings = resolved_subtypes.each_with_object([]) do |st, defs|
78
+ field = st.graphql_fields_by_name[field_name]
79
+ defs << "on #{st.name}:\n#{field.to_sdl.strip} mapping: #{field.resolve_mapping.inspect}" if st.graphql_fields_by_name.key?(field_name)
80
+ end
81
+
82
+ raise SchemaError,
83
+ "Conflicting definitions for field `#{field_name}` on the subtypes of `#{name}`. " \
84
+ "Their definitions must agree. Defs:\n\n#{def_strings.join("\n\n")}"
85
+ end
86
+
87
+ def1
88
+ end
89
+ end
90
+ end
91
+
92
+ def subtypes_indexed?
93
+ indexed_by_subtype_name = resolve_subtypes.to_h do |subtype, acc|
94
+ [subtype.name, subtype.indexed?]
95
+ end
96
+
97
+ uniq_indexed = indexed_by_subtype_name.values.uniq
98
+
99
+ if uniq_indexed.size > 1
100
+ descriptions = indexed_by_subtype_name.map do |name_value|
101
+ name, value = name_value
102
+ "#{name}: indexed? = #{value}"
103
+ end
104
+
105
+ raise SchemaError,
106
+ "The #{self.class.name} #{name} has some indexed subtypes, and some non-indexed subtypes. " \
107
+ "All subtypes must be indexed or all must NOT be indexed. Subtypes:\n" \
108
+ "#{descriptions.join("\n")}"
109
+ end
110
+
111
+ !!uniq_indexed.first
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end