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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +7 -0
  4. data/elasticgraph-schema_definition.gemspec +26 -0
  5. data/lib/elastic_graph/schema_definition/api.rb +359 -0
  6. data/lib/elastic_graph/schema_definition/factory.rb +506 -0
  7. data/lib/elastic_graph/schema_definition/indexing/derived_fields/append_only_set.rb +79 -0
  8. data/lib/elastic_graph/schema_definition/indexing/derived_fields/field_initializer_support.rb +59 -0
  9. data/lib/elastic_graph/schema_definition/indexing/derived_fields/immutable_value.rb +99 -0
  10. data/lib/elastic_graph/schema_definition/indexing/derived_fields/min_or_max_value.rb +62 -0
  11. data/lib/elastic_graph/schema_definition/indexing/derived_indexed_type.rb +346 -0
  12. data/lib/elastic_graph/schema_definition/indexing/event_envelope.rb +74 -0
  13. data/lib/elastic_graph/schema_definition/indexing/field.rb +181 -0
  14. data/lib/elastic_graph/schema_definition/indexing/field_reference.rb +51 -0
  15. data/lib/elastic_graph/schema_definition/indexing/field_type/enum.rb +65 -0
  16. data/lib/elastic_graph/schema_definition/indexing/field_type/object.rb +113 -0
  17. data/lib/elastic_graph/schema_definition/indexing/field_type/scalar.rb +51 -0
  18. data/lib/elastic_graph/schema_definition/indexing/field_type/union.rb +70 -0
  19. data/lib/elastic_graph/schema_definition/indexing/index.rb +318 -0
  20. data/lib/elastic_graph/schema_definition/indexing/json_schema_field_metadata.rb +34 -0
  21. data/lib/elastic_graph/schema_definition/indexing/json_schema_with_metadata.rb +234 -0
  22. data/lib/elastic_graph/schema_definition/indexing/list_counts_mapping.rb +53 -0
  23. data/lib/elastic_graph/schema_definition/indexing/relationship_resolver.rb +96 -0
  24. data/lib/elastic_graph/schema_definition/indexing/rollover_config.rb +25 -0
  25. data/lib/elastic_graph/schema_definition/indexing/update_target_factory.rb +54 -0
  26. data/lib/elastic_graph/schema_definition/indexing/update_target_resolver.rb +195 -0
  27. data/lib/elastic_graph/schema_definition/json_schema_pruner.rb +61 -0
  28. data/lib/elastic_graph/schema_definition/mixins/can_be_graphql_only.rb +31 -0
  29. data/lib/elastic_graph/schema_definition/mixins/has_derived_graphql_type_customizations.rb +119 -0
  30. data/lib/elastic_graph/schema_definition/mixins/has_directives.rb +65 -0
  31. data/lib/elastic_graph/schema_definition/mixins/has_documentation.rb +74 -0
  32. data/lib/elastic_graph/schema_definition/mixins/has_indices.rb +281 -0
  33. data/lib/elastic_graph/schema_definition/mixins/has_readable_to_s_and_inspect.rb +46 -0
  34. data/lib/elastic_graph/schema_definition/mixins/has_subtypes.rb +116 -0
  35. data/lib/elastic_graph/schema_definition/mixins/has_type_info.rb +181 -0
  36. data/lib/elastic_graph/schema_definition/mixins/implements_interfaces.rb +122 -0
  37. data/lib/elastic_graph/schema_definition/mixins/supports_default_value.rb +47 -0
  38. data/lib/elastic_graph/schema_definition/mixins/supports_filtering_and_aggregation.rb +267 -0
  39. data/lib/elastic_graph/schema_definition/mixins/verifies_graphql_name.rb +38 -0
  40. data/lib/elastic_graph/schema_definition/rake_tasks.rb +190 -0
  41. data/lib/elastic_graph/schema_definition/results.rb +404 -0
  42. data/lib/elastic_graph/schema_definition/schema_artifact_manager.rb +482 -0
  43. data/lib/elastic_graph/schema_definition/schema_elements/argument.rb +56 -0
  44. data/lib/elastic_graph/schema_definition/schema_elements/built_in_types.rb +1541 -0
  45. data/lib/elastic_graph/schema_definition/schema_elements/deprecated_element.rb +21 -0
  46. data/lib/elastic_graph/schema_definition/schema_elements/directive.rb +40 -0
  47. data/lib/elastic_graph/schema_definition/schema_elements/enum_type.rb +189 -0
  48. data/lib/elastic_graph/schema_definition/schema_elements/enum_value.rb +73 -0
  49. data/lib/elastic_graph/schema_definition/schema_elements/enum_value_namer.rb +89 -0
  50. data/lib/elastic_graph/schema_definition/schema_elements/enums_for_indexed_types.rb +82 -0
  51. data/lib/elastic_graph/schema_definition/schema_elements/field.rb +1085 -0
  52. data/lib/elastic_graph/schema_definition/schema_elements/field_path.rb +112 -0
  53. data/lib/elastic_graph/schema_definition/schema_elements/field_source.rb +16 -0
  54. data/lib/elastic_graph/schema_definition/schema_elements/graphql_sdl_enumerator.rb +113 -0
  55. data/lib/elastic_graph/schema_definition/schema_elements/input_field.rb +31 -0
  56. data/lib/elastic_graph/schema_definition/schema_elements/input_type.rb +60 -0
  57. data/lib/elastic_graph/schema_definition/schema_elements/interface_type.rb +72 -0
  58. data/lib/elastic_graph/schema_definition/schema_elements/list_counts_state.rb +40 -0
  59. data/lib/elastic_graph/schema_definition/schema_elements/object_type.rb +53 -0
  60. data/lib/elastic_graph/schema_definition/schema_elements/relationship.rb +218 -0
  61. data/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb +310 -0
  62. data/lib/elastic_graph/schema_definition/schema_elements/sort_order_enum_value.rb +36 -0
  63. data/lib/elastic_graph/schema_definition/schema_elements/sub_aggregation_path.rb +66 -0
  64. data/lib/elastic_graph/schema_definition/schema_elements/type_namer.rb +237 -0
  65. data/lib/elastic_graph/schema_definition/schema_elements/type_reference.rb +353 -0
  66. data/lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb +579 -0
  67. data/lib/elastic_graph/schema_definition/schema_elements/union_type.rb +157 -0
  68. data/lib/elastic_graph/schema_definition/scripting/file_system_repository.rb +77 -0
  69. data/lib/elastic_graph/schema_definition/scripting/script.rb +48 -0
  70. data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_day_of_week.painless +24 -0
  71. data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_time_of_day.painless +41 -0
  72. data/lib/elastic_graph/schema_definition/scripting/scripts/filter/by_time_of_day.painless +22 -0
  73. data/lib/elastic_graph/schema_definition/scripting/scripts/update/index_data.painless +93 -0
  74. data/lib/elastic_graph/schema_definition/state.rb +212 -0
  75. data/lib/elastic_graph/schema_definition/test_support.rb +113 -0
  76. 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