elasticgraph-schema_definition 0.18.0.0

Sign up to get free protection for your applications and to get access to all the features.
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