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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 46a031fd35c0ba70bd94ba54e3fc358c8ab9e4a24d8aceed0aeaab10902f5098
4
+ data.tar.gz: c3ceaf30c58124c56ecfd333822089b64a1c67d91d32a6c41ebc7e04f89ec018
5
+ SHA512:
6
+ metadata.gz: '08e17a7cfd0c2a8a132ea8d4a26a58c0e55a861ad204805cedc13859eacd276e20a02c2adf69099e72039064fab74144fac7c6b3eb9a26b97404cde58acb8d0a'
7
+ data.tar.gz: 1f4d48bf707f9160cf4d62d966d5a60724503baf3576d1fd5058caaafcb6ac0a592d7636144d5563c462ca9b02eb64abeb6d6988e522bc2509b148d88b874f54
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Block, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # ElasticGraph::SchemaArtifacts
2
+
3
+ Contains code related to ElasticGraph's generated schema artifacts.
@@ -0,0 +1,21 @@
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_relative "../gemspec_helper"
10
+
11
+ ElasticGraphGemspecHelper.define_elasticgraph_gem(gemspec_file: __FILE__, category: :core) do |spec, eg_version|
12
+ spec.summary = "ElasticGraph gem containing code related to generated schema artifacts."
13
+
14
+ spec.add_dependency "elasticgraph-support", eg_version
15
+
16
+ # Necessary since `ScalarType` references coercion adapters defined in the `elasticgraph-graphql` gem.
17
+ spec.add_development_dependency "elasticgraph-graphql", eg_version
18
+
19
+ # Necessary since `ScalarType` references indexing preparer defined in the `elasticgraph-indexer` gem.
20
+ spec.add_development_dependency "elasticgraph-indexer", eg_version
21
+ end
@@ -0,0 +1,34 @@
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
+ # Mixin that offers convenient helper methods on top of the basic schema artifacts.
12
+ # Intended to be mixed into every implementation of the `_SchemaArtifacts` interface.
13
+ module ArtifactsHelperMethods
14
+ def datastore_scripts
15
+ datastore_config.fetch("scripts")
16
+ end
17
+
18
+ def index_templates
19
+ datastore_config.fetch("index_templates")
20
+ end
21
+
22
+ def indices
23
+ datastore_config.fetch("indices")
24
+ end
25
+
26
+ # Builds a map of index mappings, keyed by index definition name.
27
+ def index_mappings_by_index_def_name
28
+ @index_mappings_by_index_def_name ||= index_templates
29
+ .transform_values { |config| config.fetch("template").fetch("mappings") }
30
+ .merge(indices.transform_values { |config| config.fetch("mappings") })
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,112 @@
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
+ require "elastic_graph/schema_artifacts/artifacts_helper_methods"
12
+ require "elastic_graph/schema_artifacts/runtime_metadata/schema"
13
+ require "elastic_graph/support/hash_util"
14
+ require "elastic_graph/support/memoizable_data"
15
+ require "yaml"
16
+
17
+ module ElasticGraph
18
+ module SchemaArtifacts
19
+ # Builds a `SchemaArtifacts::FromDisk` instance using the provided YAML settings.
20
+ def self.from_parsed_yaml(parsed_yaml, for_context:)
21
+ schema_artifacts = parsed_yaml.fetch("schema_artifacts") do
22
+ raise ConfigError, "Config is missing required key `schema_artifacts`."
23
+ end
24
+
25
+ if (extra_keys = schema_artifacts.keys - ["directory"]).any?
26
+ raise ConfigError, "Config has extra `schema_artifacts` keys: #{extra_keys}"
27
+ end
28
+
29
+ directory = schema_artifacts.fetch("directory") do
30
+ raise ConfigError, "Config is missing required key `schema_artifacts.directory`."
31
+ end
32
+
33
+ FromDisk.new(directory, for_context)
34
+ end
35
+
36
+ # Responsible for loading schema artifacts from disk.
37
+ class FromDisk < Support::MemoizableData.define(:artifacts_dir, :context)
38
+ include ArtifactsHelperMethods
39
+
40
+ def graphql_schema_string
41
+ @graphql_schema_string ||= read_artifact(GRAPHQL_SCHEMA_FILE)
42
+ end
43
+
44
+ def json_schemas_for(version)
45
+ unless available_json_schema_versions.include?(version)
46
+ raise MissingSchemaArtifactError, "The requested json schema version (#{version}) is not available. " \
47
+ "Available versions: #{available_json_schema_versions.sort.join(", ")}."
48
+ end
49
+
50
+ json_schemas_by_version[version]
51
+ end
52
+
53
+ def available_json_schema_versions
54
+ @available_json_schema_versions ||= begin
55
+ versioned_json_schemas_dir = ::File.join(artifacts_dir, JSON_SCHEMAS_BY_VERSION_DIRECTORY)
56
+ if ::Dir.exist?(versioned_json_schemas_dir)
57
+ ::Dir.entries(versioned_json_schemas_dir).filter_map { |it| it[/v(\d+)\.yaml/, 1]&.to_i }.to_set
58
+ else
59
+ ::Set.new
60
+ end
61
+ end
62
+ end
63
+
64
+ def latest_json_schema_version
65
+ @latest_json_schema_version ||= available_json_schema_versions.max || raise(
66
+ MissingSchemaArtifactError,
67
+ "The directory for versioned JSON schemas (#{::File.join(artifacts_dir, JSON_SCHEMAS_BY_VERSION_DIRECTORY)}) could not be found. " \
68
+ "Either the schema artifacts haven't been dumped yet or the schema artifacts directory (#{artifacts_dir}) is misconfigured."
69
+ )
70
+ end
71
+
72
+ def datastore_config
73
+ @datastore_config ||= _ = parsed_yaml_from(DATASTORE_CONFIG_FILE)
74
+ end
75
+
76
+ def runtime_metadata
77
+ @runtime_metadata ||= RuntimeMetadata::Schema.from_hash(
78
+ parsed_yaml_from(RUNTIME_METADATA_FILE),
79
+ for_context: context
80
+ )
81
+ end
82
+
83
+ private
84
+
85
+ def read_artifact(artifact_name)
86
+ file_name = ::File.join(artifacts_dir, artifact_name)
87
+
88
+ if ::File.exist?(file_name)
89
+ ::File.read(file_name)
90
+ else
91
+ raise MissingSchemaArtifactError, "Schema artifact `#{artifact_name}` could not be found. " \
92
+ "Either the schema artifacts haven't been dumped yet or the schema artifacts directory (#{artifacts_dir}) is misconfigured."
93
+ end
94
+ end
95
+
96
+ def parsed_yaml_from(artifact_name)
97
+ ::YAML.safe_load(read_artifact(artifact_name))
98
+ end
99
+
100
+ def json_schemas_by_version
101
+ @json_schemas_by_version ||= ::Hash.new do |hash, json_schema_version|
102
+ hash[json_schema_version] = load_json_schema(json_schema_version)
103
+ end
104
+ end
105
+
106
+ # Loads the given JSON schema version from disk.
107
+ def load_json_schema(json_schema_version)
108
+ parsed_yaml_from(::File.join(JSON_SCHEMAS_BY_VERSION_DIRECTORY, "v#{json_schema_version}.yaml"))
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,34 @@
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
+ # Details about our aggregation functions.
13
+ class ComputationDetail < ::Data.define(:empty_bucket_value, :function)
14
+ FUNCTION = "function"
15
+ EMPTY_BUCKET_VALUE = "empty_bucket_value"
16
+
17
+ def self.from_hash(hash)
18
+ new(
19
+ empty_bucket_value: hash[EMPTY_BUCKET_VALUE],
20
+ function: hash.fetch(FUNCTION).to_sym
21
+ )
22
+ end
23
+
24
+ def to_dumpable_hash
25
+ {
26
+ # Keys here are ordered alphabetically; please keep them that way.
27
+ EMPTY_BUCKET_VALUE => empty_bucket_value,
28
+ FUNCTION => function.to_s
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
34
+ 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
+ require "elastic_graph/schema_artifacts/runtime_metadata/hash_dumper"
10
+ require "elastic_graph/schema_artifacts/runtime_metadata/sort_field"
11
+
12
+ module ElasticGraph
13
+ module SchemaArtifacts
14
+ module RuntimeMetadata
15
+ module Enum
16
+ # Runtime metadata related to an ElasticGraph enum type.
17
+ class Type < ::Data.define(:values_by_name)
18
+ VALUES_BY_NAME = "values_by_name"
19
+
20
+ def self.from_hash(hash)
21
+ values_by_name = hash[VALUES_BY_NAME]&.transform_values do |value_hash|
22
+ Value.from_hash(value_hash)
23
+ end || {}
24
+
25
+ new(values_by_name: values_by_name)
26
+ end
27
+
28
+ def to_dumpable_hash
29
+ {
30
+ # Keys here are ordered alphabetically; please keep them that way.
31
+ VALUES_BY_NAME => HashDumper.dump_hash(values_by_name, &:to_dumpable_hash)
32
+ }
33
+ end
34
+ end
35
+
36
+ # Runtime metadata related to an ElasticGraph enum value.
37
+ class Value < ::Data.define(:sort_field, :datastore_value, :datastore_abbreviation, :alternate_original_name)
38
+ DATASTORE_VALUE = "datastore_value"
39
+ DATASTORE_ABBREVIATION = "datastore_abbreviation"
40
+ SORT_FIELD = "sort_field"
41
+ ALTERNATE_ORIGINAL_NAME = "alternate_original_name"
42
+
43
+ def self.from_hash(hash)
44
+ new(
45
+ sort_field: hash[SORT_FIELD]&.then { |h| SortField.from_hash(h) },
46
+ datastore_value: hash[DATASTORE_VALUE],
47
+ datastore_abbreviation: hash[DATASTORE_ABBREVIATION]&.to_sym,
48
+ alternate_original_name: hash[ALTERNATE_ORIGINAL_NAME]
49
+ )
50
+ end
51
+
52
+ def to_dumpable_hash
53
+ {
54
+ # Keys here are ordered alphabetically; please keep them that way.
55
+ DATASTORE_ABBREVIATION => datastore_abbreviation&.to_s,
56
+ DATASTORE_VALUE => datastore_value,
57
+ ALTERNATE_ORIGINAL_NAME => alternate_original_name,
58
+ SORT_FIELD => sort_field&.to_dumpable_hash
59
+ }
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ 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 SchemaArtifacts
13
+ module RuntimeMetadata
14
+ # Represents an extension--a class or module (potentially from outside the ElasticGraph
15
+ # code base) that implements a standard interface to plug in custom functionality.
16
+ #
17
+ # Extensions are serialized using two fields:
18
+ # - `extension_name`: the Ruby constant of the extension
19
+ # - `require_path`: file path to `require` to load the extension
20
+ #
21
+ # However, an `Extension` instance represents a loaded, resolved extension.
22
+ # We eagerly load extensions (and validate them in the `ExtensionLoader`) in
23
+ # order to surface any issues with the extension as soon as possible. We don't
24
+ # want to defer errors if we can detect any issues with the extension at boot time.
25
+ Extension = ::Data.define(:extension_class, :require_path, :extension_config) do
26
+ # @implements Extension
27
+
28
+ # Loads an extension using a serialized hash, via the provided `ExtensionLoader`.
29
+ def self.load_from_hash(hash, via:)
30
+ config = Support::HashUtil.symbolize_keys(hash["extension_config"] || {}) # : ::Hash[::Symbol, untyped]
31
+ via.load(hash.fetch("extension_name"), from: hash.fetch("require_path"), config: config)
32
+ end
33
+
34
+ # The name of the extension (based on the name of the extension class).
35
+ def extension_name
36
+ extension_class.name.to_s
37
+ end
38
+
39
+ # The serialized form of an extension.
40
+ def to_dumpable_hash
41
+ # Keys here are ordered alphabetically; please keep them that way.
42
+ {
43
+ "extension_config" => Support::HashUtil.stringify_keys(extension_config),
44
+ "extension_name" => extension_name,
45
+ "require_path" => require_path
46
+ }.reject { |_, v| v.empty? }
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,125 @@
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/schema_artifacts/runtime_metadata/extension"
11
+
12
+ module ElasticGraph
13
+ module SchemaArtifacts
14
+ module RuntimeMetadata
15
+ # Responsible for loading extensions. This loader requires an interface definition
16
+ # (a class or module with empty method definitions that just serves to define what
17
+ # loaded extensions must implement). That allows us to verify the extension implements
18
+ # the interface correctly at load time, rather than deferring exceptions to when the
19
+ # extension is later used.
20
+ #
21
+ # Note, however, that this does not guarantee no runtime exceptions from the use of the
22
+ # extension: the extension may return invalid return values, or throw exceptions when
23
+ # called. But this verifies the interface to the extent that we can.
24
+ class ExtensionLoader
25
+ def initialize(interface_def)
26
+ @interface_def = interface_def
27
+ @loaded_by_name = {}
28
+ end
29
+
30
+ # Loads the extension using the provided constant name, after requiring the `from` path.
31
+ # Memoizes the result.
32
+ def load(constant_name, from:, config:)
33
+ (@loaded_by_name[constant_name] ||= load_extension(constant_name, from)).tap do |extension|
34
+ if extension.require_path != from
35
+ raise InvalidExtensionError, "Extension `#{constant_name}` cannot be loaded from `#{from}`, " \
36
+ "since it has already been loaded from `#{extension.require_path}`."
37
+ end
38
+ end.with(extension_config: config)
39
+ end
40
+
41
+ private
42
+
43
+ def load_extension(constant_name, require_path)
44
+ require require_path
45
+ extension_class = ::Object.const_get(constant_name).tap { |ext| verify_interface(constant_name, ext) }
46
+ Extension.new(extension_class, require_path, {})
47
+ end
48
+
49
+ def verify_interface(constant_name, extension)
50
+ # @type var problems: ::Array[::String]
51
+ problems = []
52
+ problems.concat(verify_methods("class", extension.singleton_class, @interface_def.singleton_class))
53
+
54
+ if extension.is_a?(::Module)
55
+ problems.concat(verify_methods("instance", extension, @interface_def))
56
+
57
+ # We care about the name exactly matching so that we can dump the extension name in a schema
58
+ # artifact w/o having to pass around the original constant name.
59
+ if extension.name != constant_name.delete_prefix("::")
60
+ problems << "- Exposes a name (`#{extension.name}`) that differs from the provided extension name (`#{constant_name}`)"
61
+ end
62
+ else
63
+ problems << "- Is not a class or module as expected"
64
+ end
65
+
66
+ if problems.any?
67
+ raise InvalidExtensionError,
68
+ "Extension `#{constant_name}` does not implement the expected interface correctly. Problems:\n\n" \
69
+ "#{problems.join("\n")}"
70
+ end
71
+ end
72
+
73
+ def verify_methods(type, extension, interface)
74
+ interface_methods = list_instance_interface_methods(interface)
75
+ extension_methods = list_instance_interface_methods(extension)
76
+
77
+ # @type var problems: ::Array[::String]
78
+ problems = []
79
+
80
+ if (missing_methods = interface_methods - extension_methods).any?
81
+ problems << "- Missing #{type} methods: #{missing_methods.map { |m| "`#{m}`" }.join(", ")}"
82
+ end
83
+
84
+ interface_methods.intersection(extension_methods).each do |method_name|
85
+ unless parameters_match?(extension, interface, method_name)
86
+ interface_signature = signature_code_for(interface, method_name)
87
+ extension_signature = signature_code_for(extension, method_name)
88
+
89
+ problems << "- Method signature for #{type} method `#{method_name}` (`#{extension_signature}`) does not match interface (`#{interface_signature}`)"
90
+ end
91
+ end
92
+
93
+ problems
94
+ end
95
+
96
+ def list_instance_interface_methods(klass)
97
+ # Here we look at more than just the public methods. This is necessary for `initialize`.
98
+ # If it's defined on the interface definition, we want to verify it on the extension,
99
+ # but Ruby makes `initialize` private by default.
100
+ klass.instance_methods(false) +
101
+ klass.protected_instance_methods(false) +
102
+ klass.private_instance_methods(false)
103
+ end
104
+
105
+ def parameters_match?(extension, interface, method_name)
106
+ interface_parameters = interface.instance_method(method_name).parameters
107
+ extension_parameters = extension.instance_method(method_name).parameters
108
+
109
+ # Here we compare the parameters for exact equality. This is stricter than we need it
110
+ # to be (it doesn't allow the parameters to have different names, for example) but it's
111
+ # considerably simpler than us trying to determine what is truly required. For example,
112
+ # the name doesn't matter on a positional arg, but would matter on a keyword arg.
113
+ interface_parameters == extension_parameters
114
+ end
115
+
116
+ def signature_code_for(object, method_name)
117
+ # @type var file_name: ::String?
118
+ # @type var line_number: ::Integer?
119
+ file_name, line_number = object.instance_method(method_name).source_location
120
+ ::File.read(file_name.to_s).split("\n")[line_number.to_i - 1].strip
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,54 @@
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/computation_detail"
10
+ require "elastic_graph/schema_artifacts/runtime_metadata/relation"
11
+
12
+ module ElasticGraph
13
+ module SchemaArtifacts
14
+ module RuntimeMetadata
15
+ class GraphQLField < ::Data.define(:name_in_index, :relation, :computation_detail)
16
+ EMPTY = new(nil, nil, nil)
17
+ NAME_IN_INDEX = "name_in_index"
18
+ RELATION = "relation"
19
+ AGGREGATION_DETAIL = "computation_detail"
20
+
21
+ def self.from_hash(hash)
22
+ new(
23
+ name_in_index: hash[NAME_IN_INDEX],
24
+ relation: hash[RELATION]&.then { |rel_hash| Relation.from_hash(rel_hash) },
25
+ computation_detail: hash[AGGREGATION_DETAIL]&.then { |agg_hash| ComputationDetail.from_hash(agg_hash) }
26
+ )
27
+ end
28
+
29
+ def to_dumpable_hash
30
+ {
31
+ # Keys here are ordered alphabetically; please keep them that way.
32
+ AGGREGATION_DETAIL => computation_detail&.to_dumpable_hash,
33
+ NAME_IN_INDEX => name_in_index,
34
+ RELATION => relation&.to_dumpable_hash
35
+ }
36
+ end
37
+
38
+ # Indicates if we need this field in our dumped runtime metadata, when it has the given
39
+ # `name_in_graphql`. Fields that have not been customized in some way do not need to be
40
+ # included in the dumped runtime metadata.
41
+ def needed?(name_in_graphql)
42
+ !!relation || !!computation_detail || name_in_index&.!=(name_in_graphql) || false
43
+ end
44
+
45
+ def with_computation_detail(empty_bucket_value:, function:)
46
+ with(computation_detail: ComputationDetail.new(
47
+ empty_bucket_value: empty_bucket_value,
48
+ function: function
49
+ ))
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,21 @@
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
+ module HashDumper
13
+ def self.dump_hash(hash)
14
+ hash.sort_by(&:first).to_h do |key, value|
15
+ [key, yield(value)]
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,78 @@
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/schema_artifacts/runtime_metadata/index_field"
11
+ require "elastic_graph/schema_artifacts/runtime_metadata/sort_field"
12
+
13
+ module ElasticGraph
14
+ module SchemaArtifacts
15
+ module RuntimeMetadata
16
+ # Runtime metadata related to a datastore index definition.
17
+ class IndexDefinition < ::Data.define(:route_with, :rollover, :default_sort_fields, :current_sources, :fields_by_path)
18
+ ROUTE_WITH = "route_with"
19
+ ROLLOVER = "rollover"
20
+ DEFAULT_SORT_FIELDS = "default_sort_fields"
21
+ CURRENT_SOURCES = "current_sources"
22
+ FIELDS_BY_PATH = "fields_by_path"
23
+
24
+ def initialize(route_with:, rollover:, default_sort_fields:, current_sources:, fields_by_path:)
25
+ super(
26
+ route_with: route_with,
27
+ rollover: rollover,
28
+ default_sort_fields: default_sort_fields,
29
+ current_sources: current_sources.to_set,
30
+ fields_by_path: fields_by_path
31
+ )
32
+ end
33
+
34
+ def self.from_hash(hash)
35
+ new(
36
+ route_with: hash[ROUTE_WITH],
37
+ rollover: hash[ROLLOVER]&.then { |h| Rollover.from_hash(h) },
38
+ default_sort_fields: hash[DEFAULT_SORT_FIELDS]&.map { |h| SortField.from_hash(h) } || [],
39
+ current_sources: hash[CURRENT_SOURCES] || [],
40
+ fields_by_path: (hash[FIELDS_BY_PATH] || {}).transform_values { |h| IndexField.from_hash(h) }
41
+ )
42
+ end
43
+
44
+ def to_dumpable_hash
45
+ {
46
+ # Keys here are ordered alphabetically; please keep them that way.
47
+ CURRENT_SOURCES => current_sources.sort,
48
+ DEFAULT_SORT_FIELDS => default_sort_fields.map(&:to_dumpable_hash),
49
+ FIELDS_BY_PATH => HashDumper.dump_hash(fields_by_path, &:to_dumpable_hash),
50
+ ROLLOVER => rollover&.to_dumpable_hash,
51
+ ROUTE_WITH => route_with
52
+ }
53
+ end
54
+
55
+ class Rollover < ::Data.define(:frequency, :timestamp_field_path)
56
+ FREQUENCY = "frequency"
57
+ TIMESTAMP_FIELD_PATH = "timestamp_field_path"
58
+
59
+ # @implements Rollover
60
+ def self.from_hash(hash)
61
+ new(
62
+ frequency: hash.fetch(FREQUENCY).to_sym,
63
+ timestamp_field_path: hash[TIMESTAMP_FIELD_PATH]
64
+ )
65
+ end
66
+
67
+ def to_dumpable_hash
68
+ {
69
+ # Keys here are ordered alphabetically; please keep them that way.
70
+ FREQUENCY => frequency.to_s,
71
+ TIMESTAMP_FIELD_PATH => timestamp_field_path
72
+ }
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,33 @@
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
+
11
+ module ElasticGraph
12
+ module SchemaArtifacts
13
+ module RuntimeMetadata
14
+ # Runtime metadata related to a field on a datastore index definition.
15
+ class IndexField < ::Data.define(:source)
16
+ SOURCE = "source"
17
+
18
+ def self.from_hash(hash)
19
+ new(
20
+ source: hash[SOURCE] || SELF_RELATIONSHIP_NAME
21
+ )
22
+ end
23
+
24
+ def to_dumpable_hash
25
+ {
26
+ # Keys here are ordered alphabetically; please keep them that way.
27
+ SOURCE => source
28
+ }
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end