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