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