elasticgraph-schema_definition 0.18.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +7 -0
  4. data/elasticgraph-schema_definition.gemspec +26 -0
  5. data/lib/elastic_graph/schema_definition/api.rb +359 -0
  6. data/lib/elastic_graph/schema_definition/factory.rb +506 -0
  7. data/lib/elastic_graph/schema_definition/indexing/derived_fields/append_only_set.rb +79 -0
  8. data/lib/elastic_graph/schema_definition/indexing/derived_fields/field_initializer_support.rb +59 -0
  9. data/lib/elastic_graph/schema_definition/indexing/derived_fields/immutable_value.rb +99 -0
  10. data/lib/elastic_graph/schema_definition/indexing/derived_fields/min_or_max_value.rb +62 -0
  11. data/lib/elastic_graph/schema_definition/indexing/derived_indexed_type.rb +346 -0
  12. data/lib/elastic_graph/schema_definition/indexing/event_envelope.rb +74 -0
  13. data/lib/elastic_graph/schema_definition/indexing/field.rb +181 -0
  14. data/lib/elastic_graph/schema_definition/indexing/field_reference.rb +51 -0
  15. data/lib/elastic_graph/schema_definition/indexing/field_type/enum.rb +65 -0
  16. data/lib/elastic_graph/schema_definition/indexing/field_type/object.rb +113 -0
  17. data/lib/elastic_graph/schema_definition/indexing/field_type/scalar.rb +51 -0
  18. data/lib/elastic_graph/schema_definition/indexing/field_type/union.rb +70 -0
  19. data/lib/elastic_graph/schema_definition/indexing/index.rb +318 -0
  20. data/lib/elastic_graph/schema_definition/indexing/json_schema_field_metadata.rb +34 -0
  21. data/lib/elastic_graph/schema_definition/indexing/json_schema_with_metadata.rb +234 -0
  22. data/lib/elastic_graph/schema_definition/indexing/list_counts_mapping.rb +53 -0
  23. data/lib/elastic_graph/schema_definition/indexing/relationship_resolver.rb +96 -0
  24. data/lib/elastic_graph/schema_definition/indexing/rollover_config.rb +25 -0
  25. data/lib/elastic_graph/schema_definition/indexing/update_target_factory.rb +54 -0
  26. data/lib/elastic_graph/schema_definition/indexing/update_target_resolver.rb +195 -0
  27. data/lib/elastic_graph/schema_definition/json_schema_pruner.rb +61 -0
  28. data/lib/elastic_graph/schema_definition/mixins/can_be_graphql_only.rb +31 -0
  29. data/lib/elastic_graph/schema_definition/mixins/has_derived_graphql_type_customizations.rb +119 -0
  30. data/lib/elastic_graph/schema_definition/mixins/has_directives.rb +65 -0
  31. data/lib/elastic_graph/schema_definition/mixins/has_documentation.rb +74 -0
  32. data/lib/elastic_graph/schema_definition/mixins/has_indices.rb +281 -0
  33. data/lib/elastic_graph/schema_definition/mixins/has_readable_to_s_and_inspect.rb +46 -0
  34. data/lib/elastic_graph/schema_definition/mixins/has_subtypes.rb +116 -0
  35. data/lib/elastic_graph/schema_definition/mixins/has_type_info.rb +181 -0
  36. data/lib/elastic_graph/schema_definition/mixins/implements_interfaces.rb +122 -0
  37. data/lib/elastic_graph/schema_definition/mixins/supports_default_value.rb +47 -0
  38. data/lib/elastic_graph/schema_definition/mixins/supports_filtering_and_aggregation.rb +267 -0
  39. data/lib/elastic_graph/schema_definition/mixins/verifies_graphql_name.rb +38 -0
  40. data/lib/elastic_graph/schema_definition/rake_tasks.rb +190 -0
  41. data/lib/elastic_graph/schema_definition/results.rb +404 -0
  42. data/lib/elastic_graph/schema_definition/schema_artifact_manager.rb +482 -0
  43. data/lib/elastic_graph/schema_definition/schema_elements/argument.rb +56 -0
  44. data/lib/elastic_graph/schema_definition/schema_elements/built_in_types.rb +1541 -0
  45. data/lib/elastic_graph/schema_definition/schema_elements/deprecated_element.rb +21 -0
  46. data/lib/elastic_graph/schema_definition/schema_elements/directive.rb +40 -0
  47. data/lib/elastic_graph/schema_definition/schema_elements/enum_type.rb +189 -0
  48. data/lib/elastic_graph/schema_definition/schema_elements/enum_value.rb +73 -0
  49. data/lib/elastic_graph/schema_definition/schema_elements/enum_value_namer.rb +89 -0
  50. data/lib/elastic_graph/schema_definition/schema_elements/enums_for_indexed_types.rb +82 -0
  51. data/lib/elastic_graph/schema_definition/schema_elements/field.rb +1085 -0
  52. data/lib/elastic_graph/schema_definition/schema_elements/field_path.rb +112 -0
  53. data/lib/elastic_graph/schema_definition/schema_elements/field_source.rb +16 -0
  54. data/lib/elastic_graph/schema_definition/schema_elements/graphql_sdl_enumerator.rb +113 -0
  55. data/lib/elastic_graph/schema_definition/schema_elements/input_field.rb +31 -0
  56. data/lib/elastic_graph/schema_definition/schema_elements/input_type.rb +60 -0
  57. data/lib/elastic_graph/schema_definition/schema_elements/interface_type.rb +72 -0
  58. data/lib/elastic_graph/schema_definition/schema_elements/list_counts_state.rb +40 -0
  59. data/lib/elastic_graph/schema_definition/schema_elements/object_type.rb +53 -0
  60. data/lib/elastic_graph/schema_definition/schema_elements/relationship.rb +218 -0
  61. data/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb +310 -0
  62. data/lib/elastic_graph/schema_definition/schema_elements/sort_order_enum_value.rb +36 -0
  63. data/lib/elastic_graph/schema_definition/schema_elements/sub_aggregation_path.rb +66 -0
  64. data/lib/elastic_graph/schema_definition/schema_elements/type_namer.rb +237 -0
  65. data/lib/elastic_graph/schema_definition/schema_elements/type_reference.rb +353 -0
  66. data/lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb +579 -0
  67. data/lib/elastic_graph/schema_definition/schema_elements/union_type.rb +157 -0
  68. data/lib/elastic_graph/schema_definition/scripting/file_system_repository.rb +77 -0
  69. data/lib/elastic_graph/schema_definition/scripting/script.rb +48 -0
  70. data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_day_of_week.painless +24 -0
  71. data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_time_of_day.painless +41 -0
  72. data/lib/elastic_graph/schema_definition/scripting/scripts/filter/by_time_of_day.painless +22 -0
  73. data/lib/elastic_graph/schema_definition/scripting/scripts/update/index_data.painless +93 -0
  74. data/lib/elastic_graph/schema_definition/state.rb +212 -0
  75. data/lib/elastic_graph/schema_definition/test_support.rb +113 -0
  76. metadata +513 -0
@@ -0,0 +1,96 @@
1
+ # Copyright 2024 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ module ElasticGraph
10
+ module SchemaDefinition
11
+ module Indexing
12
+ # @private
13
+ class RelationshipResolver
14
+ def initialize(schema_def_state:, object_type:, relationship_name:, sourced_fields:, field_path_resolver:)
15
+ @schema_def_state = schema_def_state
16
+ @object_type = object_type
17
+ @relationship_name = relationship_name
18
+ @sourced_fields = sourced_fields
19
+ @field_path_resolver = field_path_resolver
20
+ end
21
+
22
+ def resolve
23
+ relation_field = object_type.graphql_fields_by_name[relationship_name]
24
+
25
+ if relation_field.nil?
26
+ [nil, "#{relationship_error_prefix} is not defined. Is it misspelled?"]
27
+ elsif (relationship = relation_field.relationship).nil?
28
+ [nil, "#{relationship_error_prefix} is not a relationship. It must be defined using `relates_to_one` or `relates_to_many`."]
29
+ elsif (related_type = schema_def_state.object_types_by_name[relationship.related_type.unwrap_non_null.name]).nil?
30
+ issue =
31
+ if schema_def_state.types_by_name.key?(relationship.related_type.fully_unwrapped.name)
32
+ "references a type which is not an object type: `#{relationship.related_type.name}`. Only object types can be used in relations."
33
+ else
34
+ "references an unknown type: `#{relationship.related_type.name}`. Is it misspelled?"
35
+ end
36
+
37
+ [nil, "#{relationship_error_prefix} #{issue}"]
38
+ elsif !related_type.indexed?
39
+ [nil, "#{relationship_error_prefix} references a type which is not indexed: `#{related_type.name}`. Only indexed types can be used in relations."]
40
+ else
41
+ relation_metadata = relation_field.runtime_metadata_graphql_field.relation # : SchemaArtifacts::RuntimeMetadata::Relation
42
+ foreign_key_parent_type = (relation_metadata.direction == :in) ? related_type : object_type
43
+
44
+ if (foreign_key_error = validate_foreign_key(foreign_key_parent_type, relation_metadata))
45
+ [nil, foreign_key_error]
46
+ else
47
+ [ResolvedRelationship.new(relationship_name, relation_field, relationship, related_type, relation_metadata), nil]
48
+ end
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ # @dynamic schema_def_state, object_type, relationship_name, sourced_fields, field_path_resolver
55
+ attr_reader :schema_def_state, :object_type, :relationship_name, :sourced_fields, :field_path_resolver
56
+
57
+ # Helper method for building the prefix of relationship-related error messages.
58
+ def relationship_error_prefix
59
+ sourced_fields_description =
60
+ if sourced_fields.empty?
61
+ ""
62
+ else
63
+ " (referenced from `sourced_from` on field(s): #{sourced_fields.map { |f| "`#{f.name}`" }.join(", ")})"
64
+ end
65
+
66
+ "`#{relationship_description}`#{sourced_fields_description}"
67
+ end
68
+
69
+ def validate_foreign_key(foreign_key_parent_type, relation_metadata)
70
+ foreign_key_field = field_path_resolver.resolve_public_path(foreign_key_parent_type, relation_metadata.foreign_key) { true }
71
+ # If its an inbound foreign key, verify that the foreign key exists on the related type.
72
+ # Note: we don't verify this for outbound foreign keys, because when we define a relationship with an outbound foreign
73
+ # key, we automatically define an indexing only field for the foreign key (since it exists on the same type). We don't
74
+ # do that for an inbound foreign key, though (since the foreign key exists on another type). Allowing a relationship
75
+ # definition on type A to add a field to type B's schema would be weird and surprising.
76
+ if relation_metadata.direction == :in && foreign_key_field.nil?
77
+ "#{relationship_error_prefix} uses `#{foreign_key_parent_type.name}.#{relation_metadata.foreign_key}` as the foreign key, " \
78
+ "but that field does not exist as an indexing field. To continue, define it, define a relationship on `#{foreign_key_parent_type.name}` " \
79
+ "that uses it as the foreign key, use another field as the foreign key, or remove the `#{relationship_description}` definition."
80
+ elsif foreign_key_field && foreign_key_field.type.fully_unwrapped.name != "ID"
81
+ "#{relationship_error_prefix} uses `#{foreign_key_field.fully_qualified_path}` as the foreign key, " \
82
+ "but that field is not an `ID` field as expected. To continue, change it's type, use another field " \
83
+ "as the foreign key, or remove the `#{relationship_description}` definition."
84
+ end
85
+ end
86
+
87
+ def relationship_description
88
+ "#{object_type.name}.#{relationship_name}"
89
+ end
90
+ end
91
+
92
+ # @private
93
+ ResolvedRelationship = ::Data.define(:relationship_name, :relationship_field, :relationship, :related_type, :relation_metadata)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,25 @@
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/index_definition"
10
+
11
+ module ElasticGraph
12
+ module SchemaDefinition
13
+ module Indexing
14
+ # @private
15
+ class RolloverConfig < ::Data.define(:frequency, :timestamp_field_path)
16
+ def runtime_metadata
17
+ SchemaArtifacts::RuntimeMetadata::IndexDefinition::Rollover.new(
18
+ frequency: frequency,
19
+ timestamp_field_path: timestamp_field_path.path_in_index
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
25
+ 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
+ module ElasticGraph
10
+ module SchemaDefinition
11
+ module Indexing
12
+ # Helper class that contains common logic for instantiating `UpdateTargets`.
13
+ # @private
14
+ module UpdateTargetFactory
15
+ def self.new_normal_indexing_update_target(
16
+ type:,
17
+ relationship:,
18
+ id_source:,
19
+ data_params:,
20
+ routing_value_source:,
21
+ rollover_timestamp_value_source:
22
+ )
23
+ SchemaArtifacts::RuntimeMetadata::UpdateTarget.new(
24
+ type: type,
25
+ relationship: relationship,
26
+ script_id: INDEX_DATA_UPDATE_SCRIPT_ID,
27
+ id_source: id_source,
28
+ metadata_params: standard_metadata_params.merge({
29
+ "relationship" => SchemaArtifacts::RuntimeMetadata::StaticParam.new(value: relationship)
30
+ }),
31
+ data_params: data_params,
32
+ routing_value_source: routing_value_source,
33
+ rollover_timestamp_value_source: rollover_timestamp_value_source
34
+ )
35
+ end
36
+
37
+ private_class_method def self.standard_metadata_params
38
+ @standard_metadata_params ||= {
39
+ "sourceId" => single_value_param_from("id"),
40
+ "sourceType" => single_value_param_from("type"),
41
+ "version" => single_value_param_from("version")
42
+ }
43
+ end
44
+
45
+ private_class_method def self.single_value_param_from(source_path)
46
+ SchemaArtifacts::RuntimeMetadata::DynamicParam.new(
47
+ source_path: source_path,
48
+ cardinality: :one
49
+ )
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,195 @@
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/params"
10
+ require "elastic_graph/schema_definition/indexing/update_target_factory"
11
+
12
+ module ElasticGraph
13
+ module SchemaDefinition
14
+ module Indexing
15
+ # Responsible for resolving a relationship and a set of `sourced_from` fields into an `UpdateTarget`
16
+ # that contains the instructions for how the primary type should be updated from the related type's
17
+ # source events.
18
+ #
19
+ # @private
20
+ class UpdateTargetResolver
21
+ def initialize(
22
+ object_type:,
23
+ resolved_relationship:,
24
+ sourced_fields:,
25
+ field_path_resolver:
26
+ )
27
+ @object_type = object_type
28
+ @resolved_relationship = resolved_relationship
29
+ @sourced_fields = sourced_fields
30
+ @field_path_resolver = field_path_resolver
31
+ end
32
+
33
+ # Resolves the `object_type`, `resolved_relationship`, and `sourced_fields` into an `UpdateTarget`, validating
34
+ # that everything is defined correctly.
35
+ #
36
+ # Returns a tuple of the `update_target` (if valid), and a list of errors.
37
+ def resolve
38
+ relationship_errors = validate_relationship
39
+ data_params, data_params_errors = resolve_data_params
40
+ routing_value_source, routing_error = resolve_field_source(RoutingSourceAdapter)
41
+ rollover_timestamp_value_source, rollover_timestamp_error = resolve_field_source(RolloverTimestampSourceAdapter)
42
+ equivalent_field_errors = resolved_relationship.relationship.validate_equivalent_fields(field_path_resolver)
43
+
44
+ all_errors = relationship_errors + data_params_errors + equivalent_field_errors + [routing_error, rollover_timestamp_error].compact
45
+
46
+ if all_errors.empty?
47
+ update_target = UpdateTargetFactory.new_normal_indexing_update_target(
48
+ type: object_type.name,
49
+ relationship: resolved_relationship.relationship_name,
50
+ id_source: resolved_relationship.relation_metadata.foreign_key,
51
+ data_params: data_params,
52
+ routing_value_source: routing_value_source,
53
+ rollover_timestamp_value_source: rollover_timestamp_value_source
54
+ )
55
+ end
56
+
57
+ [update_target, all_errors]
58
+ end
59
+
60
+ private
61
+
62
+ # @dynamic object_type, resolved_relationship, sourced_fields, field_path_resolver
63
+ attr_reader :object_type, :resolved_relationship, :sourced_fields, :field_path_resolver
64
+
65
+ # Applies additional validations (beyond what `RelationshipResolver` applies) on relationships that are
66
+ # used by `sourced_from` fields.
67
+ def validate_relationship
68
+ errors = [] # : ::Array[::String]
69
+
70
+ if resolved_relationship.relationship.many?
71
+ errors << "#{relationship_error_prefix} is a `relates_to_many` relationship, but `sourced_from` is only supported on a `relates_to_one` relationship."
72
+ end
73
+
74
+ relation_metadata = resolved_relationship.relationship_field.runtime_metadata_graphql_field.relation # : SchemaArtifacts::RuntimeMetadata::Relation
75
+ if relation_metadata.direction == :out
76
+ errors << "#{relationship_error_prefix} has an outbound foreign key (`dir: :out`), but `sourced_from` is only supported via inbound foreign key (`dir: :in`) relationships."
77
+ end
78
+
79
+ unless relation_metadata.additional_filter.empty?
80
+ errors << "#{relationship_error_prefix} is a `relationship` using an `additional_filter` but `sourced_from` is not supported on relationships with `additional_filter`."
81
+ end
82
+
83
+ errors
84
+ end
85
+
86
+ # Helper method for building the prefix of relationship-related error messages.
87
+ def relationship_error_prefix
88
+ sourced_fields_description = "(referenced from `sourced_from` on field(s): #{sourced_fields.map { |f| "`#{f.name}`" }.join(", ")})"
89
+ "`#{object_type.name}.#{resolved_relationship.relationship_name}` #{sourced_fields_description}"
90
+ end
91
+
92
+ # Resolves the `sourced_fields` into a data params map, validating them along the way.
93
+ #
94
+ # Returns a tuple of the data params and a list of any errors that occurred during resolution.
95
+ def resolve_data_params
96
+ related_type = resolved_relationship.related_type
97
+ errors = [] # : ::Array[::String]
98
+
99
+ data_params = sourced_fields.filter_map do |field|
100
+ field_source = field.source # : SchemaElements::FieldSource
101
+
102
+ referenced_field_path = field_path_resolver.resolve_public_path(related_type, field_source.field_path) do |parent_field|
103
+ !parent_field.type.list?
104
+ end
105
+
106
+ if referenced_field_path.nil?
107
+ explanation =
108
+ if field_source.field_path.include?(".")
109
+ "could not be resolved: some parts do not exist on their respective types as non-list fields"
110
+ else
111
+ "does not exist as an indexing field"
112
+ end
113
+
114
+ errors << "`#{object_type.name}.#{field.name}` has an invalid `sourced_from` argument: `#{related_type.name}.#{field_source.field_path}` #{explanation}."
115
+ nil
116
+ elsif referenced_field_path.type.unwrap_non_null != field.type.unwrap_non_null
117
+ errors << "The type of `#{object_type.name}.#{field.name}` is `#{field.type}`, but the type of it's source (`#{related_type.name}.#{field_source.field_path}`) is `#{referenced_field_path.type}`. These must agree to use `sourced_from`."
118
+ nil
119
+ elsif field.type.non_null?
120
+ errors << "The type of `#{object_type.name}.#{field.name}` (`#{field.type}`) is not nullable, but this is not allowed for `sourced_from` fields since the value will be `null` before the related type's event is ingested."
121
+ nil
122
+ else
123
+ param = SchemaArtifacts::RuntimeMetadata::DynamicParam.new(
124
+ source_path: referenced_field_path.path_in_index,
125
+ cardinality: :one
126
+ )
127
+
128
+ [field.name_in_index, param]
129
+ end
130
+ end.to_h
131
+
132
+ [data_params, errors]
133
+ end
134
+
135
+ # Helper method that assists with resolving `routing_value_source` and `rollover_timestamp_value_source`.
136
+ # Uses an `adapter` for the differences in these two cases.
137
+ #
138
+ # Returns a tuple of the resolved source (if successful) and an error (if invalid).
139
+ def resolve_field_source(adapter)
140
+ # For now we only support one index (so we can use the first index) but someday we may need to support multiple.
141
+ index = object_type.indices.first # : Index
142
+
143
+ field_source_graphql_path_string = adapter.get_field_source(resolved_relationship.relationship, index) do |local_need|
144
+ relationship_name = resolved_relationship.relationship_name
145
+
146
+ error = "Cannot update `#{object_type.name}` documents with data from related `#{relationship_name}` events, " \
147
+ "because #{adapter.cannot_update_reason(object_type, relationship_name)}. To fix it, add a call like this to the " \
148
+ "`#{object_type.name}.#{relationship_name}` relationship definition: `rel.equivalent_field " \
149
+ "\"[#{resolved_relationship.related_type.name} field]\", locally_named: \"#{local_need}\"`."
150
+
151
+ return [nil, error]
152
+ end
153
+
154
+ if field_source_graphql_path_string
155
+ field_path = field_path_resolver.resolve_public_path(resolved_relationship.related_type, field_source_graphql_path_string) do |parent_field|
156
+ !parent_field.type.list?
157
+ end
158
+
159
+ [field_path&.path_in_index, nil]
160
+ else
161
+ [nil, nil]
162
+ end
163
+ end
164
+
165
+ # Adapter for the `routing_value_source` case for use by `resolve_field_source`.
166
+ #
167
+ # @private
168
+ module RoutingSourceAdapter
169
+ def self.get_field_source(relationship, index, &block)
170
+ relationship.routing_value_source_for_index(index, &block)
171
+ end
172
+
173
+ def self.cannot_update_reason(object_type, relationship_name)
174
+ "`#{object_type.name}` uses custom shard routing but we don't know what `#{relationship_name}` field to use " \
175
+ "to route the `#{object_type.name}` update requests"
176
+ end
177
+ end
178
+
179
+ # Adapter for the `rollover_timestamp_value_source` case for use by `resolve_field_source`.
180
+ #
181
+ # @private
182
+ module RolloverTimestampSourceAdapter
183
+ def self.get_field_source(relationship, index, &block)
184
+ relationship.rollover_timestamp_value_source_for_index(index, &block)
185
+ end
186
+
187
+ def self.cannot_update_reason(object_type, relationship_name)
188
+ "`#{object_type.name}` uses a rollover index but we don't know what `#{relationship_name}` timestamp field to use " \
189
+ "to select an index for the `#{object_type.name}` update requests"
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,61 @@
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 SchemaDefinition
13
+ # Prunes unused type definitions from a given JSON schema.
14
+ #
15
+ # @private
16
+ class JSONSchemaPruner
17
+ def self.prune(original_json_schema)
18
+ initial_type_names = [EVENT_ENVELOPE_JSON_SCHEMA_NAME] + original_json_schema
19
+ .dig("$defs", EVENT_ENVELOPE_JSON_SCHEMA_NAME, "properties", "type", "enum")
20
+
21
+ types_to_keep = referenced_type_names(initial_type_names, original_json_schema["$defs"])
22
+
23
+ # The .select will preserve the sort order of the original hash
24
+ pruned_defs = original_json_schema["$defs"].select { |k, _v| types_to_keep.include?(k) }
25
+
26
+ original_json_schema.merge("$defs" => pruned_defs)
27
+ end
28
+
29
+ # Returns a list of type names indicating all types referenced from any type in source_type_names.
30
+ private_class_method
31
+ def self.referenced_type_names(source_type_names, original_defs)
32
+ return Set.new if source_type_names.empty?
33
+
34
+ referenced_type_defs = original_defs.select { |k, _| source_type_names.include?(k) }
35
+ ref_names = collect_ref_names(referenced_type_defs)
36
+
37
+ referenced_type_names(ref_names, original_defs) + source_type_names
38
+ end
39
+
40
+ private_class_method
41
+ def self.collect_ref_names(hash)
42
+ hash.flat_map do |key, value|
43
+ case value
44
+ when ::Hash
45
+ collect_ref_names(value)
46
+ when ::Array
47
+ value.grep(::Hash).flat_map { |subhash| collect_ref_names(subhash) }
48
+ when ::String
49
+ if key == "$ref" && (type = value[%r{\A#/\$defs/(.+)\z}, 1])
50
+ [type]
51
+ else
52
+ []
53
+ end
54
+ else
55
+ []
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,31 @@
1
+ # Copyright 2024 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ module ElasticGraph
10
+ module SchemaDefinition
11
+ # Namespace for modules that are used as mixins. Mixins are used to offer a consistent API for
12
+ # schema definition features that apply to multiple types of schema elements.
13
+ module Mixins
14
+ # Used to indicate if a type only exists in the GraphQL schema (e.g. it has no indexing component).
15
+ module CanBeGraphQLOnly
16
+ # Sets whether or not this type only exists in the GraphQL schema
17
+ #
18
+ # @param value [Boolean] whether or not this type only exists in the GraphQL schema
19
+ # @return [void]
20
+ def graphql_only(value)
21
+ @graphql_only = value
22
+ end
23
+
24
+ # @return [Boolean] whether or not this type only exists in the GraphQL schema
25
+ def graphql_only?
26
+ !!@graphql_only
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,119 @@
1
+ # Copyright 2024 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ module ElasticGraph
10
+ module SchemaDefinition
11
+ module Mixins
12
+ # Mixin that supports the customization of derived GraphQL types.
13
+ #
14
+ # For each type you define, ElasticGraph generates a number of derived GraphQL types that are needed to facilitate the ElasticGraph
15
+ # Query API. Methods in this module can be used to customize those derived GraphQL types.
16
+ module HasDerivedGraphQLTypeCustomizations
17
+ # Registers a customization block for the named derived graphql types. The provided block will get run on the named derived GraphQL
18
+ # types, allowing them to be customized.
19
+ #
20
+ # @param type_names [Array<String, :all>] names of the derived types to customize, or `:all` to customize all derived types
21
+ # @return [void]
22
+ #
23
+ # @example Customize named derived GraphQL types
24
+ # ElasticGraph.define_schema do |schema|
25
+ # schema.object_type "Campaign" do |t|
26
+ # t.field "id", "ID!"
27
+ # t.index "campaigns"
28
+ #
29
+ # t.customize_derived_types "CampaignFilterInput", "CampaignSortOrderInput" do |dt|
30
+ # # Add a `@deprecated` directive to two specific derived types.
31
+ # dt.directive "deprecated"
32
+ # end
33
+ # end
34
+ # end
35
+ #
36
+ # @example Customize all derived GraphQL types
37
+ # ElasticGraph.define_schema do |schema|
38
+ # schema.object_type "Campaign" do |t|
39
+ # t.field "id", "ID!"
40
+ # t.index "campaigns"
41
+ #
42
+ # t.customize_derived_types :all do |dt|
43
+ # # Add a `@deprecated` directive to all derived types.
44
+ # dt.directive "deprecated"
45
+ # end
46
+ # end
47
+ # end
48
+ def customize_derived_types(*type_names, &customization_block)
49
+ if type_names.include?(:all)
50
+ derived_type_customizations_for_all_types << customization_block
51
+ else
52
+ type_names.each do |t|
53
+ derived_type_customizations_by_name[t.to_s] << customization_block
54
+ end
55
+ end
56
+ end
57
+
58
+ # Registers a customization block for the named fields on the named derived GraphQL type. The provided block will get run on the
59
+ # named fields of the named derived GraphQL type, allowing them to be customized.
60
+ #
61
+ # @param type_name [String] name of the derived type containing fields you want to customize
62
+ # @param field_names [Array<String>] names of the fields on the derived types that you wish to customize
63
+ # @return [void]
64
+ #
65
+ # @example Customize named fields of a derived GraphQL type
66
+ # ElasticGraph.define_schema do |schema|
67
+ # schema.object_type "Campaign" do |t|
68
+ # t.field "id", "ID!"
69
+ # t.index "campaigns"
70
+ #
71
+ # t.customize_derived_type_fields "CampaignConnection", "pageInfo", "totalEdgeCount" do |dt|
72
+ # # Add a `@deprecated` directive to `CampaignConnection.pageInfo` and `CampaignConnection.totalEdgeCount`.
73
+ # dt.directive "deprecated"
74
+ # end
75
+ # end
76
+ # end
77
+ def customize_derived_type_fields(type_name, *field_names, &customization_block)
78
+ customizations_by_field = derived_field_customizations_by_type_and_field_name[type_name]
79
+
80
+ field_names.each do |field_name|
81
+ customizations_by_field[field_name] << customization_block
82
+ end
83
+ end
84
+
85
+ # @private
86
+ def derived_type_customizations_for_type(type)
87
+ derived_type_customizations_by_name[type.name] + derived_type_customizations_for_all_types
88
+ end
89
+
90
+ # @private
91
+ def derived_field_customizations_by_name_for_type(type)
92
+ derived_field_customizations_by_type_and_field_name[type.name]
93
+ end
94
+
95
+ # @private
96
+ def derived_type_customizations_by_name
97
+ @derived_type_customizations_by_name ||= ::Hash.new do |hash, type_name|
98
+ hash[type_name] = []
99
+ end
100
+ end
101
+
102
+ # @private
103
+ def derived_field_customizations_by_type_and_field_name
104
+ @derived_field_customizations_by_type_and_field_name ||= ::Hash.new do |outer_hash, type|
105
+ outer_hash[type] = ::Hash.new do |inner_hash, field_name|
106
+ inner_hash[field_name] = []
107
+ end
108
+ end
109
+ end
110
+
111
+ private
112
+
113
+ def derived_type_customizations_for_all_types
114
+ @derived_type_customizations_for_all_types ||= []
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,65 @@
1
+ # Copyright 2024 Block, Inc.
2
+ #
3
+ # Use of this source code is governed by an MIT-style
4
+ # license that can be found in the LICENSE file or at
5
+ # https://opensource.org/licenses/MIT.
6
+ #
7
+ # frozen_string_literal: true
8
+
9
+ module ElasticGraph
10
+ module SchemaDefinition
11
+ module Mixins
12
+ # Provides support for annotating any schema element with a GraphQL directive.
13
+ module HasDirectives
14
+ # Adds a GraphQL directive to the current schema element.
15
+ #
16
+ # @note If you’re using a custom directive rather than a standard GraphQL directive like `@deprecated`, you’ll also need to use
17
+ # {API#raw_sdl} to define the custom directive.
18
+ #
19
+ # @param name [String] name of the directive
20
+ # @param arguments [Hash<String, Object>] arguments for the directive
21
+ # @return [void]
22
+ #
23
+ # @example Add a standard GraphQL directive to a field
24
+ # ElasticGraph.define_schema do |schema|
25
+ # schema.object_type "Campaign" do |t|
26
+ # t.field "id", "ID" do |f|
27
+ # f.directive "deprecated"
28
+ # end
29
+ # end
30
+ # end
31
+ #
32
+ # @example Define a custom GraphQL directive and add it to an object type
33
+ # ElasticGraph.define_schema do |schema|
34
+ # # Define a directive we can use to annotate what system a data type comes from.
35
+ # schema.raw_sdl "directive @sourcedFrom(system: String!) on OBJECT"
36
+ #
37
+ # schema.object_type "Campaign" do |t|
38
+ # t.field "id", "ID"
39
+ # t.directive "sourcedFrom", system: "campaigns"
40
+ # end
41
+ # end
42
+ def directive(name, arguments = {})
43
+ directives << schema_def_state.factory.new_directive(name, arguments)
44
+ end
45
+
46
+ # Helper method designed for use by including classes to get the formatted directive SDL.
47
+ #
48
+ # @param suffix_with [String] suffix to add on the end of the SDL
49
+ # @param prefix_with [String] prefix to add to the beginning of the SDL
50
+ # @return [String] SDL string for the directives
51
+ # @api private
52
+ def directives_sdl(suffix_with: "", prefix_with: "")
53
+ sdl = directives.map(&:to_sdl).join(" ")
54
+ return sdl if sdl.empty?
55
+ prefix_with + sdl + suffix_with
56
+ end
57
+
58
+ # @return [Array<SchemaElements::Directive>] directives attached to this schema element
59
+ def directives
60
+ @directives ||= []
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end