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,181 @@
|
|
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/json_schema/meta_schema_validator"
|
10
|
+
|
11
|
+
module ElasticGraph
|
12
|
+
module SchemaDefinition
|
13
|
+
module Mixins
|
14
|
+
# Mixin used to specify non-GraphQL type info (datastore index and JSON schema type info).
|
15
|
+
# Exists as a mixin so we can apply the same consistent API to every place we need to use this.
|
16
|
+
# Currently it's used in 3 places:
|
17
|
+
#
|
18
|
+
# - {SchemaElements::ScalarType}: allows specification of how scalars are represented in JSON schema and the index.
|
19
|
+
# - {SchemaElements::TypeWithSubfields}: allows customization of how an object type is represented in JSON schema and the index.
|
20
|
+
# - {SchemaElements::Field}: allows customization of a specific field over the field type's standard JSON schema and the index mapping.
|
21
|
+
module HasTypeInfo
|
22
|
+
# @return [Hash<Symbol, Object>] datastore mapping options
|
23
|
+
def mapping_options
|
24
|
+
@mapping_options ||= {}
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Hash<Symbol, Object>] JSON schema options
|
28
|
+
def json_schema_options
|
29
|
+
@json_schema_options ||= {}
|
30
|
+
end
|
31
|
+
|
32
|
+
# Set of mapping parameters that it makes sense to allow customization of, based on
|
33
|
+
# [the Elasticsearch docs](https://www.elastic.co/guide/en/elasticsearch/reference/8.15/mapping-params.html).
|
34
|
+
CUSTOMIZABLE_DATASTORE_PARAMS = Set[
|
35
|
+
:analyzer,
|
36
|
+
:eager_global_ordinals,
|
37
|
+
:enabled,
|
38
|
+
:fields,
|
39
|
+
:format,
|
40
|
+
:index,
|
41
|
+
:meta, # not actually in the doc above. Added to support some `index_configurator` tests on 7.9+.
|
42
|
+
:norms,
|
43
|
+
:null_value,
|
44
|
+
:search_analyzer,
|
45
|
+
:type,
|
46
|
+
]
|
47
|
+
|
48
|
+
# Defines the Elasticsearch/OpenSearch [field mapping type](https://www.elastic.co/guide/en/elasticsearch/reference/7.10/mapping-types.html)
|
49
|
+
# and [mapping parameters](https://www.elastic.co/guide/en/elasticsearch/reference/7.10/mapping-params.html) for a field or type.
|
50
|
+
# The options passed here will be included in the generated `datastore_config.yaml` artifact that ElasticGraph uses to configure
|
51
|
+
# Elasticsearch/OpenSearch.
|
52
|
+
#
|
53
|
+
# Can be called multiple times; each time, the options will be merged into the existing options.
|
54
|
+
#
|
55
|
+
# This is required on a {SchemaElements::ScalarType}; without it, ElasticGraph would have no way to know how the datatype should be
|
56
|
+
# indexed in the datastore.
|
57
|
+
#
|
58
|
+
# On a {SchemaElements::Field}, this can be used to customize how a field is indexed. For example, `String` fields are normally
|
59
|
+
# indexed as [keywords](https://www.elastic.co/guide/en/elasticsearch/reference/7.10/keyword.html); to instead index a `String`
|
60
|
+
# field for full text search, you’d need to configure `mapping type: "text"`.
|
61
|
+
#
|
62
|
+
# On a {SchemaElements::ObjectType}, this can be used to use a specific Elasticsearch/OpenSearch data type for something that is
|
63
|
+
# modeled as an object in GraphQL. For example, we use it for the `GeoLocation` type so they get indexed in Elasticsearch using the
|
64
|
+
# [geo_point type](https://www.elastic.co/guide/en/elasticsearch/reference/7.10/geo-point.html).
|
65
|
+
#
|
66
|
+
# @param options [Hash<Symbol, Object>] mapping options--must be limited to {CUSTOMIZABLE_DATASTORE_PARAMS}
|
67
|
+
# @return [void]
|
68
|
+
#
|
69
|
+
# @example Define the mapping of a custom scalar type
|
70
|
+
# ElasticGraph.define_schema do |schema|
|
71
|
+
# schema.scalar_type "URL" do |t|
|
72
|
+
# t.mapping type: "keyword"
|
73
|
+
# t.json_schema type: "string", format: "uri"
|
74
|
+
# end
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# @example Customize the mapping of a field
|
78
|
+
# ElasticGraph.define_schema do |schema|
|
79
|
+
# schema.object_type "Card" do |t|
|
80
|
+
# t.field "id", "ID!"
|
81
|
+
#
|
82
|
+
# t.field "cardholderName", "String" do |f|
|
83
|
+
# # index this field for full text search
|
84
|
+
# f.mapping type: "text"
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# t.field "expYear", "Int" do |f|
|
88
|
+
# # Use a smaller numeric type to save space in the datastore
|
89
|
+
# f.mapping type: "short"
|
90
|
+
# f.json_schema minimum: 2000, maximum: 2099
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# t.field "expMonth", "Int" do |f|
|
94
|
+
# # Use a smaller numeric type to save space in the datastore
|
95
|
+
# f.mapping type: "byte"
|
96
|
+
# f.json_schema minimum: 1, maximum: 12
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
# t.index "cards"
|
100
|
+
# end
|
101
|
+
# end
|
102
|
+
def mapping(**options)
|
103
|
+
param_diff = (options.keys.to_set - CUSTOMIZABLE_DATASTORE_PARAMS).to_a
|
104
|
+
|
105
|
+
unless param_diff.empty?
|
106
|
+
raise SchemaError, "Some configured mapping overrides are unsupported: #{param_diff.inspect}"
|
107
|
+
end
|
108
|
+
|
109
|
+
mapping_options.update(options)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Defines the [JSON schema](https://json-schema.org/understanding-json-schema/) validations for this field or type. Validations
|
113
|
+
# defined here will be included in the generated `json_schemas.yaml` artifact, which is used by the ElasticGraph indexer to
|
114
|
+
# validate events before indexing their data in the datastore. In addition, the publisher may use `json_schemas.yaml` for code
|
115
|
+
# generation and to apply validation before publishing an event to ElasticGraph.
|
116
|
+
#
|
117
|
+
# Can be called multiple times; each time, the options will be merged into the existing options.
|
118
|
+
#
|
119
|
+
# This is _required_ on a {SchemaElements::ScalarType} (since we don’t know how a custom scalar type should be represented in
|
120
|
+
# JSON!). On a {SchemaElements::Field}, this is optional, but can be used to make the JSON schema validation stricter then it
|
121
|
+
# would otherwise be. For example, you could use `json_schema maxLength: 30` on a `String` field to limit the length.
|
122
|
+
#
|
123
|
+
# You can use any of the JSON schema validation keywords here. In addition, `nullable: false` is supported to configure the
|
124
|
+
# generated JSON schema to disallow `null` values for the field. Note that if you define a field with a non-nullable GraphQL type
|
125
|
+
# (e.g. `Int!`), the JSON schema will automatically disallow nulls. However, as explained in the
|
126
|
+
# {SchemaElements::TypeWithSubfields#field} documentation, we generally recommend against defining non-nullable GraphQL fields.
|
127
|
+
# `json_schema nullable: false` will disallow `null` values from being indexed, while still keeping the field nullable in the
|
128
|
+
# GraphQL schema. If you think you might want to make a field non-nullable in the GraphQL schema some day, it’s a good idea to use
|
129
|
+
# `json_schema nullable: false` now to ensure every indexed record has a non-null value for the field.
|
130
|
+
#
|
131
|
+
# @note We recommend using JSON schema validations in a limited fashion. Validations that are appropriate to apply when data is
|
132
|
+
# entering the system-of-record are often not appropriate on a secondary index like ElasticGraph. Events that violate a JSON
|
133
|
+
# schema validation will fail to index (typically they will be sent to the dead letter queue and page an oncall engineer). If an
|
134
|
+
# ElasticGraph instance is meant to contain all the data of some source system, you probably don’t want it applying stricter
|
135
|
+
# validations than the source system itself has. We recommend limiting your JSON schema validations to situations where
|
136
|
+
# violations would prevent ElasticGraph from operating correctly.
|
137
|
+
#
|
138
|
+
# @param options [Hash<Symbol, Object>] JSON schema options
|
139
|
+
# @return [void]
|
140
|
+
#
|
141
|
+
# @example Define the JSON schema validations of a custom scalar type
|
142
|
+
# ElasticGraph.define_schema do |schema|
|
143
|
+
# schema.scalar_type "URL" do |t|
|
144
|
+
# t.mapping type: "keyword"
|
145
|
+
#
|
146
|
+
# # JSON schema has a built-in URI format validator:
|
147
|
+
# # https://json-schema.org/understanding-json-schema/reference/string.html#resource-identifiers
|
148
|
+
# t.json_schema type: "string", format: "uri"
|
149
|
+
# end
|
150
|
+
# end
|
151
|
+
#
|
152
|
+
# @example Define additional validations on a field
|
153
|
+
# ElasticGraph.define_schema do |schema|
|
154
|
+
# schema.object_type "Card" do |t|
|
155
|
+
# t.field "id", "ID!"
|
156
|
+
#
|
157
|
+
# t.field "expYear", "Int" do |f|
|
158
|
+
# # Use JSON schema to ensure the publisher is sending us 4 digit years, not 2 digit years.
|
159
|
+
# f.json_schema minimum: 2000, maximum: 2099
|
160
|
+
# end
|
161
|
+
#
|
162
|
+
# t.field "expMonth", "Int" do |f|
|
163
|
+
# f.json_schema minimum: 1, maximum: 12
|
164
|
+
# end
|
165
|
+
#
|
166
|
+
# t.index "cards"
|
167
|
+
# end
|
168
|
+
# end
|
169
|
+
def json_schema(**options)
|
170
|
+
validatable_json_schema = Support::HashUtil.stringify_keys(options)
|
171
|
+
|
172
|
+
if (error_msg = JSONSchema.strict_meta_schema_validator.validate_with_error_message(validatable_json_schema))
|
173
|
+
raise SchemaError, "Invalid JSON schema options set on #{self}:\n\n#{error_msg}"
|
174
|
+
end
|
175
|
+
|
176
|
+
json_schema_options.update(options)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# Copyright 2024 Block, Inc.
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
#
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
require "elastic_graph/error"
|
10
|
+
|
11
|
+
module ElasticGraph
|
12
|
+
module SchemaDefinition
|
13
|
+
module Mixins
|
14
|
+
# Mixin for types that can implement interfaces ({SchemaElements::ObjectType} and {SchemaElements::InterfaceType}).
|
15
|
+
module ImplementsInterfaces
|
16
|
+
# Declares that the current type implements the specified interface, making the current type a subtype of the interface. The
|
17
|
+
# current type must define all of the fields of the named interface, with the exact same field types.
|
18
|
+
#
|
19
|
+
# @param interface_names [Array<String>] names of interface types implemented by this type
|
20
|
+
# @return [void]
|
21
|
+
#
|
22
|
+
# @example Implement an interface
|
23
|
+
# ElasticGraph.define_schema do |schema|
|
24
|
+
# schema.interface_type "Athlete" do |t|
|
25
|
+
# t.field "name", "String"
|
26
|
+
# t.field "team", "String"
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# schema.object_type "BaseballPlayer" do |t|
|
30
|
+
# t.implements "Athlete"
|
31
|
+
# t.field "name", "String"
|
32
|
+
# t.field "team", "String"
|
33
|
+
# t.field "battingAvg", "Float"
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# schema.object_type "BasketballPlayer" do |t|
|
37
|
+
# t.implements "Athlete"
|
38
|
+
# t.field "name", "String"
|
39
|
+
# t.field "team", "String"
|
40
|
+
# t.field "pointsPerGame", "Float"
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
def implements(*interface_names)
|
44
|
+
interface_refs = interface_names.map do |interface_name|
|
45
|
+
schema_def_state.type_ref(interface_name).to_final_form.tap do |interface_ref|
|
46
|
+
schema_def_state.implementations_by_interface_ref[interface_ref] << self
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
implemented_interfaces.concat(interface_refs)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Array<SchemaElements::TypeReference>] list of type references for the interface types implemented by this type
|
54
|
+
def implemented_interfaces
|
55
|
+
@implemented_interfaces ||= []
|
56
|
+
end
|
57
|
+
|
58
|
+
# Called after the schema definition is complete, before dumping artifacts. Here we validate
|
59
|
+
# the correctness of interface implementations. We defer it until this time to not require the
|
60
|
+
# interface and fields to be defined before the `implements` call.
|
61
|
+
#
|
62
|
+
# Note that the GraphQL gem on its own supports a form of "interface inheritance": if declaring
|
63
|
+
# that an object type implements an interface, and the object type is missing one or more of the
|
64
|
+
# interface fields, the GraphQL gem dynamically adds the missing interface fields to the object
|
65
|
+
# type (at least, that's the result I noted when dumping the GraphQL SDL after trying that!).
|
66
|
+
# However, we cannot allow that, because our schema definition is used to generate non-GrapQL
|
67
|
+
# artifacts (e.g. the JSON schema and the index mapping), and all the artifacts must agree
|
68
|
+
# on the fields. Therefore, we use this method to verify that the object type fully implements
|
69
|
+
# the specified interfaces.
|
70
|
+
#
|
71
|
+
# @return [void]
|
72
|
+
# @private
|
73
|
+
def verify_graphql_correctness!
|
74
|
+
schema_error_messages = implemented_interfaces.filter_map do |interface_ref|
|
75
|
+
interface = interface_ref.resolved
|
76
|
+
|
77
|
+
case interface
|
78
|
+
when SchemaElements::InterfaceType
|
79
|
+
differences = (_ = interface).interface_fields_by_name.values.filter_map do |interface_field|
|
80
|
+
my_field_sdl = graphql_fields_by_name[interface_field.name]&.to_sdl(type_structure_only: true)
|
81
|
+
interface_field_sdl = interface_field.to_sdl(type_structure_only: true)
|
82
|
+
|
83
|
+
if my_field_sdl.nil?
|
84
|
+
"missing `#{interface_field.name}`"
|
85
|
+
elsif my_field_sdl != interface_field_sdl
|
86
|
+
"`#{interface_field_sdl.strip}` vs `#{my_field_sdl.strip}`"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
unless differences.empty?
|
91
|
+
"Type `#{name}` does not correctly implement interface `#{interface_ref}` " \
|
92
|
+
"due to field differences: #{differences.join("; ")}."
|
93
|
+
end
|
94
|
+
when nil
|
95
|
+
"Type `#{name}` cannot implement `#{interface_ref}` because `#{interface_ref}` is not defined."
|
96
|
+
else
|
97
|
+
"Type `#{name}` cannot implement `#{interface_ref}` because `#{interface_ref}` is not an interface."
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
unless schema_error_messages.empty?
|
102
|
+
raise SchemaError, schema_error_messages.join("\n\n")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# @yield [SchemaElements::Argument] an argument
|
107
|
+
# @yieldreturn [Boolean] whether or not to include the argument in the generated GraphQL SDL
|
108
|
+
# @return [String] SDL string of the type
|
109
|
+
def to_sdl(&field_arg_selector)
|
110
|
+
name_section =
|
111
|
+
if implemented_interfaces.empty?
|
112
|
+
name
|
113
|
+
else
|
114
|
+
"#{name} implements #{implemented_interfaces.join(" & ")}"
|
115
|
+
end
|
116
|
+
|
117
|
+
generate_sdl(name_section: name_section, &field_arg_selector)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,47 @@
|
|
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/support/graphql_formatter"
|
10
|
+
|
11
|
+
module ElasticGraph
|
12
|
+
module SchemaDefinition
|
13
|
+
module Mixins
|
14
|
+
# A mixin designed to be included in a schema element class that supports default values.
|
15
|
+
# Designed to be `prepended` so that it can hook into `initialize`.
|
16
|
+
module SupportsDefaultValue
|
17
|
+
# @private
|
18
|
+
def initialize(...)
|
19
|
+
__skip__ = super # steep can't type this.
|
20
|
+
@default_value = NO_DEFAULT_PROVIDED
|
21
|
+
end
|
22
|
+
|
23
|
+
# Used to specify the default value for this field or argument.
|
24
|
+
#
|
25
|
+
# @param default_value [Object] default value for this field or argument
|
26
|
+
# @return [void]
|
27
|
+
def default(default_value)
|
28
|
+
@default_value = default_value
|
29
|
+
end
|
30
|
+
|
31
|
+
# Generates SDL for the default value. Suitable for inclusion in the schema elememnts `#to_sdl`.
|
32
|
+
#
|
33
|
+
# @return [String]
|
34
|
+
def default_value_sdl
|
35
|
+
return nil if @default_value == NO_DEFAULT_PROVIDED
|
36
|
+
" = #{Support::GraphQLFormatter.serialize(@default_value)}"
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# A sentinel value that we can use to detect when a default has been provided.
|
42
|
+
# We can't use `nil` to detect if a default has been provided because `nil` is a valid default value!
|
43
|
+
NO_DEFAULT_PROVIDED = Module.new
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,267 @@
|
|
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 Mixins
|
12
|
+
# Responsible for building object types for filtering and aggregation, from an existing object type.
|
13
|
+
#
|
14
|
+
# This is specifically designed to support {SchemaElements::TypeWithSubfields} (where we have the fields directly available) and
|
15
|
+
# {SchemaElements::UnionType} (where we will need to compute the list of fields by resolving the subtypes and merging their fields).
|
16
|
+
#
|
17
|
+
# @private
|
18
|
+
module SupportsFilteringAndAggregation
|
19
|
+
# Indicates if this type supports a given feature (e.g. `filterable?`).
|
20
|
+
def supports?(&feature_predicate)
|
21
|
+
# If the type uses a custom mapping type we don't know if it can support a feature, so we assume it can't.
|
22
|
+
# TODO: clean this up using an interface instead of checking mapping options.
|
23
|
+
return false if has_custom_mapping_type?
|
24
|
+
|
25
|
+
graphql_fields_by_name.values.any?(&feature_predicate)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Inverse of `supports?`.
|
29
|
+
def does_not_support?(&feature_predicate)
|
30
|
+
!supports?(&feature_predicate)
|
31
|
+
end
|
32
|
+
|
33
|
+
def derived_graphql_types
|
34
|
+
return [] if graphql_only?
|
35
|
+
|
36
|
+
indexed_agg_type = to_indexed_aggregation_type
|
37
|
+
indexed_aggregation_pagination_types =
|
38
|
+
if indexed_agg_type
|
39
|
+
schema_def_state.factory.build_relay_pagination_types(indexed_agg_type.name)
|
40
|
+
else
|
41
|
+
[] # : ::Array[SchemaElements::ObjectType]
|
42
|
+
end
|
43
|
+
|
44
|
+
sub_aggregation_types = sub_aggregation_types_for_nested_field_references.flat_map do |type|
|
45
|
+
[type] + schema_def_state.factory.build_relay_pagination_types(type.name, support_pagination: false) do |t|
|
46
|
+
# Record metadata that is necessary for elasticgraph-graphql to correctly recognize and handle
|
47
|
+
# this sub-aggregation correctly.
|
48
|
+
t.runtime_metadata_overrides = {elasticgraph_category: :nested_sub_aggregation_connection}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
document_pagination_types =
|
53
|
+
if indexed?
|
54
|
+
schema_def_state.factory.build_relay_pagination_types(name, include_total_edge_count: true, derived_indexed_types: (_ = self).derived_indexed_types)
|
55
|
+
elsif schema_def_state.paginated_collection_element_types.include?(name)
|
56
|
+
schema_def_state.factory.build_relay_pagination_types(name, include_total_edge_count: true)
|
57
|
+
else
|
58
|
+
[] # : ::Array[SchemaElements::ObjectType]
|
59
|
+
end
|
60
|
+
|
61
|
+
sort_order_enum_type = schema_def_state.enums_for_indexed_types.sort_order_enum_for(self)
|
62
|
+
derived_sort_order_enum_types = [sort_order_enum_type].compact + (sort_order_enum_type&.derived_graphql_types || [])
|
63
|
+
|
64
|
+
to_input_filters +
|
65
|
+
document_pagination_types +
|
66
|
+
indexed_aggregation_pagination_types +
|
67
|
+
sub_aggregation_types +
|
68
|
+
derived_sort_order_enum_types +
|
69
|
+
build_aggregation_sub_aggregations_types + [
|
70
|
+
indexed_agg_type,
|
71
|
+
to_grouped_by_type,
|
72
|
+
to_aggregated_values_type
|
73
|
+
].compact
|
74
|
+
end
|
75
|
+
|
76
|
+
def has_custom_mapping_type?
|
77
|
+
mapping_type = mapping_options[:type]
|
78
|
+
mapping_type && mapping_type != "object"
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# Converts the type to the corresponding input filter type.
|
84
|
+
def to_input_filters
|
85
|
+
return [] if does_not_support?(&:filterable?)
|
86
|
+
|
87
|
+
schema_def_state.factory.build_standard_filter_input_types_for_index_object_type(name) do |t|
|
88
|
+
graphql_fields_by_name.values.each do |field|
|
89
|
+
if field.filterable?
|
90
|
+
t.graphql_fields_by_name[field.name] = field.to_filter_field(parent_type: t)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Generates the `*SubAggregation` types for all of the `mapping type: "nested"` fields that reference this type.
|
97
|
+
# A different `*SubAggregation` type needs to be generated for each nested field reference, and for each parent nesting
|
98
|
+
# context of that nested field reference. This is necessary because we will support different available `sub_aggregations`
|
99
|
+
# based on the parents of a particular nested field.
|
100
|
+
#
|
101
|
+
# For example, given a `Player` object type definition and a `Team` type definition like this:
|
102
|
+
#
|
103
|
+
# schema.object_type "Team" do |t|
|
104
|
+
# t.field "id", "ID!"
|
105
|
+
# t.field "name", "String"
|
106
|
+
# t.field "players", "[Player!]!" do |f|
|
107
|
+
# f.mapping type: "nested"
|
108
|
+
# end
|
109
|
+
# t.index "teams"
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# ...we will generate a `TeamPlayerSubAggregation` type which will have a `sub_aggregations` field which can have
|
113
|
+
# `parent_team` and `seasons` fields (assuming `Player` has a `seasons` nested field...).
|
114
|
+
def sub_aggregation_types_for_nested_field_references
|
115
|
+
schema_def_state.user_defined_field_references_by_type_name.fetch(name) { [] }.select(&:nested?).flat_map do |nested_field_ref|
|
116
|
+
schema_def_state.sub_aggregation_paths_for(nested_field_ref.parent_type).map do |path|
|
117
|
+
schema_def_state.factory.new_object_type type_ref.as_sub_aggregation(parent_doc_types: path.parent_doc_types).name do |t|
|
118
|
+
t.documentation "Return type representing a bucket of `#{name}` objects for a sub-aggregation within each `#{type_ref.as_parent_aggregation(parent_doc_types: path.parent_doc_types).name}`."
|
119
|
+
|
120
|
+
t.field schema_def_state.schema_elements.count_detail, "AggregationCountDetail", graphql_only: true do |f|
|
121
|
+
f.documentation "Details of the count of `#{name}` documents in a sub-aggregation bucket."
|
122
|
+
end
|
123
|
+
|
124
|
+
if supports?(&:groupable?)
|
125
|
+
t.field schema_def_state.schema_elements.grouped_by, type_ref.as_grouped_by.name, graphql_only: true do |f|
|
126
|
+
f.documentation "Used to specify the `#{name}` fields to group by. The returned values identify each sub-aggregation bucket."
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
if supports?(&:aggregatable?)
|
131
|
+
t.field schema_def_state.schema_elements.aggregated_values, type_ref.as_aggregated_values.name, graphql_only: true do |f|
|
132
|
+
f.documentation "Provides computed aggregated values over all `#{name}` documents in a sub-aggregation bucket."
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
if graphql_fields_by_name.values.any?(&:sub_aggregatable?)
|
137
|
+
sub_aggs_name = type_ref.as_aggregation_sub_aggregations(parent_doc_types: path.parent_doc_types + [name]).name
|
138
|
+
t.field schema_def_state.schema_elements.sub_aggregations, sub_aggs_name, graphql_only: true do |f|
|
139
|
+
f.documentation "Used to perform sub-aggregations of `#{t.name}` data."
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Builds the `*AggregationSubAggregations` types. For example, for an indexed type named `Team` which has nested fields,
|
148
|
+
# this would generate a `TeamAggregationSubAggregations` type. This type provides access to the various sub-aggregation
|
149
|
+
# fields.
|
150
|
+
def build_aggregation_sub_aggregations_types
|
151
|
+
# The sub-aggregation types do not generate correctly for abstract types, so for now we omit sub-aggregations for abstract types.
|
152
|
+
return [] if abstract?
|
153
|
+
|
154
|
+
sub_aggregatable_fields = graphql_fields_by_name.values.select(&:sub_aggregatable?)
|
155
|
+
return [] if sub_aggregatable_fields.empty?
|
156
|
+
|
157
|
+
schema_def_state.sub_aggregation_paths_for(self).map do |path|
|
158
|
+
agg_sub_aggs_type_ref = type_ref.as_aggregation_sub_aggregations(
|
159
|
+
parent_doc_types: path.parent_doc_types,
|
160
|
+
field_path: path.field_path
|
161
|
+
)
|
162
|
+
|
163
|
+
schema_def_state.factory.new_object_type agg_sub_aggs_type_ref.name do |t|
|
164
|
+
under_field_description = "under `#{path.field_path_string}` " unless path.field_path.empty?
|
165
|
+
t.documentation "Provides access to the `#{schema_def_state.schema_elements.sub_aggregations}` #{under_field_description}within each `#{type_ref.as_parent_aggregation(parent_doc_types: path.parent_doc_types).name}`."
|
166
|
+
|
167
|
+
sub_aggregatable_fields.each do |field|
|
168
|
+
if field.nested?
|
169
|
+
unwrapped_type = field.type_for_derived_types.fully_unwrapped
|
170
|
+
field_type_name = unwrapped_type
|
171
|
+
.as_sub_aggregation(parent_doc_types: path.parent_doc_types)
|
172
|
+
.as_connection
|
173
|
+
.name
|
174
|
+
|
175
|
+
field.define_sub_aggregations_field(parent_type: t, type: field_type_name) do |f|
|
176
|
+
f.argument schema_def_state.schema_elements.filter, unwrapped_type.as_filter_input.name do |a|
|
177
|
+
a.documentation "Used to filter the `#{unwrapped_type.name}` documents included in this sub-aggregation based on the provided criteria."
|
178
|
+
end
|
179
|
+
|
180
|
+
f.argument schema_def_state.schema_elements.first, "Int" do |a|
|
181
|
+
a.documentation "Determines how many sub-aggregation buckets should be returned."
|
182
|
+
end
|
183
|
+
end
|
184
|
+
else
|
185
|
+
field_type_name = type_ref.as_aggregation_sub_aggregations(
|
186
|
+
parent_doc_types: path.parent_doc_types,
|
187
|
+
field_path: path.field_path + [field]
|
188
|
+
).name
|
189
|
+
|
190
|
+
field.define_sub_aggregations_field(parent_type: t, type: field_type_name)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def to_indexed_aggregation_type
|
198
|
+
return nil unless indexed?
|
199
|
+
|
200
|
+
schema_def_state.factory.new_object_type type_ref.as_aggregation.name do |t|
|
201
|
+
t.documentation "Return type representing a bucket of `#{name}` documents for an aggregations query."
|
202
|
+
|
203
|
+
if supports?(&:groupable?)
|
204
|
+
t.field schema_def_state.schema_elements.grouped_by, type_ref.as_grouped_by.name, graphql_only: true do |f|
|
205
|
+
f.documentation "Used to specify the `#{name}` fields to group by. The returned values identify each aggregation bucket."
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
t.field schema_def_state.schema_elements.count, "JsonSafeLong!", graphql_only: true do |f|
|
210
|
+
f.documentation "The count of `#{name}` documents in an aggregation bucket."
|
211
|
+
end
|
212
|
+
|
213
|
+
if supports?(&:aggregatable?)
|
214
|
+
t.field schema_def_state.schema_elements.aggregated_values, type_ref.as_aggregated_values.name, graphql_only: true do |f|
|
215
|
+
f.documentation "Provides computed aggregated values over all `#{name}` documents in an aggregation bucket."
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# The sub-aggregation types do not generate correctly for abstract types, so for now we omit sub-aggregations for abstract types.
|
220
|
+
if !abstract? && supports?(&:sub_aggregatable?)
|
221
|
+
t.field schema_def_state.schema_elements.sub_aggregations, type_ref.as_aggregation_sub_aggregations.name, graphql_only: true do |f|
|
222
|
+
f.documentation "Used to perform sub-aggregations of `#{t.name}` data."
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Record metadata that is necessary for elasticgraph-graphql to correctly recognize and handle
|
227
|
+
# this indexed aggregation type correctly.
|
228
|
+
t.runtime_metadata_overrides = {source_type: name, elasticgraph_category: :indexed_aggregation}
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def to_grouped_by_type
|
233
|
+
# If the type uses a custom mapping type we don't know how it can be aggregated, so we assume it needs no aggregation type.
|
234
|
+
# TODO: clean this up using an interface instead of checking mapping options.
|
235
|
+
return nil if has_custom_mapping_type?
|
236
|
+
|
237
|
+
new_non_empty_object_type type_ref.as_grouped_by.name do |t|
|
238
|
+
t.documentation "Type used to specify the `#{name}` fields to group by for aggregations."
|
239
|
+
|
240
|
+
graphql_fields_by_name.values.each do |field|
|
241
|
+
field.define_grouped_by_field(t)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def to_aggregated_values_type
|
247
|
+
# If the type uses a custom mapping type we don't know how it can be aggregated, so we assume it needs no aggregation type.
|
248
|
+
# TODO: clean this up using an interface instead of checking mapping options.
|
249
|
+
return nil if has_custom_mapping_type?
|
250
|
+
|
251
|
+
new_non_empty_object_type type_ref.as_aggregated_values.name do |t|
|
252
|
+
t.documentation "Type used to perform aggregation computations on `#{name}` fields."
|
253
|
+
|
254
|
+
graphql_fields_by_name.values.each do |field|
|
255
|
+
field.define_aggregated_values_field(t)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def new_non_empty_object_type(name, &block)
|
261
|
+
type = schema_def_state.factory.new_object_type(name, &block)
|
262
|
+
type unless type.graphql_fields_by_name.empty?
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
@@ -0,0 +1,38 @@
|
|
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/constants"
|
10
|
+
|
11
|
+
module ElasticGraph
|
12
|
+
module SchemaDefinition
|
13
|
+
module Mixins
|
14
|
+
# Used to verify the validity of the name of GraphQL schema elements.
|
15
|
+
#
|
16
|
+
# @note This mixin is designed to be used via `prepend`, so it can add a constructor override that enforces
|
17
|
+
# the GraphQL name pattern as the object is built.
|
18
|
+
module VerifiesGraphQLName
|
19
|
+
# @private
|
20
|
+
def initialize(...)
|
21
|
+
__skip__ = super(...) # __skip__ tells Steep to ignore this
|
22
|
+
|
23
|
+
VerifiesGraphQLName.verify_name!(name)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Raises if the provided name is invalid.
|
27
|
+
#
|
28
|
+
# @param name [String] name of GraphQL schema element
|
29
|
+
# @return [void]
|
30
|
+
# @raise [InvalidGraphQLNameError] if the name is invalid
|
31
|
+
def self.verify_name!(name)
|
32
|
+
return if GRAPHQL_NAME_PATTERN.match?(name)
|
33
|
+
raise InvalidGraphQLNameError, "Not a valid GraphQL name: `#{name}`. #{GRAPHQL_NAME_VALIDITY_DESCRIPTION}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|