elasticgraph-schema_artifacts 0.18.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (23) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +3 -0
  4. data/elasticgraph-schema_artifacts.gemspec +21 -0
  5. data/lib/elastic_graph/schema_artifacts/artifacts_helper_methods.rb +34 -0
  6. data/lib/elastic_graph/schema_artifacts/from_disk.rb +112 -0
  7. data/lib/elastic_graph/schema_artifacts/runtime_metadata/computation_detail.rb +34 -0
  8. data/lib/elastic_graph/schema_artifacts/runtime_metadata/enum.rb +65 -0
  9. data/lib/elastic_graph/schema_artifacts/runtime_metadata/extension.rb +51 -0
  10. data/lib/elastic_graph/schema_artifacts/runtime_metadata/extension_loader.rb +125 -0
  11. data/lib/elastic_graph/schema_artifacts/runtime_metadata/graphql_field.rb +54 -0
  12. data/lib/elastic_graph/schema_artifacts/runtime_metadata/hash_dumper.rb +21 -0
  13. data/lib/elastic_graph/schema_artifacts/runtime_metadata/index_definition.rb +78 -0
  14. data/lib/elastic_graph/schema_artifacts/runtime_metadata/index_field.rb +33 -0
  15. data/lib/elastic_graph/schema_artifacts/runtime_metadata/object_type.rb +81 -0
  16. data/lib/elastic_graph/schema_artifacts/runtime_metadata/params.rb +81 -0
  17. data/lib/elastic_graph/schema_artifacts/runtime_metadata/relation.rb +39 -0
  18. data/lib/elastic_graph/schema_artifacts/runtime_metadata/scalar_type.rb +94 -0
  19. data/lib/elastic_graph/schema_artifacts/runtime_metadata/schema.rb +99 -0
  20. data/lib/elastic_graph/schema_artifacts/runtime_metadata/schema_element_names.rb +165 -0
  21. data/lib/elastic_graph/schema_artifacts/runtime_metadata/sort_field.rb +47 -0
  22. data/lib/elastic_graph/schema_artifacts/runtime_metadata/update_target.rb +80 -0
  23. metadata +273 -0
@@ -0,0 +1,81 @@
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_artifacts/runtime_metadata/graphql_field"
10
+ require "elastic_graph/schema_artifacts/runtime_metadata/hash_dumper"
11
+ require "elastic_graph/schema_artifacts/runtime_metadata/update_target"
12
+
13
+ module ElasticGraph
14
+ module SchemaArtifacts
15
+ module RuntimeMetadata
16
+ # Provides runtime metadata related to object types.
17
+ class ObjectType < ::Data.define(
18
+ :update_targets,
19
+ :index_definition_names,
20
+ :graphql_fields_by_name,
21
+ :elasticgraph_category,
22
+ # Indicates the name of the GraphQL type from which this type was generated. Note that a `nil` value doesn't
23
+ # imply that this type was user-defined; we have recently introduced this metadata and are not yet setting
24
+ # it for all generated GraphQL types. For now, we are only setting it for specific cases where we need it.
25
+ :source_type,
26
+ :graphql_only_return_type
27
+ )
28
+ UPDATE_TARGETS = "update_targets"
29
+ INDEX_DEFINITION_NAMES = "index_definition_names"
30
+ GRAPHQL_FIELDS_BY_NAME = "graphql_fields_by_name"
31
+ ELASTICGRAPH_CATEGORY = "elasticgraph_category"
32
+ SOURCE_TYPE = "source_type"
33
+ GRAPHQL_ONLY_RETURN_TYPE = "graphql_only_return_type"
34
+
35
+ def initialize(update_targets:, index_definition_names:, graphql_fields_by_name:, elasticgraph_category:, source_type:, graphql_only_return_type:)
36
+ graphql_fields_by_name = graphql_fields_by_name.select { |name, field| field.needed?(name) }
37
+
38
+ super(
39
+ update_targets: update_targets,
40
+ index_definition_names: index_definition_names,
41
+ graphql_fields_by_name: graphql_fields_by_name,
42
+ elasticgraph_category: elasticgraph_category,
43
+ source_type: source_type,
44
+ graphql_only_return_type: graphql_only_return_type
45
+ )
46
+ end
47
+
48
+ def self.from_hash(hash)
49
+ update_targets = hash[UPDATE_TARGETS]&.map do |update_target_hash|
50
+ UpdateTarget.from_hash(update_target_hash)
51
+ end || []
52
+
53
+ graphql_fields_by_name = hash[GRAPHQL_FIELDS_BY_NAME]&.transform_values do |field_hash|
54
+ GraphQLField.from_hash(field_hash)
55
+ end || {}
56
+
57
+ new(
58
+ update_targets: update_targets,
59
+ index_definition_names: hash[INDEX_DEFINITION_NAMES] || [],
60
+ graphql_fields_by_name: graphql_fields_by_name,
61
+ elasticgraph_category: hash[ELASTICGRAPH_CATEGORY]&.to_sym || nil,
62
+ source_type: hash[SOURCE_TYPE] || nil,
63
+ graphql_only_return_type: !!hash[GRAPHQL_ONLY_RETURN_TYPE]
64
+ )
65
+ end
66
+
67
+ def to_dumpable_hash
68
+ {
69
+ # Keys here are ordered alphabetically; please keep them that way.
70
+ ELASTICGRAPH_CATEGORY => elasticgraph_category&.to_s,
71
+ GRAPHQL_FIELDS_BY_NAME => HashDumper.dump_hash(graphql_fields_by_name, &:to_dumpable_hash),
72
+ GRAPHQL_ONLY_RETURN_TYPE => graphql_only_return_type ? true : nil,
73
+ INDEX_DEFINITION_NAMES => index_definition_names,
74
+ SOURCE_TYPE => source_type,
75
+ UPDATE_TARGETS => update_targets.map(&:to_dumpable_hash)
76
+ }
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,81 @@
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_artifacts/runtime_metadata/hash_dumper"
10
+ require "elastic_graph/support/hash_util"
11
+
12
+ module ElasticGraph
13
+ module SchemaArtifacts
14
+ module RuntimeMetadata
15
+ module Param
16
+ def self.dump_params_hash(hash_of_params)
17
+ hash_of_params.sort_by(&:first).to_h { |name, param| [name, param.to_dumpable_hash(name)] }
18
+ end
19
+
20
+ def self.load_params_hash(hash_of_hashes)
21
+ hash_of_hashes.to_h { |name, hash| [name, from_hash(hash, name)] }
22
+ end
23
+
24
+ def self.from_hash(hash, name)
25
+ if hash.key?(StaticParam::VALUE)
26
+ StaticParam.from_hash(hash)
27
+ else
28
+ DynamicParam.from_hash(hash, name)
29
+ end
30
+ end
31
+ end
32
+
33
+ # Represents metadata about dynamic params we pass to our update scripts.
34
+ class DynamicParam < ::Data.define(:source_path, :cardinality)
35
+ SOURCE_PATH = "source_path"
36
+ CARDINALITY = "cardinality"
37
+
38
+ def self.from_hash(hash, name)
39
+ new(
40
+ source_path: hash[SOURCE_PATH] || name,
41
+ cardinality: hash.fetch(CARDINALITY).to_sym
42
+ )
43
+ end
44
+
45
+ def to_dumpable_hash(param_name)
46
+ {
47
+ # Keys here are ordered alphabetically; please keep them that way.
48
+ CARDINALITY => cardinality.to_s,
49
+ SOURCE_PATH => (source_path if source_path != param_name)
50
+ }
51
+ end
52
+
53
+ def value_for(event_or_prepared_record)
54
+ case cardinality
55
+ when :many then Support::HashUtil.fetch_leaf_values_at_path(event_or_prepared_record, source_path) { [] }
56
+ when :one then Support::HashUtil.fetch_value_at_path(event_or_prepared_record, source_path) { nil }
57
+ end
58
+ end
59
+ end
60
+
61
+ class StaticParam < ::Data.define(:value)
62
+ VALUE = "value"
63
+
64
+ def self.from_hash(hash)
65
+ new(value: hash.fetch(VALUE))
66
+ end
67
+
68
+ def to_dumpable_hash(param_name)
69
+ {
70
+ # Keys here are ordered alphabetically; please keep them that way.
71
+ VALUE => value
72
+ }
73
+ end
74
+
75
+ def value_for(event_or_prepared_record)
76
+ value
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,39 @@
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 SchemaArtifacts
11
+ module RuntimeMetadata
12
+ class Relation < ::Data.define(:foreign_key, :direction, :additional_filter, :foreign_key_nested_paths)
13
+ FOREIGN_KEY = "foreign_key"
14
+ DIRECTION = "direction"
15
+ ADDITIONAL_FILTER = "additional_filter"
16
+ FOREIGN_KEY_NESTED_PATHS = "foreign_key_nested_paths"
17
+
18
+ def self.from_hash(hash)
19
+ new(
20
+ foreign_key: hash[FOREIGN_KEY],
21
+ direction: hash.fetch(DIRECTION).to_sym,
22
+ additional_filter: hash[ADDITIONAL_FILTER] || {},
23
+ foreign_key_nested_paths: hash[FOREIGN_KEY_NESTED_PATHS] || []
24
+ )
25
+ end
26
+
27
+ def to_dumpable_hash
28
+ {
29
+ # Keys here are ordered alphabetically; please keep them that way.
30
+ ADDITIONAL_FILTER => additional_filter,
31
+ DIRECTION => direction.to_s,
32
+ FOREIGN_KEY => foreign_key,
33
+ FOREIGN_KEY_NESTED_PATHS => foreign_key_nested_paths
34
+ }
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,94 @@
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_artifacts/runtime_metadata/extension_loader"
10
+
11
+ module ElasticGraph
12
+ module SchemaArtifacts
13
+ module RuntimeMetadata
14
+ # Provides runtime metadata related to scalar types.
15
+ class ScalarType < ::Data.define(:coercion_adapter_ref, :indexing_preparer_ref)
16
+ def self.coercion_adapter_extension_loader
17
+ @coercion_adapter_extension_loader ||= ExtensionLoader.new(ScalarCoercionAdapterInterface)
18
+ end
19
+
20
+ def self.indexing_preparer_extension_loader
21
+ @indexing_preparer_extension_loader ||= ExtensionLoader.new(ScalarIndexingPreparerInterface)
22
+ end
23
+
24
+ DEFAULT_COERCION_ADAPTER_REF = {
25
+ "extension_name" => "ElasticGraph::GraphQL::ScalarCoercionAdapters::NoOp",
26
+ "require_path" => "elastic_graph/graphql/scalar_coercion_adapters/no_op"
27
+ }
28
+
29
+ DEFAULT_INDEXING_PREPARER_REF = {
30
+ "extension_name" => "ElasticGraph::Indexer::IndexingPreparers::NoOp",
31
+ "require_path" => "elastic_graph/indexer/indexing_preparers/no_op"
32
+ }
33
+
34
+ # Loads multiple `ScalarType`s from a hash mapping a scalar type name to its
35
+ # serialized hash form (matching what `to_dumpable_hash` returns). We expose a method
36
+ # this way because we want to use a single loader for all `ScalarType`s that
37
+ # need to get loaded, as it performs some caching for efficiency.
38
+ def self.load_many(scalar_type_hashes_by_name)
39
+ scalar_type_hashes_by_name.transform_values do |hash|
40
+ new(
41
+ coercion_adapter_ref: hash.fetch("coercion_adapter"),
42
+ # `indexing_preparer` is new as of Q4 2022, and as such is not present in schema artifacts
43
+ # dumped before then. Therefore, we allow for the key to not be present in the runtime
44
+ # metadata--important so that we don't have a "chicken and egg" problem where the rake tasks
45
+ # that need to be loaded to dump new schema artifacts fail at load time due to the missing key.
46
+ indexing_preparer_ref: hash.fetch("indexing_preparer", DEFAULT_INDEXING_PREPARER_REF)
47
+ )
48
+ end
49
+ end
50
+
51
+ # Loads the coercion adapter. This is done lazily on first access (rather than eagerly in `load_many`)
52
+ # to allow us to remove a runtime dependency of `elasticgraph-schema_artifacts` on `elasticgraph-graphql`.
53
+ # The built-in coercion adapters are defined in `elasticgraph-graphql`, and we want to be able to load
54
+ # runtime metadata without requiring the `elasticgraph-graphql` gem (and its dependencies) to be available.
55
+ # For example, we use runtime metadata from `elasticgraph-indexer` but do not want `elasticgraph-graphql`
56
+ # to be loaded as part of that.
57
+ #
58
+ # elasticgraph-graphql provides the one caller that calls this method, ensuring that the adapters are
59
+ # available to be loaded.
60
+ def load_coercion_adapter
61
+ Extension.load_from_hash(coercion_adapter_ref, via: self.class.coercion_adapter_extension_loader)
62
+ end
63
+
64
+ def load_indexing_preparer
65
+ Extension.load_from_hash(indexing_preparer_ref, via: self.class.indexing_preparer_extension_loader)
66
+ end
67
+
68
+ def to_dumpable_hash
69
+ {
70
+ # Keys here are ordered alphabetically; please keep them that way.
71
+ "coercion_adapter" => load_coercion_adapter.to_dumpable_hash,
72
+ "indexing_preparer" => load_indexing_preparer.to_dumpable_hash
73
+ }
74
+ end
75
+
76
+ # `to_h` is used internally by `Value#with` and we want `#to_dumpable_hash` to be the public API.
77
+ private :to_h
78
+ end
79
+
80
+ class ScalarCoercionAdapterInterface
81
+ def self.coerce_input(value, ctx)
82
+ end
83
+
84
+ def self.coerce_result(value, ctx)
85
+ end
86
+ end
87
+
88
+ class ScalarIndexingPreparerInterface
89
+ def self.prepare_for_indexing(value)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,99 @@
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_artifacts/runtime_metadata/enum"
10
+ require "elastic_graph/schema_artifacts/runtime_metadata/extension"
11
+ require "elastic_graph/schema_artifacts/runtime_metadata/extension_loader"
12
+ require "elastic_graph/schema_artifacts/runtime_metadata/hash_dumper"
13
+ require "elastic_graph/schema_artifacts/runtime_metadata/index_definition"
14
+ require "elastic_graph/schema_artifacts/runtime_metadata/object_type"
15
+ require "elastic_graph/schema_artifacts/runtime_metadata/scalar_type"
16
+ require "elastic_graph/schema_artifacts/runtime_metadata/schema_element_names"
17
+ require "elastic_graph/support/hash_util"
18
+
19
+ module ElasticGraph
20
+ module SchemaArtifacts
21
+ module RuntimeMetadata
22
+ # Entry point for runtime metadata for an entire schema.
23
+ class Schema < ::Data.define(
24
+ :object_types_by_name,
25
+ :scalar_types_by_name,
26
+ :enum_types_by_name,
27
+ :index_definitions_by_name,
28
+ :schema_element_names,
29
+ :graphql_extension_modules,
30
+ :static_script_ids_by_scoped_name
31
+ )
32
+ OBJECT_TYPES_BY_NAME = "object_types_by_name"
33
+ SCALAR_TYPES_BY_NAME = "scalar_types_by_name"
34
+ ENUM_TYPES_BY_NAME = "enum_types_by_name"
35
+ INDEX_DEFINITIONS_BY_NAME = "index_definitions_by_name"
36
+ SCHEMA_ELEMENT_NAMES = "schema_element_names"
37
+ GRAPHQL_EXTENSION_MODULES = "graphql_extension_modules"
38
+ STATIC_SCRIPT_IDS_BY_NAME = "static_script_ids_by_scoped_name"
39
+
40
+ def self.from_hash(hash, for_context:)
41
+ object_types_by_name = hash[OBJECT_TYPES_BY_NAME]&.transform_values do |type_hash|
42
+ ObjectType.from_hash(type_hash)
43
+ end || {}
44
+
45
+ scalar_types_by_name = hash[SCALAR_TYPES_BY_NAME]&.then do |subhash|
46
+ ScalarType.load_many(subhash)
47
+ end || {}
48
+
49
+ enum_types_by_name = hash[ENUM_TYPES_BY_NAME]&.transform_values do |type_hash|
50
+ Enum::Type.from_hash(type_hash)
51
+ end || {}
52
+
53
+ index_definitions_by_name = hash[INDEX_DEFINITIONS_BY_NAME]&.transform_values do |index_hash|
54
+ IndexDefinition.from_hash(index_hash)
55
+ end || {}
56
+
57
+ schema_element_names = SchemaElementNames.from_hash(hash.fetch(SCHEMA_ELEMENT_NAMES))
58
+
59
+ loader = ExtensionLoader.new(Module.new)
60
+ graphql_extension_modules =
61
+ if for_context == :graphql
62
+ hash[GRAPHQL_EXTENSION_MODULES]&.map do |ext_mod_hash|
63
+ Extension.load_from_hash(ext_mod_hash, via: loader)
64
+ end || []
65
+ else
66
+ # Avoid loading GraphQL extrnsion modules if we're not in a GraphQL context. We can't count
67
+ # on the extension modules even being available to load in other contexts.
68
+ [] # : ::Array[Extension]
69
+ end
70
+
71
+ static_script_ids_by_scoped_name = hash[STATIC_SCRIPT_IDS_BY_NAME] || {}
72
+
73
+ new(
74
+ object_types_by_name: object_types_by_name,
75
+ scalar_types_by_name: scalar_types_by_name,
76
+ enum_types_by_name: enum_types_by_name,
77
+ index_definitions_by_name: index_definitions_by_name,
78
+ schema_element_names: schema_element_names,
79
+ graphql_extension_modules: graphql_extension_modules,
80
+ static_script_ids_by_scoped_name: static_script_ids_by_scoped_name
81
+ )
82
+ end
83
+
84
+ def to_dumpable_hash
85
+ Support::HashUtil.recursively_prune_nils_and_empties_from({
86
+ # Keys here are ordered alphabetically; please keep them that way.
87
+ ENUM_TYPES_BY_NAME => HashDumper.dump_hash(enum_types_by_name, &:to_dumpable_hash),
88
+ GRAPHQL_EXTENSION_MODULES => graphql_extension_modules.map(&:to_dumpable_hash),
89
+ INDEX_DEFINITIONS_BY_NAME => HashDumper.dump_hash(index_definitions_by_name, &:to_dumpable_hash),
90
+ OBJECT_TYPES_BY_NAME => HashDumper.dump_hash(object_types_by_name, &:to_dumpable_hash),
91
+ SCALAR_TYPES_BY_NAME => HashDumper.dump_hash(scalar_types_by_name, &:to_dumpable_hash),
92
+ SCHEMA_ELEMENT_NAMES => schema_element_names.to_dumpable_hash,
93
+ STATIC_SCRIPT_IDS_BY_NAME => static_script_ids_by_scoped_name
94
+ })
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,165 @@
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/error"
11
+
12
+ module ElasticGraph
13
+ module SchemaArtifacts
14
+ module RuntimeMetadata
15
+ # Defines a generic schema element names API. Defined as a separate class to facilitate easy testing.
16
+ class SchemaElementNamesDefinition
17
+ def self.new(*element_names)
18
+ ::Data.define(:form, :overrides, :exposed_name_by_canonical_name, :canonical_name_by_exposed_name) do
19
+ const_set(:ELEMENT_NAMES, element_names)
20
+
21
+ define_method :initialize do |form:, overrides: {}|
22
+ extend(CONVERTERS.fetch(form.to_s) do
23
+ raise SchemaError,
24
+ "Invalid schema element name form: #{form.inspect}. " \
25
+ "Only valid values are: #{CONVERTERS.keys.inspect}."
26
+ end)
27
+
28
+ unused_keys = overrides.keys.map(&:to_s) - element_names.map(&:to_s)
29
+ if unused_keys.any?
30
+ raise SchemaError,
31
+ "`overrides` contains entries that do not match any schema " \
32
+ "elements: #{unused_keys.to_a.inspect}. Are any misspelled?"
33
+ end
34
+
35
+ exposed_name_by_canonical_name = element_names.each_with_object({}) do |element, names|
36
+ names[element] = overrides.fetch(element) do
37
+ overrides.fetch(element.to_s) do
38
+ normalize_case(element.to_s)
39
+ end
40
+ end.to_s
41
+ end.freeze
42
+
43
+ canonical_name_by_exposed_name = exposed_name_by_canonical_name.invert
44
+ validate_no_name_collisions(canonical_name_by_exposed_name, exposed_name_by_canonical_name)
45
+
46
+ super(
47
+ form: form,
48
+ overrides: overrides,
49
+ exposed_name_by_canonical_name: exposed_name_by_canonical_name,
50
+ canonical_name_by_exposed_name: canonical_name_by_exposed_name
51
+ )
52
+ end
53
+
54
+ # standard:disable Lint/NestedMethodDefinition
55
+ element_names.each do |element|
56
+ method_name = SnakeCaseConverter.normalize_case(element.to_s)
57
+ define_method(method_name) { exposed_name_by_canonical_name.fetch(element) }
58
+ end
59
+
60
+ # Returns the _canonical_ name for the given _exposed name_. The canonical name
61
+ # is the name we use within the source code of our framework; the exposed name
62
+ # is the name exposed in the specific GraphQL schema based on the configuration
63
+ # of the project.
64
+ def canonical_name_for(exposed_name)
65
+ canonical_name_by_exposed_name[exposed_name.to_s]
66
+ end
67
+
68
+ def self.from_hash(hash)
69
+ new(
70
+ form: hash.fetch(FORM).to_sym,
71
+ overrides: hash[OVERRIDES] || {}
72
+ )
73
+ end
74
+
75
+ def to_dumpable_hash
76
+ {
77
+ # Keys here are ordered alphabetically; please keep them that way.
78
+ FORM => form.to_s,
79
+ OVERRIDES => overrides
80
+ }
81
+ end
82
+
83
+ def to_s
84
+ "#<#{self.class.name} form=#{form}, overrides=#{overrides}>"
85
+ end
86
+ alias_method :inspect, :to_s
87
+
88
+ private
89
+
90
+ def validate_no_name_collisions(canonical_name_by_exposed_name, exposed_name_by_canonical_name)
91
+ return if canonical_name_by_exposed_name.size == exposed_name_by_canonical_name.size
92
+
93
+ collisions = exposed_name_by_canonical_name
94
+ .group_by { |k, v| v }
95
+ .reject { |v, kv_pairs| kv_pairs.size == 1 }
96
+ .transform_values { |kv_pairs| kv_pairs.map(&:first) }
97
+ .map do |duplicate_exposed_name, canonical_names|
98
+ "#{canonical_names.inspect} all map to the same exposed name: #{duplicate_exposed_name}"
99
+ end.join(" and ")
100
+
101
+ raise SchemaError, collisions
102
+ end
103
+ # standard:enable Lint/NestedMethodDefinition
104
+ end
105
+ end
106
+
107
+ FORM = "form"
108
+ OVERRIDES = "overrides"
109
+
110
+ module SnakeCaseConverter
111
+ extend self
112
+
113
+ def normalize_case(name)
114
+ name.gsub(/([[:upper:]])/) { "_#{$1.downcase}" }
115
+ end
116
+ end
117
+
118
+ module CamelCaseConverter
119
+ extend self
120
+
121
+ def normalize_case(name)
122
+ name.gsub(/_(\w)/) { $1.upcase }
123
+ end
124
+ end
125
+
126
+ CONVERTERS = {
127
+ "snake_case" => SnakeCaseConverter,
128
+ "camelCase" => CamelCaseConverter
129
+ }
130
+ end
131
+
132
+ SchemaElementNames = SchemaElementNamesDefinition.new(
133
+ # Filter arg and operation names:
134
+ :filter,
135
+ :equal_to_any_of, :gt, :gte, :lt, :lte, :matches, :matches_phrase, :matches_query, :any_of, :all_of, :not,
136
+ :time_of_day, :any_satisfy,
137
+ # Directives
138
+ :eg_latency_slo, :ms,
139
+ # For sorting.
140
+ :order_by,
141
+ # For aggregation
142
+ :grouped_by, :count, :count_detail, :aggregated_values, :sub_aggregations,
143
+ # Date/time grouping aggregation fields
144
+ :as_date_time, :as_date, :as_time_of_day, :as_day_of_week,
145
+ # Date/time grouping aggregation arguments
146
+ :offset, :amount, :unit, :time_zone, :truncation_unit,
147
+ # TODO: Drop support for legacy grouping schema that uses `granularity` and `offset_days`
148
+ :granularity, :offset_days,
149
+ # For aggregation counts.
150
+ :approximate_value, :exact_value, :upper_bound,
151
+ # For pagination.
152
+ :first, :after, :last, :before,
153
+ :edges, :node, :nodes, :cursor,
154
+ :page_info, :start_cursor, :end_cursor, :total_edge_count, :has_previous_page, :has_next_page,
155
+ # Subfields of `GeoLocation`/`GeoLocationFilterInput`:
156
+ :latitude, :longitude, :near, :max_distance,
157
+ # Subfields of `MatchesQueryFilterInput`/`MatchesPhraseFilterInput`
158
+ :query, :phrase, :allowed_edits_per_term, :require_all_terms,
159
+ # Aggregated values field names:
160
+ :exact_min, :exact_max, :approximate_min, :approximate_max, :approximate_avg, :approximate_sum, :exact_sum,
161
+ :approximate_distinct_value_count
162
+ )
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,47 @@
1
+ # Copyright 2024 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ require "elastic_graph/error"
10
+
11
+ module ElasticGraph
12
+ module SchemaArtifacts
13
+ module RuntimeMetadata
14
+ class SortField < ::Data.define(:field_path, :direction)
15
+ def initialize(field_path:, direction:)
16
+ unless direction == :asc || direction == :desc
17
+ raise SchemaError, "Sort direction `#{direction.inspect}` is invalid; it must be `:asc` or `:desc`"
18
+ end
19
+
20
+ super(field_path: field_path, direction: direction)
21
+ end
22
+
23
+ FIELD_PATH = "field_path"
24
+ DIRECTION = "direction"
25
+
26
+ def self.from_hash(hash)
27
+ new(
28
+ field_path: hash[FIELD_PATH],
29
+ direction: hash.fetch(DIRECTION).to_sym
30
+ )
31
+ end
32
+
33
+ def to_dumpable_hash
34
+ {
35
+ # Keys here are ordered alphabetically; please keep them that way.
36
+ DIRECTION => direction.to_s,
37
+ FIELD_PATH => field_path
38
+ }
39
+ end
40
+
41
+ def to_query_clause
42
+ {field_path => {"order" => direction.to_s}}
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,80 @@
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_artifacts/runtime_metadata/params"
11
+
12
+ module ElasticGraph
13
+ module SchemaArtifacts
14
+ module RuntimeMetadata
15
+ # Provides runtime metadata related to the targets of datastore `update` calls.
16
+ class UpdateTarget < ::Data.define(
17
+ :type,
18
+ :relationship,
19
+ :script_id,
20
+ :id_source,
21
+ :routing_value_source,
22
+ :rollover_timestamp_value_source,
23
+ :data_params,
24
+ :metadata_params
25
+ )
26
+ TYPE = "type"
27
+ RELATIONSHIP = "relationship"
28
+ SCRIPT_ID = "script_id"
29
+ ID_SOURCE = "id_source"
30
+ ROUTING_VALUE_SOURCE = "routing_value_source"
31
+ ROLLOVER_TIMESTAMP_VALUE_SOURCE = "rollover_timestamp_value_source"
32
+ DATA_PARAMS = "data_params"
33
+ METADATA_PARAMS = "metadata_params"
34
+
35
+ def self.from_hash(hash)
36
+ new(
37
+ type: hash[TYPE],
38
+ relationship: hash[RELATIONSHIP],
39
+ script_id: hash[SCRIPT_ID],
40
+ id_source: hash[ID_SOURCE],
41
+ routing_value_source: hash[ROUTING_VALUE_SOURCE],
42
+ rollover_timestamp_value_source: hash[ROLLOVER_TIMESTAMP_VALUE_SOURCE],
43
+ data_params: Param.load_params_hash(hash[DATA_PARAMS] || {}),
44
+ metadata_params: Param.load_params_hash(hash[METADATA_PARAMS] || {})
45
+ )
46
+ end
47
+
48
+ def to_dumpable_hash
49
+ {
50
+ # Keys here are ordered alphabetically; please keep them that way.
51
+ DATA_PARAMS => Param.dump_params_hash(data_params),
52
+ ID_SOURCE => id_source,
53
+ METADATA_PARAMS => Param.dump_params_hash(metadata_params),
54
+ RELATIONSHIP => relationship,
55
+ ROLLOVER_TIMESTAMP_VALUE_SOURCE => rollover_timestamp_value_source,
56
+ ROUTING_VALUE_SOURCE => routing_value_source,
57
+ SCRIPT_ID => script_id,
58
+ TYPE => type
59
+ }
60
+ end
61
+
62
+ def for_normal_indexing?
63
+ script_id == INDEX_DATA_UPDATE_SCRIPT_ID
64
+ end
65
+
66
+ def params_for(doc_id:, event:, prepared_record:)
67
+ data = data_params.to_h do |name, param|
68
+ [name, param.value_for(prepared_record)]
69
+ end
70
+
71
+ meta = metadata_params.to_h do |name, param|
72
+ [name, param.value_for(event)]
73
+ end
74
+
75
+ meta.merge({"id" => doc_id, "data" => data})
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end