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