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/constants"
|
10
|
+
require "elastic_graph/schema_definition/indexing/json_schema_field_metadata"
|
11
|
+
require "elastic_graph/schema_definition/indexing/list_counts_mapping"
|
12
|
+
require "elastic_graph/support/hash_util"
|
13
|
+
require "elastic_graph/support/memoizable_data"
|
14
|
+
|
15
|
+
module ElasticGraph
|
16
|
+
module SchemaDefinition
|
17
|
+
module Indexing
|
18
|
+
# Represents a field in a JSON document during indexing.
|
19
|
+
#
|
20
|
+
# @api private
|
21
|
+
class Field < Support::MemoizableData.define(
|
22
|
+
:name,
|
23
|
+
:name_in_index,
|
24
|
+
:type,
|
25
|
+
:json_schema_layers,
|
26
|
+
:indexing_field_type,
|
27
|
+
:accuracy_confidence,
|
28
|
+
:json_schema_customizations,
|
29
|
+
:mapping_customizations,
|
30
|
+
:source,
|
31
|
+
:runtime_field_script
|
32
|
+
)
|
33
|
+
# JSON schema overrides that automatically apply to specific mapping types so that the JSON schema
|
34
|
+
# validation will reject values which cannot be indexed into fields of a specific mapping type.
|
35
|
+
#
|
36
|
+
# @see https://www.elastic.co/guide/en/elasticsearch/reference/current/number.html Elasticsearch numeric field type documentation
|
37
|
+
# @note We don't handle `integer` here because it's the default numeric type (handled by our definition of the `Int` scalar type).
|
38
|
+
# @note Likewise, we don't handle `long` here because a custom scalar type must be used for that since GraphQL's `Int` type can't handle long values.
|
39
|
+
JSON_SCHEMA_OVERRIDES_BY_MAPPING_TYPE = {
|
40
|
+
"byte" => {"minimum" => -(2**7), "maximum" => (2**7) - 1},
|
41
|
+
"short" => {"minimum" => -(2**15), "maximum" => (2**15) - 1},
|
42
|
+
"keyword" => {"maxLength" => DEFAULT_MAX_KEYWORD_LENGTH},
|
43
|
+
"text" => {"maxLength" => DEFAULT_MAX_TEXT_LENGTH}
|
44
|
+
}
|
45
|
+
|
46
|
+
# @return [Hash<String, Object>] the mapping for this field. The returned hash should be composed entirely
|
47
|
+
# of Ruby primitives that, when converted to a JSON string, match the structure required by
|
48
|
+
# [Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html).
|
49
|
+
def mapping
|
50
|
+
@mapping ||= begin
|
51
|
+
raw_mapping = indexing_field_type
|
52
|
+
.to_mapping
|
53
|
+
.merge(Support::HashUtil.stringify_keys(mapping_customizations))
|
54
|
+
|
55
|
+
if (object_type = type.fully_unwrapped.as_object_type) && type.list? && mapping_customizations[:type] == "nested"
|
56
|
+
# If it's an object list field using the `nested` type, we need to add a `__counts` field to
|
57
|
+
# the mapping for all of its subfields which are lists.
|
58
|
+
ListCountsMapping.merged_into(raw_mapping, for_type: object_type)
|
59
|
+
else
|
60
|
+
raw_mapping
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [Hash<String, Object>] the JSON schema definition for this field. The returned object should
|
66
|
+
# be composed entirely of Ruby primitives that, when converted to a JSON string, match the
|
67
|
+
# requirements of [the JSON schema spec](https://json-schema.org/).
|
68
|
+
def json_schema
|
69
|
+
json_schema_layers
|
70
|
+
.reverse # resolve layers from innermost to outermost wrappings
|
71
|
+
.reduce(inner_json_schema) { |acc, layer| process_layer(layer, acc) }
|
72
|
+
.merge(outer_json_schema_customizations)
|
73
|
+
.then { |h| Support::HashUtil.stringify_keys(h) }
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [JSONSchemaFieldMetadata] additional ElasticGraph metadata to be stored in the JSON schema for this field.
|
77
|
+
def json_schema_metadata
|
78
|
+
JSONSchemaFieldMetadata.new(type: type.name, name_in_index: name_in_index)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Builds a hash containing the mapping for the provided fields, normalizing it in the same way that the
|
82
|
+
# datastore does so that consistency checks between our index configuration and what's in the datastore
|
83
|
+
# work properly.
|
84
|
+
#
|
85
|
+
# @param fields [Array<Field>] fields to generate a mapping hash from
|
86
|
+
# @return [Hash<String, Object>] generated mapping hash
|
87
|
+
def self.normalized_mapping_hash_for(fields)
|
88
|
+
# When an object field has `properties`, the datastore normalizes the mapping by dropping
|
89
|
+
# the `type => object` (it's implicit, as `properties` are only valid on an object...).
|
90
|
+
# OTOH, when there are no properties, the datastore normalizes the mapping by dropping the
|
91
|
+
# empty `properties` entry and instead returning `type => object`.
|
92
|
+
return {"type" => "object"} if fields.empty?
|
93
|
+
|
94
|
+
# Partition the fields into runtime fields and normal fields based on the presence of runtime_script
|
95
|
+
runtime_fields, normal_fields = fields.partition(&:runtime_field_script)
|
96
|
+
|
97
|
+
mapping_hash = {
|
98
|
+
"properties" => normal_fields.to_h { |f| [f.name_in_index, f.mapping] }
|
99
|
+
}
|
100
|
+
unless runtime_fields.empty?
|
101
|
+
mapping_hash["runtime"] = runtime_fields.to_h do |f|
|
102
|
+
[f.name_in_index, f.mapping.merge({"script" => {"source" => f.runtime_field_script}})]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
mapping_hash
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def inner_json_schema
|
112
|
+
user_specified_customizations =
|
113
|
+
if user_specified_json_schema_customizations_go_on_outside?
|
114
|
+
{} # : ::Hash[::String, untyped]
|
115
|
+
else
|
116
|
+
Support::HashUtil.stringify_keys(json_schema_customizations)
|
117
|
+
end
|
118
|
+
|
119
|
+
customizations_from_mapping = JSON_SCHEMA_OVERRIDES_BY_MAPPING_TYPE[mapping["type"]] || {}
|
120
|
+
customizations = customizations_from_mapping.merge(user_specified_customizations)
|
121
|
+
customizations = indexing_field_type.format_field_json_schema_customizations(customizations)
|
122
|
+
|
123
|
+
ref = {"$ref" => "#/$defs/#{type.unwrapped_name}"}
|
124
|
+
return ref if customizations.empty?
|
125
|
+
|
126
|
+
# Combine any customizations with type ref under an "allOf" subschema:
|
127
|
+
# All of these properties must hold true for the type to be valid.
|
128
|
+
#
|
129
|
+
# Note that if we simply combine the customizations with the `$ref`
|
130
|
+
# at the same level, it will not work, because other subschema
|
131
|
+
# properties are ignored when they are in the same object as a `$ref`:
|
132
|
+
# https://github.com/json-schema-org/JSON-Schema-Test-Suite/blob/2.0.0/tests/draft7/ref.json#L165-L168
|
133
|
+
{"allOf" => [ref, customizations]}
|
134
|
+
end
|
135
|
+
|
136
|
+
def outer_json_schema_customizations
|
137
|
+
return {} unless user_specified_json_schema_customizations_go_on_outside?
|
138
|
+
Support::HashUtil.stringify_keys(json_schema_customizations)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Indicates if the user-specified JSON schema customizations should go on the inside
|
142
|
+
# (where they normally go) or on the outside. They only go on the outside when it's
|
143
|
+
# an array field, because then they apply to the array itself instead of the items in the
|
144
|
+
# array.
|
145
|
+
def user_specified_json_schema_customizations_go_on_outside?
|
146
|
+
json_schema_layers.include?(:array)
|
147
|
+
end
|
148
|
+
|
149
|
+
def process_layer(layer, schema)
|
150
|
+
case layer
|
151
|
+
when :nullable
|
152
|
+
make_nullable(schema)
|
153
|
+
when :array
|
154
|
+
make_array(schema)
|
155
|
+
else
|
156
|
+
# :nocov: - layer is only ever `:nullable` or `:array` so we never get here
|
157
|
+
schema
|
158
|
+
# :nocov:
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def make_nullable(schema)
|
163
|
+
# Here we use "anyOf" to ensure that JSON can either match the schema OR null.
|
164
|
+
#
|
165
|
+
# (Using "oneOf" would mean that if we had a schema that also allowed null,
|
166
|
+
# null would never be allowed, since "oneOf" must match exactly one subschema).
|
167
|
+
{
|
168
|
+
"anyOf" => [
|
169
|
+
schema,
|
170
|
+
{"type" => "null"}
|
171
|
+
]
|
172
|
+
}
|
173
|
+
end
|
174
|
+
|
175
|
+
def make_array(schema)
|
176
|
+
{"type" => "array", "items" => schema}
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,51 @@
|
|
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 Indexing
|
12
|
+
# @!parse class FieldReference < ::Data; end
|
13
|
+
FieldReference = ::Data.define(
|
14
|
+
:name,
|
15
|
+
:name_in_index,
|
16
|
+
:type,
|
17
|
+
:mapping_options,
|
18
|
+
:json_schema_options,
|
19
|
+
:accuracy_confidence,
|
20
|
+
:source,
|
21
|
+
:runtime_field_script
|
22
|
+
)
|
23
|
+
|
24
|
+
# A lazy reference to a {Field}. It contains all attributes needed to build a {Field}, but the referenced `type` may not be
|
25
|
+
# resolvable yet (which is why this exists).
|
26
|
+
#
|
27
|
+
# @api private
|
28
|
+
class FieldReference < ::Data
|
29
|
+
# @return [Field, nil] the {Field} this reference resolves to (if it can be resolved)
|
30
|
+
def resolve
|
31
|
+
return nil unless (resolved_type = type.fully_unwrapped.resolved)
|
32
|
+
|
33
|
+
Indexing::Field.new(
|
34
|
+
name: name,
|
35
|
+
name_in_index: name_in_index,
|
36
|
+
type: type,
|
37
|
+
json_schema_layers: type.json_schema_layers,
|
38
|
+
indexing_field_type: resolved_type.to_indexing_field_type,
|
39
|
+
accuracy_confidence: accuracy_confidence,
|
40
|
+
json_schema_customizations: json_schema_options,
|
41
|
+
mapping_customizations: mapping_options,
|
42
|
+
source: source,
|
43
|
+
runtime_field_script: runtime_field_script
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
# @dynamic initialize, with, name, name_in_index, type, mapping_options, json_schema_options, accuracy_confidence, source, runtime_field_script
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,65 @@
|
|
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 Indexing
|
12
|
+
# Contains implementation logic for the different types of indexing fields.
|
13
|
+
#
|
14
|
+
# @api private
|
15
|
+
module FieldType
|
16
|
+
# @!parse class Enum < ::Data; end
|
17
|
+
Enum = ::Data.define(:enum_value_names)
|
18
|
+
|
19
|
+
# Responsible for the JSON schema and mapping of a {SchemaElements::EnumType}.
|
20
|
+
#
|
21
|
+
# @!attribute [r] enum_value_names
|
22
|
+
# @return [Array<String>] list of names of values in this enum type.
|
23
|
+
#
|
24
|
+
# @api private
|
25
|
+
class Enum < ::Data
|
26
|
+
# @return [Hash<String, ::Object>] the JSON schema for this enum type.
|
27
|
+
def to_json_schema
|
28
|
+
{"type" => "string", "enum" => enum_value_names}
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Hash<String, ::Object>] the datastore mapping for this enum type.
|
32
|
+
def to_mapping
|
33
|
+
{"type" => "keyword"}
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Hash<String, ::Object>] additional ElasticGraph metadata to put in the JSON schema for this enum type.
|
37
|
+
def json_schema_field_metadata_by_field_name
|
38
|
+
{}
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param customizations [Hash<String, ::Object>] JSON schema customizations
|
42
|
+
# @return [Hash<String, ::Object>] formatted customizations.
|
43
|
+
def format_field_json_schema_customizations(customizations)
|
44
|
+
# Since an enum type already restricts the values to a small set of allowed values, we do not need to keep
|
45
|
+
# other customizations (such as the `maxLength` field customization EG automatically applies to fields
|
46
|
+
# indexed as a `keyword`--we don't allow enum values to exceed that length, anyway).
|
47
|
+
#
|
48
|
+
# It's desirable to restrict what customizations are applied because when a publisher uses the JSON schema
|
49
|
+
# to generate code using a library such as https://github.com/pwall567/json-kotlin-schema-codegen, we found
|
50
|
+
# that the presence of extra field customizations inhibits the library's ability to generate code in the way
|
51
|
+
# we want (it causes the type of the enum to change since the JSON schema changes from a direct `$ref` to
|
52
|
+
# being wrapped in an `allOf`).
|
53
|
+
#
|
54
|
+
# However, we still want to apply `enum` customizations--this allows a user to "narrow" the set of allowed
|
55
|
+
# values for a field. For example, a `Currency` enum could contain every currency, and a user may want to
|
56
|
+
# restrict a specific `currency` field to a subset of currencies (e.g. to just USD, CAD, and EUR).
|
57
|
+
customizations.slice("enum")
|
58
|
+
end
|
59
|
+
|
60
|
+
# @dynamic initialize, enum_value_names
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,113 @@
|
|
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
|
+
require "elastic_graph/support/hash_util"
|
11
|
+
require "elastic_graph/support/memoizable_data"
|
12
|
+
|
13
|
+
module ElasticGraph
|
14
|
+
module SchemaDefinition
|
15
|
+
module Indexing
|
16
|
+
module FieldType
|
17
|
+
# Responsible for the JSON schema and mapping of a {SchemaElements::ObjectType}.
|
18
|
+
#
|
19
|
+
# @!attribute [r] type_name
|
20
|
+
# @return [String] name of the object type
|
21
|
+
# @!attribute [r] subfields
|
22
|
+
# @return [Array<Field>] the subfields of this object type
|
23
|
+
# @!attribute [r] mapping_options
|
24
|
+
# @return [Hash<String, ::Object>] options to be included in the mapping
|
25
|
+
# @!attribute [r] json_schema_options
|
26
|
+
# @return [Hash<String, ::Object>] options to be included in the JSON schema
|
27
|
+
#
|
28
|
+
# @api private
|
29
|
+
class Object < Support::MemoizableData.define(:type_name, :subfields, :mapping_options, :json_schema_options)
|
30
|
+
# @return [Hash<String, ::Object>] the datastore mapping for this object type.
|
31
|
+
def to_mapping
|
32
|
+
@to_mapping ||= begin
|
33
|
+
base_mapping = Field.normalized_mapping_hash_for(subfields)
|
34
|
+
# When a custom mapping type is used, we need to omit `properties`, because custom mapping
|
35
|
+
# types generally don't use `properties` (and if you need to use `properties` with a custom
|
36
|
+
# type, you're responsible for defining the properties).
|
37
|
+
base_mapping = base_mapping.except("properties") if (mapping_options[:type] || "object") != "object"
|
38
|
+
base_mapping.merge(Support::HashUtil.stringify_keys(mapping_options))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Hash<String, ::Object>] the JSON schema for this object type.
|
43
|
+
def to_json_schema
|
44
|
+
@to_json_schema ||=
|
45
|
+
if json_schema_options.empty?
|
46
|
+
# Fields that are `sourced_from` an alternate type must not be included in this types JSON schema,
|
47
|
+
# since events of this type won't include them.
|
48
|
+
other_source_subfields, json_schema_candidate_subfields = subfields.partition(&:source)
|
49
|
+
validate_sourced_fields_have_no_json_schema_overrides(other_source_subfields)
|
50
|
+
json_schema_subfields = json_schema_candidate_subfields.reject(&:runtime_field_script)
|
51
|
+
|
52
|
+
{
|
53
|
+
"type" => "object",
|
54
|
+
"properties" => json_schema_subfields.to_h { |f| [f.name, f.json_schema] }.merge(json_schema_typename_field),
|
55
|
+
# Note: `__typename` is intentionally not included in the `required` list. If `__typename` is present
|
56
|
+
# we want it validated (as we do by merging in `json_schema_typename_field`) but we only want
|
57
|
+
# to require it in the context of a union type. The union's json schema requires the field.
|
58
|
+
"required" => json_schema_subfields.map(&:name).freeze
|
59
|
+
}.freeze
|
60
|
+
else
|
61
|
+
Support::HashUtil.stringify_keys(json_schema_options)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [Hash<String, ::Object>] additional ElasticGraph metadata to put in the JSON schema for this object type.
|
66
|
+
def json_schema_field_metadata_by_field_name
|
67
|
+
subfields.to_h { |f| [f.name, f.json_schema_metadata] }
|
68
|
+
end
|
69
|
+
|
70
|
+
# @param customizations [Hash<String, ::Object>] JSON schema customizations
|
71
|
+
# @return [Hash<String, ::Object>] formatted customizations.
|
72
|
+
def format_field_json_schema_customizations(customizations)
|
73
|
+
customizations
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def after_initialize
|
79
|
+
subfields.freeze
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns a __typename property which we use for union types.
|
83
|
+
#
|
84
|
+
# This must always be set to the name of the type (thus the const value).
|
85
|
+
#
|
86
|
+
# We also add a "default" value. This does not impact validation, but rather
|
87
|
+
# aids tools like our kotlin codegen to save publishers from having to set the
|
88
|
+
# property explicitly when creating events.
|
89
|
+
def json_schema_typename_field
|
90
|
+
{
|
91
|
+
"__typename" => {
|
92
|
+
"type" => "string",
|
93
|
+
"const" => type_name,
|
94
|
+
"default" => type_name
|
95
|
+
}
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
def validate_sourced_fields_have_no_json_schema_overrides(other_source_subfields)
|
100
|
+
problem_fields = other_source_subfields.reject { |f| f.json_schema_customizations.empty? }
|
101
|
+
return if problem_fields.empty?
|
102
|
+
|
103
|
+
field_descriptions = problem_fields.map(&:name).sort.map { |f| "`#{f}`" }.join(", ")
|
104
|
+
raise SchemaError,
|
105
|
+
"`#{type_name}` has #{problem_fields.size} field(s) (#{field_descriptions}) that are `sourced_from` " \
|
106
|
+
"another type and also have JSON schema customizations. Instead, put the JSON schema " \
|
107
|
+
"customizations on the source type's field definitions."
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,51 @@
|
|
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/hash_util"
|
10
|
+
|
11
|
+
module ElasticGraph
|
12
|
+
module SchemaDefinition
|
13
|
+
module Indexing
|
14
|
+
module FieldType
|
15
|
+
# @!parse class Scalar < ::Data; end
|
16
|
+
Scalar = ::Data.define(:scalar_type)
|
17
|
+
|
18
|
+
# Responsible for the JSON schema and mapping of a {SchemaElements::ScalarType}.
|
19
|
+
#
|
20
|
+
# @!attribute [r] scalar_type
|
21
|
+
# @return [SchemaElements::ScalarType] the scalar type
|
22
|
+
#
|
23
|
+
# @api private
|
24
|
+
class Scalar < ::Data
|
25
|
+
# @return [Hash<String, ::Object>] the datastore mapping for this scalar type.
|
26
|
+
def to_mapping
|
27
|
+
Support::HashUtil.stringify_keys(scalar_type.mapping_options)
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Hash<String, ::Object>] the JSON schema for this scalar type.
|
31
|
+
def to_json_schema
|
32
|
+
Support::HashUtil.stringify_keys(scalar_type.json_schema_options)
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [Hash<String, ::Object>] additional ElasticGraph metadata to put in the JSON schema for this scalar type.
|
36
|
+
def json_schema_field_metadata_by_field_name
|
37
|
+
{}
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param customizations [Hash<String, ::Object>] JSON schema customizations
|
41
|
+
# @return [Hash<String, ::Object>] formatted customizations.
|
42
|
+
def format_field_json_schema_customizations(customizations)
|
43
|
+
customizations
|
44
|
+
end
|
45
|
+
|
46
|
+
# @dynamic initialize, scalar_type
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,70 @@
|
|
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_definition/indexing/field_type/object"
|
10
|
+
require "elastic_graph/support/hash_util"
|
11
|
+
|
12
|
+
module ElasticGraph
|
13
|
+
module SchemaDefinition
|
14
|
+
module Indexing
|
15
|
+
module FieldType
|
16
|
+
# Responsible for the JSON schema and mapping of a {SchemaElements::UnionType}.
|
17
|
+
#
|
18
|
+
# @note In JSON schema, we model this with a `oneOf`, and a `__typename` field on each subtype.
|
19
|
+
# @note Within the mapping, we have a single object type that has a set union of the properties
|
20
|
+
# of the subtypes (and also a `__typename` keyword field).
|
21
|
+
#
|
22
|
+
# @!attribute [r] subtypes_by_name
|
23
|
+
# @return [Hash<String, Object>] the subtypes of the union, keyed by name.
|
24
|
+
#
|
25
|
+
# @api private
|
26
|
+
class Union < ::Data.define(:subtypes_by_name)
|
27
|
+
# @return [Hash<String, ::Object>] the JSON schema for this union type.
|
28
|
+
def to_json_schema
|
29
|
+
subtype_json_schemas = subtypes_by_name.keys.map { |name| {"$ref" => "#/$defs/#{name}"} }
|
30
|
+
|
31
|
+
# A union type can represent multiple subtypes, referenced by the "anyOf" clause below.
|
32
|
+
# We also add a requirement for the presence of __typename to indicate which type
|
33
|
+
# is being referenced (this property is pre-defined on the type itself as a constant).
|
34
|
+
#
|
35
|
+
# Note: Although both "oneOf" and "anyOf" keywords are valid for combining schemas
|
36
|
+
# to form a union, and validate equivalently when no object can satisfy multiple of the
|
37
|
+
# subschemas (which is the case here given the __typename requirements are mutually
|
38
|
+
# exclusive), we chose to use "oneOf" here because it works better with this library:
|
39
|
+
# https://github.com/pwall567/json-kotlin-schema-codegen
|
40
|
+
{
|
41
|
+
"required" => %w[__typename],
|
42
|
+
"oneOf" => subtype_json_schemas
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Hash<String, ::Object>] the datastore mapping for this union type.
|
47
|
+
def to_mapping
|
48
|
+
mapping_subfields = subtypes_by_name.values.map(&:subfields).reduce([], :union)
|
49
|
+
|
50
|
+
Support::HashUtil.deep_merge(
|
51
|
+
Field.normalized_mapping_hash_for(mapping_subfields),
|
52
|
+
{"properties" => {"__typename" => {"type" => "keyword"}}}
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [Hash<String, ::Object>] additional ElasticGraph metadata to put in the JSON schema for this union type.
|
57
|
+
def json_schema_field_metadata_by_field_name
|
58
|
+
{}
|
59
|
+
end
|
60
|
+
|
61
|
+
# @param customizations [Hash<String, ::Object>] JSON schema customizations
|
62
|
+
# @return [Hash<String, ::Object>] formatted customizations.
|
63
|
+
def format_field_json_schema_customizations(customizations)
|
64
|
+
customizations
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|