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,1085 @@
|
|
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 "delegate"
|
10
|
+
require "elastic_graph/constants"
|
11
|
+
require "elastic_graph/schema_definition/indexing/field"
|
12
|
+
require "elastic_graph/schema_definition/indexing/field_reference"
|
13
|
+
require "elastic_graph/schema_definition/mixins/has_directives"
|
14
|
+
require "elastic_graph/schema_definition/mixins/has_documentation"
|
15
|
+
require "elastic_graph/schema_definition/mixins/has_readable_to_s_and_inspect"
|
16
|
+
require "elastic_graph/schema_definition/mixins/has_type_info"
|
17
|
+
require "elastic_graph/schema_definition/mixins/verifies_graphql_name"
|
18
|
+
require "elastic_graph/support/graphql_formatter"
|
19
|
+
|
20
|
+
module ElasticGraph
|
21
|
+
module SchemaDefinition
|
22
|
+
module SchemaElements
|
23
|
+
# Represents a [GraphQL field](https://spec.graphql.org/October2021/#sec-Language.Fields).
|
24
|
+
#
|
25
|
+
# @example Define a GraphQL field
|
26
|
+
# ElasticGraph.define_schema do |schema|
|
27
|
+
# schema.object_type "Widget" do |t|
|
28
|
+
# t.field "id", "ID" do |f|
|
29
|
+
# # `f` in this block is a Field object
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# @!attribute [r] name
|
35
|
+
# @return [String] name of the field
|
36
|
+
# @!attribute [r] schema_def_state
|
37
|
+
# @return [State] schema definition state
|
38
|
+
# @!attribute [r] graphql_only
|
39
|
+
# @return [Boolean] true if this field exists only in the GraphQL schema and is not indexed
|
40
|
+
#
|
41
|
+
# @!attribute [rw] original_type
|
42
|
+
# @private
|
43
|
+
# @!attribute [rw] parent_type
|
44
|
+
# @private
|
45
|
+
# @!attribute [rw] original_type_for_derived_types
|
46
|
+
# @private
|
47
|
+
# @!attribute [rw] accuracy_confidence
|
48
|
+
# @private
|
49
|
+
# @!attribute [rw] filter_customizations
|
50
|
+
# @private
|
51
|
+
# @!attribute [rw] grouped_by_customizations
|
52
|
+
# @private
|
53
|
+
# @!attribute [rw] sub_aggregations_customizations
|
54
|
+
# @private
|
55
|
+
# @!attribute [rw] aggregated_values_customizations
|
56
|
+
# @private
|
57
|
+
# @!attribute [rw] sort_order_enum_value_customizations
|
58
|
+
# @private
|
59
|
+
# @!attribute [rw] args
|
60
|
+
# @private
|
61
|
+
# @!attribute [rw] sortable
|
62
|
+
# @private
|
63
|
+
# @!attribute [rw] filterable
|
64
|
+
# @private
|
65
|
+
# @!attribute [rw] aggregatable
|
66
|
+
# @private
|
67
|
+
# @!attribute [rw] groupable
|
68
|
+
# @private
|
69
|
+
# @!attribute [rw] source
|
70
|
+
# @private
|
71
|
+
# @!attribute [rw] runtime_field_script
|
72
|
+
# @private
|
73
|
+
# @!attribute [rw] relationship
|
74
|
+
# @private
|
75
|
+
# @!attribute [rw] singular_name
|
76
|
+
# @private
|
77
|
+
# @!attribute [rw] runtime_metadata_graphql_field
|
78
|
+
# @private
|
79
|
+
# @!attribute [rw] non_nullable_in_json_schema
|
80
|
+
# @private
|
81
|
+
# @!attribute [rw] backing_indexing_field
|
82
|
+
# @private
|
83
|
+
# @!attribute [rw] as_input
|
84
|
+
# @private
|
85
|
+
# @!attribute [rw] legacy_grouping_schema
|
86
|
+
# @private
|
87
|
+
class Field < Struct.new(
|
88
|
+
:name, :original_type, :parent_type, :original_type_for_derived_types, :schema_def_state, :accuracy_confidence,
|
89
|
+
:filter_customizations, :grouped_by_customizations, :sub_aggregations_customizations,
|
90
|
+
:aggregated_values_customizations, :sort_order_enum_value_customizations,
|
91
|
+
:args, :sortable, :filterable, :aggregatable, :groupable, :graphql_only, :source, :runtime_field_script, :relationship, :singular_name,
|
92
|
+
:runtime_metadata_graphql_field, :non_nullable_in_json_schema, :backing_indexing_field, :as_input,
|
93
|
+
:legacy_grouping_schema
|
94
|
+
)
|
95
|
+
include Mixins::HasDocumentation
|
96
|
+
include Mixins::HasDirectives
|
97
|
+
include Mixins::HasTypeInfo
|
98
|
+
include Mixins::HasReadableToSAndInspect.new { |f| "#{f.parent_type.name}.#{f.name}: #{f.type}" }
|
99
|
+
|
100
|
+
# @private
|
101
|
+
def initialize(
|
102
|
+
name:, type:, parent_type:, schema_def_state:,
|
103
|
+
accuracy_confidence: :high, name_in_index: name,
|
104
|
+
runtime_metadata_graphql_field: SchemaArtifacts::RuntimeMetadata::GraphQLField::EMPTY,
|
105
|
+
type_for_derived_types: nil, graphql_only: nil, singular: nil,
|
106
|
+
sortable: nil, filterable: nil, aggregatable: nil, groupable: nil,
|
107
|
+
backing_indexing_field: nil, as_input: false, legacy_grouping_schema: false
|
108
|
+
)
|
109
|
+
type_ref = schema_def_state.type_ref(type)
|
110
|
+
super(
|
111
|
+
name: name,
|
112
|
+
original_type: type_ref,
|
113
|
+
parent_type: parent_type,
|
114
|
+
original_type_for_derived_types: type_for_derived_types ? schema_def_state.type_ref(type_for_derived_types) : type_ref,
|
115
|
+
schema_def_state: schema_def_state,
|
116
|
+
accuracy_confidence: accuracy_confidence,
|
117
|
+
filter_customizations: [],
|
118
|
+
grouped_by_customizations: [],
|
119
|
+
sub_aggregations_customizations: [],
|
120
|
+
aggregated_values_customizations: [],
|
121
|
+
sort_order_enum_value_customizations: [],
|
122
|
+
args: {},
|
123
|
+
sortable: sortable,
|
124
|
+
filterable: filterable,
|
125
|
+
aggregatable: aggregatable,
|
126
|
+
groupable: groupable,
|
127
|
+
graphql_only: graphql_only,
|
128
|
+
source: nil,
|
129
|
+
runtime_field_script: nil,
|
130
|
+
# Note: we named the keyword argument `singular` (with no `_name` suffix) for consistency with
|
131
|
+
# other schema definition APIs, which also use `singular:` instead of `singular_name:`. We include
|
132
|
+
# the `_name` suffix on the attribute for clarity.
|
133
|
+
singular_name: singular,
|
134
|
+
runtime_metadata_graphql_field: runtime_metadata_graphql_field.with(name_in_index: name_in_index),
|
135
|
+
non_nullable_in_json_schema: false,
|
136
|
+
backing_indexing_field: backing_indexing_field,
|
137
|
+
as_input: as_input,
|
138
|
+
legacy_grouping_schema: legacy_grouping_schema
|
139
|
+
)
|
140
|
+
|
141
|
+
if name != name_in_index && name_in_index&.include?(".") && !graphql_only
|
142
|
+
raise SchemaError, "#{self} has an invalid `name_in_index`: #{name_in_index.inspect}. Only `graphql_only: true` fields can have a `name_in_index` that references a child field."
|
143
|
+
end
|
144
|
+
|
145
|
+
schema_def_state.register_user_defined_field(self)
|
146
|
+
yield self if block_given?
|
147
|
+
end
|
148
|
+
|
149
|
+
# @private
|
150
|
+
@@initialize_param_names = instance_method(:initialize).parameters.map(&:last).to_set
|
151
|
+
|
152
|
+
# must come after we capture the initialize params.
|
153
|
+
prepend Mixins::VerifiesGraphQLName
|
154
|
+
|
155
|
+
# @return [TypeReference] the type of this field
|
156
|
+
def type
|
157
|
+
# Here we lazily convert the `original_type` to an input type as needed. This must be lazy because
|
158
|
+
# the logic of `as_input` depends on detecting whether the type is an enum type, which it may not
|
159
|
+
# be able to do right away--we assume not if we can't tell, and retry every time this method is called.
|
160
|
+
original_type.to_final_form(as_input: as_input)
|
161
|
+
end
|
162
|
+
|
163
|
+
# @return [TypeReference] the type of the corresponding field on derived types (usually this is the same as {#type}).
|
164
|
+
#
|
165
|
+
# @private
|
166
|
+
def type_for_derived_types
|
167
|
+
original_type_for_derived_types.to_final_form(as_input: as_input)
|
168
|
+
end
|
169
|
+
|
170
|
+
# @note For each field defined in your schema that is filterable, a corresponding filtering field will be created on the
|
171
|
+
# `*FilterInput` type derived from the parent object type.
|
172
|
+
#
|
173
|
+
# Registers a customization callback that will be applied to the corresponding filtering field that will be generated for this
|
174
|
+
# field.
|
175
|
+
#
|
176
|
+
# @yield [Field] derived filtering field
|
177
|
+
# @return [void]
|
178
|
+
# @see #customize_aggregated_values_field
|
179
|
+
# @see #customize_grouped_by_field
|
180
|
+
# @see #customize_sort_order_enum_values
|
181
|
+
# @see #customize_sub_aggregations_field
|
182
|
+
# @see #on_each_generated_schema_element
|
183
|
+
#
|
184
|
+
# @example Mark `CampaignFilterInput.organizationId` with `@deprecated`
|
185
|
+
# ElasticGraph.define_schema do |schema|
|
186
|
+
# schema.object_type "Campaign" do |t|
|
187
|
+
# t.field "id", "ID"
|
188
|
+
#
|
189
|
+
# t.field "organizationId", "ID" do |f|
|
190
|
+
# f.customize_filter_field do |ff|
|
191
|
+
# ff.directive "deprecated"
|
192
|
+
# end
|
193
|
+
# end
|
194
|
+
#
|
195
|
+
# t.index "campaigns"
|
196
|
+
# end
|
197
|
+
# end
|
198
|
+
def customize_filter_field(&customization_block)
|
199
|
+
filter_customizations << customization_block
|
200
|
+
end
|
201
|
+
|
202
|
+
# @note For each field defined in your schema that is aggregatable, a corresponding `aggregatedValues` field will be created on the
|
203
|
+
# `*AggregatedValues` type derived from the parent object type.
|
204
|
+
#
|
205
|
+
# Registers a customization callback that will be applied to the corresponding `aggregatedValues` field that will be generated for
|
206
|
+
# this field.
|
207
|
+
#
|
208
|
+
# @yield [Field] derived aggregated values field
|
209
|
+
# @return [void]
|
210
|
+
# @see #customize_filter_field
|
211
|
+
# @see #customize_grouped_by_field
|
212
|
+
# @see #customize_sort_order_enum_values
|
213
|
+
# @see #customize_sub_aggregations_field
|
214
|
+
# @see #on_each_generated_schema_element
|
215
|
+
#
|
216
|
+
# @example Mark `CampaignAggregatedValues.adImpressions` with `@deprecated`
|
217
|
+
# ElasticGraph.define_schema do |schema|
|
218
|
+
# schema.object_type "Campaign" do |t|
|
219
|
+
# t.field "id", "ID"
|
220
|
+
#
|
221
|
+
# t.field "adImpressions", "Int" do |f|
|
222
|
+
# f.customize_aggregated_values_field do |avf|
|
223
|
+
# avf.directive "deprecated"
|
224
|
+
# end
|
225
|
+
# end
|
226
|
+
#
|
227
|
+
# t.index "campaigns"
|
228
|
+
# end
|
229
|
+
# end
|
230
|
+
def customize_aggregated_values_field(&customization_block)
|
231
|
+
aggregated_values_customizations << customization_block
|
232
|
+
end
|
233
|
+
|
234
|
+
# @note For each field defined in your schema that is groupable, a corresponding `groupedBy` field will be created on the
|
235
|
+
# `*AggregationGroupedBy` type derived from the parent object type.
|
236
|
+
#
|
237
|
+
# Registers a customization callback that will be applied to the corresponding `groupedBy` field that will be generated for this
|
238
|
+
# field.
|
239
|
+
#
|
240
|
+
# @yield [Field] derived grouped by field
|
241
|
+
# @return [void]
|
242
|
+
# @see #customize_aggregated_values_field
|
243
|
+
# @see #customize_filter_field
|
244
|
+
# @see #customize_sort_order_enum_values
|
245
|
+
# @see #customize_sub_aggregations_field
|
246
|
+
# @see #on_each_generated_schema_element
|
247
|
+
#
|
248
|
+
# @example Mark `CampaignGroupedBy.organizationId` with `@deprecated`
|
249
|
+
# ElasticGraph.define_schema do |schema|
|
250
|
+
# schema.object_type "Campaign" do |t|
|
251
|
+
# t.field "id", "ID"
|
252
|
+
#
|
253
|
+
# t.field "organizationId", "ID" do |f|
|
254
|
+
# f.customize_grouped_by_field do |gbf|
|
255
|
+
# gbf.directive "deprecated"
|
256
|
+
# end
|
257
|
+
# end
|
258
|
+
#
|
259
|
+
# t.index "campaigns"
|
260
|
+
# end
|
261
|
+
# end
|
262
|
+
def customize_grouped_by_field(&customization_block)
|
263
|
+
grouped_by_customizations << customization_block
|
264
|
+
end
|
265
|
+
|
266
|
+
# @note For each field defined in your schema that is sub-aggregatable (e.g. list fields indexed using the `nested` mapping type),
|
267
|
+
# a corresponding field will be created on the `*AggregationSubAggregations` type derived from the parent object type.
|
268
|
+
#
|
269
|
+
# Registers a customization callback that will be applied to the corresponding `subAggregations` field that will be generated for
|
270
|
+
# this field.
|
271
|
+
#
|
272
|
+
# @yield [Field] derived sub-aggregations field
|
273
|
+
# @return [void]
|
274
|
+
# @see #customize_aggregated_values_field
|
275
|
+
# @see #customize_filter_field
|
276
|
+
# @see #customize_grouped_by_field
|
277
|
+
# @see #customize_sort_order_enum_values
|
278
|
+
# @see #on_each_generated_schema_element
|
279
|
+
#
|
280
|
+
# @example Mark `TransactionAggregationSubAggregations.fees` with `@deprecated`
|
281
|
+
# ElasticGraph.define_schema do |schema|
|
282
|
+
# schema.object_type "Transaction" do |t|
|
283
|
+
# t.field "id", "ID"
|
284
|
+
#
|
285
|
+
# t.field "fees", "[Money!]!" do |f|
|
286
|
+
# f.mapping type: "nested"
|
287
|
+
#
|
288
|
+
# f.customize_sub_aggregations_field do |saf|
|
289
|
+
# # Adds a `@deprecated` directive to the `PaymentAggregationSubAggregations.fees`
|
290
|
+
# # field without also adding it to the `Payment.fees` field.
|
291
|
+
# saf.directive "deprecated"
|
292
|
+
# end
|
293
|
+
# end
|
294
|
+
#
|
295
|
+
# t.index "transactions"
|
296
|
+
# end
|
297
|
+
#
|
298
|
+
# schema.object_type "Money" do |t|
|
299
|
+
# t.field "amount", "Int"
|
300
|
+
# t.field "currency", "String"
|
301
|
+
# end
|
302
|
+
# end
|
303
|
+
def customize_sub_aggregations_field(&customization_block)
|
304
|
+
sub_aggregations_customizations << customization_block
|
305
|
+
end
|
306
|
+
|
307
|
+
# @note for each sortable field, enum values will be generated on the derived sort order enum type allowing you to
|
308
|
+
# sort by the field `ASC` or `DESC`.
|
309
|
+
#
|
310
|
+
# Registers a customization callback that will be applied to the corresponding enum values that will be generated for this field
|
311
|
+
# on the derived `SortOrder` enum type.
|
312
|
+
#
|
313
|
+
# @yield [SortOrderEnumValue] derived sort order enum value
|
314
|
+
# @return [void]
|
315
|
+
# @see #customize_aggregated_values_field
|
316
|
+
# @see #customize_filter_field
|
317
|
+
# @see #customize_grouped_by_field
|
318
|
+
# @see #customize_sub_aggregations_field
|
319
|
+
# @see #on_each_generated_schema_element
|
320
|
+
#
|
321
|
+
# @example Mark `CampaignSortOrder.organizationId_(ASC|DESC)` with `@deprecated`
|
322
|
+
# ElasticGraph.define_schema do |schema|
|
323
|
+
# schema.object_type "Campaign" do |t|
|
324
|
+
# t.field "id", "ID"
|
325
|
+
#
|
326
|
+
# t.field "organizationId", "ID" do |f|
|
327
|
+
# f.customize_sort_order_enum_values do |soev|
|
328
|
+
# soev.directive "deprecated"
|
329
|
+
# end
|
330
|
+
# end
|
331
|
+
#
|
332
|
+
# t.index "campaigns"
|
333
|
+
# end
|
334
|
+
# end
|
335
|
+
def customize_sort_order_enum_values(&customization_block)
|
336
|
+
sort_order_enum_value_customizations << customization_block
|
337
|
+
end
|
338
|
+
|
339
|
+
# When you define a {Field} on an {ObjectType} or {InterfaceType}, ElasticGraph generates up to 6 different GraphQL schema elements
|
340
|
+
# for it:
|
341
|
+
#
|
342
|
+
# * A {Field} is generated on the parent {ObjectType} or {InterfaceType} (that is, this field itself). This is used by clients to
|
343
|
+
# ask for values for the field in a response.
|
344
|
+
# * A {Field} may be generated on the `*FilterInput` {InputType} derived from the parent {ObjectType} or {InterfaceType}. This is
|
345
|
+
# used by clients to specify how the query should filter.
|
346
|
+
# * A {Field} may be generated on the `*AggregationGroupedBy` {ObjectType} derived from the parent {ObjectType} or {InterfaceType}.
|
347
|
+
# This is used by clients to specify how aggregations should be grouped.
|
348
|
+
# * A {Field} may be generated on the `*AggregatedValues` {ObjectType} derived from the parent {ObjectType} or {InterfaceType}.
|
349
|
+
# This is used by clients to apply aggregation functions (e.g. `sum`, `max`, `min`, etc) to a set of field values for a group.
|
350
|
+
# * A {Field} may be generated on the `*AggregationSubAggregations` {ObjectType} derived from the parent {ObjectType} or
|
351
|
+
# {InterfaceType}. This is used by clients to perform sub-aggregations on list fields indexed using the `nested` mapping type.
|
352
|
+
# * Multiple {EnumValue}s (both `*_ASC` and `*_DESC`) are generated on the `*SortOrder` {EnumType} derived from the parent indexed
|
353
|
+
# {ObjectType}. This is used by clients to sort by a field.
|
354
|
+
#
|
355
|
+
# This method registers a customization callback which is applied to every element that is generated for this field.
|
356
|
+
#
|
357
|
+
# @yield [Field, EnumValue] the schema element
|
358
|
+
# @return [void]
|
359
|
+
# @see #customize_aggregated_values_field
|
360
|
+
# @see #customize_filter_field
|
361
|
+
# @see #customize_grouped_by_field
|
362
|
+
# @see #customize_sort_order_enum_values
|
363
|
+
# @see #customize_sub_aggregations_field
|
364
|
+
#
|
365
|
+
# @example
|
366
|
+
# ElasticGraph.define_schema do |schema|
|
367
|
+
# schema.object_type "Transaction" do |t|
|
368
|
+
# t.field "id", "ID"
|
369
|
+
#
|
370
|
+
# t.field "amount", "Int" do |f|
|
371
|
+
# f.on_each_generated_schema_element do |element|
|
372
|
+
# # Adds a `@deprecated` directive to every GraphQL schema element generated for `amount`:
|
373
|
+
# #
|
374
|
+
# # - The `Transaction.amount` field.
|
375
|
+
# # - The `TransactionFilterInput.amount` field.
|
376
|
+
# # - The `TransactionAggregationGroupedBy.amount` field.
|
377
|
+
# # - The `TransactionAggregatedValues.amount` field.
|
378
|
+
# # - The `TransactionSortOrder.amount_ASC` and`TransactionSortOrder.amount_DESC` enum values.
|
379
|
+
# element.directive "deprecated"
|
380
|
+
# end
|
381
|
+
# end
|
382
|
+
#
|
383
|
+
# t.index "transactions"
|
384
|
+
# end
|
385
|
+
# end
|
386
|
+
def on_each_generated_schema_element(&customization_block)
|
387
|
+
customization_block.call(self)
|
388
|
+
customize_filter_field(&customization_block)
|
389
|
+
customize_aggregated_values_field(&customization_block)
|
390
|
+
customize_grouped_by_field(&customization_block)
|
391
|
+
customize_sub_aggregations_field(&customization_block)
|
392
|
+
customize_sort_order_enum_values(&customization_block)
|
393
|
+
end
|
394
|
+
|
395
|
+
# (see Mixins::HasTypeInfo#json_schema)
|
396
|
+
def json_schema(nullable: nil, **options)
|
397
|
+
if options.key?(:type)
|
398
|
+
raise SchemaError, "Cannot override JSON schema type of field `#{name}` with `#{options.fetch(:type)}`"
|
399
|
+
end
|
400
|
+
|
401
|
+
case nullable
|
402
|
+
when true
|
403
|
+
raise SchemaError, "`nullable: true` is not allowed on a field--just declare the GraphQL field as being nullable (no `!` suffix) instead."
|
404
|
+
when false
|
405
|
+
self.non_nullable_in_json_schema = true
|
406
|
+
end
|
407
|
+
|
408
|
+
super(**options)
|
409
|
+
end
|
410
|
+
|
411
|
+
# Configures ElasticGraph to source a field’s value from a related object. This can be used to denormalize data at ingestion time to
|
412
|
+
# support filtering, grouping, sorting, or aggregating data on a field from a related object.
|
413
|
+
#
|
414
|
+
# @param relationship [String] name of a relationship defined with {TypeWithSubfields#relates_to_one} using an inbound foreign key
|
415
|
+
# which contains the the field you wish to source values from
|
416
|
+
# @param field_path [String] dot-separated path to the field on the related type containing values that should be copied to this
|
417
|
+
# field
|
418
|
+
# @return [void]
|
419
|
+
#
|
420
|
+
# @example Source `City.currency` from `Country.currency`
|
421
|
+
# ElasticGraph.define_schema do |schema|
|
422
|
+
# schema.object_type "Country" do |t|
|
423
|
+
# t.field "id", "ID"
|
424
|
+
# t.field "name", "String"
|
425
|
+
# t.field "currency", "String"
|
426
|
+
# t.relates_to_one "capitalCity", "City", via: "capitalCityId", dir: :out
|
427
|
+
# t.index "countries"
|
428
|
+
# end
|
429
|
+
#
|
430
|
+
# schema.object_type "City" do |t|
|
431
|
+
# t.field "id", "ID"
|
432
|
+
# t.field "name", "String"
|
433
|
+
# t.relates_to_one "capitalOf", "Country", via: "capitalCityId", dir: :in
|
434
|
+
#
|
435
|
+
# t.field "currency", "String" do |f|
|
436
|
+
# f.sourced_from "capitalOf", "currency"
|
437
|
+
# end
|
438
|
+
#
|
439
|
+
# t.index "cities"
|
440
|
+
# end
|
441
|
+
# end
|
442
|
+
def sourced_from(relationship, field_path)
|
443
|
+
self.source = schema_def_state.factory.new_field_source(
|
444
|
+
relationship_name: relationship,
|
445
|
+
field_path: field_path
|
446
|
+
)
|
447
|
+
end
|
448
|
+
|
449
|
+
# @private
|
450
|
+
def runtime_script(script)
|
451
|
+
self.runtime_field_script = script
|
452
|
+
end
|
453
|
+
|
454
|
+
# Registers an old name that this field used to have in a prior version of the schema.
|
455
|
+
#
|
456
|
+
# @note In situations where this API applies, ElasticGraph will give you an error message indicating that you need to use this API
|
457
|
+
# or {TypeWithSubfields#deleted_field}. Likewise, when ElasticGraph no longer needs to know about this, it'll give you a warning
|
458
|
+
# indicating the call to this method can be removed.
|
459
|
+
#
|
460
|
+
# @param old_name [String] old name this field used to have in a prior version of the schema
|
461
|
+
# @return [void]
|
462
|
+
#
|
463
|
+
# @example Indicate that `Widget.description` used to be called `Widget.notes`.
|
464
|
+
# ElasticGraph.define_schema do |schema|
|
465
|
+
# schema.object_type "Widget" do |t|
|
466
|
+
# t.field "description", "String" do |f|
|
467
|
+
# f.renamed_from "notes"
|
468
|
+
# end
|
469
|
+
# end
|
470
|
+
# end
|
471
|
+
def renamed_from(old_name)
|
472
|
+
schema_def_state.register_renamed_field(
|
473
|
+
parent_type.name,
|
474
|
+
from: old_name,
|
475
|
+
to: name,
|
476
|
+
defined_at: caller_locations(1, 1).first, # : ::Thread::Backtrace::Location
|
477
|
+
defined_via: %(field.renamed_from "#{old_name}")
|
478
|
+
)
|
479
|
+
end
|
480
|
+
|
481
|
+
# @return [String] the name of this field in the datastore index
|
482
|
+
def name_in_index
|
483
|
+
runtime_metadata_graphql_field.name_in_index
|
484
|
+
end
|
485
|
+
|
486
|
+
# @private
|
487
|
+
def to_sdl(type_structure_only: false, default_value_sdl: nil, &arg_selector)
|
488
|
+
if type_structure_only
|
489
|
+
"#{name}#{args_sdl(joiner: ", ", &arg_selector)}: #{type.name}"
|
490
|
+
else
|
491
|
+
args_sdl = args_sdl(joiner: "\n ", after_opening_paren: "\n ", &arg_selector)
|
492
|
+
"#{formatted_documentation}#{name}#{args_sdl}: #{type.name}#{default_value_sdl} #{directives_sdl}".strip
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
# Indicates if this field is sortable. Sortable fields will have corresponding `_ASC` and `_DESC` values generated in the
|
497
|
+
# sort order {EnumType} of the parent indexed type.
|
498
|
+
#
|
499
|
+
# By default, the sortability is inferred by the field type and mapping. For example, list fields are not sortable,
|
500
|
+
# and fields mapped as `text` are not sortable either. Fields are sortable in most other cases.
|
501
|
+
#
|
502
|
+
# The `sortable: true` option can be used to force a field to be sortable.
|
503
|
+
#
|
504
|
+
# @return [Boolean] true if this field is sortable
|
505
|
+
def sortable?
|
506
|
+
return sortable unless sortable.nil?
|
507
|
+
|
508
|
+
# List fields are not sortable by default. We'd need to provide the datastore a sort mode option:
|
509
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html#_sort_mode_option
|
510
|
+
return false if type.list?
|
511
|
+
|
512
|
+
# Boolean fields are not sortable by default.
|
513
|
+
# - Boolean: sorting all falses before all trues (or whatever) is not generally interesting.
|
514
|
+
return false if type.unwrap_non_null.boolean?
|
515
|
+
|
516
|
+
# Elasticsearch/OpenSearch do not support sorting text fields:
|
517
|
+
# > Text fields are not used for sorting...
|
518
|
+
# (from https://www.elastic.co/guide/en/elasticsearch/reference/current/the datastore.html#text)
|
519
|
+
return false if text?
|
520
|
+
|
521
|
+
# If the type uses custom mapping type we don't know how if the datastore can sort by it, so we assume it's not sortable.
|
522
|
+
return false if type.as_object_type&.has_custom_mapping_type?
|
523
|
+
|
524
|
+
# Default every other field to being sortable.
|
525
|
+
true
|
526
|
+
end
|
527
|
+
|
528
|
+
# Indicates if this field is filterable. Filterable fields will be available in the GraphQL schema under the `filter` argument.
|
529
|
+
#
|
530
|
+
# Most fields are filterable, except when:
|
531
|
+
#
|
532
|
+
# - It's a relation. Relation fields require us to load the related data from another index and can't be filtered on.
|
533
|
+
# - The field is an object type that isn't itself filterable (e.g. due to having no filterable fields or whatever).
|
534
|
+
# - Explicitly disabled with `filterable: false`.
|
535
|
+
#
|
536
|
+
# @return [Boolean]
|
537
|
+
def filterable?
|
538
|
+
# Object types that use custom index mappings (as `GeoLocation` does) aren't filterable
|
539
|
+
# by default since we can't guess what datastore filtering capabilities they have. We've implemented
|
540
|
+
# filtering support for `GeoLocation` fields, though, so we need to explicitly make it fliterable here.
|
541
|
+
# TODO: clean this up using an interface instead of checking for `GeoLocation`.
|
542
|
+
return true if type.fully_unwrapped.name == "GeoLocation"
|
543
|
+
|
544
|
+
return false if relationship || type.fully_unwrapped.as_object_type&.does_not_support?(&:filterable?)
|
545
|
+
return true if filterable.nil?
|
546
|
+
filterable
|
547
|
+
end
|
548
|
+
|
549
|
+
# Indicates if this field is groupable. Groupable fields will be available under `groupedBy` for an aggregations query.
|
550
|
+
#
|
551
|
+
# Groupability is inferred based on the field type and mapping type, or you can use the `groupable: true` option to force it.
|
552
|
+
#
|
553
|
+
# @return [Boolean]
|
554
|
+
def groupable?
|
555
|
+
# If the groupability of the field was specified explicitly when the field was defined, use the specified value.
|
556
|
+
return groupable unless groupable.nil?
|
557
|
+
|
558
|
+
# We don't want the `id` field of an indexed type to be available to group by, because it's the unique primary key
|
559
|
+
# and the groupings would each contain one document. It's simpler and more efficient to just query the raw documents
|
560
|
+
# instead.
|
561
|
+
return false if parent_type.indexed? && name == "id"
|
562
|
+
|
563
|
+
return false if relationship || type.fully_unwrapped.as_object_type&.does_not_support?(&:groupable?)
|
564
|
+
|
565
|
+
# We don't support grouping an entire list of values, but we do support grouping on individual values in a list.
|
566
|
+
# However, we only do so when a `singular_name` has been provided (so that we know what to call the grouped_by field).
|
567
|
+
# The semantics are a little odd (since one document can be duplicated in multiple grouping buckets) so we're ok
|
568
|
+
# with not offering it by default--the user has to opt-in by telling us what to call the field in its singular form.
|
569
|
+
return list_field_groupable_by_single_values? if type.list? && type.fully_unwrapped.leaf?
|
570
|
+
|
571
|
+
# Nested fields will be supported through specific nested aggregation support, and do not
|
572
|
+
# work as expected when grouping on the root document type.
|
573
|
+
return false if nested?
|
574
|
+
|
575
|
+
# Text fields cannot be efficiently grouped on, so make them non-groupable by default.
|
576
|
+
return false if text?
|
577
|
+
|
578
|
+
# In all other cases, default to being groupable.
|
579
|
+
true
|
580
|
+
end
|
581
|
+
|
582
|
+
# Indicates if this field is aggregatable. Aggregatable fields will be available under `aggregatedValues` for an aggregations query.
|
583
|
+
#
|
584
|
+
# Aggregatability is inferred based on the field type and mapping type, or you can use the `aggregatable: true` option to force it.
|
585
|
+
#
|
586
|
+
# @return [Boolean]
|
587
|
+
def aggregatable?
|
588
|
+
return aggregatable unless aggregatable.nil?
|
589
|
+
return false if relationship
|
590
|
+
|
591
|
+
# We don't yet support aggregating over subfields of a `nested` field.
|
592
|
+
# TODO: add support for aggregating over subfields of `nested` fields.
|
593
|
+
return false if nested?
|
594
|
+
|
595
|
+
# Text fields are not efficiently aggregatable (and you'll often get errors from the datastore if you attempt to aggregate them).
|
596
|
+
return false if text?
|
597
|
+
|
598
|
+
type_for_derived_types.fully_unwrapped.as_object_type&.supports?(&:aggregatable?) || index_leaf?
|
599
|
+
end
|
600
|
+
|
601
|
+
# Indicates if this field can be used as the basis for a sub-aggregation. Sub-aggregatable fields will be available under
|
602
|
+
# `subAggregations` for an aggregations query.
|
603
|
+
#
|
604
|
+
# Only nested fields, and object fields which have nested fields, can be sub-aggregated.
|
605
|
+
#
|
606
|
+
# @return [Boolean]
|
607
|
+
def sub_aggregatable?
|
608
|
+
return false if relationship
|
609
|
+
|
610
|
+
nested? || type_for_derived_types.fully_unwrapped.as_object_type&.supports?(&:sub_aggregatable?)
|
611
|
+
end
|
612
|
+
|
613
|
+
# Defines an argument on the field.
|
614
|
+
#
|
615
|
+
# @note ElasticGraph takes care of defining arguments for all the query features it supports, so there is generally no need to use
|
616
|
+
# this API, and it has no way to interpret arbitrary arguments defined on a field. However, it can be useful for extensions that
|
617
|
+
# extend the {ElasticGraph::GraphQL} query engine. For example, {ElasticGraph::Apollo} uses this API to satisfy the [Apollo
|
618
|
+
# federation subgraph spec](https://www.apollographql.com/docs/federation/federation-spec/).
|
619
|
+
#
|
620
|
+
# @param name [String] name of the argument
|
621
|
+
# @param value_type [String] type of the argument in GraphQL SDL syntax
|
622
|
+
# @yield [Argument] for further customization
|
623
|
+
#
|
624
|
+
# @example Define an argument on a field
|
625
|
+
# ElasticGraph.define_schema do |schema|
|
626
|
+
# schema.object_type "Product" do |t|
|
627
|
+
# t.field "name", "String" do |f|
|
628
|
+
# f.argument "language", "String"
|
629
|
+
# end
|
630
|
+
# end
|
631
|
+
# end
|
632
|
+
def argument(name, value_type, &block)
|
633
|
+
args[name] = schema_def_state.factory.new_argument(
|
634
|
+
self,
|
635
|
+
name,
|
636
|
+
schema_def_state.type_ref(value_type),
|
637
|
+
&block
|
638
|
+
)
|
639
|
+
end
|
640
|
+
|
641
|
+
# The index mapping type in effect for this field. This could come from either the field definition or from the type definition.
|
642
|
+
#
|
643
|
+
# @return [String]
|
644
|
+
def mapping_type
|
645
|
+
backing_indexing_field&.mapping_type || (resolve_mapping || {})["type"]
|
646
|
+
end
|
647
|
+
|
648
|
+
# @private
|
649
|
+
def list_field_groupable_by_single_values?
|
650
|
+
(type.list? || backing_indexing_field&.type&.list?) && !singular_name.nil?
|
651
|
+
end
|
652
|
+
|
653
|
+
# @private
|
654
|
+
def define_aggregated_values_field(parent_type)
|
655
|
+
return unless aggregatable?
|
656
|
+
|
657
|
+
unwrapped_type_for_derived_types = type_for_derived_types.fully_unwrapped
|
658
|
+
aggregated_values_type =
|
659
|
+
if index_leaf?
|
660
|
+
unwrapped_type_for_derived_types.resolved.aggregated_values_type
|
661
|
+
else
|
662
|
+
unwrapped_type_for_derived_types.as_aggregated_values
|
663
|
+
end
|
664
|
+
|
665
|
+
parent_type.field name, aggregated_values_type.name, name_in_index: name_in_index, graphql_only: true do |f|
|
666
|
+
f.documentation derived_documentation("Computed aggregate values for the `#{name}` field")
|
667
|
+
aggregated_values_customizations.each { |block| block.call(f) }
|
668
|
+
end
|
669
|
+
end
|
670
|
+
|
671
|
+
# @private
|
672
|
+
def define_grouped_by_field(parent_type)
|
673
|
+
return unless (field_name = grouped_by_field_name)
|
674
|
+
|
675
|
+
parent_type.field field_name, grouped_by_field_type_name, name_in_index: name_in_index, graphql_only: true do |f|
|
676
|
+
add_grouped_by_field_documentation(f)
|
677
|
+
|
678
|
+
define_legacy_timestamp_grouping_arguments_if_needed(f) if legacy_grouping_schema
|
679
|
+
|
680
|
+
grouped_by_customizations.each { |block| block.call(f) }
|
681
|
+
end
|
682
|
+
end
|
683
|
+
|
684
|
+
# @private
|
685
|
+
def grouped_by_field_type_name
|
686
|
+
unwrapped_type = type_for_derived_types.fully_unwrapped
|
687
|
+
if unwrapped_type.scalar_type_needing_grouped_by_object? && !legacy_grouping_schema
|
688
|
+
unwrapped_type.with_reverted_override.as_grouped_by.name
|
689
|
+
elsif unwrapped_type.leaf?
|
690
|
+
unwrapped_type.name
|
691
|
+
else
|
692
|
+
unwrapped_type.as_grouped_by.name
|
693
|
+
end
|
694
|
+
end
|
695
|
+
|
696
|
+
# @private
|
697
|
+
def add_grouped_by_field_documentation(field)
|
698
|
+
text = if list_field_groupable_by_single_values?
|
699
|
+
derived_documentation(
|
700
|
+
"The individual value from `#{name}` for this group",
|
701
|
+
list_field_grouped_by_doc_note("`#{name}`")
|
702
|
+
)
|
703
|
+
elsif type.list? && type.fully_unwrapped.object?
|
704
|
+
derived_documentation(
|
705
|
+
"The `#{name}` field value for this group",
|
706
|
+
list_field_grouped_by_doc_note("the selected subfields of `#{name}`")
|
707
|
+
)
|
708
|
+
elsif type_for_derived_types.fully_unwrapped.scalar_type_needing_grouped_by_object? && !legacy_grouping_schema
|
709
|
+
derived_documentation("Offers the different grouping options for the `#{name}` value within this group")
|
710
|
+
else
|
711
|
+
derived_documentation("The `#{name}` field value for this group")
|
712
|
+
end
|
713
|
+
|
714
|
+
field.documentation text
|
715
|
+
end
|
716
|
+
|
717
|
+
# @private
|
718
|
+
def grouped_by_field_name
|
719
|
+
return nil unless groupable?
|
720
|
+
list_field_groupable_by_single_values? ? singular_name : name
|
721
|
+
end
|
722
|
+
|
723
|
+
# @private
|
724
|
+
def define_sub_aggregations_field(parent_type:, type:)
|
725
|
+
parent_type.field name, type, name_in_index: name_in_index, graphql_only: true do |f|
|
726
|
+
f.documentation derived_documentation("Used to perform a sub-aggregation of `#{name}`")
|
727
|
+
sub_aggregations_customizations.each { |c| c.call(f) }
|
728
|
+
|
729
|
+
yield f if block_given?
|
730
|
+
end
|
731
|
+
end
|
732
|
+
|
733
|
+
# @private
|
734
|
+
def to_filter_field(parent_type:, for_single_value: !type_for_derived_types.list?)
|
735
|
+
type_prefix = text? ? "Text" : type_for_derived_types.fully_unwrapped.name
|
736
|
+
filter_type = schema_def_state
|
737
|
+
.type_ref(type_prefix)
|
738
|
+
.as_static_derived_type(filter_field_category(for_single_value))
|
739
|
+
.name
|
740
|
+
|
741
|
+
params = to_h
|
742
|
+
.slice(*@@initialize_param_names)
|
743
|
+
.merge(type: filter_type, parent_type: parent_type, name_in_index: name_in_index, type_for_derived_types: nil)
|
744
|
+
|
745
|
+
schema_def_state.factory.new_field(**params).tap do |f|
|
746
|
+
f.documentation derived_documentation(
|
747
|
+
"Used to filter on the `#{name}` field",
|
748
|
+
"Will be ignored if `null` or an empty object is passed"
|
749
|
+
)
|
750
|
+
|
751
|
+
filter_customizations.each { |c| c.call(f) }
|
752
|
+
end
|
753
|
+
end
|
754
|
+
|
755
|
+
# @private
|
756
|
+
def define_relay_pagination_arguments!
|
757
|
+
argument schema_def_state.schema_elements.first.to_sym, "Int" do |a|
|
758
|
+
a.documentation <<~EOS
|
759
|
+
Used in conjunction with the `after` argument to forward-paginate through the `#{name}`.
|
760
|
+
When provided, limits the number of returned results to the first `n` after the provided
|
761
|
+
`after` cursor (or from the start of the `#{name}`, if no `after` cursor is provided).
|
762
|
+
|
763
|
+
See the [Relay GraphQL Cursor Connections
|
764
|
+
Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info.
|
765
|
+
EOS
|
766
|
+
end
|
767
|
+
|
768
|
+
argument schema_def_state.schema_elements.after.to_sym, "Cursor" do |a|
|
769
|
+
a.documentation <<~EOS
|
770
|
+
Used to forward-paginate through the `#{name}`. When provided, the next page after the
|
771
|
+
provided cursor will be returned.
|
772
|
+
|
773
|
+
See the [Relay GraphQL Cursor Connections
|
774
|
+
Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info.
|
775
|
+
EOS
|
776
|
+
end
|
777
|
+
|
778
|
+
argument schema_def_state.schema_elements.last.to_sym, "Int" do |a|
|
779
|
+
a.documentation <<~EOS
|
780
|
+
Used in conjunction with the `before` argument to backward-paginate through the `#{name}`.
|
781
|
+
When provided, limits the number of returned results to the last `n` before the provided
|
782
|
+
`before` cursor (or from the end of the `#{name}`, if no `before` cursor is provided).
|
783
|
+
|
784
|
+
See the [Relay GraphQL Cursor Connections
|
785
|
+
Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info.
|
786
|
+
EOS
|
787
|
+
end
|
788
|
+
|
789
|
+
argument schema_def_state.schema_elements.before.to_sym, "Cursor" do |a|
|
790
|
+
a.documentation <<~EOS
|
791
|
+
Used to backward-paginate through the `#{name}`. When provided, the previous page before the
|
792
|
+
provided cursor will be returned.
|
793
|
+
|
794
|
+
See the [Relay GraphQL Cursor Connections
|
795
|
+
Specification](https://relay.dev/graphql/connections.htm#sec-Arguments) for more info.
|
796
|
+
EOS
|
797
|
+
end
|
798
|
+
end
|
799
|
+
|
800
|
+
# Converts this field to an `Indexing::FieldReference`, which contains all the attributes involved
|
801
|
+
# in building an `Indexing::Field`. Notably, we cannot actually convert to an `Indexing::Field` at
|
802
|
+
# the point this method is called, because the referenced field type may not have been defined
|
803
|
+
# yet. We don't need an actual `Indexing::Field` until the very end of the schema definition process,
|
804
|
+
# when we are dumping the artifacts. However, we need this at field definition time so that we
|
805
|
+
# can correctly detect duplicate indexing field issues when a field is defined. (This is used
|
806
|
+
# in `TypeWithSubfields#field`).
|
807
|
+
#
|
808
|
+
# @private
|
809
|
+
def to_indexing_field_reference
|
810
|
+
return nil if graphql_only
|
811
|
+
|
812
|
+
Indexing::FieldReference.new(
|
813
|
+
name: name,
|
814
|
+
name_in_index: name_in_index,
|
815
|
+
type: non_nullable_in_json_schema ? type.wrap_non_null : type,
|
816
|
+
mapping_options: mapping_options,
|
817
|
+
json_schema_options: json_schema_options,
|
818
|
+
accuracy_confidence: accuracy_confidence,
|
819
|
+
source: source,
|
820
|
+
runtime_field_script: runtime_field_script
|
821
|
+
)
|
822
|
+
end
|
823
|
+
|
824
|
+
# Converts this field to its `IndexingField` form.
|
825
|
+
#
|
826
|
+
# @private
|
827
|
+
def to_indexing_field
|
828
|
+
to_indexing_field_reference&.resolve
|
829
|
+
end
|
830
|
+
|
831
|
+
# @private
|
832
|
+
def resolve_mapping
|
833
|
+
to_indexing_field&.mapping
|
834
|
+
end
|
835
|
+
|
836
|
+
# Returns the string paths to the list fields that we need to index counts for.
|
837
|
+
# We do this to support the ability to filter on the size of a list.
|
838
|
+
#
|
839
|
+
# @private
|
840
|
+
def paths_to_lists_for_count_indexing(has_list_ancestor: false)
|
841
|
+
self_path = (has_list_ancestor || type.list?) ? [name_in_index] : []
|
842
|
+
|
843
|
+
nested_paths =
|
844
|
+
# Nested fields get indexed as separate hidden documents:
|
845
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/8.8/nested.html
|
846
|
+
#
|
847
|
+
# Given that, the counts of any `nested` list subfields will go in a `__counts` field on the
|
848
|
+
# separate hidden document.
|
849
|
+
if !nested? && (object_type = type.fully_unwrapped.as_object_type)
|
850
|
+
object_type.indexing_fields_by_name_in_index.values.flat_map do |sub_field|
|
851
|
+
sub_field.paths_to_lists_for_count_indexing(has_list_ancestor: has_list_ancestor || type.list?).map do |sub_path|
|
852
|
+
"#{name_in_index}#{LIST_COUNTS_FIELD_PATH_KEY_SEPARATOR}#{sub_path}"
|
853
|
+
end
|
854
|
+
end
|
855
|
+
else
|
856
|
+
[]
|
857
|
+
end
|
858
|
+
|
859
|
+
self_path + nested_paths
|
860
|
+
end
|
861
|
+
|
862
|
+
# Indicates if this field is a leaf value in the index. Note that GraphQL leaf values
|
863
|
+
# are always leaf values in the index but the inverse is not always true. For example,
|
864
|
+
# a `GeoLocation` field is not a leaf in GraphQL (because `GeoLocation` is an object
|
865
|
+
# type with subfields) but in the index we use a single `geo_point` mapping type, which
|
866
|
+
# is a single unit, so we consider it an index leaf.
|
867
|
+
#
|
868
|
+
# @private
|
869
|
+
def index_leaf?
|
870
|
+
type_for_derived_types.fully_unwrapped.leaf? || DATASTORE_PROPERTYLESS_OBJECT_TYPES.include?(mapping_type)
|
871
|
+
end
|
872
|
+
|
873
|
+
# @private
|
874
|
+
ACCURACY_SCORES = {
|
875
|
+
# :high is assigned to `Field`s that are generated directly from GraphQL fields or :extra_fields.
|
876
|
+
# For these, we know everything available to us in the schema about them.
|
877
|
+
high: 3,
|
878
|
+
|
879
|
+
# :medium is assigned to `Field`s that are inferred from the id fields required by a relation.
|
880
|
+
# We make logical guesses about the `indexing_field_type` but if the field is also manually defined,
|
881
|
+
# it could be slightly different (e.g. additional json schema validations), so we have medium
|
882
|
+
# confidence of these.
|
883
|
+
medium: 2,
|
884
|
+
|
885
|
+
# :low is assigned to the ElastcField inferred for the foreign key of an inbound relation. The
|
886
|
+
# nullability/cardinality of the foreign key field cannot be known from the relation metadata,
|
887
|
+
# so we just guess what seems safest (`[:nullable]`). If the field is defined another way
|
888
|
+
# we should prefer it, so we give these fields :low confidence.
|
889
|
+
low: 1
|
890
|
+
}
|
891
|
+
|
892
|
+
# Given two fields, picks the one that is most accurate. If they have the same accuracy
|
893
|
+
# confidence, yields to a block to force it to deal with the discrepancy, unless the fields
|
894
|
+
# are exactly equal (in which case we can return either).
|
895
|
+
#
|
896
|
+
# @private
|
897
|
+
def self.pick_most_accurate_from(field1, field2, to_comparable: ->(it) { it })
|
898
|
+
return field1 if to_comparable.call(field1) == to_comparable.call(field2)
|
899
|
+
yield if field1.accuracy_confidence == field2.accuracy_confidence
|
900
|
+
# Array#max_by can return nil (when called on an empty array), but our steep type is non-nil.
|
901
|
+
# Since it's not smart enough to realize the non-empty-array-usage of `max_by` won't return nil,
|
902
|
+
# we have to cast it to untyped here.
|
903
|
+
_ = [field1, field2].max_by { |f| ACCURACY_SCORES.fetch(f.accuracy_confidence) }
|
904
|
+
end
|
905
|
+
|
906
|
+
# Indicates if the field uses the `nested` mapping type.
|
907
|
+
#
|
908
|
+
# @private
|
909
|
+
def nested?
|
910
|
+
mapping_type == "nested"
|
911
|
+
end
|
912
|
+
|
913
|
+
private
|
914
|
+
|
915
|
+
def args_sdl(joiner:, after_opening_paren: "", &arg_selector)
|
916
|
+
selected_args = args.values.select(&arg_selector)
|
917
|
+
args_sdl = selected_args.map(&:to_sdl).flat_map { |s| s.split("\n") }.join(joiner)
|
918
|
+
return nil if args_sdl.empty?
|
919
|
+
"(#{after_opening_paren}#{args_sdl})"
|
920
|
+
end
|
921
|
+
|
922
|
+
# Indicates if the field uses the `text` mapping type.
|
923
|
+
def text?
|
924
|
+
mapping_type == "text"
|
925
|
+
end
|
926
|
+
|
927
|
+
def define_legacy_timestamp_grouping_arguments_if_needed(grouping_field)
|
928
|
+
case type.fully_unwrapped.name
|
929
|
+
when "Date"
|
930
|
+
grouping_field.argument schema_def_state.schema_elements.granularity, "DateGroupingGranularity!" do |a|
|
931
|
+
a.documentation "Determines the grouping granularity for this field."
|
932
|
+
end
|
933
|
+
|
934
|
+
grouping_field.argument schema_def_state.schema_elements.offset_days, "Int" do |a|
|
935
|
+
a.documentation <<~EOS
|
936
|
+
Number of days (positive or negative) to shift the `Date` boundaries of each date grouping bucket.
|
937
|
+
|
938
|
+
For example, when grouping by `YEAR`, this can be used to align the buckets with fiscal or school years instead of calendar years.
|
939
|
+
EOS
|
940
|
+
end
|
941
|
+
when "DateTime"
|
942
|
+
grouping_field.argument schema_def_state.schema_elements.granularity, "DateTimeGroupingGranularity!" do |a|
|
943
|
+
a.documentation "Determines the grouping granularity for this field."
|
944
|
+
end
|
945
|
+
|
946
|
+
grouping_field.argument schema_def_state.schema_elements.time_zone, "TimeZone" do |a|
|
947
|
+
a.documentation "The time zone to use when determining which grouping a `DateTime` value falls in."
|
948
|
+
a.default "UTC"
|
949
|
+
end
|
950
|
+
|
951
|
+
grouping_field.argument schema_def_state.schema_elements.offset, "DateTimeGroupingOffsetInput" do |a|
|
952
|
+
a.documentation <<~EOS
|
953
|
+
Amount of offset (positive or negative) to shift the `DateTime` boundaries of each grouping bucket.
|
954
|
+
|
955
|
+
For example, when grouping by `WEEK`, you can shift by 24 hours to change what day-of-week weeks are considered to start on.
|
956
|
+
EOS
|
957
|
+
end
|
958
|
+
end
|
959
|
+
end
|
960
|
+
|
961
|
+
def list_field_grouped_by_doc_note(individual_value_selection_description)
|
962
|
+
<<~EOS.strip
|
963
|
+
Note: `#{name}` is a collection field, but selecting this field will group on individual values of #{individual_value_selection_description}.
|
964
|
+
That means that a document may be grouped into multiple aggregation groupings (i.e. when its `#{name}`
|
965
|
+
field has multiple values) leading to some data duplication in the response. However, if a value shows
|
966
|
+
up in `#{name}` multiple times for a single document, that document will only be included in the group
|
967
|
+
once
|
968
|
+
EOS
|
969
|
+
end
|
970
|
+
|
971
|
+
# Determines the suffix of the filter field derived for this field. The suffix used determines
|
972
|
+
# the filtering capabilities (e.g. filtering on a single value vs a list of values with `any_satisfy`).
|
973
|
+
def filter_field_category(for_single_value)
|
974
|
+
return :filter_input if for_single_value
|
975
|
+
|
976
|
+
# For an index leaf field, there are no further nesting paths to traverse. We want to directly
|
977
|
+
# use a `ListFilterInput` type (e.g. `IntListFilterInput`) to offer `any_satisfy` filtering at this level.
|
978
|
+
return :list_filter_input if index_leaf?
|
979
|
+
|
980
|
+
# If it's a list-of-objects field we require the user to tell us what mapping type they want to
|
981
|
+
# use, which determines the suffix (and is handled below). Otherwise, we want to use `FieldsListFilterInput`.
|
982
|
+
# We are within a list filtering context (as indicated by `for_single_value` being false) without
|
983
|
+
# being at an index leaf field, so we must use `FieldsListFilterInput` as there are further nesting paths
|
984
|
+
# on the document and we want to provide `any_satisfy` at the leaf fields.
|
985
|
+
return :fields_list_filter_input unless type_for_derived_types.list?
|
986
|
+
|
987
|
+
case mapping_type
|
988
|
+
when "nested" then :list_filter_input
|
989
|
+
when "object" then :fields_list_filter_input
|
990
|
+
else
|
991
|
+
raise SchemaError, <<~EOS
|
992
|
+
`#{parent_type.name}.#{name}` is a list-of-objects field, but the mapping type has not been explicitly specified. Elasticsearch and OpenSearch
|
993
|
+
offer two ways to index list-of-objects fields. It cannot be changed on an existing field without dropping the index and recreating it (losing
|
994
|
+
any existing indexed data!), and there are nuanced tradeoffs involved here, so ElasticGraph provides no default mapping in this situation.
|
995
|
+
|
996
|
+
If you're currently prototyping and don't want to spend time weighing this tradeoff, we recommend you do this:
|
997
|
+
|
998
|
+
```
|
999
|
+
t.field "#{name}", "#{type.name}" do |f|
|
1000
|
+
# Here we are opting for flexibility (nested) over pure performance (object).
|
1001
|
+
# TODO: evaluate if we want to stick with `nested` before going to production.
|
1002
|
+
f.mapping type: "nested"
|
1003
|
+
end
|
1004
|
+
```
|
1005
|
+
|
1006
|
+
Read on for details of the tradeoff involved here.
|
1007
|
+
|
1008
|
+
-----------------------------------------------------------------------------------------------------------------------------
|
1009
|
+
|
1010
|
+
Here are the options:
|
1011
|
+
|
1012
|
+
1) `f.mapping type: "object"` will cause each field path to be indexed as a separate "flattened" list.
|
1013
|
+
|
1014
|
+
For example, given a `Film` document like this:
|
1015
|
+
|
1016
|
+
```
|
1017
|
+
{
|
1018
|
+
"name": "The Empire Strikes Back",
|
1019
|
+
"characters": [
|
1020
|
+
{"first": "Luke", "last": "Skywalker"},
|
1021
|
+
{"first": "Han", "last": "Solo"}
|
1022
|
+
]
|
1023
|
+
}
|
1024
|
+
```
|
1025
|
+
|
1026
|
+
...the data will look like this in the inverted Lucene index:
|
1027
|
+
|
1028
|
+
```
|
1029
|
+
{
|
1030
|
+
"name": "The Empire Strikes Back",
|
1031
|
+
"characters.first": ["Luke", "Han"],
|
1032
|
+
"characters.last": ["Skywalker", "Solo"]
|
1033
|
+
}
|
1034
|
+
```
|
1035
|
+
|
1036
|
+
This is highly efficient, but there is no way to search on multiple fields of a character and be sure that the matching values came from the same character.
|
1037
|
+
ElasticGraph models this in the filtering API it offers for this case:
|
1038
|
+
|
1039
|
+
```
|
1040
|
+
query {
|
1041
|
+
films(filter: {
|
1042
|
+
characters: {
|
1043
|
+
first: {#{schema_def_state.schema_elements.any_satisfy}: {#{schema_def_state.schema_elements.equal_to_any_of}: ["Luke"]}}
|
1044
|
+
last: {#{schema_def_state.schema_elements.any_satisfy}: {#{schema_def_state.schema_elements.equal_to_any_of}: ["Skywalker"]}}
|
1045
|
+
}
|
1046
|
+
}) {
|
1047
|
+
# ...
|
1048
|
+
}
|
1049
|
+
}
|
1050
|
+
```
|
1051
|
+
|
1052
|
+
As suggested by this filtering API, this will match any film that has a character with a first name of "Luke" and a character
|
1053
|
+
with the last name of "Skywalker", but this could be satisfied by two separate characters.
|
1054
|
+
|
1055
|
+
2) `f.mapping type: "nested"` will cause each _object_ in the list to be indexed as a separate hidden document, preserving the independence of each.
|
1056
|
+
|
1057
|
+
Given a `Film` document like "The Empire Strikes Back" from above, the `nested` type will index separate hidden documents for each character. This
|
1058
|
+
allows ElasticGraph to offer this filtering API instead:
|
1059
|
+
|
1060
|
+
```
|
1061
|
+
query {
|
1062
|
+
films(filter: {
|
1063
|
+
characters: {#{schema_def_state.schema_elements.any_satisfy}: {
|
1064
|
+
first: {#{schema_def_state.schema_elements.equal_to_any_of}: ["Luke"]}
|
1065
|
+
last: {#{schema_def_state.schema_elements.equal_to_any_of}: ["Skywalker"]}
|
1066
|
+
}}
|
1067
|
+
}) {
|
1068
|
+
# ...
|
1069
|
+
}
|
1070
|
+
}
|
1071
|
+
```
|
1072
|
+
|
1073
|
+
As suggested by this filtering API, this will only match films that have a character named "Luke Skywalker". However, the Elasticsearch docs[^1][^2] warn
|
1074
|
+
that the `nested` mapping type can lead to performance problems, and index sorting cannot be configured[^3] when the `nested` type is used.
|
1075
|
+
|
1076
|
+
[^1]: https://www.elastic.co/guide/en/elasticsearch/reference/8.10/nested.html
|
1077
|
+
[^2]: https://www.elastic.co/guide/en/elasticsearch/reference/8.10/joining-queries.html
|
1078
|
+
[^3]: https://www.elastic.co/guide/en/elasticsearch/reference/8.10/index-modules-index-sorting.html
|
1079
|
+
EOS
|
1080
|
+
end
|
1081
|
+
end
|
1082
|
+
end
|
1083
|
+
end
|
1084
|
+
end
|
1085
|
+
end
|