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,579 @@
|
|
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_artifacts/runtime_metadata/relation"
|
11
|
+
require "elastic_graph/schema_definition/indexing/field"
|
12
|
+
require "elastic_graph/schema_definition/indexing/field_type/object"
|
13
|
+
require "elastic_graph/schema_definition/mixins/can_be_graphql_only"
|
14
|
+
require "elastic_graph/schema_definition/mixins/has_derived_graphql_type_customizations"
|
15
|
+
require "elastic_graph/schema_definition/mixins/has_directives"
|
16
|
+
require "elastic_graph/schema_definition/mixins/has_documentation"
|
17
|
+
require "elastic_graph/schema_definition/mixins/has_type_info"
|
18
|
+
require "elastic_graph/schema_definition/mixins/supports_filtering_and_aggregation"
|
19
|
+
require "elastic_graph/schema_definition/mixins/verifies_graphql_name"
|
20
|
+
require "elastic_graph/schema_definition/schema_elements/list_counts_state"
|
21
|
+
|
22
|
+
module ElasticGraph
|
23
|
+
module SchemaDefinition
|
24
|
+
module SchemaElements
|
25
|
+
# Defines common functionality for all GraphQL types that have subfields:
|
26
|
+
#
|
27
|
+
# - {InputType}
|
28
|
+
# - {InterfaceType}
|
29
|
+
# - {ObjectType}
|
30
|
+
#
|
31
|
+
# @abstract
|
32
|
+
#
|
33
|
+
# @!attribute [rw] schema_kind
|
34
|
+
# @private
|
35
|
+
# @!attribute [rw] schema_def_state
|
36
|
+
# @private
|
37
|
+
# @!attribute [rw] type_ref
|
38
|
+
# @private
|
39
|
+
# @!attribute [rw] reserved_field_names
|
40
|
+
# @private
|
41
|
+
# @!attribute [rw] graphql_fields_by_name
|
42
|
+
# @private
|
43
|
+
# @!attribute [rw] indexing_fields_by_name_in_index
|
44
|
+
# @private
|
45
|
+
# @!attribute [rw] field_factory
|
46
|
+
# @private
|
47
|
+
# @!attribute [rw] wrapping_type
|
48
|
+
# @private
|
49
|
+
# @!attribute [rw] relay_pagination_type
|
50
|
+
# @private
|
51
|
+
class TypeWithSubfields < Struct.new(
|
52
|
+
:schema_kind, :schema_def_state, :type_ref, :reserved_field_names,
|
53
|
+
:graphql_fields_by_name, :indexing_fields_by_name_in_index, :field_factory,
|
54
|
+
:wrapping_type, :relay_pagination_type
|
55
|
+
)
|
56
|
+
prepend Mixins::VerifiesGraphQLName
|
57
|
+
include Mixins::CanBeGraphQLOnly
|
58
|
+
include Mixins::HasDocumentation
|
59
|
+
include Mixins::HasDirectives
|
60
|
+
include Mixins::HasDerivedGraphQLTypeCustomizations
|
61
|
+
include Mixins::HasTypeInfo
|
62
|
+
|
63
|
+
# The following methods are provided by `Struct.new`:
|
64
|
+
# @dynamic type_ref
|
65
|
+
|
66
|
+
# The following methods are provided by `SupportsFilteringAndAggregation`:
|
67
|
+
# @dynamic derived_graphql_types
|
68
|
+
|
69
|
+
# The following methods are provided by `CanBeGraphQLOnly`:
|
70
|
+
# @dynamic graphql_only?
|
71
|
+
|
72
|
+
# @private
|
73
|
+
def initialize(schema_kind, schema_def_state, name, wrapping_type:, field_factory:)
|
74
|
+
# `any_satisfy`, `any_of`/`all_of`, and `not` are "reserved" field names. They are reserved for usage by
|
75
|
+
# ElasticGraph itself in the `*FilterInput` types it generates. If we allow them to be used as field
|
76
|
+
# names, we'll run into conflicts when we later generate the `*FilterInput` type.
|
77
|
+
#
|
78
|
+
# Note that we don't have the same kind of conflict for the other filtering operators (e.g.
|
79
|
+
# `equal_to_any_of`, `gt`, etc) because on the generated filter structure, those are leaf
|
80
|
+
# nodes. They never exist alongside document field names on a filter type, but these do,
|
81
|
+
# so we have to guard against them here.
|
82
|
+
reserved_field_names = [
|
83
|
+
schema_def_state.schema_elements.all_of,
|
84
|
+
schema_def_state.schema_elements.any_of,
|
85
|
+
schema_def_state.schema_elements.any_satisfy,
|
86
|
+
schema_def_state.schema_elements.not
|
87
|
+
].to_set
|
88
|
+
|
89
|
+
# @type var graphql_fields_by_name: ::Hash[::String, Field]
|
90
|
+
graphql_fields_by_name = {}
|
91
|
+
# @type var indexing_fields_by_name_in_index: ::Hash[::String, Field]
|
92
|
+
indexing_fields_by_name_in_index = {}
|
93
|
+
|
94
|
+
super(
|
95
|
+
schema_kind,
|
96
|
+
schema_def_state,
|
97
|
+
schema_def_state.type_ref(name).to_final_form,
|
98
|
+
reserved_field_names,
|
99
|
+
graphql_fields_by_name,
|
100
|
+
indexing_fields_by_name_in_index,
|
101
|
+
field_factory,
|
102
|
+
wrapping_type,
|
103
|
+
false
|
104
|
+
)
|
105
|
+
|
106
|
+
yield self
|
107
|
+
end
|
108
|
+
|
109
|
+
# @return [String] the name of this GraphQL type
|
110
|
+
def name
|
111
|
+
type_ref.name
|
112
|
+
end
|
113
|
+
|
114
|
+
# Defines a [GraphQL field](https://spec.graphql.org/October2021/#sec-Language.Fields) on this type.
|
115
|
+
#
|
116
|
+
# @param name [String] name of the field
|
117
|
+
# @param type [String] type of the field as a [type reference](https://spec.graphql.org/October2021/#sec-Type-References). The named type must be
|
118
|
+
# one of {BuiltInTypes ElasticGraph's built-in types} or a type that has been defined in your schema.
|
119
|
+
# @param graphql_only [Boolean] if `true`, ElasticGraph will define the field as a GraphQL field but omit it from the indexing
|
120
|
+
# artifacts (`json_schemas.yaml` and `datastore_config.yaml`). This can be used along with `name_in_index` to support careful
|
121
|
+
# schema evolution.
|
122
|
+
# @param indexing_only [Boolean] if `true`, ElasticGraph will define the field for indexing (in the `json_schemas.yaml` and
|
123
|
+
# `datastore_config.yaml` schema artifact) but will omit it from the GraphQL schema. This can be useful to begin indexing a field
|
124
|
+
# before you expose it in GraphQL so that you can fully backfill it first.
|
125
|
+
# @option options [String] name_in_index the name of the field in the datastore index. Can be used to back a GraphQL field with a
|
126
|
+
# differently named field in the index.
|
127
|
+
# @option options [String] singular can be used on a list field (e.g. `t.field "tags", "[String!]!", singular: "tag"`) to tell
|
128
|
+
# ElasticGraph what the singular form of a field's name is. When provided, ElasticGraph will define a `groupedBy` field (using the
|
129
|
+
# singular form) allowing clients to group by individual values from the field.
|
130
|
+
# @option options [Boolean] aggregatable force-enables or disables the ability for aggregation queries to aggregate over this field.
|
131
|
+
# When not provided, ElasticGraph will infer field aggregatability based on the field's GraphQL type and mapping type.
|
132
|
+
# @option options [Boolean] filterable force-enables or disables the ability for queries to filter by this field. When not provided,
|
133
|
+
# ElasticGraph will infer field filterability based on the field's GraphQL type and mapping type.
|
134
|
+
# @option options [Boolean] groupable force-enables or disables the ability for aggregation queries to group by this field. When
|
135
|
+
# not provided, ElasticGraph will infer field groupability based on the field's GraphQL type and mapping type.
|
136
|
+
# @option options [Boolean] sortable force-enables or disables the ability for queries to sort by this field. When not provided,
|
137
|
+
# ElasticGraph will infer field sortability based on the field's GraphQL type and mapping type.
|
138
|
+
# @yield [Field] the field for further customization
|
139
|
+
# @return [void]
|
140
|
+
#
|
141
|
+
# @see #paginated_collection_field
|
142
|
+
# @see #relates_to_many
|
143
|
+
# @see #relates_to_one
|
144
|
+
#
|
145
|
+
# @note Be careful about defining non-nullable fields. Changing a field’s type from non-nullable (e.g. `Int!`) to nullable (e.g.
|
146
|
+
# `Int`) is a breaking change for clients. Making a field non-nullable may also prevent you from applying permissioning to a field
|
147
|
+
# via an AuthZ layer (as such a layer would have no way to force a field value to `null` when for a client denied field access).
|
148
|
+
# Therefore, we recommend limiting your use of `!` to only a few situations such as defining a type’s primary key (e.g.
|
149
|
+
# `t.field "id", "ID!"`) or defining a list field (e.g. `t.field "authors", "[String!]!"`) since empty lists already provide a
|
150
|
+
# "no data" representation. You can still configure the ElasticGraph indexer to require a non-null value for a field using
|
151
|
+
# `f.json_schema nullable: false`.
|
152
|
+
#
|
153
|
+
# @note ElasticGraph’s understanding of datastore capabilities may override your configured
|
154
|
+
# `aggregatable`/`filterable`/`groupable`/`sortable` options. For example, a field indexed as `text` for full text search will
|
155
|
+
# not be sortable or groupable even if you pass `sortable: true, groupable: true` when defining the field, because [text fields
|
156
|
+
# cannot be efficiently sorted by or grouped on](https://www.elastic.co/guide/en/elasticsearch/reference/8.15/text.html#text).
|
157
|
+
#
|
158
|
+
# @example Define a field with documentation
|
159
|
+
# ElasticGraph.define_schema do |schema|
|
160
|
+
# schema.object_type "Campaign" do |t|
|
161
|
+
# t.field "id", "ID" do |f|
|
162
|
+
# f.documentation "The Campaign's identifier."
|
163
|
+
# end
|
164
|
+
# end
|
165
|
+
# end
|
166
|
+
#
|
167
|
+
# @example Omit a new field from the GraphQL schema until its data has been backfilled
|
168
|
+
# ElasticGraph.define_schema do |schema|
|
169
|
+
# schema.object_type "Campaign" do |t|
|
170
|
+
# t.field "id", "ID"
|
171
|
+
#
|
172
|
+
# # TODO: remove `indexing_only: true` once the data for this field has been fully backfilled
|
173
|
+
# t.field "endDate", "Date", indexing_only: true
|
174
|
+
# end
|
175
|
+
# end
|
176
|
+
#
|
177
|
+
# @example Use `graphql_only` to introduce a new name for an existing field
|
178
|
+
# ElasticGraph.define_schema do |schema|
|
179
|
+
# schema.object_type "Campaign" do |t|
|
180
|
+
# t.field "id", "ID"
|
181
|
+
#
|
182
|
+
# t.field "endOn", "Date" do |f|
|
183
|
+
# f.directive "deprecated", reason: "Use `endDate` instead."
|
184
|
+
# end
|
185
|
+
#
|
186
|
+
# # We've decided we want to call the field `endDate` instead of `endOn`, but the data
|
187
|
+
# # for this field is currently indexed in `endOn`, so we can use `graphql_only` and
|
188
|
+
# # `name_in_index` to expose the existing data under a new field name.
|
189
|
+
# t.field "endDate", "Date", name_in_index: "endOn", graphql_only: true
|
190
|
+
# end
|
191
|
+
# end
|
192
|
+
def field(name, type, graphql_only: false, indexing_only: false, **options)
|
193
|
+
if reserved_field_names.include?(name)
|
194
|
+
raise SchemaError, "Invalid field name: `#{self.name}.#{name}`. `#{name}` is reserved for use by " \
|
195
|
+
"ElasticGraph as a filtering operator. To use it for a field name, add " \
|
196
|
+
"the `schema_element_name_overrides` option (on `ElasticGraph::SchemaDefinition::RakeTasks.new`) to " \
|
197
|
+
"configure an alternate name for the `#{name}` operator."
|
198
|
+
end
|
199
|
+
|
200
|
+
options = {name_in_index: nil}.merge(options) if graphql_only
|
201
|
+
|
202
|
+
field_factory.call(
|
203
|
+
name: name,
|
204
|
+
type: type,
|
205
|
+
graphql_only: graphql_only,
|
206
|
+
parent_type: wrapping_type,
|
207
|
+
**options
|
208
|
+
) do |field|
|
209
|
+
yield field if block_given?
|
210
|
+
|
211
|
+
unless indexing_only
|
212
|
+
register_field(field.name, field, graphql_fields_by_name, "GraphQL", :indexing_only)
|
213
|
+
end
|
214
|
+
|
215
|
+
unless graphql_only
|
216
|
+
register_field(field.name_in_index, field, indexing_fields_by_name_in_index, "indexing", :graphql_only) do |f|
|
217
|
+
f.to_indexing_field_reference
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# Registers the name of a field that existed in a prior version of the schema but has been deleted.
|
224
|
+
#
|
225
|
+
# @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
|
226
|
+
# or {Field#renamed_from}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
|
227
|
+
# the call to this method can be removed.
|
228
|
+
#
|
229
|
+
# @param field_name [String] name of field that used to exist but has been deleted
|
230
|
+
# @return [void]
|
231
|
+
#
|
232
|
+
# @example Indicate that `Widget.description` has been deleted
|
233
|
+
# ElasticGraph.define_schema do |schema|
|
234
|
+
# schema.object_type "Widget" do |t|
|
235
|
+
# t.deleted_field "description"
|
236
|
+
# end
|
237
|
+
# end
|
238
|
+
def deleted_field(field_name)
|
239
|
+
schema_def_state.register_deleted_field(
|
240
|
+
name,
|
241
|
+
field_name,
|
242
|
+
defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
|
243
|
+
defined_via: %(type.deleted_field "#{field_name}")
|
244
|
+
)
|
245
|
+
end
|
246
|
+
|
247
|
+
# Registers an old name that this type used to have in a prior version of the schema.
|
248
|
+
#
|
249
|
+
# @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
|
250
|
+
# or {API#deleted_type}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning indicating
|
251
|
+
# the call to this method can be removed.
|
252
|
+
#
|
253
|
+
# @param old_name [String] old name this field used to have in a prior version of the schema
|
254
|
+
# @return [void]
|
255
|
+
#
|
256
|
+
# @example Indicate that `Widget` used to be called `Component`.
|
257
|
+
# ElasticGraph.define_schema do |schema|
|
258
|
+
# schema.object_type "Widget" do |t|
|
259
|
+
# t.renamed_from "Component"
|
260
|
+
# end
|
261
|
+
# end
|
262
|
+
def renamed_from(old_name)
|
263
|
+
schema_def_state.register_renamed_type(
|
264
|
+
name,
|
265
|
+
from: old_name,
|
266
|
+
defined_at: caller_locations(2, 1).first, # : ::Thread::Backtrace::Location
|
267
|
+
defined_via: %(type.renamed_from "#{old_name}")
|
268
|
+
)
|
269
|
+
end
|
270
|
+
|
271
|
+
# An alternative to {#field} for when you have a list field that you want exposed as a [paginated Relay
|
272
|
+
# connection](https://relay.dev/graphql/connections.htm) rather than as a simple list.
|
273
|
+
#
|
274
|
+
# @note Bear in mind that pagination does not have much efficiency benefit in this case: all elements of the collection will be
|
275
|
+
# retrieved when fetching this field from the datastore. The pagination implementation will just trim down the collection before
|
276
|
+
# returning it.
|
277
|
+
#
|
278
|
+
# @param name [String] name of the field
|
279
|
+
# @param element_type [String] name of the type of element in the collection
|
280
|
+
# @param name_in_index [String] the name of the field in the datastore index. Can be used to back a GraphQL field with a
|
281
|
+
# differently named field in the index.
|
282
|
+
# @param singular [String] indicates what the singular form of a field's name is. When provided, ElasticGraph will define a
|
283
|
+
# `groupedBy` field (using the singular form) allowing clients to group by individual values from the field.
|
284
|
+
# @yield [Field] the field for further customization
|
285
|
+
# @return [void]
|
286
|
+
#
|
287
|
+
# @see #field
|
288
|
+
# @see #relates_to_many
|
289
|
+
# @see #relates_to_one
|
290
|
+
#
|
291
|
+
# @example Define `Author.books` as a paginated collection field
|
292
|
+
# ElasticGraph.define_schema do |schema|
|
293
|
+
# schema.object_type "Author" do |t|
|
294
|
+
# t.field "id", "ID"
|
295
|
+
# t.field "name", "String"
|
296
|
+
# t.paginated_collection_field "books", "String"
|
297
|
+
# t.index "authors"
|
298
|
+
# end
|
299
|
+
# end
|
300
|
+
def paginated_collection_field(name, element_type, name_in_index: name, singular: nil, &block)
|
301
|
+
element_type_ref = schema_def_state.type_ref(element_type).to_final_form
|
302
|
+
element_type = element_type_ref.name
|
303
|
+
|
304
|
+
schema_def_state.paginated_collection_element_types << element_type
|
305
|
+
|
306
|
+
backing_indexing_field = field(name, "[#{element_type}!]!", indexing_only: true, name_in_index: name_in_index, &block)
|
307
|
+
|
308
|
+
field(
|
309
|
+
name,
|
310
|
+
element_type_ref.as_connection.name,
|
311
|
+
name_in_index: name_in_index,
|
312
|
+
type_for_derived_types: "[#{element_type}]",
|
313
|
+
groupable: !!singular,
|
314
|
+
sortable: false,
|
315
|
+
graphql_only: true,
|
316
|
+
singular: singular,
|
317
|
+
backing_indexing_field: backing_indexing_field
|
318
|
+
) do |f|
|
319
|
+
f.define_relay_pagination_arguments!
|
320
|
+
block&.call(f)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# Defines a "has one" relationship between the current indexed type and another indexed type by defining a field clients
|
325
|
+
# can use to navigate across indexed types in a single GraphQL query.
|
326
|
+
#
|
327
|
+
# @param field_name [String] name of the relationship field
|
328
|
+
# @param type [String] name of the related type
|
329
|
+
# @param via [String] name of the foreign key field
|
330
|
+
# @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
|
331
|
+
# references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type and references the `id` of
|
332
|
+
# the related type.
|
333
|
+
# @yield [Relationship] the generated relationship fields, for further customization
|
334
|
+
# @return [void]
|
335
|
+
#
|
336
|
+
# @see #field
|
337
|
+
# @see #relates_to_many
|
338
|
+
#
|
339
|
+
# @example Use `relates_to_one` to define `Player.team`
|
340
|
+
# ElasticGraph.define_schema do |schema|
|
341
|
+
# schema.object_type "Team" do |t|
|
342
|
+
# t.field "id", "ID"
|
343
|
+
# t.field "name", "String"
|
344
|
+
# t.field "homeCity", "String"
|
345
|
+
# t.index "teams"
|
346
|
+
# end
|
347
|
+
#
|
348
|
+
# schema.object_type "Player" do |t|
|
349
|
+
# t.field "id", "ID"
|
350
|
+
# t.field "name", "String"
|
351
|
+
# t.relates_to_one "team", "Team", via: "teamId", dir: :out
|
352
|
+
# t.index "players"
|
353
|
+
# end
|
354
|
+
# end
|
355
|
+
def relates_to_one(field_name, type, via:, dir:, &block)
|
356
|
+
foreign_key_type = schema_def_state.type_ref(type).non_null? ? "ID!" : "ID"
|
357
|
+
relates_to(field_name, type, via: via, dir: dir, foreign_key_type: foreign_key_type, cardinality: :one, related_type: type, &block)
|
358
|
+
end
|
359
|
+
|
360
|
+
# Defines a "has many" relationship between the current indexed type and another indexed type by defining a pair of fields clients
|
361
|
+
# can use to navigate across indexed types in a single GraphQL query. The pair of generated fields will be [Relay Connection
|
362
|
+
# types](https://relay.dev/graphql/connections.htm#sec-Connection-Types) allowing you to filter, sort, paginate, and aggregated the
|
363
|
+
# related data.
|
364
|
+
#
|
365
|
+
# @param field_name [String] name of the relationship field
|
366
|
+
# @param type [String] name of the related type
|
367
|
+
# @param via [String] name of the foreign key field
|
368
|
+
# @param dir [:in, :out] direction of the foreign key. Use `:in` for an inbound foreign key that resides on the related type and
|
369
|
+
# references the `id` of this type. Use `:out` for an outbound foreign key that resides on this type and references the `id` of
|
370
|
+
# the related type.
|
371
|
+
# @param singular [String] singular form of the `field_name`; will be used (along with an `Aggregations` suffix) for the name of
|
372
|
+
# the generated aggregations field
|
373
|
+
# @yield [Relationship] the generated relationship fields, for further customization
|
374
|
+
# @return [void]
|
375
|
+
#
|
376
|
+
# @see #field
|
377
|
+
# @see #paginated_collection_field
|
378
|
+
# @see #relates_to_one
|
379
|
+
#
|
380
|
+
# @example Use `relates_to_many` to define `Team.players` and `Team.playerAggregations`
|
381
|
+
# ElasticGraph.define_schema do |schema|
|
382
|
+
# schema.object_type "Team" do |t|
|
383
|
+
# t.field "id", "ID"
|
384
|
+
# t.field "name", "String"
|
385
|
+
# t.field "homeCity", "String"
|
386
|
+
# t.relates_to_many "players", "Player", via: "teamId", dir: :in, singular: "player"
|
387
|
+
# t.index "teams"
|
388
|
+
# end
|
389
|
+
#
|
390
|
+
# schema.object_type "Player" do |t|
|
391
|
+
# t.field "id", "ID"
|
392
|
+
# t.field "name", "String"
|
393
|
+
# t.field "teamId", "ID"
|
394
|
+
# t.index "players"
|
395
|
+
# end
|
396
|
+
# end
|
397
|
+
def relates_to_many(field_name, type, via:, dir:, singular:)
|
398
|
+
foreign_key_type = (dir == :out) ? "[ID!]!" : "ID"
|
399
|
+
type_ref = schema_def_state.type_ref(type).to_final_form
|
400
|
+
|
401
|
+
relates_to(field_name, type_ref.as_connection.name, via: via, dir: dir, foreign_key_type: foreign_key_type, cardinality: :many, related_type: type) do |f|
|
402
|
+
f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
|
403
|
+
a.documentation "Used to filter the returned `#{field_name}` based on the provided criteria."
|
404
|
+
end
|
405
|
+
|
406
|
+
f.argument schema_def_state.schema_elements.order_by, "[#{type_ref.as_sort_order.name}!]" do |a|
|
407
|
+
a.documentation "Used to specify how the returned `#{field_name}` should be sorted."
|
408
|
+
end
|
409
|
+
|
410
|
+
f.define_relay_pagination_arguments!
|
411
|
+
|
412
|
+
yield f if block_given?
|
413
|
+
end
|
414
|
+
|
415
|
+
aggregations_name = schema_def_state.schema_elements.normalize_case("#{singular}_aggregations")
|
416
|
+
relates_to(aggregations_name, type_ref.as_aggregation.as_connection.name, via: via, dir: dir, foreign_key_type: foreign_key_type, cardinality: :many, related_type: type) do |f|
|
417
|
+
f.argument schema_def_state.schema_elements.filter, type_ref.as_filter_input.name do |a|
|
418
|
+
a.documentation "Used to filter the `#{type}` documents that get aggregated over based on the provided criteria."
|
419
|
+
end
|
420
|
+
|
421
|
+
f.define_relay_pagination_arguments!
|
422
|
+
|
423
|
+
yield f if block_given?
|
424
|
+
|
425
|
+
f.documentation f.derived_documentation("Aggregations over the `#{field_name}` data")
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
# Converts the type to GraphQL SDL syntax.
|
430
|
+
#
|
431
|
+
# @private
|
432
|
+
def to_sdl(&field_arg_selector)
|
433
|
+
generate_sdl(name_section: name, &field_arg_selector)
|
434
|
+
end
|
435
|
+
|
436
|
+
# @private
|
437
|
+
def generate_sdl(name_section:, &field_arg_selector)
|
438
|
+
<<~SDL
|
439
|
+
#{formatted_documentation}#{schema_kind} #{name_section} #{directives_sdl(suffix_with: " ")}{
|
440
|
+
#{fields_sdl(&field_arg_selector)}
|
441
|
+
}
|
442
|
+
SDL
|
443
|
+
end
|
444
|
+
|
445
|
+
# @private
|
446
|
+
def aggregated_values_type
|
447
|
+
schema_def_state.type_ref("NonNumeric").as_aggregated_values
|
448
|
+
end
|
449
|
+
|
450
|
+
# @private
|
451
|
+
def indexed?
|
452
|
+
false
|
453
|
+
end
|
454
|
+
|
455
|
+
# @private
|
456
|
+
def to_indexing_field_type
|
457
|
+
Indexing::FieldType::Object.new(
|
458
|
+
type_name: name,
|
459
|
+
subfields: indexing_fields_by_name_in_index.values.map(&:to_indexing_field).compact,
|
460
|
+
mapping_options: mapping_options,
|
461
|
+
json_schema_options: json_schema_options
|
462
|
+
)
|
463
|
+
end
|
464
|
+
|
465
|
+
# @private
|
466
|
+
def current_sources
|
467
|
+
indexing_fields_by_name_in_index.values.flat_map do |field|
|
468
|
+
child_field_sources = field.type.fully_unwrapped.as_object_type&.current_sources || []
|
469
|
+
[field.source&.relationship_name || SELF_RELATIONSHIP_NAME] + child_field_sources
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
# @private
|
474
|
+
def index_field_runtime_metadata_tuples(
|
475
|
+
# path from the overall document root
|
476
|
+
path_prefix: "",
|
477
|
+
# the source of the parent field
|
478
|
+
parent_source: SELF_RELATIONSHIP_NAME,
|
479
|
+
# tracks the state of the list counts field
|
480
|
+
list_counts_state: ListCountsState::INITIAL
|
481
|
+
)
|
482
|
+
indexing_fields_by_name_in_index.flat_map do |name, field|
|
483
|
+
path = path_prefix + name
|
484
|
+
source = field.source&.relationship_name || parent_source
|
485
|
+
index_field = SchemaArtifacts::RuntimeMetadata::IndexField.new(source: source)
|
486
|
+
|
487
|
+
list_count_field_tuples = field.paths_to_lists_for_count_indexing.map do |subpath|
|
488
|
+
[list_counts_state.path_to_count_subfield(subpath), index_field] # : [::String, SchemaArtifacts::RuntimeMetadata::IndexField]
|
489
|
+
end
|
490
|
+
|
491
|
+
if (object_type = field.type.fully_unwrapped.as_object_type)
|
492
|
+
new_list_counts_state =
|
493
|
+
if field.type.list? && field.nested?
|
494
|
+
ListCountsState.new_list_counts_field(at: "#{path}.#{LIST_COUNTS_FIELD}")
|
495
|
+
else
|
496
|
+
list_counts_state[name]
|
497
|
+
end
|
498
|
+
|
499
|
+
object_type.index_field_runtime_metadata_tuples(
|
500
|
+
path_prefix: "#{path}.",
|
501
|
+
parent_source: source,
|
502
|
+
list_counts_state: new_list_counts_state
|
503
|
+
)
|
504
|
+
else
|
505
|
+
[[path, index_field]] # : ::Array[[::String, SchemaArtifacts::RuntimeMetadata::IndexField]]
|
506
|
+
end + list_count_field_tuples
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
private
|
511
|
+
|
512
|
+
def fields_sdl(&arg_selector)
|
513
|
+
graphql_fields_by_name.values
|
514
|
+
.map { |f| f.to_sdl(&arg_selector) }
|
515
|
+
.flat_map { |sdl| sdl.split("\n") }
|
516
|
+
.join("\n ")
|
517
|
+
end
|
518
|
+
|
519
|
+
def register_field(name, field, registry, registry_type, only_option_to_fix, &to_comparable)
|
520
|
+
if (existing_field = registry[name])
|
521
|
+
field = Field.pick_most_accurate_from(field, existing_field, to_comparable: to_comparable || ->(f) { f }) do
|
522
|
+
raise SchemaError, "Duplicate #{registry_type} field on Type #{self.name}: #{name}. " \
|
523
|
+
"To resolve this, set `#{only_option_to_fix}: true` on one of the fields."
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
527
|
+
registry[name] = field
|
528
|
+
end
|
529
|
+
|
530
|
+
def relates_to(field_name, type, via:, dir:, foreign_key_type:, cardinality:, related_type:)
|
531
|
+
field(field_name, type, sortable: false, filterable: false, groupable: false, graphql_only: true) do |field|
|
532
|
+
relationship = schema_def_state.factory.new_relationship(
|
533
|
+
field,
|
534
|
+
cardinality: cardinality,
|
535
|
+
related_type: schema_def_state.type_ref(related_type).to_final_form,
|
536
|
+
foreign_key: via,
|
537
|
+
direction: dir
|
538
|
+
)
|
539
|
+
|
540
|
+
yield relationship if block_given?
|
541
|
+
|
542
|
+
field.relationship = relationship
|
543
|
+
field.runtime_metadata_graphql_field = field.runtime_metadata_graphql_field.with(relation: relationship.runtime_metadata)
|
544
|
+
|
545
|
+
if dir == :out
|
546
|
+
register_inferred_foreign_key_fields(from_type: [via, foreign_key_type], to_other: ["id", "ID!"], related_type: relationship.related_type)
|
547
|
+
else
|
548
|
+
register_inferred_foreign_key_fields(from_type: ["id", "ID!"], to_other: [via, foreign_key_type], related_type: relationship.related_type)
|
549
|
+
end
|
550
|
+
end
|
551
|
+
end
|
552
|
+
|
553
|
+
def register_inferred_foreign_key_fields(from_type:, to_other:, related_type:)
|
554
|
+
# The root `Query` object shouldn't have inferred foreign key fields (it's not indexed).
|
555
|
+
return if name.to_s == "Query"
|
556
|
+
|
557
|
+
from_field_name, from_type_name = from_type
|
558
|
+
field(from_field_name, from_type_name, indexing_only: true, accuracy_confidence: :medium)
|
559
|
+
|
560
|
+
# If it's a self-referential, we also should add a foreign key field for the other end of the relation.
|
561
|
+
if name == related_type.unwrap_non_null.name
|
562
|
+
# This must be `:low` confidence for cases where we have a self-referential type that goes both
|
563
|
+
# directions, such as:
|
564
|
+
#
|
565
|
+
# s.object_type "MyTypeBothDirections" do |t|
|
566
|
+
# t.relates_to_one "parent", "MyTypeBothDirections!", via: "children_ids", dir: :in
|
567
|
+
# t.relates_to_many "children", "MyTypeBothDirections", via: "children_ids", dir: :out
|
568
|
+
# end
|
569
|
+
#
|
570
|
+
# In such a circumstance, the `from_type` side may be more accurate (and will be defined on the `field`
|
571
|
+
# call above) and we want it preferred over this definition here.
|
572
|
+
to_field_name, to_type_name = to_other
|
573
|
+
field(to_field_name, to_type_name, indexing_only: true, accuracy_confidence: :low)
|
574
|
+
end
|
575
|
+
end
|
576
|
+
end
|
577
|
+
end
|
578
|
+
end
|
579
|
+
end
|