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,218 @@
|
|
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/error"
|
11
|
+
require "elastic_graph/schema_definition/schema_elements/field"
|
12
|
+
require "elastic_graph/support/hash_util"
|
13
|
+
|
14
|
+
module ElasticGraph
|
15
|
+
module SchemaDefinition
|
16
|
+
module SchemaElements
|
17
|
+
# Wraps a {Field} to provide additional relationship-specific functionality when defining a field via
|
18
|
+
# {TypeWithSubfields#relates_to_one} or {TypeWithSubfields#relates_to_many}.
|
19
|
+
#
|
20
|
+
# @example Define relationships between two types
|
21
|
+
# ElasticGraph.define_schema do |schema|
|
22
|
+
# schema.object_type "Orchestra" do |t|
|
23
|
+
# t.field "id", "ID"
|
24
|
+
# t.relates_to_many "musicians", "Musician", via: "orchestraId", dir: :in, singular: "musician" do |r|
|
25
|
+
# # In this block, `r` is a `Relationship`.
|
26
|
+
# end
|
27
|
+
# t.index "orchestras"
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# schema.object_type "Musician" do |t|
|
31
|
+
# t.field "id", "ID"
|
32
|
+
# t.field "instrument", "String"
|
33
|
+
# t.relates_to_one "orchestra", "Orchestra", via: "orchestraId", dir: :out do |r|
|
34
|
+
# # In this block, `r` is a `Relationship`.
|
35
|
+
# end
|
36
|
+
# t.index "musicians"
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
class Relationship < DelegateClass(Field)
|
40
|
+
# @dynamic related_type
|
41
|
+
|
42
|
+
# @return [ObjectType, InterfaceType, UnionType] the type this relationship relates to
|
43
|
+
attr_reader :related_type
|
44
|
+
|
45
|
+
# @private
|
46
|
+
def initialize(field, cardinality:, related_type:, foreign_key:, direction:)
|
47
|
+
super(field)
|
48
|
+
@cardinality = cardinality
|
49
|
+
@related_type = related_type
|
50
|
+
@foreign_key = foreign_key
|
51
|
+
@direction = direction
|
52
|
+
@equivalent_field_paths_by_local_path = {}
|
53
|
+
@additional_filter = {}
|
54
|
+
end
|
55
|
+
|
56
|
+
# Adds additional filter conditions to a relationship beyond the foreign key.
|
57
|
+
#
|
58
|
+
# @param filter [Hash<Symbol, Object>, Hash<String, Object>] additional filter conditions for this relationship
|
59
|
+
# @return [void]
|
60
|
+
#
|
61
|
+
# @example Define additional filter conditions on a `relates_to_one` relationship
|
62
|
+
# ElasticGraph.define_schema do |schema|
|
63
|
+
# schema.object_type "Orchestra" do |t|
|
64
|
+
# t.field "id", "ID"
|
65
|
+
# t.relates_to_many "musicians", "Musician", via: "orchestraId", dir: :in, singular: "musician"
|
66
|
+
# t.relates_to_one "firstViolin", "Musician", via: "orchestraId", dir: :in do |r|
|
67
|
+
# r.additional_filter isFirstViolon: true
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# t.index "orchestras"
|
71
|
+
# end
|
72
|
+
#
|
73
|
+
# schema.object_type "Musician" do |t|
|
74
|
+
# t.field "id", "ID"
|
75
|
+
# t.field "instrument", "String"
|
76
|
+
# t.field "isFirstViolon", "Boolean"
|
77
|
+
# t.relates_to_one "orchestra", "Orchestra", via: "orchestraId", dir: :out
|
78
|
+
# t.index "musicians"
|
79
|
+
# end
|
80
|
+
# end
|
81
|
+
def additional_filter(filter)
|
82
|
+
stringified_filter = Support::HashUtil.stringify_keys(filter)
|
83
|
+
@additional_filter = Support::HashUtil.deep_merge(@additional_filter, stringified_filter)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Indicates that `path` (a field on the related type) is the equivalent of `locally_named` on this type.
|
87
|
+
#
|
88
|
+
# Use this API to specify a local field's equivalent path on the related type. This must be used on relationships used by
|
89
|
+
# {Field#sourced_from} when the local type uses {Indexing::Index#route_with} or {Indexing::Index#rollover} so that
|
90
|
+
# ElasticGraph can determine what field from the related type to use to route the update requests to the correct index and shard.
|
91
|
+
#
|
92
|
+
# @param path [String] path to a routing or rollover field on the related type
|
93
|
+
# @param locally_named [String] path on the local type to the equivalent field
|
94
|
+
# @return [void]
|
95
|
+
#
|
96
|
+
# @example
|
97
|
+
# ElasticGraph.define_schema do |schema|
|
98
|
+
# schema.object_type "Campaign" do |t|
|
99
|
+
# t.field "id", "ID!"
|
100
|
+
# t.field "name", "String"
|
101
|
+
# t.field "createdAt", "DateTime"
|
102
|
+
#
|
103
|
+
# t.relates_to_one "launchPlan", "CampaignLaunchPlan", via: "campaignId", dir: :in do |r|
|
104
|
+
# r.equivalent_field "campaignCreatedAt", locally_named: "createdAt"
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
# t.field "launchDate", "Date" do |f|
|
108
|
+
# f.sourced_from "launchPlan", "launchDate"
|
109
|
+
# end
|
110
|
+
#
|
111
|
+
# t.index "campaigns"do |i|
|
112
|
+
# i.rollover :yearly, "createdAt"
|
113
|
+
# end
|
114
|
+
# end
|
115
|
+
#
|
116
|
+
# schema.object_type "CampaignLaunchPlan" do |t|
|
117
|
+
# t.field "id", "ID"
|
118
|
+
# t.field "campaignId", "ID"
|
119
|
+
# t.field "campaignCreatedAt", "DateTime"
|
120
|
+
# t.field "launchDate", "Date"
|
121
|
+
#
|
122
|
+
# t.index "campaign_launch_plans"
|
123
|
+
# end
|
124
|
+
# end
|
125
|
+
def equivalent_field(path, locally_named: path)
|
126
|
+
if @equivalent_field_paths_by_local_path.key?(locally_named)
|
127
|
+
raise SchemaError, "`equivalent_field` has been called multiple times on `#{parent_type.name}.#{name}` with the same " \
|
128
|
+
"`locally_named` value (#{locally_named.inspect}), but each local field can have only one `equivalent_field`."
|
129
|
+
else
|
130
|
+
@equivalent_field_paths_by_local_path[locally_named] = path
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Gets the `routing_value_source` from this relationship for the given `index`, based on the configured
|
135
|
+
# routing used by `index` and the configured equivalent fields.
|
136
|
+
#
|
137
|
+
# Returns the GraphQL field name (not the `name_in_index`).
|
138
|
+
#
|
139
|
+
# @private
|
140
|
+
def routing_value_source_for_index(index)
|
141
|
+
return nil unless index.uses_custom_routing?
|
142
|
+
|
143
|
+
@equivalent_field_paths_by_local_path.fetch(index.routing_field_path.path) do |local_need|
|
144
|
+
yield local_need
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Gets the `rollover_timestamp_value_source` from this relationship for the given `index`, based on the
|
149
|
+
# configured equivalent fields and the rollover configuration used by `index`.
|
150
|
+
#
|
151
|
+
# Returns the GraphQL field name (not the `name_in_index`).
|
152
|
+
#
|
153
|
+
# @private
|
154
|
+
def rollover_timestamp_value_source_for_index(index)
|
155
|
+
return nil unless (rollover_config = index.rollover_config)
|
156
|
+
|
157
|
+
@equivalent_field_paths_by_local_path.fetch(rollover_config.timestamp_field_path.path) do |local_need|
|
158
|
+
yield local_need
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# @private
|
163
|
+
def validate_equivalent_fields(field_path_resolver)
|
164
|
+
resolved_related_type = (_ = related_type.as_object_type) # : indexableType
|
165
|
+
|
166
|
+
@equivalent_field_paths_by_local_path.flat_map do |local_path_string, related_type_path_string|
|
167
|
+
errors = [] # : ::Array[::String]
|
168
|
+
|
169
|
+
local_path = resolve_and_validate_field_path(parent_type, local_path_string, field_path_resolver) do |error|
|
170
|
+
errors << error
|
171
|
+
end
|
172
|
+
|
173
|
+
related_type_path = resolve_and_validate_field_path(resolved_related_type, related_type_path_string, field_path_resolver) do |error|
|
174
|
+
errors << error
|
175
|
+
end
|
176
|
+
|
177
|
+
if local_path && related_type_path && local_path.type.unwrap_non_null != related_type_path.type.unwrap_non_null
|
178
|
+
errors << "Field `#{related_type_path.full_description}` is defined as an equivalent of " \
|
179
|
+
"`#{local_path.full_description}` via an `equivalent_field` definition on `#{parent_type.name}.#{name}`, " \
|
180
|
+
"but their types do not agree. To continue, change one or the other so that they agree."
|
181
|
+
end
|
182
|
+
|
183
|
+
errors
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# @private
|
188
|
+
def many?
|
189
|
+
@cardinality == :many
|
190
|
+
end
|
191
|
+
|
192
|
+
# @private
|
193
|
+
def runtime_metadata
|
194
|
+
field_path_resolver = SchemaElements::FieldPath::Resolver.new
|
195
|
+
resolved_related_type = (_ = related_type.unwrap_list.as_object_type) # : indexableType
|
196
|
+
foreign_key_nested_paths = field_path_resolver.determine_nested_paths(resolved_related_type, @foreign_key)
|
197
|
+
foreign_key_nested_paths ||= [] # : ::Array[::String]
|
198
|
+
SchemaArtifacts::RuntimeMetadata::Relation.new(foreign_key: @foreign_key, direction: @direction, additional_filter: @additional_filter, foreign_key_nested_paths: foreign_key_nested_paths)
|
199
|
+
end
|
200
|
+
|
201
|
+
private
|
202
|
+
|
203
|
+
def resolve_and_validate_field_path(type, field_path_string, field_path_resolver)
|
204
|
+
field_path = field_path_resolver.resolve_public_path(type, field_path_string) do |parent_field|
|
205
|
+
!parent_field.type.list?
|
206
|
+
end
|
207
|
+
|
208
|
+
if field_path.nil?
|
209
|
+
yield "Field `#{type.name}.#{field_path_string}` (referenced from an `equivalent_field` defined on " \
|
210
|
+
"`#{parent_type.name}.#{name}`) does not exist. Either define it or correct the `equivalent_field` definition."
|
211
|
+
end
|
212
|
+
|
213
|
+
field_path
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
@@ -0,0 +1,310 @@
|
|
1
|
+
# Copyright 2024 Block, Inc.
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
#
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
require "elastic_graph/schema_artifacts/runtime_metadata/scalar_type"
|
10
|
+
require "elastic_graph/schema_definition/indexing/field_type/scalar"
|
11
|
+
require "elastic_graph/schema_definition/mixins/can_be_graphql_only"
|
12
|
+
require "elastic_graph/schema_definition/mixins/has_derived_graphql_type_customizations"
|
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
|
+
|
19
|
+
module ElasticGraph
|
20
|
+
module SchemaDefinition
|
21
|
+
module SchemaElements
|
22
|
+
# {include:API#scalar_type}
|
23
|
+
#
|
24
|
+
# @example Define a scalar type
|
25
|
+
# ElasticGraph.define_schema do |schema|
|
26
|
+
# schema.scalar_type "URL" do |t|
|
27
|
+
# t.mapping type: "keyword"
|
28
|
+
# t.json_schema type: "string", format: "uri"
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# @!attribute [r] schema_def_state
|
33
|
+
# @return [State] schema definition state
|
34
|
+
# @!attribute [rw] type_ref
|
35
|
+
# @private
|
36
|
+
# @!attribute [rw] mapping_type
|
37
|
+
# @private
|
38
|
+
# @!attribute [rw] runtime_metadata
|
39
|
+
# @private
|
40
|
+
# @!attribute [rw] aggregated_values_customizations
|
41
|
+
# @private
|
42
|
+
class ScalarType < Struct.new(:schema_def_state, :type_ref, :mapping_type, :runtime_metadata, :aggregated_values_customizations)
|
43
|
+
# `Struct.new` provides the following methods:
|
44
|
+
# @dynamic type_ref, runtime_metadata
|
45
|
+
prepend Mixins::VerifiesGraphQLName
|
46
|
+
include Mixins::CanBeGraphQLOnly
|
47
|
+
include Mixins::HasDocumentation
|
48
|
+
include Mixins::HasDirectives
|
49
|
+
include Mixins::HasDerivedGraphQLTypeCustomizations
|
50
|
+
include Mixins::HasReadableToSAndInspect.new { |t| t.name }
|
51
|
+
|
52
|
+
# `HasTypeInfo` provides the following methods:
|
53
|
+
# @dynamic mapping_options, json_schema_options
|
54
|
+
include Mixins::HasTypeInfo
|
55
|
+
|
56
|
+
# @dynamic graphql_only?
|
57
|
+
|
58
|
+
# @private
|
59
|
+
def initialize(schema_def_state, name)
|
60
|
+
super(schema_def_state, schema_def_state.type_ref(name).to_final_form)
|
61
|
+
|
62
|
+
# Default the runtime metadata before yielding, so it can be overridden as needed.
|
63
|
+
self.runtime_metadata = SchemaArtifacts::RuntimeMetadata::ScalarType.new(
|
64
|
+
coercion_adapter_ref: SchemaArtifacts::RuntimeMetadata::ScalarType::DEFAULT_COERCION_ADAPTER_REF,
|
65
|
+
indexing_preparer_ref: SchemaArtifacts::RuntimeMetadata::ScalarType::DEFAULT_INDEXING_PREPARER_REF
|
66
|
+
)
|
67
|
+
|
68
|
+
yield self
|
69
|
+
|
70
|
+
missing = [
|
71
|
+
("`mapping`" if mapping_options.empty?),
|
72
|
+
("`json_schema`" if json_schema_options.empty?)
|
73
|
+
].compact
|
74
|
+
|
75
|
+
if missing.any?
|
76
|
+
raise SchemaError, "Scalar types require `mapping` and `json_schema` to be configured, but `#{name}` lacks #{missing.join(" and ")}."
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [String] name of the scalar type
|
81
|
+
def name
|
82
|
+
type_ref.name
|
83
|
+
end
|
84
|
+
|
85
|
+
# (see Mixins::HasTypeInfo#mapping)
|
86
|
+
def mapping(**options)
|
87
|
+
self.mapping_type = options.fetch(:type) do
|
88
|
+
raise SchemaError, "Must specify a mapping `type:` on custom scalars but was missing on the `#{name}` type."
|
89
|
+
end
|
90
|
+
|
91
|
+
super
|
92
|
+
end
|
93
|
+
|
94
|
+
# Specifies the scalar coercion adapter that should be used for this scalar type. The scalar coercion adapter is responsible
|
95
|
+
# for validating and coercing scalar input values, and converting scalar return values to a form suitable for JSON serialization.
|
96
|
+
#
|
97
|
+
# @note For examples of scalar coercion adapters, see `ElasticGraph::GraphQL::ScalarCoercionAdapters`.
|
98
|
+
# @note If the `defined_at` require path requires any directories be put on the Ruby `$LOAD_PATH`, you are responsible for doing
|
99
|
+
# that before booting {ElasticGraph::GraphQL}.
|
100
|
+
#
|
101
|
+
# @param adapter_name [String] fully qualified Ruby class name of the adapter
|
102
|
+
# @param defined_at [String] the `require` path of the adapter
|
103
|
+
# @return [void]
|
104
|
+
#
|
105
|
+
# @example Register a coercion adapter
|
106
|
+
# ElasticGraph.define_schema do |schema|
|
107
|
+
# schema.scalar_type "PhoneNumber" do |t|
|
108
|
+
# t.mapping type: "keyword"
|
109
|
+
# t.json_schema type: "string", pattern: "^\\+[1-9][0-9]{1,14}$"
|
110
|
+
# t.coerce_with "CoercionAdapters::PhoneNumber", defined_at: "./coercion_adapters/phone_number"
|
111
|
+
# end
|
112
|
+
# end
|
113
|
+
def coerce_with(adapter_name, defined_at:)
|
114
|
+
self.runtime_metadata = runtime_metadata.with(coercion_adapter_ref: {
|
115
|
+
"extension_name" => adapter_name,
|
116
|
+
"require_path" => defined_at
|
117
|
+
}).tap(&:load_coercion_adapter) # verify the adapter is valid.
|
118
|
+
end
|
119
|
+
|
120
|
+
# Specifies an indexing preparer that should be used for this scalar type. The indexing preparer is responsible for preparing
|
121
|
+
# scalar values before indexing them, performing any desired formatting or normalization.
|
122
|
+
#
|
123
|
+
# @note For examples of scalar coercion adapters, see `ElasticGraph::Indexer::IndexingPreparers`.
|
124
|
+
# @note If the `defined_at` require path requires any directories be put on the Ruby `$LOAD_PATH`, you are responsible for doing
|
125
|
+
# that before booting {ElasticGraph::GraphQL}.
|
126
|
+
#
|
127
|
+
# @param preparer_name [String] fully qualified Ruby class name of the indexing preparer
|
128
|
+
# @param defined_at [String] the `require` path of the preparer
|
129
|
+
# @return [void]
|
130
|
+
#
|
131
|
+
# @example Register an indexing preparer
|
132
|
+
# ElasticGraph.define_schema do |schema|
|
133
|
+
# schema.scalar_type "PhoneNumber" do |t|
|
134
|
+
# t.mapping type: "keyword"
|
135
|
+
# t.json_schema type: "string", pattern: "^\\+[1-9][0-9]{1,14}$"
|
136
|
+
#
|
137
|
+
# t.prepare_for_indexing_with "IndexingPreparers::PhoneNumber",
|
138
|
+
# defined_at: "./indexing_preparers/phone_number"
|
139
|
+
# end
|
140
|
+
# end
|
141
|
+
def prepare_for_indexing_with(preparer_name, defined_at:)
|
142
|
+
self.runtime_metadata = runtime_metadata.with(indexing_preparer_ref: {
|
143
|
+
"extension_name" => preparer_name,
|
144
|
+
"require_path" => defined_at
|
145
|
+
}).tap(&:load_indexing_preparer) # verify the preparer is valid.
|
146
|
+
end
|
147
|
+
|
148
|
+
# @return [String] the GraphQL SDL form of this scalar
|
149
|
+
def to_sdl
|
150
|
+
"#{formatted_documentation}scalar #{name} #{directives_sdl}"
|
151
|
+
end
|
152
|
+
|
153
|
+
# Registers a block which will be used to customize the derived `*AggregatedValues` object type.
|
154
|
+
#
|
155
|
+
# @private
|
156
|
+
def customize_aggregated_values_type(&block)
|
157
|
+
self.aggregated_values_customizations = block
|
158
|
+
end
|
159
|
+
|
160
|
+
# @private
|
161
|
+
def aggregated_values_type
|
162
|
+
if aggregated_values_customizations
|
163
|
+
type_ref.as_aggregated_values
|
164
|
+
else
|
165
|
+
schema_def_state.type_ref("NonNumeric").as_aggregated_values
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# @private
|
170
|
+
def to_indexing_field_type
|
171
|
+
Indexing::FieldType::Scalar.new(scalar_type: self)
|
172
|
+
end
|
173
|
+
|
174
|
+
# @private
|
175
|
+
def derived_graphql_types
|
176
|
+
return [] if graphql_only?
|
177
|
+
|
178
|
+
pagination_types =
|
179
|
+
if schema_def_state.paginated_collection_element_types.include?(name)
|
180
|
+
schema_def_state.factory.build_relay_pagination_types(name, include_total_edge_count: true)
|
181
|
+
else
|
182
|
+
[] # : ::Array[ObjectType]
|
183
|
+
end
|
184
|
+
|
185
|
+
(to_input_filters + pagination_types).tap do |derived_types|
|
186
|
+
if (aggregated_values_type = to_aggregated_values_type)
|
187
|
+
derived_types << aggregated_values_type
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# @private
|
193
|
+
def indexed?
|
194
|
+
false
|
195
|
+
end
|
196
|
+
|
197
|
+
private
|
198
|
+
|
199
|
+
EQUAL_TO_ANY_OF_DOC = <<~EOS
|
200
|
+
Matches records where the field value is equal to any of the provided values.
|
201
|
+
This works just like an IN operator in SQL.
|
202
|
+
|
203
|
+
Will be ignored when `null` is passed. When an empty list is passed, will cause this
|
204
|
+
part of the filter to match no documents. When `null` is passed in the list, will
|
205
|
+
match records where the field value is `null`.
|
206
|
+
EOS
|
207
|
+
|
208
|
+
GT_DOC = <<~EOS
|
209
|
+
Matches records where the field value is greater than (>) the provided value.
|
210
|
+
|
211
|
+
Will be ignored when `null` is passed.
|
212
|
+
EOS
|
213
|
+
|
214
|
+
GTE_DOC = <<~EOS
|
215
|
+
Matches records where the field value is greater than or equal to (>=) the provided value.
|
216
|
+
|
217
|
+
Will be ignored when `null` is passed.
|
218
|
+
EOS
|
219
|
+
|
220
|
+
LT_DOC = <<~EOS
|
221
|
+
Matches records where the field value is less than (<) the provided value.
|
222
|
+
|
223
|
+
Will be ignored when `null` is passed.
|
224
|
+
EOS
|
225
|
+
|
226
|
+
LTE_DOC = <<~EOS
|
227
|
+
Matches records where the field value is less than or equal to (<=) the provided value.
|
228
|
+
|
229
|
+
Will be ignored when `null` is passed.
|
230
|
+
EOS
|
231
|
+
|
232
|
+
def to_input_filters
|
233
|
+
# Note: all fields on inputs should be nullable, to support parameterized queries where
|
234
|
+
# the parameters are allowed to be set to `null`. We also now support nulls within lists.
|
235
|
+
|
236
|
+
# For floats, we may want to remove the `equal_to_any_of` operator at some point.
|
237
|
+
# In many languages. checking exact equality with floats is problematic.
|
238
|
+
# For example, in IRB:
|
239
|
+
#
|
240
|
+
# 2.7.1 :003 > 0.3 == (0.1 + 0.2)
|
241
|
+
# => false
|
242
|
+
#
|
243
|
+
# However, it's not yet clear if that issue will come up with GraphQL, because
|
244
|
+
# float values are serialized on the wire as JSON, using an exact decimal
|
245
|
+
# string representation. So for now we are keeping `equal_to_any_of`.
|
246
|
+
schema_def_state.factory.build_standard_filter_input_types_for_index_leaf_type(name) do |t|
|
247
|
+
# Normally, we use a nullable type for `equal_to_any_of`, to allow a filter expression like this:
|
248
|
+
#
|
249
|
+
# filter: {optional_field: {equal_to_any_of: [null]}}
|
250
|
+
#
|
251
|
+
# That filter expression matches documents where `optional_field == null`. However,
|
252
|
+
# we cannot support this:
|
253
|
+
#
|
254
|
+
# filter: {tags: {any_satisfy: {equal_to_any_of: [null]}}}
|
255
|
+
#
|
256
|
+
# We can't support that because we implement filtering on `null` using an `exists` query:
|
257
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/8.10/query-dsl-exists-query.html
|
258
|
+
#
|
259
|
+
# ...but that works based on the field existing (or not), and does not let us filter on the
|
260
|
+
# presence or absence of `null` within a list.
|
261
|
+
#
|
262
|
+
# So, here we make the field non-null if we're in an `any_satisfy` context (as indicated by
|
263
|
+
# the type ending with `ListElementFilterInput`).
|
264
|
+
equal_to_any_of_type = t.type_ref.list_element_filter_input? ? "[#{name}!]" : "[#{name}]"
|
265
|
+
|
266
|
+
t.field schema_def_state.schema_elements.equal_to_any_of, equal_to_any_of_type do |f|
|
267
|
+
f.documentation EQUAL_TO_ANY_OF_DOC
|
268
|
+
end
|
269
|
+
|
270
|
+
if mapping_type_efficiently_comparable?
|
271
|
+
t.field schema_def_state.schema_elements.gt, name do |f|
|
272
|
+
f.documentation GT_DOC
|
273
|
+
end
|
274
|
+
|
275
|
+
t.field schema_def_state.schema_elements.gte, name do |f|
|
276
|
+
f.documentation GTE_DOC
|
277
|
+
end
|
278
|
+
|
279
|
+
t.field schema_def_state.schema_elements.lt, name do |f|
|
280
|
+
f.documentation LT_DOC
|
281
|
+
end
|
282
|
+
|
283
|
+
t.field schema_def_state.schema_elements.lte, name do |f|
|
284
|
+
f.documentation LTE_DOC
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def to_aggregated_values_type
|
291
|
+
return nil unless (customization_block = aggregated_values_customizations)
|
292
|
+
schema_def_state.factory.new_aggregated_values_type_for_index_leaf_type(name, &customization_block)
|
293
|
+
end
|
294
|
+
|
295
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html
|
296
|
+
# https://www.elastic.co/guide/en/elasticsearch/reference/7.13/number.html#number
|
297
|
+
NUMERIC_TYPES = %w[long integer short byte double float half_float scaled_float unsigned_long].to_set
|
298
|
+
DATE_TYPES = %w[date date_nanos].to_set
|
299
|
+
# The Elasticsearch/OpenSearch docs do not exhaustively give a list of types on which range queries are efficient,
|
300
|
+
# but the docs are clear that it is efficient on numeric and date types, and is inefficient on string
|
301
|
+
# types: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html
|
302
|
+
COMPARABLE_TYPES = NUMERIC_TYPES | DATE_TYPES
|
303
|
+
|
304
|
+
def mapping_type_efficiently_comparable?
|
305
|
+
COMPARABLE_TYPES.include?(mapping_type)
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
@@ -0,0 +1,36 @@
|
|
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/schema_definition/mixins/has_readable_to_s_and_inspect"
|
11
|
+
require "elastic_graph/schema_definition/schema_elements/enum_value"
|
12
|
+
|
13
|
+
module ElasticGraph
|
14
|
+
module SchemaDefinition
|
15
|
+
module SchemaElements
|
16
|
+
# Simple wrapper around an {EnumValue} so that we can expose the `sort_order_field_path` to {Field} customization callbacks.
|
17
|
+
class SortOrderEnumValue < DelegateClass(EnumValue)
|
18
|
+
include Mixins::HasReadableToSAndInspect.new { |v| v.name }
|
19
|
+
|
20
|
+
# @dynamic sort_order_field_path
|
21
|
+
|
22
|
+
# @return [Array<Field>] path to the field from the root of the indexed {ObjectType}
|
23
|
+
attr_reader :sort_order_field_path
|
24
|
+
|
25
|
+
# @private
|
26
|
+
def initialize(enum_value, sort_order_field_path)
|
27
|
+
# We've told steep that SortOrderEnumValue is subclass of EnumValue
|
28
|
+
# but here are supering to the `DelegateClass`'s initialize, not `EnumValue`'s,
|
29
|
+
# so we have to use `__skip__`
|
30
|
+
__skip__ = super(enum_value)
|
31
|
+
@sort_order_field_path = sort_order_field_path
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# Copyright 2024 Block, Inc.
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
#
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
module ElasticGraph
|
10
|
+
module SchemaDefinition
|
11
|
+
module SchemaElements
|
12
|
+
# Abstraction responsible for identifying paths to sub-aggregations, and, on that basis, determining
|
13
|
+
# what the type names should be.
|
14
|
+
#
|
15
|
+
# @private
|
16
|
+
SubAggregationPath = ::Data.define(
|
17
|
+
# List of index document types within which the target type exists. This contains the set of parent
|
18
|
+
# index document types--that is, types which are indexed or are themselves used as a `nested` field
|
19
|
+
# on a parent of it. Parent objects which are not "index documents" (e.g. directly at an index level
|
20
|
+
# or a nested field level) are omitted; we omit them because we don't offer sub-aggregations for such
|
21
|
+
# a field, and the set of sub-aggregations we are going to offer is the basis for generating separate
|
22
|
+
# `*SubAggregation` types.
|
23
|
+
:parent_doc_types,
|
24
|
+
# List of fields forming a path from the last parent doc type.
|
25
|
+
:field_path
|
26
|
+
) do
|
27
|
+
# @implements SubAggregationPath
|
28
|
+
|
29
|
+
# Determines the set of sub aggregation paths for the given type.
|
30
|
+
def self.paths_for(type, schema_def_state:)
|
31
|
+
root_paths = type.indexed? ? [SubAggregationPath.new([type.name], [])] : [] # : ::Array[SubAggregationPath]
|
32
|
+
|
33
|
+
non_relation_field_refs = schema_def_state
|
34
|
+
.user_defined_field_references_by_type_name.fetch(type.name) { [] }
|
35
|
+
# Relationship fields are the only case where types can reference each other in circular fashion.
|
36
|
+
# If we don't reject that case here, we can get stuck in infinite recursion.
|
37
|
+
.reject(&:relationship)
|
38
|
+
|
39
|
+
root_paths + non_relation_field_refs.flat_map do |field_ref|
|
40
|
+
# Here we call `schema_def_state.sub_aggregation_paths_for` rather than directly
|
41
|
+
# recursing to give schema_def_state a chance to cache the results.
|
42
|
+
parent_paths = schema_def_state.sub_aggregation_paths_for(field_ref.parent_type)
|
43
|
+
|
44
|
+
if field_ref.nested?
|
45
|
+
parent_paths.map { |path| path.plus_parent(field_ref.type_for_derived_types.fully_unwrapped.name) }
|
46
|
+
else
|
47
|
+
parent_paths.map { |path| path.plus_field(field_ref) }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def plus_parent(parent)
|
53
|
+
with(parent_doc_types: parent_doc_types + [parent], field_path: [])
|
54
|
+
end
|
55
|
+
|
56
|
+
def plus_field(field)
|
57
|
+
with(field_path: field_path + [field])
|
58
|
+
end
|
59
|
+
|
60
|
+
def field_path_string
|
61
|
+
field_path.map(&:name).join(".")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|