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