elasticgraph-schema_definition 0.18.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +7 -0
- data/elasticgraph-schema_definition.gemspec +26 -0
- data/lib/elastic_graph/schema_definition/api.rb +359 -0
- data/lib/elastic_graph/schema_definition/factory.rb +506 -0
- data/lib/elastic_graph/schema_definition/indexing/derived_fields/append_only_set.rb +79 -0
- data/lib/elastic_graph/schema_definition/indexing/derived_fields/field_initializer_support.rb +59 -0
- data/lib/elastic_graph/schema_definition/indexing/derived_fields/immutable_value.rb +99 -0
- data/lib/elastic_graph/schema_definition/indexing/derived_fields/min_or_max_value.rb +62 -0
- data/lib/elastic_graph/schema_definition/indexing/derived_indexed_type.rb +346 -0
- data/lib/elastic_graph/schema_definition/indexing/event_envelope.rb +74 -0
- data/lib/elastic_graph/schema_definition/indexing/field.rb +181 -0
- data/lib/elastic_graph/schema_definition/indexing/field_reference.rb +51 -0
- data/lib/elastic_graph/schema_definition/indexing/field_type/enum.rb +65 -0
- data/lib/elastic_graph/schema_definition/indexing/field_type/object.rb +113 -0
- data/lib/elastic_graph/schema_definition/indexing/field_type/scalar.rb +51 -0
- data/lib/elastic_graph/schema_definition/indexing/field_type/union.rb +70 -0
- data/lib/elastic_graph/schema_definition/indexing/index.rb +318 -0
- data/lib/elastic_graph/schema_definition/indexing/json_schema_field_metadata.rb +34 -0
- data/lib/elastic_graph/schema_definition/indexing/json_schema_with_metadata.rb +234 -0
- data/lib/elastic_graph/schema_definition/indexing/list_counts_mapping.rb +53 -0
- data/lib/elastic_graph/schema_definition/indexing/relationship_resolver.rb +96 -0
- data/lib/elastic_graph/schema_definition/indexing/rollover_config.rb +25 -0
- data/lib/elastic_graph/schema_definition/indexing/update_target_factory.rb +54 -0
- data/lib/elastic_graph/schema_definition/indexing/update_target_resolver.rb +195 -0
- data/lib/elastic_graph/schema_definition/json_schema_pruner.rb +61 -0
- data/lib/elastic_graph/schema_definition/mixins/can_be_graphql_only.rb +31 -0
- data/lib/elastic_graph/schema_definition/mixins/has_derived_graphql_type_customizations.rb +119 -0
- data/lib/elastic_graph/schema_definition/mixins/has_directives.rb +65 -0
- data/lib/elastic_graph/schema_definition/mixins/has_documentation.rb +74 -0
- data/lib/elastic_graph/schema_definition/mixins/has_indices.rb +281 -0
- data/lib/elastic_graph/schema_definition/mixins/has_readable_to_s_and_inspect.rb +46 -0
- data/lib/elastic_graph/schema_definition/mixins/has_subtypes.rb +116 -0
- data/lib/elastic_graph/schema_definition/mixins/has_type_info.rb +181 -0
- data/lib/elastic_graph/schema_definition/mixins/implements_interfaces.rb +122 -0
- data/lib/elastic_graph/schema_definition/mixins/supports_default_value.rb +47 -0
- data/lib/elastic_graph/schema_definition/mixins/supports_filtering_and_aggregation.rb +267 -0
- data/lib/elastic_graph/schema_definition/mixins/verifies_graphql_name.rb +38 -0
- data/lib/elastic_graph/schema_definition/rake_tasks.rb +190 -0
- data/lib/elastic_graph/schema_definition/results.rb +404 -0
- data/lib/elastic_graph/schema_definition/schema_artifact_manager.rb +482 -0
- data/lib/elastic_graph/schema_definition/schema_elements/argument.rb +56 -0
- data/lib/elastic_graph/schema_definition/schema_elements/built_in_types.rb +1541 -0
- data/lib/elastic_graph/schema_definition/schema_elements/deprecated_element.rb +21 -0
- data/lib/elastic_graph/schema_definition/schema_elements/directive.rb +40 -0
- data/lib/elastic_graph/schema_definition/schema_elements/enum_type.rb +189 -0
- data/lib/elastic_graph/schema_definition/schema_elements/enum_value.rb +73 -0
- data/lib/elastic_graph/schema_definition/schema_elements/enum_value_namer.rb +89 -0
- data/lib/elastic_graph/schema_definition/schema_elements/enums_for_indexed_types.rb +82 -0
- data/lib/elastic_graph/schema_definition/schema_elements/field.rb +1085 -0
- data/lib/elastic_graph/schema_definition/schema_elements/field_path.rb +112 -0
- data/lib/elastic_graph/schema_definition/schema_elements/field_source.rb +16 -0
- data/lib/elastic_graph/schema_definition/schema_elements/graphql_sdl_enumerator.rb +113 -0
- data/lib/elastic_graph/schema_definition/schema_elements/input_field.rb +31 -0
- data/lib/elastic_graph/schema_definition/schema_elements/input_type.rb +60 -0
- data/lib/elastic_graph/schema_definition/schema_elements/interface_type.rb +72 -0
- data/lib/elastic_graph/schema_definition/schema_elements/list_counts_state.rb +40 -0
- data/lib/elastic_graph/schema_definition/schema_elements/object_type.rb +53 -0
- data/lib/elastic_graph/schema_definition/schema_elements/relationship.rb +218 -0
- data/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb +310 -0
- data/lib/elastic_graph/schema_definition/schema_elements/sort_order_enum_value.rb +36 -0
- data/lib/elastic_graph/schema_definition/schema_elements/sub_aggregation_path.rb +66 -0
- data/lib/elastic_graph/schema_definition/schema_elements/type_namer.rb +237 -0
- data/lib/elastic_graph/schema_definition/schema_elements/type_reference.rb +353 -0
- data/lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb +579 -0
- data/lib/elastic_graph/schema_definition/schema_elements/union_type.rb +157 -0
- data/lib/elastic_graph/schema_definition/scripting/file_system_repository.rb +77 -0
- data/lib/elastic_graph/schema_definition/scripting/script.rb +48 -0
- data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_day_of_week.painless +24 -0
- data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_time_of_day.painless +41 -0
- data/lib/elastic_graph/schema_definition/scripting/scripts/filter/by_time_of_day.painless +22 -0
- data/lib/elastic_graph/schema_definition/scripting/scripts/update/index_data.painless +93 -0
- data/lib/elastic_graph/schema_definition/state.rb +212 -0
- data/lib/elastic_graph/schema_definition/test_support.rb +113 -0
- 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
|