elasticgraph-schema_definition 0.18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +7 -0
  4. data/elasticgraph-schema_definition.gemspec +26 -0
  5. data/lib/elastic_graph/schema_definition/api.rb +359 -0
  6. data/lib/elastic_graph/schema_definition/factory.rb +506 -0
  7. data/lib/elastic_graph/schema_definition/indexing/derived_fields/append_only_set.rb +79 -0
  8. data/lib/elastic_graph/schema_definition/indexing/derived_fields/field_initializer_support.rb +59 -0
  9. data/lib/elastic_graph/schema_definition/indexing/derived_fields/immutable_value.rb +99 -0
  10. data/lib/elastic_graph/schema_definition/indexing/derived_fields/min_or_max_value.rb +62 -0
  11. data/lib/elastic_graph/schema_definition/indexing/derived_indexed_type.rb +346 -0
  12. data/lib/elastic_graph/schema_definition/indexing/event_envelope.rb +74 -0
  13. data/lib/elastic_graph/schema_definition/indexing/field.rb +181 -0
  14. data/lib/elastic_graph/schema_definition/indexing/field_reference.rb +51 -0
  15. data/lib/elastic_graph/schema_definition/indexing/field_type/enum.rb +65 -0
  16. data/lib/elastic_graph/schema_definition/indexing/field_type/object.rb +113 -0
  17. data/lib/elastic_graph/schema_definition/indexing/field_type/scalar.rb +51 -0
  18. data/lib/elastic_graph/schema_definition/indexing/field_type/union.rb +70 -0
  19. data/lib/elastic_graph/schema_definition/indexing/index.rb +318 -0
  20. data/lib/elastic_graph/schema_definition/indexing/json_schema_field_metadata.rb +34 -0
  21. data/lib/elastic_graph/schema_definition/indexing/json_schema_with_metadata.rb +234 -0
  22. data/lib/elastic_graph/schema_definition/indexing/list_counts_mapping.rb +53 -0
  23. data/lib/elastic_graph/schema_definition/indexing/relationship_resolver.rb +96 -0
  24. data/lib/elastic_graph/schema_definition/indexing/rollover_config.rb +25 -0
  25. data/lib/elastic_graph/schema_definition/indexing/update_target_factory.rb +54 -0
  26. data/lib/elastic_graph/schema_definition/indexing/update_target_resolver.rb +195 -0
  27. data/lib/elastic_graph/schema_definition/json_schema_pruner.rb +61 -0
  28. data/lib/elastic_graph/schema_definition/mixins/can_be_graphql_only.rb +31 -0
  29. data/lib/elastic_graph/schema_definition/mixins/has_derived_graphql_type_customizations.rb +119 -0
  30. data/lib/elastic_graph/schema_definition/mixins/has_directives.rb +65 -0
  31. data/lib/elastic_graph/schema_definition/mixins/has_documentation.rb +74 -0
  32. data/lib/elastic_graph/schema_definition/mixins/has_indices.rb +281 -0
  33. data/lib/elastic_graph/schema_definition/mixins/has_readable_to_s_and_inspect.rb +46 -0
  34. data/lib/elastic_graph/schema_definition/mixins/has_subtypes.rb +116 -0
  35. data/lib/elastic_graph/schema_definition/mixins/has_type_info.rb +181 -0
  36. data/lib/elastic_graph/schema_definition/mixins/implements_interfaces.rb +122 -0
  37. data/lib/elastic_graph/schema_definition/mixins/supports_default_value.rb +47 -0
  38. data/lib/elastic_graph/schema_definition/mixins/supports_filtering_and_aggregation.rb +267 -0
  39. data/lib/elastic_graph/schema_definition/mixins/verifies_graphql_name.rb +38 -0
  40. data/lib/elastic_graph/schema_definition/rake_tasks.rb +190 -0
  41. data/lib/elastic_graph/schema_definition/results.rb +404 -0
  42. data/lib/elastic_graph/schema_definition/schema_artifact_manager.rb +482 -0
  43. data/lib/elastic_graph/schema_definition/schema_elements/argument.rb +56 -0
  44. data/lib/elastic_graph/schema_definition/schema_elements/built_in_types.rb +1541 -0
  45. data/lib/elastic_graph/schema_definition/schema_elements/deprecated_element.rb +21 -0
  46. data/lib/elastic_graph/schema_definition/schema_elements/directive.rb +40 -0
  47. data/lib/elastic_graph/schema_definition/schema_elements/enum_type.rb +189 -0
  48. data/lib/elastic_graph/schema_definition/schema_elements/enum_value.rb +73 -0
  49. data/lib/elastic_graph/schema_definition/schema_elements/enum_value_namer.rb +89 -0
  50. data/lib/elastic_graph/schema_definition/schema_elements/enums_for_indexed_types.rb +82 -0
  51. data/lib/elastic_graph/schema_definition/schema_elements/field.rb +1085 -0
  52. data/lib/elastic_graph/schema_definition/schema_elements/field_path.rb +112 -0
  53. data/lib/elastic_graph/schema_definition/schema_elements/field_source.rb +16 -0
  54. data/lib/elastic_graph/schema_definition/schema_elements/graphql_sdl_enumerator.rb +113 -0
  55. data/lib/elastic_graph/schema_definition/schema_elements/input_field.rb +31 -0
  56. data/lib/elastic_graph/schema_definition/schema_elements/input_type.rb +60 -0
  57. data/lib/elastic_graph/schema_definition/schema_elements/interface_type.rb +72 -0
  58. data/lib/elastic_graph/schema_definition/schema_elements/list_counts_state.rb +40 -0
  59. data/lib/elastic_graph/schema_definition/schema_elements/object_type.rb +53 -0
  60. data/lib/elastic_graph/schema_definition/schema_elements/relationship.rb +218 -0
  61. data/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb +310 -0
  62. data/lib/elastic_graph/schema_definition/schema_elements/sort_order_enum_value.rb +36 -0
  63. data/lib/elastic_graph/schema_definition/schema_elements/sub_aggregation_path.rb +66 -0
  64. data/lib/elastic_graph/schema_definition/schema_elements/type_namer.rb +237 -0
  65. data/lib/elastic_graph/schema_definition/schema_elements/type_reference.rb +353 -0
  66. data/lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb +579 -0
  67. data/lib/elastic_graph/schema_definition/schema_elements/union_type.rb +157 -0
  68. data/lib/elastic_graph/schema_definition/scripting/file_system_repository.rb +77 -0
  69. data/lib/elastic_graph/schema_definition/scripting/script.rb +48 -0
  70. data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_day_of_week.painless +24 -0
  71. data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_time_of_day.painless +41 -0
  72. data/lib/elastic_graph/schema_definition/scripting/scripts/filter/by_time_of_day.painless +22 -0
  73. data/lib/elastic_graph/schema_definition/scripting/scripts/update/index_data.painless +93 -0
  74. data/lib/elastic_graph/schema_definition/state.rb +212 -0
  75. data/lib/elastic_graph/schema_definition/test_support.rb +113 -0
  76. metadata +513 -0
@@ -0,0 +1,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