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