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.
- 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
|