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