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,237 @@
|
|
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/error"
|
12
|
+
|
13
|
+
module ElasticGraph
|
14
|
+
module SchemaDefinition
|
15
|
+
module SchemaElements
|
16
|
+
# Abstraction for generating derived GraphQL type names based on a collection of formats. A default set of formats is included, and
|
17
|
+
# overrides can be provided to customize the format we use for naming derived types.
|
18
|
+
class TypeNamer < ::Struct.new(:formats, :regexes, :name_overrides, :reverse_overrides)
|
19
|
+
# Initializes a new `TypeNamer` with the provided format overrides.
|
20
|
+
# The keys in `overrides` must match the keys in `DEFAULT_FORMATS` and the values must have
|
21
|
+
# the same placeholders as are present in the default formats.
|
22
|
+
#
|
23
|
+
# @private
|
24
|
+
def initialize(format_overrides: {}, name_overrides: {})
|
25
|
+
@used_names = []
|
26
|
+
name_overrides = name_overrides.transform_keys(&:to_s)
|
27
|
+
|
28
|
+
validate_format_overrides(format_overrides)
|
29
|
+
validate_name_overrides(name_overrides)
|
30
|
+
|
31
|
+
formats = DEFAULT_FORMATS.merge(format_overrides)
|
32
|
+
regexes = formats.transform_values { |format| /\A#{format.gsub(PLACEHOLDER_REGEX, "(\\w+)")}\z/ }
|
33
|
+
reverse_overrides = name_overrides.to_h { |k, v| [v, k] }
|
34
|
+
|
35
|
+
super(formats: formats, regexes: regexes, name_overrides: name_overrides, reverse_overrides: reverse_overrides)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the configured name for the given `standard_name`.
|
39
|
+
#
|
40
|
+
# By default, the returned name will just be the string form of the given `standard_name`, but if
|
41
|
+
# the `TypeNamer` was instantiated with an override for the given `standard_name`, that will be
|
42
|
+
# returned instead.
|
43
|
+
#
|
44
|
+
# @private
|
45
|
+
def name_for(standard_name)
|
46
|
+
string_name = standard_name.to_s
|
47
|
+
@used_names << string_name
|
48
|
+
name_overrides.fetch(string_name, string_name)
|
49
|
+
end
|
50
|
+
|
51
|
+
# If the given `potentially_overriden_name` is an overridden name, returns the name from before the
|
52
|
+
# override was applied. Note: this may not be the true "original" name that ElasticGraph would have
|
53
|
+
# have used (e.g. it could still be customized by `formats`) but it will be the name that would
|
54
|
+
# be used without any `name_overrides`.
|
55
|
+
#
|
56
|
+
# @private
|
57
|
+
def revert_override_for(potentially_overriden_name)
|
58
|
+
reverse_overrides.fetch(potentially_overriden_name, potentially_overriden_name)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Generates a derived type name based on the provided format name and arguments. The given arguments must match
|
62
|
+
# the placeholders in the format. If the format name is unknown or the arguments are invalid, a `ConfigError` is raised.
|
63
|
+
#
|
64
|
+
# Note: this does not apply any configured `name_overrides`. It's up to the caller to apply that when desired.
|
65
|
+
#
|
66
|
+
# @private
|
67
|
+
def generate_name_for(format_name, **args)
|
68
|
+
format = formats.fetch(format_name) do
|
69
|
+
suggestions = FORMAT_SUGGESTER.correct(format_name).map(&:inspect)
|
70
|
+
raise ConfigError, "Unknown format name: #{format_name.inspect}. Possible alternatives: #{suggestions.join(", ")}."
|
71
|
+
end
|
72
|
+
|
73
|
+
expected_placeholders = REQUIRED_PLACEHOLDERS.fetch(format_name)
|
74
|
+
if (missing_placeholders = expected_placeholders - args.keys).any?
|
75
|
+
raise ConfigError, "The arguments (#{args.inspect}) provided for `#{format_name}` format (#{format.inspect}) omits required key(s): #{missing_placeholders.join(", ")}."
|
76
|
+
end
|
77
|
+
|
78
|
+
if (extra_placeholders = args.keys - expected_placeholders).any?
|
79
|
+
raise ConfigError, "The arguments (#{args.inspect}) provided for `#{format_name}` format (#{format.inspect}) contains extra key(s): #{extra_placeholders.join(", ")}."
|
80
|
+
end
|
81
|
+
|
82
|
+
format % args
|
83
|
+
end
|
84
|
+
|
85
|
+
# Given a `name` that has been generated for the given `format`, extracts the `base` parameter value that was used
|
86
|
+
# to generate `name`.
|
87
|
+
#
|
88
|
+
# Raises an error if the given `format` does not support `base` extraction. (To extract `base`, it's required that
|
89
|
+
# `base` is the only placeholder in the format.)
|
90
|
+
#
|
91
|
+
# Returns `nil` if the given `format` does support `base` extraction but `name` does not match the `format`.
|
92
|
+
#
|
93
|
+
# @private
|
94
|
+
def extract_base_from(name, format:)
|
95
|
+
unless REQUIRED_PLACEHOLDERS.fetch(format) == [:base]
|
96
|
+
raise InvalidArgumentValueError, "The `#{format}` format does not support base extraction."
|
97
|
+
end
|
98
|
+
|
99
|
+
regexes.fetch(format).match(name)&.captures&.first
|
100
|
+
end
|
101
|
+
|
102
|
+
# Indicates if the given `name` matches the format for the provided `format_name`.
|
103
|
+
#
|
104
|
+
# Note: our formats are not "mutually exclusive"--some names can match more than one format, so the
|
105
|
+
# fact that a name matches a format does not guarantee it was generated by that format.
|
106
|
+
#
|
107
|
+
# @private
|
108
|
+
def matches_format?(name, format_name)
|
109
|
+
regexes.fetch(format_name).match?(name)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Returns a hash containing the entries of `name_overrides` which have not been used.
|
113
|
+
# These are likely to be typos, and they can be used to warn the user.
|
114
|
+
#
|
115
|
+
# @private
|
116
|
+
def unused_name_overrides
|
117
|
+
name_overrides.except(*@used_names.uniq)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns a set containing all names that got passed to `name_for`: essentially, these are the
|
121
|
+
# candidates for valid name overrides.
|
122
|
+
#
|
123
|
+
# Can be used (in conjunction with `unused_name_overrides`) to provide suggested
|
124
|
+
# alternatives to the user.
|
125
|
+
#
|
126
|
+
# @private
|
127
|
+
def used_names
|
128
|
+
@used_names.to_set
|
129
|
+
end
|
130
|
+
|
131
|
+
# Extracts the names of the placeholders from the provided format.
|
132
|
+
#
|
133
|
+
# @private
|
134
|
+
def self.placeholders_in(format)
|
135
|
+
format.scan(PLACEHOLDER_REGEX).flatten.map(&:to_sym)
|
136
|
+
end
|
137
|
+
|
138
|
+
# The default formats used for derived GraphQL type names. These formats can be customized by providing `derived_type_name_formats`
|
139
|
+
# to {RakeTasks} or {Local::RakeTasks}.
|
140
|
+
#
|
141
|
+
# @return [Hash<Symbol, String>]
|
142
|
+
DEFAULT_FORMATS = {
|
143
|
+
AggregatedValues: "%{base}AggregatedValues",
|
144
|
+
Aggregation: "%{base}Aggregation",
|
145
|
+
Connection: "%{base}Connection",
|
146
|
+
Edge: "%{base}Edge",
|
147
|
+
FieldsListFilterInput: "%{base}FieldsListFilterInput",
|
148
|
+
FilterInput: "%{base}FilterInput",
|
149
|
+
GroupedBy: "%{base}GroupedBy",
|
150
|
+
InputEnum: "%{base}Input",
|
151
|
+
ListElementFilterInput: "%{base}ListElementFilterInput",
|
152
|
+
ListFilterInput: "%{base}ListFilterInput",
|
153
|
+
SortOrder: "%{base}SortOrder",
|
154
|
+
SubAggregation: "%{parent_types}%{base}SubAggregation",
|
155
|
+
SubAggregations: "%{parent_agg_type}%{field_path}SubAggregations"
|
156
|
+
}.freeze
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
# https://rubular.com/r/EJMY0zHZiC5HQm
|
161
|
+
PLACEHOLDER_REGEX = /%\{(\w+)\}/
|
162
|
+
|
163
|
+
REQUIRED_PLACEHOLDERS = DEFAULT_FORMATS.transform_values { |format| placeholders_in(format) }
|
164
|
+
FORMAT_SUGGESTER = ::DidYouMean::SpellChecker.new(dictionary: DEFAULT_FORMATS.keys)
|
165
|
+
DEFINITE_ENUM_FORMATS = [:SortOrder].to_set
|
166
|
+
DEFINITE_OBJECT_FORMATS = DEFAULT_FORMATS.keys.to_set - DEFINITE_ENUM_FORMATS - [:InputEnum].to_set
|
167
|
+
TYPES_THAT_CANNOT_BE_OVERRIDDEN = STOCK_GRAPHQL_SCALARS.union(["Query"]).freeze
|
168
|
+
|
169
|
+
def validate_format_overrides(format_overrides)
|
170
|
+
format_problems = format_overrides.flat_map do |format_name, format|
|
171
|
+
validate_format(format_name, format)
|
172
|
+
end
|
173
|
+
|
174
|
+
notify_problems(format_problems, "Provided derived type name formats")
|
175
|
+
end
|
176
|
+
|
177
|
+
def validate_format(format_name, format)
|
178
|
+
if (required_placeholders = REQUIRED_PLACEHOLDERS[format_name])
|
179
|
+
placeholders = self.class.placeholders_in(format)
|
180
|
+
placeholder_problems = [] # : ::Array[String]
|
181
|
+
|
182
|
+
if (missing_placeholders = required_placeholders - placeholders).any?
|
183
|
+
placeholder_problems << "The #{format_name} format #{format.inspect} is missing required placeholders: #{missing_placeholders.join(", ")}. " \
|
184
|
+
"Example valid format: #{DEFAULT_FORMATS.fetch(format_name).inspect}."
|
185
|
+
end
|
186
|
+
|
187
|
+
if (extra_placeholders = placeholders - required_placeholders).any?
|
188
|
+
placeholder_problems << "The #{format_name} format #{format.inspect} has excess placeholders: #{extra_placeholders.join(", ")}. " \
|
189
|
+
"Example valid format: #{DEFAULT_FORMATS.fetch(format_name).inspect}."
|
190
|
+
end
|
191
|
+
|
192
|
+
example_name = format % placeholders.to_h { |placeholder| [placeholder.to_sym, placeholder.capitalize] }
|
193
|
+
unless GRAPHQL_NAME_PATTERN.match(example_name)
|
194
|
+
placeholder_problems << "The #{format_name} format #{format.inspect} does not produce a valid GraphQL type name. " +
|
195
|
+
GRAPHQL_NAME_VALIDITY_DESCRIPTION
|
196
|
+
end
|
197
|
+
|
198
|
+
placeholder_problems
|
199
|
+
else
|
200
|
+
suggestions = FORMAT_SUGGESTER.correct(format_name).map(&:inspect)
|
201
|
+
["Unknown format name: #{format_name.inspect}. Possible alternatives: #{suggestions.join(", ")}."]
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def validate_name_overrides(name_overrides)
|
206
|
+
duplicate_problems = name_overrides
|
207
|
+
.group_by { |k, v| v }
|
208
|
+
.transform_values { |kv_pairs| kv_pairs.map(&:first) }
|
209
|
+
.select { |_, v| v.size > 1 }
|
210
|
+
.map do |override, source_names|
|
211
|
+
"Multiple names (#{source_names.sort.join(", ")}) map to the same override: #{override}, which is not supported."
|
212
|
+
end
|
213
|
+
|
214
|
+
invalid_name_problems = name_overrides.filter_map do |source_name, override|
|
215
|
+
unless GRAPHQL_NAME_PATTERN.match(override)
|
216
|
+
"`#{override}` (the override for `#{source_name}`) is not a valid GraphQL type name. " +
|
217
|
+
GRAPHQL_NAME_VALIDITY_DESCRIPTION
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
cant_override_problems = TYPES_THAT_CANNOT_BE_OVERRIDDEN.intersection(name_overrides.keys).map do |type_name|
|
222
|
+
"`#{type_name}` cannot be overridden because it is part of the GraphQL spec."
|
223
|
+
end
|
224
|
+
|
225
|
+
notify_problems(duplicate_problems + invalid_name_problems + cant_override_problems, "Provided type name overrides")
|
226
|
+
end
|
227
|
+
|
228
|
+
def notify_problems(problems, source_description)
|
229
|
+
return if problems.empty?
|
230
|
+
|
231
|
+
raise ConfigError, "#{source_description} have #{problems.size} problem(s):\n\n" \
|
232
|
+
"#{problems.map.with_index(1) { |problem, i| "#{i}. #{problem}" }.join("\n\n")}"
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
@@ -0,0 +1,353 @@
|
|
1
|
+
# Copyright 2024 Block, Inc.
|
2
|
+
#
|
3
|
+
# Use of this source code is governed by an MIT-style
|
4
|
+
# license that can be found in the LICENSE file or at
|
5
|
+
# https://opensource.org/licenses/MIT.
|
6
|
+
#
|
7
|
+
# frozen_string_literal: true
|
8
|
+
|
9
|
+
require "elastic_graph/error"
|
10
|
+
require "elastic_graph/schema_artifacts/runtime_metadata/schema_element_names"
|
11
|
+
require "elastic_graph/schema_definition/mixins/verifies_graphql_name"
|
12
|
+
require "elastic_graph/schema_definition/schema_elements/type_namer"
|
13
|
+
require "elastic_graph/support/memoizable_data"
|
14
|
+
require "forwardable"
|
15
|
+
|
16
|
+
module ElasticGraph
|
17
|
+
module SchemaDefinition
|
18
|
+
module SchemaElements
|
19
|
+
# Represents a reference to a type. This is basically just a name of a type,
|
20
|
+
# with the ability to resolve it to an actual type object on demand. In addition,
|
21
|
+
# we provide some useful logic that is based entirely on the type name.
|
22
|
+
#
|
23
|
+
# This is necessary because GraphQL does not require that types are defined
|
24
|
+
# before they are referenced. (And also you can have circular type dependencies).
|
25
|
+
# Therefore, we need to use a reference to a type initially, and can later resolve
|
26
|
+
# it to a concrete type object as needed.
|
27
|
+
#
|
28
|
+
# @private
|
29
|
+
class TypeReference < Support::MemoizableData.define(:name, :schema_def_state)
|
30
|
+
extend Forwardable
|
31
|
+
# @dynamic type_namer
|
32
|
+
def_delegator :schema_def_state, :type_namer
|
33
|
+
|
34
|
+
# Extracts the type without any non-null or list wrappings it has.
|
35
|
+
def fully_unwrapped
|
36
|
+
schema_def_state.type_ref(unwrapped_name)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Removes any non-null wrappings the type has.
|
40
|
+
def unwrap_non_null
|
41
|
+
schema_def_state.type_ref(name.delete_suffix("!"))
|
42
|
+
end
|
43
|
+
|
44
|
+
def wrap_non_null
|
45
|
+
return self if non_null?
|
46
|
+
schema_def_state.type_ref("#{name}!")
|
47
|
+
end
|
48
|
+
|
49
|
+
# Removes the list wrapping if this is a list.
|
50
|
+
#
|
51
|
+
# If the outer wrapping is non-null, unwraps that as well.
|
52
|
+
def unwrap_list
|
53
|
+
schema_def_state.type_ref(unwrap_non_null.name.delete_prefix("[").delete_suffix("]"))
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns the `ObjectType`, `UnionType` or `InterfaceType` object to which this
|
57
|
+
# type name refers, if it is the name of one of those kinds of types.
|
58
|
+
#
|
59
|
+
# Ignores any non-null wrapping on the type, if there is one.
|
60
|
+
def as_object_type
|
61
|
+
type = _ = unwrap_non_null.resolved
|
62
|
+
type if type.respond_to?(:graphql_fields_by_name)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns `true` if this is known to be an object type of some sort (including interface types,
|
66
|
+
# union types, and proper object types).
|
67
|
+
#
|
68
|
+
# Returns `false` if this is known to be a leaf type of some sort (either a scalar or enum).
|
69
|
+
# Returns `false` if this is a list type (either a list of objects or leafs).
|
70
|
+
#
|
71
|
+
# Raises an error if it cannot be determined either from the name or by resolving the type.
|
72
|
+
#
|
73
|
+
# Ignores any non-null wrapping on the type, if there is one.
|
74
|
+
def object?
|
75
|
+
return unwrap_non_null.object? if non_null?
|
76
|
+
|
77
|
+
if (resolved_type = resolved)
|
78
|
+
return resolved_type.respond_to?(:graphql_fields_by_name)
|
79
|
+
end
|
80
|
+
|
81
|
+
# For derived GraphQL types, the name usually implies what kind of type it is.
|
82
|
+
# The derived types get generated last, so this prediate may be called before the
|
83
|
+
# type has been defined.
|
84
|
+
case schema_kind_implied_by_name
|
85
|
+
when :object
|
86
|
+
true
|
87
|
+
when :enum
|
88
|
+
false
|
89
|
+
else
|
90
|
+
# If we can't determine the type from the name, just raise an error.
|
91
|
+
raise SchemaError, "Type `#{name}` cannot be resolved. Is it misspelled?"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def enum?
|
96
|
+
return unwrap_non_null.enum? if non_null?
|
97
|
+
|
98
|
+
if (resolved_type = resolved)
|
99
|
+
return resolved_type.is_a?(EnumType)
|
100
|
+
end
|
101
|
+
|
102
|
+
# For derived GraphQL types, the name usually implies what kind of type it is.
|
103
|
+
# The derived types get generated last, so this prediate may be called before the
|
104
|
+
# type has been defined.
|
105
|
+
case schema_kind_implied_by_name
|
106
|
+
when :object
|
107
|
+
false
|
108
|
+
when :enum
|
109
|
+
true
|
110
|
+
else
|
111
|
+
# If we can't determine the type from the name, just raise an error.
|
112
|
+
raise SchemaError, "Type `#{name}` cannot be resolved. Is it misspelled?"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns `true` if this is known to be a scalar type or enum type.
|
117
|
+
# Returns `false` if this is known to be an object type or list type of any sort.
|
118
|
+
#
|
119
|
+
# Raises an error if it cannot be determined either from the name or by resolving the type.
|
120
|
+
#
|
121
|
+
# Ignores any non-null wrapping on the type, if there is one.
|
122
|
+
def leaf?
|
123
|
+
!list? && !object?
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns `true` if this is a list type.
|
127
|
+
#
|
128
|
+
# Ignores any non-null wrapping on the type, if there is one.
|
129
|
+
def list?
|
130
|
+
name.start_with?("[")
|
131
|
+
end
|
132
|
+
|
133
|
+
# Returns `true` if this is a non-null type.
|
134
|
+
def non_null?
|
135
|
+
name.end_with?("!")
|
136
|
+
end
|
137
|
+
|
138
|
+
def boolean?
|
139
|
+
name == "Boolean"
|
140
|
+
end
|
141
|
+
|
142
|
+
def to_s
|
143
|
+
name
|
144
|
+
end
|
145
|
+
|
146
|
+
def resolved
|
147
|
+
schema_def_state.types_by_name[name]
|
148
|
+
end
|
149
|
+
|
150
|
+
def unwrapped_name
|
151
|
+
name
|
152
|
+
.sub(/\A\[+/, "") # strip `[` characters from the start: https://rubular.com/r/tHVBBQkQUMMVVz
|
153
|
+
.sub(/[\]!]+\z/, "") # strip `]` and `!` characters from the end: https://rubular.com/r/pC8C0i7EpvHDbf
|
154
|
+
end
|
155
|
+
|
156
|
+
# Generally speaking, scalar types have `grouped_by` fields which are scalars of the same types,
|
157
|
+
# and object types have `grouped_by` fields which are special `[object_type]GroupedBy` types.
|
158
|
+
#
|
159
|
+
# ...except for some special cases (Date and DateTime), which this predicate detects.
|
160
|
+
def scalar_type_needing_grouped_by_object?
|
161
|
+
%w[Date DateTime].include?(type_namer.revert_override_for(name))
|
162
|
+
end
|
163
|
+
|
164
|
+
# Returns a new `TypeReference` with any type name overrides reverted (to provide the "original" type name).
|
165
|
+
def with_reverted_override
|
166
|
+
schema_def_state.type_ref(type_namer.revert_override_for(name))
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns all the JSON schema array/nullable layers of a type, from outermost to innermost.
|
170
|
+
# For example, [[Int]] will return [:nullable, :array, :nullable, :array, :nullable]
|
171
|
+
def json_schema_layers
|
172
|
+
@json_schema_layers ||= begin
|
173
|
+
layers, inner_type = peel_json_schema_layers_once
|
174
|
+
|
175
|
+
if layers.empty? || inner_type == self
|
176
|
+
layers
|
177
|
+
else
|
178
|
+
layers + inner_type.json_schema_layers
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Most of ElasticGraph's derived GraphQL types have a static suffix (e.g. the full type name
|
184
|
+
# is source_type + suffix). This is a map of all of these.
|
185
|
+
STATIC_FORMAT_NAME_BY_CATEGORY = TypeNamer::REQUIRED_PLACEHOLDERS.filter_map do |format_name, placeholders|
|
186
|
+
if placeholders == [:base]
|
187
|
+
as_snake_case = SchemaArtifacts::RuntimeMetadata::SchemaElementNamesDefinition::SnakeCaseConverter
|
188
|
+
.normalize_case(format_name.to_s)
|
189
|
+
.delete_prefix("_")
|
190
|
+
|
191
|
+
[as_snake_case.to_sym, format_name]
|
192
|
+
end
|
193
|
+
end.to_h
|
194
|
+
|
195
|
+
# Converts the TypeReference to its final form (i.e. the from that will be used in rendered schema artifacts).
|
196
|
+
# This handles multiple bits of type name customization based on the configured `type_name_overrides` and
|
197
|
+
# `derived_type_name_formats` settings (via the `TypeNamer`):
|
198
|
+
#
|
199
|
+
# - If the `as_input` is `true` and this is a reference to an enum type, converts to the `InputEnum` format.
|
200
|
+
# - If there is a configured name override that applies to this type, uses it.
|
201
|
+
def to_final_form(as_input: false)
|
202
|
+
unwrapped = fully_unwrapped
|
203
|
+
inner_name = type_namer.name_for(unwrapped.name)
|
204
|
+
|
205
|
+
if as_input && schema_def_state.type_ref(inner_name).enum?
|
206
|
+
inner_name = type_namer.name_for(
|
207
|
+
type_namer.generate_name_for(:InputEnum, base: inner_name)
|
208
|
+
)
|
209
|
+
end
|
210
|
+
|
211
|
+
renamed_with_same_wrappings(inner_name)
|
212
|
+
end
|
213
|
+
|
214
|
+
# Builds a `TypeReference` for a statically named derived type for the given `category.
|
215
|
+
#
|
216
|
+
# In addition, a dynamic method `as_[category]` is also provided (defined further below).
|
217
|
+
def as_static_derived_type(category)
|
218
|
+
renamed_with_same_wrappings(type_namer.generate_name_for(
|
219
|
+
STATIC_FORMAT_NAME_BY_CATEGORY.fetch(category),
|
220
|
+
base: fully_unwrapped.name
|
221
|
+
))
|
222
|
+
end
|
223
|
+
|
224
|
+
# Generates the type name used for a sub-aggregation. This type has `grouped_by`, `aggregated_values`,
|
225
|
+
# `count` and `sub_aggregations` sub-fields to expose the different bits of aggregation functionality.
|
226
|
+
#
|
227
|
+
# The type name is based both on the type reference name and on the set of `parent_doc_types`
|
228
|
+
# that exist above it. The `parent_doc_types` are used in the name because we plan to offer different sub-aggregations
|
229
|
+
# under it based on where it is in the document structure. A type which is `nested` at multiple levels in different
|
230
|
+
# document contexts needs separate types generated for each case so that we can offer the correct contextual
|
231
|
+
# sub-aggregations that can be offered for each case.
|
232
|
+
def as_sub_aggregation(parent_doc_types:)
|
233
|
+
renamed_with_same_wrappings(type_namer.generate_name_for(
|
234
|
+
:SubAggregation,
|
235
|
+
base: fully_unwrapped.name,
|
236
|
+
parent_types: parent_doc_types.join
|
237
|
+
))
|
238
|
+
end
|
239
|
+
|
240
|
+
# Generates the type name used for a `sub_aggregations` field. A `sub_aggregations` field is
|
241
|
+
# available alongside `grouped_by`, `count`, and `aggregated_values` on an aggregation or
|
242
|
+
# sub-aggregation node. This type is used in two situations:
|
243
|
+
#
|
244
|
+
# 1. It is used directly under `nodes`/`edges { node }` on an Aggregation or SubAggregation.
|
245
|
+
# It provides access to each of the sub-aggregations that are available in that context.
|
246
|
+
# 2. It is used underneath that `SubAggregations` object for single object fields which have
|
247
|
+
# fields under them that are sub-aggregatable.
|
248
|
+
#
|
249
|
+
# The fields (and types of those fields) used for one of these types is contextual based on
|
250
|
+
# what the parent doc types are (so that we can offer sub-aggregations of the parent doc types!)
|
251
|
+
# and the field path (for the 2nd case).
|
252
|
+
def as_aggregation_sub_aggregations(parent_doc_types: [fully_unwrapped.name], field_path: [])
|
253
|
+
field_part = field_path.map { |f| to_title_case(f.name) }.join
|
254
|
+
|
255
|
+
renamed_with_same_wrappings(type_namer.generate_name_for(
|
256
|
+
:SubAggregations,
|
257
|
+
parent_agg_type: parent_aggregation_type(parent_doc_types),
|
258
|
+
field_path: field_part
|
259
|
+
))
|
260
|
+
end
|
261
|
+
|
262
|
+
def as_parent_aggregation(parent_doc_types:)
|
263
|
+
schema_def_state.type_ref(parent_aggregation_type(parent_doc_types))
|
264
|
+
end
|
265
|
+
|
266
|
+
# Here we iterate over our mapping and generate dynamic methods for each category.
|
267
|
+
STATIC_FORMAT_NAME_BY_CATEGORY.keys.each do |category|
|
268
|
+
define_method(:"as_#{category}") do
|
269
|
+
# @type self: TypeReference
|
270
|
+
as_static_derived_type(category)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def list_filter_input?
|
275
|
+
matches_format_of?(:list_filter_input)
|
276
|
+
end
|
277
|
+
|
278
|
+
def list_element_filter_input?
|
279
|
+
matches_format_of?(:list_element_filter_input)
|
280
|
+
end
|
281
|
+
|
282
|
+
# These methods are defined dynamically above:
|
283
|
+
# @dynamic as_aggregated_values
|
284
|
+
# @dynamic as_grouped_by
|
285
|
+
# @dynamic as_aggregation
|
286
|
+
# @dynamic as_connection
|
287
|
+
# @dynamic as_edge
|
288
|
+
# @dynamic as_fields_list_filter_input
|
289
|
+
# @dynamic as_filter_input
|
290
|
+
# @dynamic as_input_enum
|
291
|
+
# @dynamic as_list_element_filter_input, list_element_filter_input?
|
292
|
+
# @dynamic as_list_filter_input, list_filter_input?
|
293
|
+
# @dynamic as_sort_order
|
294
|
+
|
295
|
+
private
|
296
|
+
|
297
|
+
def after_initialize
|
298
|
+
Mixins::VerifiesGraphQLName.verify_name!(unwrapped_name)
|
299
|
+
end
|
300
|
+
|
301
|
+
def peel_json_schema_layers_once
|
302
|
+
if list?
|
303
|
+
return [[:array], unwrap_list] if non_null?
|
304
|
+
return [[:nullable, :array], unwrap_list]
|
305
|
+
end
|
306
|
+
|
307
|
+
return [[], unwrap_non_null] if non_null?
|
308
|
+
[[:nullable], self]
|
309
|
+
end
|
310
|
+
|
311
|
+
def matches_format_of?(category)
|
312
|
+
format_name = STATIC_FORMAT_NAME_BY_CATEGORY.fetch(category)
|
313
|
+
type_namer.matches_format?(name, format_name)
|
314
|
+
end
|
315
|
+
|
316
|
+
def parent_aggregation_type(parent_doc_types)
|
317
|
+
__skip__ = case parent_doc_types
|
318
|
+
in [single_parent_type]
|
319
|
+
type_namer.generate_name_for(:Aggregation, base: single_parent_type)
|
320
|
+
in [*parent_types, last_parent_type]
|
321
|
+
type_namer.generate_name_for(:SubAggregation, parent_types: parent_types.join, base: last_parent_type)
|
322
|
+
else
|
323
|
+
raise SchemaError, "Unexpected `parent_doc_types`: #{parent_doc_types.inspect}. `parent_doc_types` must not be empty."
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def renamed_with_same_wrappings(new_name)
|
328
|
+
pre_wrappings, post_wrappings = name.split(GRAPHQL_NAME_WITHIN_LARGER_STRING_PATTERN)
|
329
|
+
schema_def_state.type_ref("#{pre_wrappings}#{new_name}#{post_wrappings}")
|
330
|
+
end
|
331
|
+
|
332
|
+
ENUM_FORMATS = TypeNamer::DEFINITE_ENUM_FORMATS
|
333
|
+
OBJECT_FORMATS = TypeNamer::DEFINITE_OBJECT_FORMATS
|
334
|
+
|
335
|
+
def schema_kind_implied_by_name
|
336
|
+
name = type_namer.revert_override_for(self.name)
|
337
|
+
return :enum if ENUM_FORMATS.any? { |f| type_namer.matches_format?(name, f) }
|
338
|
+
return :object if OBJECT_FORMATS.any? { |f| type_namer.matches_format?(name, f) }
|
339
|
+
|
340
|
+
if (as_output_enum_name = type_namer.extract_base_from(name, format: :InputEnum))
|
341
|
+
:enum if ENUM_FORMATS.any? { |f| type_namer.matches_format?(as_output_enum_name, f) }
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
def to_title_case(name)
|
346
|
+
CamelCaseConverter.normalize_case(name).sub(/\A(\w)/, &:upcase)
|
347
|
+
end
|
348
|
+
|
349
|
+
CamelCaseConverter = SchemaArtifacts::RuntimeMetadata::SchemaElementNamesDefinition::CamelCaseConverter
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|