elasticgraph-schema_artifacts 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 (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