graph_weaver 0.2.2 → 0.4.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 +4 -4
- data/CHANGELOG.md +65 -0
- data/CLAUDE.md +70 -0
- data/Gemfile.lock +21 -21
- data/README.md +2 -0
- data/docs/errors.md +25 -5
- data/docs/federation.md +100 -0
- data/docs/generated_modules.md +7 -4
- data/docs/getting_started.md +52 -0
- data/docs/scalars.md +15 -2
- data/lib/graph_weaver/client.rb +4 -3
- data/lib/graph_weaver/codegen/emit.rb +46 -10
- data/lib/graph_weaver/codegen/enum_type.rb +5 -5
- data/lib/graph_weaver/codegen/nodes.rb +21 -1
- data/lib/graph_weaver/codegen.rb +201 -13
- data/lib/graph_weaver/errors.rb +145 -19
- data/lib/graph_weaver/hints.rb +6 -1
- data/lib/graph_weaver/input_struct.rb +8 -0
- data/lib/graph_weaver/response.rb +2 -2
- data/lib/graph_weaver/version.rb +1 -1
- data/lib/graph_weaver.rb +109 -41
- metadata +3 -1
|
@@ -90,19 +90,19 @@ class GraphWeaver::Codegen
|
|
|
90
90
|
# Attach app-owned helper modules to every struct generated from a
|
|
91
91
|
# GraphQL type — the logic stays in your code, generation wires it in:
|
|
92
92
|
#
|
|
93
|
-
# GraphWeaver.
|
|
93
|
+
# GraphWeaver.extend_type("Pet", PetHelpers)
|
|
94
94
|
#
|
|
95
95
|
# Or build the mixin inline — the block is module_eval'd into a fresh
|
|
96
96
|
# module auto-named GraphWeaver::TypeHelpers::<Type>. Handy for quick
|
|
97
97
|
# decoration; srb tc can't see into block-defined methods, so prefer
|
|
98
98
|
# a named module where static checking matters:
|
|
99
99
|
#
|
|
100
|
-
# GraphWeaver.
|
|
100
|
+
# GraphWeaver.extend_type("Pet") do
|
|
101
101
|
# def display_name = "#{name} the pet"
|
|
102
102
|
# end
|
|
103
103
|
#
|
|
104
104
|
# Additive: repeated registrations (and client-scoped ones) stack.
|
|
105
|
-
def
|
|
105
|
+
def extend_type(graphql_name, *mixins, requires: nil, &block)
|
|
106
106
|
entry = type_registry[graphql_name.to_s] ||= { mixins: [], requires: [] }
|
|
107
107
|
add_type_helpers(entry, graphql_name, mixins, requires, block)
|
|
108
108
|
end
|
|
@@ -111,7 +111,7 @@ class GraphWeaver::Codegen
|
|
|
111
111
|
@type_registry ||= {}
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
-
# shared with Client#
|
|
114
|
+
# shared with Client#extend_type: build/validate the mixins and
|
|
115
115
|
# append them to a registry entry
|
|
116
116
|
def add_type_helpers(entry, graphql_name, mixins, requires, block)
|
|
117
117
|
mixins = mixins.dup
|
|
@@ -143,7 +143,7 @@ class GraphWeaver::Codegen
|
|
|
143
143
|
end
|
|
144
144
|
|
|
145
145
|
module GraphWeaver
|
|
146
|
-
# Home of block-built type helpers (
|
|
146
|
+
# Home of block-built type helpers (extend_type with a block), which
|
|
147
147
|
# need constant names so generated files can reference them.
|
|
148
148
|
module TypeHelpers; end
|
|
149
149
|
end
|
|
@@ -147,7 +147,7 @@ class GraphWeaver::Codegen
|
|
|
147
147
|
|
|
148
148
|
attr_reader :class_name, :fields
|
|
149
149
|
# the GraphQL type this struct was generated from, and any registered
|
|
150
|
-
# helper modules to include (see Codegen.
|
|
150
|
+
# helper modules to include (see Codegen.extend_type)
|
|
151
151
|
attr_accessor :graphql_type, :mixins
|
|
152
152
|
|
|
153
153
|
def initialize(class_name)
|
|
@@ -277,6 +277,26 @@ class GraphWeaver::Codegen
|
|
|
277
277
|
def nested = self
|
|
278
278
|
end
|
|
279
279
|
|
|
280
|
+
# A reference to a union hoisted into the shared unions module (a named
|
|
281
|
+
# shared fragment spread as a whole union field): the query references
|
|
282
|
+
# <Name>::Type and dispatches through <Name>.from_h, where <Name> is the
|
|
283
|
+
# alias the query module gives GraphQLUnions::<Name>. The type family lives
|
|
284
|
+
# once in the shared module, so the same union across queries is one Ruby
|
|
285
|
+
# type — nested is nil, nothing is emitted here.
|
|
286
|
+
class UnionRefNode < Node
|
|
287
|
+
attr_reader :class_name
|
|
288
|
+
|
|
289
|
+
def initialize(class_name)
|
|
290
|
+
@class_name = class_name
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def bare_type = "#{class_name}::Type"
|
|
294
|
+
|
|
295
|
+
def cast(expr, _depth)
|
|
296
|
+
"#{class_name}.from_h(#{expr})"
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
280
300
|
# An input-object variable: emitted as a module-level T::Struct whose
|
|
281
301
|
# serialize produces the wire hash. Inputs never cast FROM the wire.
|
|
282
302
|
# Joins the coerce protocol so execute kwargs accept plain hashes,
|
data/lib/graph_weaver/codegen.rb
CHANGED
|
@@ -49,8 +49,12 @@ class GraphWeaver::Codegen
|
|
|
49
49
|
# mixin modules, each keyed by GraphQL name). inputs_namespace: is the
|
|
50
50
|
# shared-inputs workflow (see GraphWeaver.generate!): variable types
|
|
51
51
|
# live once in that module and the query module aliases what it uses.
|
|
52
|
+
# unions_namespace:/hoistable_unions: are the parallel shared-unions
|
|
53
|
+
# workflow — a whole-union field spread as a named shared fragment resolves
|
|
54
|
+
# to one canonical type in that module (see used_union_names).
|
|
52
55
|
def initialize(schema:, query:, module_name: nil, client: nil, default_module_name: nil,
|
|
53
|
-
scalars: nil, enums: nil, types: nil, inputs_namespace: nil
|
|
56
|
+
scalars: nil, enums: nil, types: nil, inputs_namespace: nil, unions_namespace: nil,
|
|
57
|
+
hoistable_unions: nil)
|
|
54
58
|
@schema = schema
|
|
55
59
|
@query = query.strip
|
|
56
60
|
@module_name = module_name
|
|
@@ -59,6 +63,12 @@ class GraphWeaver::Codegen
|
|
|
59
63
|
@enums = enums || {}
|
|
60
64
|
@types = types || {}
|
|
61
65
|
@inputs_namespace = inputs_namespace
|
|
66
|
+
# the shared-unions workflow: unions_namespace names the module hoisted
|
|
67
|
+
# unions live in; hoistable_unions is the set of shared fragment names this
|
|
68
|
+
# query may hoist (spreads it inlined, minus any it shadows locally)
|
|
69
|
+
@unions_namespace = unions_namespace
|
|
70
|
+
@hoistable_unions = hoistable_unions || []
|
|
71
|
+
@used_unions = []
|
|
62
72
|
@client_const = self.class.client_const(client)
|
|
63
73
|
|
|
64
74
|
if client && @client_const.nil?
|
|
@@ -111,6 +121,11 @@ class GraphWeaver::Codegen
|
|
|
111
121
|
{ inputs: @variable_inputs.keys, enums: @variable_enums.keys, mapped: @mapped_enums.keys }
|
|
112
122
|
end
|
|
113
123
|
|
|
124
|
+
# The shared union fragments this query hoisted, by name — the generate!
|
|
125
|
+
# workflow unions these across queries to decide what the shared unions
|
|
126
|
+
# module must contain.
|
|
127
|
+
def used_union_names = @used_unions.dup
|
|
128
|
+
|
|
114
129
|
# The shared inputs artifact: the named input/enum types — plus
|
|
115
130
|
# everything they transitively reference — emitted once per schema as
|
|
116
131
|
# a manifest (inputs.rb) plus one file per type under inputs/, so a
|
|
@@ -138,6 +153,36 @@ class GraphWeaver::Codegen
|
|
|
138
153
|
emit_inputs_files
|
|
139
154
|
end
|
|
140
155
|
|
|
156
|
+
# The shared unions artifact: each named shared fragment a query hoisted,
|
|
157
|
+
# built once against the schema as <module_name>::<Name>, so the same union
|
|
158
|
+
# across queries resolves to one Ruby type family. `fragments` is the loaded
|
|
159
|
+
# shared-fragment table (nested spreads resolve through it); `names` the
|
|
160
|
+
# fragments to build. Returns { "unions.rb" => source }.
|
|
161
|
+
def self.generate_unions(schema:, module_name:, fragments:, names:,
|
|
162
|
+
scalars: nil, enums: nil, types: nil)
|
|
163
|
+
codegen = new(schema:, query: "", module_name:, scalars:, enums:, types:)
|
|
164
|
+
codegen.generate_unions(fragments, names)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def generate_unions(fragments, names)
|
|
168
|
+
unless @module_name&.match?(/\A[A-Z]\w*(::[A-Z]\w*)*\z/)
|
|
169
|
+
raise ArgumentError, "unions module name must be a constant name, got #{@module_name.inspect}"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
@requires = []
|
|
173
|
+
@mapped_enums = {}
|
|
174
|
+
# nested spreads inside a shared fragment resolve through the whole table
|
|
175
|
+
@fragments = fragments
|
|
176
|
+
|
|
177
|
+
unions = names.uniq.sort.map do |name|
|
|
178
|
+
fragment = fragments.fetch(name)
|
|
179
|
+
type = @schema.get_type(fragment.type.name)
|
|
180
|
+
UnionNode.new(camelize(name), union_members(type, fragment.selections))
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
emit_unions_file(unions)
|
|
184
|
+
end
|
|
185
|
+
|
|
141
186
|
VarDef = Struct.new(:kwarg, :wire, :node, :required)
|
|
142
187
|
|
|
143
188
|
# Names that cannot appear bare in generated Ruby: keywords aren't
|
|
@@ -168,6 +213,7 @@ class GraphWeaver::Codegen
|
|
|
168
213
|
@variable_enums = {}
|
|
169
214
|
@variable_inputs = {}
|
|
170
215
|
@mapped_enums = {}
|
|
216
|
+
@used_unions = []
|
|
171
217
|
# requires the generated file needs (custom scalars, enum mappings,
|
|
172
218
|
# type helpers all contribute)
|
|
173
219
|
@requires = []
|
|
@@ -214,13 +260,98 @@ class GraphWeaver::Codegen
|
|
|
214
260
|
# whose schema introspects lazily). Global registrations skip this:
|
|
215
261
|
# they may target a different client's server.
|
|
216
262
|
def self.validate_registration!(schema, kind, name)
|
|
263
|
+
# register_scalar("Type.field", ...) overrides one field's scalar — validate
|
|
264
|
+
# the field exists and is a scalar, not that a type named "Type.field" exists.
|
|
265
|
+
if kind == "scalar" && name.include?(".")
|
|
266
|
+
return if scalar_field?(schema, name)
|
|
267
|
+
|
|
268
|
+
raise GraphWeaver::Error, "register_scalar(#{name.inspect}) matches no scalar field in this schema"
|
|
269
|
+
end
|
|
270
|
+
|
|
217
271
|
return if schema.get_type(name)
|
|
218
272
|
|
|
219
273
|
suggestion = defined?(DidYouMean::SpellChecker) &&
|
|
220
274
|
DidYouMean::SpellChecker.new(dictionary: schema.types.keys).correct(name).first
|
|
221
275
|
hint = suggestion ? " — did you mean '#{suggestion}'?" : ""
|
|
222
|
-
|
|
276
|
+
# the type registry is reached via extend_type; scalars/enums via register_*
|
|
277
|
+
method = kind == "type" ? "extend_type" : "register_#{kind}"
|
|
278
|
+
raise GraphWeaver::Error, "#{method}(#{name.inspect}) matches no type in this schema#{hint}"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Whether `coordinate` ("Type.field") names an existing scalar field — the
|
|
282
|
+
# validation for a per-field register_scalar override.
|
|
283
|
+
def self.scalar_field?(schema, coordinate)
|
|
284
|
+
type_name, field_name = coordinate.split(".", 2)
|
|
285
|
+
return false unless field_name
|
|
286
|
+
|
|
287
|
+
field = schema.get_field(type_name, field_name)
|
|
288
|
+
!!field && field.type.unwrap.kind.name == "SCALAR"
|
|
289
|
+
rescue StandardError
|
|
290
|
+
false
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Parse every fragment file under `paths` into one { name => FragmentDefinition }
|
|
294
|
+
# map — reusable fragments a query can spread. Fragment files hold only
|
|
295
|
+
# fragments (no operations); names are unique across them.
|
|
296
|
+
def self.load_fragments(paths)
|
|
297
|
+
Array(paths).flat_map { |dir| Dir[File.join(dir, "*.graphql")].sort }.each_with_object({}) do |file, out|
|
|
298
|
+
doc = GraphQL.parse(File.read(file))
|
|
299
|
+
if doc.definitions.grep(GraphQL::Language::Nodes::OperationDefinition).any?
|
|
300
|
+
raise GraphWeaver::Error, "#{file}: fragment files define only fragments, no operations"
|
|
301
|
+
end
|
|
302
|
+
doc.definitions.grep(GraphQL::Language::Nodes::FragmentDefinition).each do |frag|
|
|
303
|
+
raise GraphWeaver::Error, "duplicate shared fragment '#{frag.name}' (#{file})" if out.key?(frag.name)
|
|
304
|
+
out[frag.name] = frag
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# The shared fragments a query spreads (transitively), excluding any it
|
|
310
|
+
# shadows with a local definition of the same name — the names
|
|
311
|
+
# inline_fragments appends, and the set the generate! workflow may hoist
|
|
312
|
+
# when they sit on a whole-union field.
|
|
313
|
+
def self.shared_fragment_spreads(query, shared)
|
|
314
|
+
return [] if shared.empty?
|
|
315
|
+
|
|
316
|
+
doc = GraphQL.parse(query)
|
|
317
|
+
local = doc.definitions.grep(GraphQL::Language::Nodes::FragmentDefinition).map(&:name)
|
|
318
|
+
reachable_fragments(fragment_spreads(doc.definitions), shared, local)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Append the shared fragments a query spreads (transitively) to its source, so
|
|
322
|
+
# the sent query is self-contained. Unused shared fragments are left out.
|
|
323
|
+
def self.inline_fragments(query, shared)
|
|
324
|
+
used = shared_fragment_spreads(query, shared)
|
|
325
|
+
return query if used.empty?
|
|
326
|
+
|
|
327
|
+
"#{query.rstrip}\n\n#{used.sort.map { |name| shared.fetch(name).to_query_string }.join("\n\n")}\n"
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# BFS over spreads, following shared fragments into their own spreads; a
|
|
331
|
+
# locally-defined or unknown spread is left for schema validation to judge.
|
|
332
|
+
def self.reachable_fragments(spreads, shared, local)
|
|
333
|
+
needed = []
|
|
334
|
+
queue = spreads.dup
|
|
335
|
+
until queue.empty?
|
|
336
|
+
name = queue.shift
|
|
337
|
+
next if needed.include?(name) || local.include?(name) || !shared.key?(name)
|
|
338
|
+
|
|
339
|
+
needed << name
|
|
340
|
+
queue.concat(fragment_spreads([shared.fetch(name)]))
|
|
341
|
+
end
|
|
342
|
+
needed
|
|
223
343
|
end
|
|
344
|
+
private_class_method :reachable_fragments
|
|
345
|
+
|
|
346
|
+
# Names of every fragment spread reachable in these AST nodes.
|
|
347
|
+
def self.fragment_spreads(nodes, acc = [])
|
|
348
|
+
nodes.each do |node|
|
|
349
|
+
acc << node.name if node.is_a?(GraphQL::Language::Nodes::FragmentSpread)
|
|
350
|
+
fragment_spreads(node.selections, acc) if node.respond_to?(:selections)
|
|
351
|
+
end
|
|
352
|
+
acc
|
|
353
|
+
end
|
|
354
|
+
private_class_method :fragment_spreads
|
|
224
355
|
|
|
225
356
|
# Structured shape for a schema-validation error: message plus its first
|
|
226
357
|
# source location, so ValidationError#errors is inspectable.
|
|
@@ -249,6 +380,10 @@ class GraphWeaver::Codegen
|
|
|
249
380
|
node.graphql_type = type.graphql_name
|
|
250
381
|
node.mixins = type_mixins(type.graphql_name)
|
|
251
382
|
taken = [class_name]
|
|
383
|
+
# Dedup structurally-identical dispatch-union fields on this struct: the
|
|
384
|
+
# same union selected two ways (unblockOptions vs selectedOption) shares
|
|
385
|
+
# one Ruby type, so consumers get one exhaustive `case ... T.absurd`.
|
|
386
|
+
union_cache = {}
|
|
252
387
|
|
|
253
388
|
gather(type, selections).each do |key, field_nodes|
|
|
254
389
|
field_name = field_nodes.first.name
|
|
@@ -289,9 +424,20 @@ class GraphWeaver::Codegen
|
|
|
289
424
|
|
|
290
425
|
name = pick_name(member.graphql_name, key, taken)
|
|
291
426
|
nilable_type_ref(field_type) { NarrowedNode.new(object_node(member, sub_selections, name)) }
|
|
427
|
+
elsif @unions_namespace && (frag = lone_shared_spread(sub_selections)) &&
|
|
428
|
+
@hoistable_unions.include?(frag)
|
|
429
|
+
# a whole-union field spread as a named shared fragment: hoist to
|
|
430
|
+
# the shared unions module so the same union across queries is one
|
|
431
|
+
# Ruby type family (one exhaustive `case ... T.absurd`).
|
|
432
|
+
@used_unions << frag unless @used_unions.include?(frag)
|
|
433
|
+
ref = UnionRefNode.new(camelize(frag))
|
|
434
|
+
type_ref(field_type) { ref }
|
|
292
435
|
else
|
|
293
|
-
|
|
294
|
-
|
|
436
|
+
members = union_members(core, sub_selections)
|
|
437
|
+
# reuse an identical sibling union (pick_name/name only on a miss)
|
|
438
|
+
union = (union_cache[union_signature(members)] ||=
|
|
439
|
+
UnionNode.new(pick_name(core.graphql_name, key, taken), members))
|
|
440
|
+
type_ref(field_type) { union }
|
|
295
441
|
end
|
|
296
442
|
when "ENUM"
|
|
297
443
|
if (mapped = mapped_enum_node(core))
|
|
@@ -303,7 +449,8 @@ class GraphWeaver::Codegen
|
|
|
303
449
|
type_ref(field_type) { EnumNode.new(name, core.values.keys.sort) }
|
|
304
450
|
end
|
|
305
451
|
when "SCALAR"
|
|
306
|
-
|
|
452
|
+
coordinate = "#{type.graphql_name}.#{field_name}"
|
|
453
|
+
type_ref(field_type) { scalar_node(core.graphql_name, coordinate) }
|
|
307
454
|
else
|
|
308
455
|
raise GraphWeaver::Error, "unsupported kind: #{core.kind.name}"
|
|
309
456
|
end
|
|
@@ -339,6 +486,17 @@ class GraphWeaver::Codegen
|
|
|
339
486
|
selections.grep(GraphQL::Language::Nodes::Field).map { |field| field.alias || field.name }
|
|
340
487
|
end
|
|
341
488
|
|
|
489
|
+
# The fragment name when a selection is exactly one bare fragment spread
|
|
490
|
+
# (`{ ...F }`) — the shape a union field must have to hoist into the shared
|
|
491
|
+
# unions module. A spread carrying directives (@skip/@include), or mixed with
|
|
492
|
+
# other fields, stays a locally-emitted union.
|
|
493
|
+
def lone_shared_spread(selections)
|
|
494
|
+
return unless selections.size == 1
|
|
495
|
+
|
|
496
|
+
spread = selections.first
|
|
497
|
+
spread.name if spread.is_a?(GraphQL::Language::Nodes::FragmentSpread) && spread.directives.empty?
|
|
498
|
+
end
|
|
499
|
+
|
|
342
500
|
# does the flattened selection (as seen by member) include at least one
|
|
343
501
|
# field guaranteed to be present in a matching response?
|
|
344
502
|
def unconditional_field?(member, selections)
|
|
@@ -365,19 +523,45 @@ class GraphWeaver::Codegen
|
|
|
365
523
|
# concrete type: one member struct per possible type; wire dispatch
|
|
366
524
|
# reads __typename, so the query must select it. For interfaces, the
|
|
367
525
|
# interface's own field selections gather into every member.
|
|
368
|
-
|
|
526
|
+
# The union's member structs (graphql type name => ObjectNode), sorted for
|
|
527
|
+
# deterministic output. Dispatch reads __typename, so the query must select
|
|
528
|
+
# it; for interfaces the interface-level fields gather into every member.
|
|
529
|
+
def union_members(type, selections)
|
|
369
530
|
unless gather(type, selections).key?("__typename")
|
|
370
531
|
raise ArgumentError,
|
|
371
|
-
"select __typename on #{type.graphql_name} so
|
|
532
|
+
"select __typename on #{type.graphql_name} so the union can dispatch — " \
|
|
372
533
|
"or narrow to a single `... on Type` condition (no dispatch needed)"
|
|
373
534
|
end
|
|
374
535
|
|
|
375
|
-
|
|
376
|
-
members = @schema.possible_types(type).sort_by(&:graphql_name).to_h do |possible|
|
|
536
|
+
@schema.possible_types(type).sort_by(&:graphql_name).to_h do |possible|
|
|
377
537
|
[possible.graphql_name, object_node(possible, selections, camelize(possible.graphql_name))]
|
|
378
538
|
end
|
|
539
|
+
end
|
|
379
540
|
|
|
380
|
-
|
|
541
|
+
# A name-independent structural fingerprint of a union's members, so two
|
|
542
|
+
# occurrences that generate identical structs collapse to one Ruby type.
|
|
543
|
+
def union_signature(members)
|
|
544
|
+
members.map { |gname, member| "#{gname}=#{signature(member)}" }.sort.join(",")
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
# Structural signature of a node — ignores the generated class name (which
|
|
548
|
+
# varies per occurrence), keying on GraphQL type, selection shape, and
|
|
549
|
+
# nullability so only genuinely-identical shapes collapse.
|
|
550
|
+
def signature(node)
|
|
551
|
+
case node
|
|
552
|
+
when NonNull then "!#{signature(node.of)}"
|
|
553
|
+
when List then "[#{signature(node.of)}]"
|
|
554
|
+
when NarrowedNode then "?#{signature(node.nested)}"
|
|
555
|
+
when Scalar then "s:#{node.bare_type}"
|
|
556
|
+
when EnumNode then "e:#{node.values.sort.join("|")}"
|
|
557
|
+
when MappedEnum then "m:#{node.graphql_name}"
|
|
558
|
+
when ObjectNode
|
|
559
|
+
inner = node.fields.map { |f| "#{f.prop}=#{signature(f.node)}" }.sort.join(",")
|
|
560
|
+
"o:#{node.graphql_type}(#{inner})"
|
|
561
|
+
when UnionNode then "u:(#{union_signature(node.members)})"
|
|
562
|
+
when UnionRefNode then "ur:#{node.class_name}" # hoisted — identity is its shared name
|
|
563
|
+
else "x:#{node.object_id}" # unknown node kind — never collapse
|
|
564
|
+
end
|
|
381
565
|
end
|
|
382
566
|
|
|
383
567
|
# Build a node from an AST type reference (variable definitions), where
|
|
@@ -460,9 +644,13 @@ class GraphWeaver::Codegen
|
|
|
460
644
|
|
|
461
645
|
# A Scalar node, recording any requires its registered type needs so the
|
|
462
646
|
# generated file can require them (collected across the whole query).
|
|
463
|
-
# Resolution:
|
|
464
|
-
|
|
465
|
-
|
|
647
|
+
# Resolution, most specific first: a per-field override (`Type.field`), then
|
|
648
|
+
# the scalar-name registration — each checked client-scoped, then global.
|
|
649
|
+
def scalar_node(name, coordinate = nil)
|
|
650
|
+
scalar =
|
|
651
|
+
(coordinate && (@scalars[coordinate] || GraphWeaver::Codegen.scalar_registry[coordinate])) ||
|
|
652
|
+
@scalars[name.to_s] ||
|
|
653
|
+
GraphWeaver::Codegen.scalar(name)
|
|
466
654
|
@requires.concat(scalar.requires)
|
|
467
655
|
Scalar.new(scalar)
|
|
468
656
|
end
|