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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +3 -0
- data/elasticgraph-schema_artifacts.gemspec +21 -0
- data/lib/elastic_graph/schema_artifacts/artifacts_helper_methods.rb +34 -0
- data/lib/elastic_graph/schema_artifacts/from_disk.rb +112 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/computation_detail.rb +34 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/enum.rb +65 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/extension.rb +51 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/extension_loader.rb +125 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/graphql_field.rb +54 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/hash_dumper.rb +21 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/index_definition.rb +78 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/index_field.rb +33 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/object_type.rb +81 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/params.rb +81 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/relation.rb +39 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/scalar_type.rb +94 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/schema.rb +99 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/schema_element_names.rb +165 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/sort_field.rb +47 -0
- data/lib/elastic_graph/schema_artifacts/runtime_metadata/update_target.rb +80 -0
- 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
|