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.
- 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
|