elasticgraph-schema_definition 0.18.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +7 -0
- data/elasticgraph-schema_definition.gemspec +26 -0
- data/lib/elastic_graph/schema_definition/api.rb +359 -0
- data/lib/elastic_graph/schema_definition/factory.rb +506 -0
- data/lib/elastic_graph/schema_definition/indexing/derived_fields/append_only_set.rb +79 -0
- data/lib/elastic_graph/schema_definition/indexing/derived_fields/field_initializer_support.rb +59 -0
- data/lib/elastic_graph/schema_definition/indexing/derived_fields/immutable_value.rb +99 -0
- data/lib/elastic_graph/schema_definition/indexing/derived_fields/min_or_max_value.rb +62 -0
- data/lib/elastic_graph/schema_definition/indexing/derived_indexed_type.rb +346 -0
- data/lib/elastic_graph/schema_definition/indexing/event_envelope.rb +74 -0
- data/lib/elastic_graph/schema_definition/indexing/field.rb +181 -0
- data/lib/elastic_graph/schema_definition/indexing/field_reference.rb +51 -0
- data/lib/elastic_graph/schema_definition/indexing/field_type/enum.rb +65 -0
- data/lib/elastic_graph/schema_definition/indexing/field_type/object.rb +113 -0
- data/lib/elastic_graph/schema_definition/indexing/field_type/scalar.rb +51 -0
- data/lib/elastic_graph/schema_definition/indexing/field_type/union.rb +70 -0
- data/lib/elastic_graph/schema_definition/indexing/index.rb +318 -0
- data/lib/elastic_graph/schema_definition/indexing/json_schema_field_metadata.rb +34 -0
- data/lib/elastic_graph/schema_definition/indexing/json_schema_with_metadata.rb +234 -0
- data/lib/elastic_graph/schema_definition/indexing/list_counts_mapping.rb +53 -0
- data/lib/elastic_graph/schema_definition/indexing/relationship_resolver.rb +96 -0
- data/lib/elastic_graph/schema_definition/indexing/rollover_config.rb +25 -0
- data/lib/elastic_graph/schema_definition/indexing/update_target_factory.rb +54 -0
- data/lib/elastic_graph/schema_definition/indexing/update_target_resolver.rb +195 -0
- data/lib/elastic_graph/schema_definition/json_schema_pruner.rb +61 -0
- data/lib/elastic_graph/schema_definition/mixins/can_be_graphql_only.rb +31 -0
- data/lib/elastic_graph/schema_definition/mixins/has_derived_graphql_type_customizations.rb +119 -0
- data/lib/elastic_graph/schema_definition/mixins/has_directives.rb +65 -0
- data/lib/elastic_graph/schema_definition/mixins/has_documentation.rb +74 -0
- data/lib/elastic_graph/schema_definition/mixins/has_indices.rb +281 -0
- data/lib/elastic_graph/schema_definition/mixins/has_readable_to_s_and_inspect.rb +46 -0
- data/lib/elastic_graph/schema_definition/mixins/has_subtypes.rb +116 -0
- data/lib/elastic_graph/schema_definition/mixins/has_type_info.rb +181 -0
- data/lib/elastic_graph/schema_definition/mixins/implements_interfaces.rb +122 -0
- data/lib/elastic_graph/schema_definition/mixins/supports_default_value.rb +47 -0
- data/lib/elastic_graph/schema_definition/mixins/supports_filtering_and_aggregation.rb +267 -0
- data/lib/elastic_graph/schema_definition/mixins/verifies_graphql_name.rb +38 -0
- data/lib/elastic_graph/schema_definition/rake_tasks.rb +190 -0
- data/lib/elastic_graph/schema_definition/results.rb +404 -0
- data/lib/elastic_graph/schema_definition/schema_artifact_manager.rb +482 -0
- data/lib/elastic_graph/schema_definition/schema_elements/argument.rb +56 -0
- data/lib/elastic_graph/schema_definition/schema_elements/built_in_types.rb +1541 -0
- data/lib/elastic_graph/schema_definition/schema_elements/deprecated_element.rb +21 -0
- data/lib/elastic_graph/schema_definition/schema_elements/directive.rb +40 -0
- data/lib/elastic_graph/schema_definition/schema_elements/enum_type.rb +189 -0
- data/lib/elastic_graph/schema_definition/schema_elements/enum_value.rb +73 -0
- data/lib/elastic_graph/schema_definition/schema_elements/enum_value_namer.rb +89 -0
- data/lib/elastic_graph/schema_definition/schema_elements/enums_for_indexed_types.rb +82 -0
- data/lib/elastic_graph/schema_definition/schema_elements/field.rb +1085 -0
- data/lib/elastic_graph/schema_definition/schema_elements/field_path.rb +112 -0
- data/lib/elastic_graph/schema_definition/schema_elements/field_source.rb +16 -0
- data/lib/elastic_graph/schema_definition/schema_elements/graphql_sdl_enumerator.rb +113 -0
- data/lib/elastic_graph/schema_definition/schema_elements/input_field.rb +31 -0
- data/lib/elastic_graph/schema_definition/schema_elements/input_type.rb +60 -0
- data/lib/elastic_graph/schema_definition/schema_elements/interface_type.rb +72 -0
- data/lib/elastic_graph/schema_definition/schema_elements/list_counts_state.rb +40 -0
- data/lib/elastic_graph/schema_definition/schema_elements/object_type.rb +53 -0
- data/lib/elastic_graph/schema_definition/schema_elements/relationship.rb +218 -0
- data/lib/elastic_graph/schema_definition/schema_elements/scalar_type.rb +310 -0
- data/lib/elastic_graph/schema_definition/schema_elements/sort_order_enum_value.rb +36 -0
- data/lib/elastic_graph/schema_definition/schema_elements/sub_aggregation_path.rb +66 -0
- data/lib/elastic_graph/schema_definition/schema_elements/type_namer.rb +237 -0
- data/lib/elastic_graph/schema_definition/schema_elements/type_reference.rb +353 -0
- data/lib/elastic_graph/schema_definition/schema_elements/type_with_subfields.rb +579 -0
- data/lib/elastic_graph/schema_definition/schema_elements/union_type.rb +157 -0
- data/lib/elastic_graph/schema_definition/scripting/file_system_repository.rb +77 -0
- data/lib/elastic_graph/schema_definition/scripting/script.rb +48 -0
- data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_day_of_week.painless +24 -0
- data/lib/elastic_graph/schema_definition/scripting/scripts/field/as_time_of_day.painless +41 -0
- data/lib/elastic_graph/schema_definition/scripting/scripts/filter/by_time_of_day.painless +22 -0
- data/lib/elastic_graph/schema_definition/scripting/scripts/update/index_data.painless +93 -0
- data/lib/elastic_graph/schema_definition/state.rb +212 -0
- data/lib/elastic_graph/schema_definition/test_support.rb +113 -0
- metadata +513 -0
@@ -0,0 +1,482 @@
|
|
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 "did_you_mean"
|
10
|
+
require "elastic_graph/constants"
|
11
|
+
require "elastic_graph/schema_definition/json_schema_pruner"
|
12
|
+
require "elastic_graph/support/memoizable_data"
|
13
|
+
require "fileutils"
|
14
|
+
require "graphql"
|
15
|
+
require "tempfile"
|
16
|
+
require "yaml"
|
17
|
+
|
18
|
+
module ElasticGraph
|
19
|
+
module SchemaDefinition
|
20
|
+
# Manages schema artifacts. Note: not tested directly. Instead, the `RakeTasks` tests drive this class.
|
21
|
+
#
|
22
|
+
# Note that we use `abort` instead of `raise` here for exceptions that require the user to perform an action
|
23
|
+
# to resolve. The output from `abort` is cleaner (no stack trace, etc) which improves the signal-to-noise
|
24
|
+
# ratio for the user to (hopefully) make it easier to understand what to do, without needing to wade through
|
25
|
+
# extra output.
|
26
|
+
#
|
27
|
+
# @private
|
28
|
+
class SchemaArtifactManager
|
29
|
+
# @dynamic schema_definition_results
|
30
|
+
attr_reader :schema_definition_results
|
31
|
+
|
32
|
+
def initialize(schema_definition_results:, schema_artifacts_directory:, enforce_json_schema_version:, output:, max_diff_lines: 50)
|
33
|
+
@schema_definition_results = schema_definition_results
|
34
|
+
@schema_artifacts_directory = schema_artifacts_directory
|
35
|
+
@enforce_json_schema_version = enforce_json_schema_version
|
36
|
+
@output = output
|
37
|
+
@max_diff_lines = max_diff_lines
|
38
|
+
|
39
|
+
@json_schemas_artifact = new_yaml_artifact(
|
40
|
+
JSON_SCHEMAS_FILE,
|
41
|
+
JSONSchemaPruner.prune(schema_definition_results.current_public_json_schema),
|
42
|
+
extra_comment_lines: [
|
43
|
+
"This is the \"public\" JSON schema file and is intended to be provided to publishers so that",
|
44
|
+
"they can perform code generation and event validation."
|
45
|
+
]
|
46
|
+
)
|
47
|
+
|
48
|
+
# Here we round-trip the SDL string through the GraphQL gem's formatting logic. This provides
|
49
|
+
# nice, consistent formatting (alphabetical order, consistent spacing, etc) and also prunes out
|
50
|
+
# any "orphaned" schema types (that is, types that are defined but never referenced).
|
51
|
+
# We also prepend a line break so there's a blank line between the comment block and the
|
52
|
+
# schema elements.
|
53
|
+
graphql_schema = ::GraphQL::Schema.from_definition(schema_definition_results.graphql_schema_string).to_definition.chomp
|
54
|
+
|
55
|
+
unversioned_artifacts = [
|
56
|
+
new_yaml_artifact(DATASTORE_CONFIG_FILE, schema_definition_results.datastore_config),
|
57
|
+
new_yaml_artifact(RUNTIME_METADATA_FILE, pruned_runtime_metadata(graphql_schema).to_dumpable_hash),
|
58
|
+
@json_schemas_artifact,
|
59
|
+
new_raw_artifact(GRAPHQL_SCHEMA_FILE, "\n" + graphql_schema)
|
60
|
+
]
|
61
|
+
|
62
|
+
versioned_artifacts = build_desired_versioned_json_schemas(@json_schemas_artifact.desired_contents).values.map do |versioned_schema|
|
63
|
+
new_versioned_json_schema_artifact(versioned_schema)
|
64
|
+
end
|
65
|
+
|
66
|
+
@artifacts = (unversioned_artifacts + versioned_artifacts).sort_by(&:file_name)
|
67
|
+
notify_about_unused_type_name_overrides
|
68
|
+
notify_about_unused_enum_value_overrides
|
69
|
+
end
|
70
|
+
|
71
|
+
# Dumps all the schema artifacts to disk.
|
72
|
+
def dump_artifacts
|
73
|
+
check_if_needs_json_schema_version_bump do |recommended_json_schema_version|
|
74
|
+
if @enforce_json_schema_version
|
75
|
+
# @type var setter_location: ::Thread::Backtrace::Location
|
76
|
+
# We use `_ =` because while `json_schema_version_setter_location` can be nil,
|
77
|
+
# it'll never be nil if we get here and we want the type to be non-nilable.
|
78
|
+
setter_location = _ = schema_definition_results.json_schema_version_setter_location
|
79
|
+
setter_location_path = ::Pathname.new(setter_location.absolute_path.to_s).relative_path_from(::Dir.pwd)
|
80
|
+
|
81
|
+
abort "A change has been attempted to `json_schemas.yaml`, but the `json_schema_version` has not been correspondingly incremented. Please " \
|
82
|
+
"increase the schema's version, and then run the `schema_artifacts:dump` command again.\n\n" \
|
83
|
+
"To update the schema version to the expected version, change line #{setter_location.lineno} at `#{setter_location_path}` to:\n" \
|
84
|
+
" `schema.json_schema_version #{recommended_json_schema_version}`\n\n" \
|
85
|
+
"Alternately, pass `enforce_json_schema_version: false` to `ElasticGraph::SchemaDefinition::RakeTasks.new` to allow the JSON schemas " \
|
86
|
+
"file to change without requiring a version bump, but that is only recommended for non-production applications during initial schema prototyping."
|
87
|
+
else
|
88
|
+
@output.puts <<~EOS
|
89
|
+
WARNING: the `json_schemas.yaml` artifact is being updated without the `json_schema_version` being correspondingly incremented.
|
90
|
+
This is not recommended for production applications, but is currently allowed because you have set `enforce_json_schema_version: false`.
|
91
|
+
EOS
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
::FileUtils.mkdir_p(@schema_artifacts_directory)
|
96
|
+
@artifacts.each { |artifact| artifact.dump(@output) }
|
97
|
+
end
|
98
|
+
|
99
|
+
# Checks that all schema artifacts are up-to-date, raising an exception if not.
|
100
|
+
def check_artifacts
|
101
|
+
out_of_date_artifacts = @artifacts.select(&:out_of_date?)
|
102
|
+
|
103
|
+
if out_of_date_artifacts.empty?
|
104
|
+
descriptions = @artifacts.map.with_index(1) { |art, i| "#{i}. #{art.file_name}" }
|
105
|
+
@output.puts <<~EOS
|
106
|
+
Your schema artifacts are all up to date:
|
107
|
+
#{descriptions.join("\n")}
|
108
|
+
|
109
|
+
EOS
|
110
|
+
else
|
111
|
+
abort artifacts_out_of_date_error(out_of_date_artifacts)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def notify_about_unused_type_name_overrides
|
118
|
+
type_namer = @schema_definition_results.state.type_namer
|
119
|
+
return if (unused_overrides = type_namer.unused_name_overrides).empty?
|
120
|
+
|
121
|
+
suggester = ::DidYouMean::SpellChecker.new(dictionary: type_namer.used_names.to_a)
|
122
|
+
warnings = unused_overrides.map.with_index(1) do |(unused_name, _), index|
|
123
|
+
alternatives = suggester.correct(unused_name).map { |alt| "`#{alt}`" }
|
124
|
+
"#{index}. The type name override `#{unused_name}` does not match any type in your GraphQL schema and has been ignored." \
|
125
|
+
"#{" Possible alternatives: #{alternatives.join(", ")}." unless alternatives.empty?}"
|
126
|
+
end
|
127
|
+
|
128
|
+
@output.puts <<~EOS
|
129
|
+
WARNING: #{unused_overrides.size} of the `type_name_overrides` do not match any type(s) in your GraphQL schema:
|
130
|
+
|
131
|
+
#{warnings.join("\n")}
|
132
|
+
EOS
|
133
|
+
end
|
134
|
+
|
135
|
+
def notify_about_unused_enum_value_overrides
|
136
|
+
enum_value_namer = @schema_definition_results.state.enum_value_namer
|
137
|
+
return if (unused_overrides = enum_value_namer.unused_overrides).empty?
|
138
|
+
|
139
|
+
used_value_names_by_type_name = enum_value_namer.used_value_names_by_type_name
|
140
|
+
type_suggester = ::DidYouMean::SpellChecker.new(dictionary: used_value_names_by_type_name.keys)
|
141
|
+
index = 0
|
142
|
+
warnings = unused_overrides.flat_map do |type_name, overrides|
|
143
|
+
if used_value_names_by_type_name.key?(type_name)
|
144
|
+
value_suggester = ::DidYouMean::SpellChecker.new(dictionary: used_value_names_by_type_name.fetch(type_name))
|
145
|
+
overrides.map do |(value_name), _|
|
146
|
+
alternatives = value_suggester.correct(value_name).map { |alt| "`#{alt}`" }
|
147
|
+
"#{index += 1}. The enum value override `#{type_name}.#{value_name}` does not match any enum value in your GraphQL schema and has been ignored." \
|
148
|
+
"#{" Possible alternatives: #{alternatives.join(", ")}." unless alternatives.empty?}"
|
149
|
+
end
|
150
|
+
else
|
151
|
+
alternatives = type_suggester.correct(type_name).map { |alt| "`#{alt}`" }
|
152
|
+
["#{index += 1}. `enum_value_overrides_by_type` has a `#{type_name}` key, which does not match any enum type in your GraphQL schema and has been ignored." \
|
153
|
+
"#{" Possible alternatives: #{alternatives.join(", ")}." unless alternatives.empty?}"]
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
@output.puts <<~EOS
|
158
|
+
WARNING: some of the `enum_value_overrides_by_type` do not match any type(s)/value(s) in your GraphQL schema:
|
159
|
+
|
160
|
+
#{warnings.join("\n")}
|
161
|
+
EOS
|
162
|
+
end
|
163
|
+
|
164
|
+
def build_desired_versioned_json_schemas(current_public_json_schema)
|
165
|
+
versioned_parsed_yamls = ::Dir.glob(::File.join(@schema_artifacts_directory, JSON_SCHEMAS_BY_VERSION_DIRECTORY, "v*.yaml")).map do |file|
|
166
|
+
::YAML.safe_load_file(file)
|
167
|
+
end + [current_public_json_schema]
|
168
|
+
|
169
|
+
results_by_json_schema_version = versioned_parsed_yamls.to_h do |parsed_yaml|
|
170
|
+
merged_schema = @schema_definition_results.merge_field_metadata_into_json_schema(parsed_yaml)
|
171
|
+
[merged_schema.json_schema_version, merged_schema]
|
172
|
+
end
|
173
|
+
|
174
|
+
report_json_schema_merge_errors(results_by_json_schema_version.values)
|
175
|
+
report_json_schema_merge_warnings
|
176
|
+
|
177
|
+
results_by_json_schema_version.transform_values(&:json_schema)
|
178
|
+
end
|
179
|
+
|
180
|
+
def report_json_schema_merge_errors(merged_results)
|
181
|
+
json_schema_versions_by_missing_field = ::Hash.new { |h, k| h[k] = [] }
|
182
|
+
json_schema_versions_by_missing_type = ::Hash.new { |h, k| h[k] = [] }
|
183
|
+
json_schema_versions_by_missing_necessary_field = ::Hash.new { |h, k| h[k] = [] }
|
184
|
+
|
185
|
+
merged_results.each do |result|
|
186
|
+
result.missing_fields.each do |field|
|
187
|
+
json_schema_versions_by_missing_field[field] << result.json_schema_version
|
188
|
+
end
|
189
|
+
|
190
|
+
result.missing_types.each do |type|
|
191
|
+
json_schema_versions_by_missing_type[type] << result.json_schema_version
|
192
|
+
end
|
193
|
+
|
194
|
+
result.missing_necessary_fields.each do |missing_necessary_field|
|
195
|
+
json_schema_versions_by_missing_necessary_field[missing_necessary_field] << result.json_schema_version
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
missing_field_errors = json_schema_versions_by_missing_field.map do |field, json_schema_versions|
|
200
|
+
missing_field_error_for(field, json_schema_versions)
|
201
|
+
end
|
202
|
+
|
203
|
+
missing_type_errors = json_schema_versions_by_missing_type.map do |type, json_schema_versions|
|
204
|
+
missing_type_error_for(type, json_schema_versions)
|
205
|
+
end
|
206
|
+
|
207
|
+
missing_necessary_field_errors = json_schema_versions_by_missing_necessary_field.map do |field, json_schema_versions|
|
208
|
+
missing_necessary_field_error_for(field, json_schema_versions)
|
209
|
+
end
|
210
|
+
|
211
|
+
definition_conflict_errors = merged_results
|
212
|
+
.flat_map { |result| result.definition_conflicts.to_a }
|
213
|
+
.group_by(&:name)
|
214
|
+
.map do |name, deprecated_elements|
|
215
|
+
<<~EOS
|
216
|
+
The schema definition of `#{name}` has conflicts. To resolve the conflict, remove the unneeded definitions from the following:
|
217
|
+
|
218
|
+
#{format_deprecated_elements(deprecated_elements)}
|
219
|
+
EOS
|
220
|
+
end
|
221
|
+
|
222
|
+
errors = missing_field_errors + missing_type_errors + missing_necessary_field_errors + definition_conflict_errors
|
223
|
+
return if errors.empty?
|
224
|
+
|
225
|
+
abort errors.join("\n\n")
|
226
|
+
end
|
227
|
+
|
228
|
+
def report_json_schema_merge_warnings
|
229
|
+
unused_elements = @schema_definition_results.unused_deprecated_elements
|
230
|
+
return if unused_elements.empty?
|
231
|
+
|
232
|
+
@output.puts <<~EOS
|
233
|
+
The schema definition has #{unused_elements.size} unneeded reference(s) to deprecated schema elements. These can all be safely deleted:
|
234
|
+
|
235
|
+
#{format_deprecated_elements(unused_elements)}
|
236
|
+
|
237
|
+
EOS
|
238
|
+
end
|
239
|
+
|
240
|
+
def format_deprecated_elements(deprecated_elements)
|
241
|
+
descriptions = deprecated_elements
|
242
|
+
.sort_by { |e| [e.defined_at.path, e.defined_at.lineno] }
|
243
|
+
.map(&:description)
|
244
|
+
.uniq
|
245
|
+
|
246
|
+
descriptions.each.with_index(1).map { |desc, idx| "#{idx}. #{desc}" }.join("\n")
|
247
|
+
end
|
248
|
+
|
249
|
+
def missing_field_error_for(qualified_field, json_schema_versions)
|
250
|
+
type, field = qualified_field.split(".")
|
251
|
+
|
252
|
+
<<~EOS
|
253
|
+
The `#{qualified_field}` field (which existed in #{describe_json_schema_versions(json_schema_versions, "and")}) no longer exists in the current schema definition.
|
254
|
+
ElasticGraph cannot guess what it should do with this field's data when ingesting events at #{old_versions(json_schema_versions)}.
|
255
|
+
To continue, do one of the following:
|
256
|
+
|
257
|
+
1. If the `#{qualified_field}` field has been renamed, indicate this by calling `field.renamed_from "#{field}"` on the renamed field.
|
258
|
+
2. If the `#{qualified_field}` field has been dropped, indicate this by calling `type.deleted_field "#{field}"` on the `#{type}` type.
|
259
|
+
3. Alternately, if no publishers or in-flight events use #{describe_json_schema_versions(json_schema_versions, "or")}, delete #{files_noun_phrase(json_schema_versions)} from `#{JSON_SCHEMAS_BY_VERSION_DIRECTORY}`, and no further changes are required.
|
260
|
+
EOS
|
261
|
+
end
|
262
|
+
|
263
|
+
def missing_type_error_for(type, json_schema_versions)
|
264
|
+
<<~EOS
|
265
|
+
The `#{type}` type (which existed in #{describe_json_schema_versions(json_schema_versions, "and")}) no longer exists in the current schema definition.
|
266
|
+
ElasticGraph cannot guess what it should do with this type's data when ingesting events at #{old_versions(json_schema_versions)}.
|
267
|
+
To continue, do one of the following:
|
268
|
+
|
269
|
+
1. If the `#{type}` type has been renamed, indicate this by calling `type.renamed_from "#{type}"` on the renamed type.
|
270
|
+
2. If the `#{type}` field has been dropped, indicate this by calling `schema.deleted_type "#{type}"` on the schema.
|
271
|
+
3. Alternately, if no publishers or in-flight events use #{describe_json_schema_versions(json_schema_versions, "or")}, delete #{files_noun_phrase(json_schema_versions)} from `#{JSON_SCHEMAS_BY_VERSION_DIRECTORY}`, and no further changes are required.
|
272
|
+
EOS
|
273
|
+
end
|
274
|
+
|
275
|
+
def missing_necessary_field_error_for(field, json_schema_versions)
|
276
|
+
path = field.fully_qualified_path.split(".").last
|
277
|
+
# :nocov: -- we only cover one side of this ternary.
|
278
|
+
has_or_have = (json_schema_versions.size == 1) ? "has" : "have"
|
279
|
+
# :nocov:
|
280
|
+
|
281
|
+
<<~EOS
|
282
|
+
#{describe_json_schema_versions(json_schema_versions, "and")} #{has_or_have} no field that maps to the #{field.field_type} field path of `#{field.fully_qualified_path}`.
|
283
|
+
Since the field path is required for #{field.field_type}, ElasticGraph cannot ingest events that lack it. To continue, do one of the following:
|
284
|
+
|
285
|
+
1. If the `#{field.fully_qualified_path}` field has been renamed, indicate this by calling `field.renamed_from "#{path}"` on the renamed field rather than using `deleted_field`.
|
286
|
+
2. Alternately, if no publishers or in-flight events use #{describe_json_schema_versions(json_schema_versions, "or")}, delete #{files_noun_phrase(json_schema_versions)} from `#{JSON_SCHEMAS_BY_VERSION_DIRECTORY}`, and no further changes are required.
|
287
|
+
EOS
|
288
|
+
end
|
289
|
+
|
290
|
+
def describe_json_schema_versions(json_schema_versions, conjunction)
|
291
|
+
json_schema_versions = json_schema_versions.sort
|
292
|
+
|
293
|
+
# Steep doesn't support pattern matching yet, so have to skip type checking here.
|
294
|
+
__skip__ = case json_schema_versions
|
295
|
+
in [single_version]
|
296
|
+
"JSON schema version #{single_version}"
|
297
|
+
in [version1, version2]
|
298
|
+
"JSON schema versions #{version1} #{conjunction} #{version2}"
|
299
|
+
else
|
300
|
+
*versions, last_version = json_schema_versions
|
301
|
+
"JSON schema versions #{versions.join(", ")}, #{conjunction} #{last_version}"
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def old_versions(json_schema_versions)
|
306
|
+
return "this old version" if json_schema_versions.size == 1
|
307
|
+
"these old versions"
|
308
|
+
end
|
309
|
+
|
310
|
+
def files_noun_phrase(json_schema_versions)
|
311
|
+
return "its file" if json_schema_versions.size == 1
|
312
|
+
"their files"
|
313
|
+
end
|
314
|
+
|
315
|
+
def artifacts_out_of_date_error(out_of_date_artifacts)
|
316
|
+
# @type var diffs: ::Array[[SchemaArtifact[untyped], ::String]]
|
317
|
+
diffs = []
|
318
|
+
|
319
|
+
descriptions = out_of_date_artifacts.map.with_index(1) do |artifact, index|
|
320
|
+
reason =
|
321
|
+
if (diff = artifact.diff(color: @output.tty?))
|
322
|
+
description, diff = truncate_diff(diff, @max_diff_lines)
|
323
|
+
diffs << [artifact, diff]
|
324
|
+
"see [#{diffs.size}] below for the #{description}"
|
325
|
+
else
|
326
|
+
"file does not exist"
|
327
|
+
end
|
328
|
+
|
329
|
+
"#{index}. #{artifact.file_name} (#{reason})"
|
330
|
+
end
|
331
|
+
|
332
|
+
diffs = diffs.map.with_index(1) do |(artifact, diff), index|
|
333
|
+
<<~EOS
|
334
|
+
[#{index}] #{artifact.file_name} diff:
|
335
|
+
#{diff}
|
336
|
+
EOS
|
337
|
+
end
|
338
|
+
|
339
|
+
<<~EOS.strip
|
340
|
+
#{out_of_date_artifacts.size} schema artifact(s) are out of date. Run `rake schema_artifacts:dump` to update the following artifact(s):
|
341
|
+
|
342
|
+
#{descriptions.join("\n")}
|
343
|
+
|
344
|
+
#{diffs.join("\n")}
|
345
|
+
EOS
|
346
|
+
end
|
347
|
+
|
348
|
+
def truncate_diff(diff, lines)
|
349
|
+
diff_lines = diff.lines
|
350
|
+
|
351
|
+
if diff_lines.size <= lines
|
352
|
+
["diff", diff]
|
353
|
+
else
|
354
|
+
truncated = diff_lines.first(lines).join
|
355
|
+
["first #{lines} lines of the diff", truncated]
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
def new_yaml_artifact(file_name, desired_contents, extra_comment_lines: [])
|
360
|
+
SchemaArtifact.new(
|
361
|
+
::File.join(@schema_artifacts_directory, file_name),
|
362
|
+
desired_contents,
|
363
|
+
->(hash) { ::YAML.dump(hash) },
|
364
|
+
->(string) { ::YAML.safe_load(string) },
|
365
|
+
extra_comment_lines
|
366
|
+
)
|
367
|
+
end
|
368
|
+
|
369
|
+
def new_versioned_json_schema_artifact(desired_contents)
|
370
|
+
# File name depends on the schema_version field in the json schema.
|
371
|
+
schema_version = desired_contents[JSON_SCHEMA_VERSION_KEY]
|
372
|
+
|
373
|
+
new_yaml_artifact(
|
374
|
+
::File.join(JSON_SCHEMAS_BY_VERSION_DIRECTORY, "v#{schema_version}.yaml"),
|
375
|
+
desired_contents,
|
376
|
+
extra_comment_lines: [
|
377
|
+
"This JSON schema file contains internal ElasticGraph metadata and should be considered private.",
|
378
|
+
"The unversioned JSON schema file is public and intended to be provided to publishers."
|
379
|
+
]
|
380
|
+
)
|
381
|
+
end
|
382
|
+
|
383
|
+
def new_raw_artifact(file_name, desired_contents)
|
384
|
+
SchemaArtifact.new(
|
385
|
+
::File.join(@schema_artifacts_directory, file_name),
|
386
|
+
desired_contents,
|
387
|
+
_ = :itself.to_proc,
|
388
|
+
_ = :itself.to_proc,
|
389
|
+
[]
|
390
|
+
)
|
391
|
+
end
|
392
|
+
|
393
|
+
def check_if_needs_json_schema_version_bump(&block)
|
394
|
+
if @json_schemas_artifact.out_of_date?
|
395
|
+
existing_schema_version = @json_schemas_artifact.existing_dumped_contents&.dig(JSON_SCHEMA_VERSION_KEY) || -1
|
396
|
+
desired_schema_version = @json_schemas_artifact.desired_contents[JSON_SCHEMA_VERSION_KEY]
|
397
|
+
|
398
|
+
if existing_schema_version >= desired_schema_version
|
399
|
+
yield existing_schema_version + 1
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
def pruned_runtime_metadata(graphql_schema_string)
|
405
|
+
schema = ::GraphQL::Schema.from_definition(graphql_schema_string)
|
406
|
+
runtime_meta = schema_definition_results.runtime_metadata
|
407
|
+
|
408
|
+
schema_type_names = schema.types.keys
|
409
|
+
pruned_enum_types = runtime_meta.enum_types_by_name.slice(*schema_type_names)
|
410
|
+
pruned_scalar_types = runtime_meta.scalar_types_by_name.slice(*schema_type_names)
|
411
|
+
pruned_object_types = runtime_meta.object_types_by_name.slice(*schema_type_names)
|
412
|
+
|
413
|
+
runtime_meta.with(
|
414
|
+
enum_types_by_name: pruned_enum_types,
|
415
|
+
scalar_types_by_name: pruned_scalar_types,
|
416
|
+
object_types_by_name: pruned_object_types
|
417
|
+
)
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
# @private
|
422
|
+
class SchemaArtifact < Support::MemoizableData.define(:file_name, :desired_contents, :dumper, :loader, :extra_comment_lines)
|
423
|
+
def dump(output)
|
424
|
+
if out_of_date?
|
425
|
+
dirname = File.dirname(file_name)
|
426
|
+
FileUtils.mkdir_p(dirname) # Create directory if needed.
|
427
|
+
|
428
|
+
::File.write(file_name, dumped_contents)
|
429
|
+
output.puts "Dumped schema artifact to `#{file_name}`."
|
430
|
+
else
|
431
|
+
output.puts "`#{file_name}` is already up to date."
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
def out_of_date?
|
436
|
+
(_ = existing_dumped_contents) != desired_contents
|
437
|
+
end
|
438
|
+
|
439
|
+
def existing_dumped_contents
|
440
|
+
return nil unless exists?
|
441
|
+
|
442
|
+
# We drop the first 2 lines because it is the comment block containing dynamic elements.
|
443
|
+
file_contents = ::File.read(file_name).split("\n").drop(2).join("\n")
|
444
|
+
loader.call(file_contents)
|
445
|
+
end
|
446
|
+
|
447
|
+
def diff(color:)
|
448
|
+
return nil unless exists?
|
449
|
+
|
450
|
+
::Tempfile.create do |f|
|
451
|
+
f.write(dumped_contents.chomp)
|
452
|
+
f.fsync
|
453
|
+
|
454
|
+
`git diff --no-index #{file_name} #{f.path}#{" --color" if color}`
|
455
|
+
.gsub(file_name, "existing_contents")
|
456
|
+
.gsub(f.path, "/updated_contents")
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
private
|
461
|
+
|
462
|
+
def exists?
|
463
|
+
return !!@exists if defined?(@exists)
|
464
|
+
@exists = ::File.exist?(file_name)
|
465
|
+
end
|
466
|
+
|
467
|
+
def dumped_contents
|
468
|
+
@dumped_contents ||= "#{comment_preamble}\n#{dumper.call(desired_contents)}"
|
469
|
+
end
|
470
|
+
|
471
|
+
def comment_preamble
|
472
|
+
lines = [
|
473
|
+
"Generated by `rake schema_artifacts:dump`.",
|
474
|
+
"DO NOT EDIT BY HAND. Any edits will be lost the next time the rake task is run."
|
475
|
+
]
|
476
|
+
|
477
|
+
lines = extra_comment_lines + [""] + lines unless extra_comment_lines.empty?
|
478
|
+
lines.map { |line| "# #{line}".strip }.join("\n")
|
479
|
+
end
|
480
|
+
end
|
481
|
+
end
|
482
|
+
end
|
@@ -0,0 +1,56 @@
|
|
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_definition/mixins/has_directives"
|
10
|
+
require "elastic_graph/schema_definition/mixins/has_documentation"
|
11
|
+
require "elastic_graph/schema_definition/mixins/has_readable_to_s_and_inspect"
|
12
|
+
require "elastic_graph/schema_definition/mixins/supports_default_value"
|
13
|
+
require "elastic_graph/schema_definition/mixins/verifies_graphql_name"
|
14
|
+
|
15
|
+
module ElasticGraph
|
16
|
+
module SchemaDefinition
|
17
|
+
# Namespace for classes which represent GraphQL schema elements.
|
18
|
+
module SchemaElements
|
19
|
+
# Represents a [GraphQL argument](https://spec.graphql.org/October2021/#sec-Language.Arguments).
|
20
|
+
#
|
21
|
+
# @!attribute [r] schema_def_state
|
22
|
+
# @return [State] state of the schema
|
23
|
+
# @!attribute [r] parent_field
|
24
|
+
# @return [Field] field which has this argument
|
25
|
+
# @!attribute [r] name
|
26
|
+
# @return [String] name of the argument
|
27
|
+
# @!attribute [r] original_value_type
|
28
|
+
# @return [TypeReference] type of the argument, as originally provided
|
29
|
+
# @see #value_type
|
30
|
+
class Argument < Struct.new(:schema_def_state, :parent_field, :name, :original_value_type)
|
31
|
+
prepend Mixins::VerifiesGraphQLName
|
32
|
+
prepend Mixins::SupportsDefaultValue
|
33
|
+
include Mixins::HasDocumentation
|
34
|
+
include Mixins::HasDirectives
|
35
|
+
include Mixins::HasReadableToSAndInspect.new { |a| "#{a.parent_field.parent_type.name}.#{a.parent_field.name}(#{a.name}: #{a.value_type})" }
|
36
|
+
|
37
|
+
# @return [String] GraphQL SDL form of the argument
|
38
|
+
def to_sdl
|
39
|
+
"#{formatted_documentation}#{name}: #{value_type}#{default_value_sdl}#{directives_sdl(prefix_with: " ")}"
|
40
|
+
end
|
41
|
+
|
42
|
+
# When the argument type is an enum, and we're configured with different naming for input vs output enums,
|
43
|
+
# we need to convert the value type to its input form. Note that this intentionally happens lazily (rather than
|
44
|
+
# doing this when `Argument` is instantiated), because the referenced type need not exist when the argument
|
45
|
+
# is defined, and we may not be able to figure out if it's an enum until the type has been defined. So, we
|
46
|
+
# apply this lazily.
|
47
|
+
#
|
48
|
+
# @return [TypeReference] the type of the argument
|
49
|
+
# @see #original_value_type
|
50
|
+
def value_type
|
51
|
+
original_value_type.to_final_form(as_input: true)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|