lighthouse-graphql 0.1.0 → 0.2.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 +168 -1
- data/README.md +46 -2
- data/docs/authorization.md +116 -57
- data/docs/best-practices.md +16 -3
- data/docs/caching.md +161 -0
- data/docs/configuration.md +83 -4
- data/docs/custom-directives.md +5 -1
- data/docs/directives.md +117 -38
- data/docs/getting-started.md +84 -0
- data/docs/mutations.md +260 -0
- data/docs/performance.md +138 -0
- data/docs/relationships.md +80 -33
- data/docs/resolvers.md +179 -0
- data/docs/validation.md +132 -0
- data/lib/generators/lighthouse/builder_generator.rb +30 -0
- data/lib/generators/lighthouse/class_generator.rb +61 -0
- data/lib/generators/lighthouse/install/install_generator.rb +127 -0
- data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +8 -0
- data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +57 -0
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +67 -0
- data/lib/generators/lighthouse/install/templates/schema.graphql.tt +19 -0
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -0
- data/lib/generators/lighthouse/mutation_generator.rb +30 -0
- data/lib/generators/lighthouse/query_generator.rb +29 -0
- data/lib/generators/lighthouse/validator_generator.rb +36 -0
- data/lib/lighthouse/cache_control.rb +126 -0
- data/lib/lighthouse/caching.rb +68 -0
- data/lib/lighthouse/config.rb +85 -19
- data/lib/lighthouse/contracts/arg_builder.rb +8 -12
- data/lib/lighthouse/contracts/arg_manipulator.rb +8 -12
- data/lib/lighthouse/contracts/directive.rb +10 -14
- data/lib/lighthouse/contracts/field_manipulator.rb +8 -12
- data/lib/lighthouse/contracts/field_middleware.rb +7 -11
- data/lib/lighthouse/contracts/field_resolver.rb +9 -13
- data/lib/lighthouse/dataloader_sources/association_loader.rb +28 -19
- data/lib/lighthouse/dataloader_sources/model_loader.rb +10 -14
- data/lib/lighthouse/dataloader_sources/relation_count_loader.rb +16 -0
- data/lib/lighthouse/dataloader_sources/relation_page_loader.rb +18 -0
- data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +57 -69
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +26 -57
- data/lib/lighthouse/directive_resolvers/association_resolvers.rb +161 -0
- data/lib/lighthouse/directive_resolvers/auth_resolver.rb +41 -38
- data/lib/lighthouse/directive_resolvers/base.rb +142 -86
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +58 -56
- data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +133 -0
- data/lib/lighthouse/directive_resolvers/can_resolvers.rb +177 -0
- data/lib/lighthouse/directive_resolvers/convention_resolver.rb +70 -0
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +42 -48
- data/lib/lighthouse/directive_resolvers/field_resolver.rb +27 -68
- data/lib/lighthouse/directive_resolvers/find_resolver.rb +42 -52
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +39 -45
- data/lib/lighthouse/directive_resolvers/guard_resolver.rb +16 -22
- data/lib/lighthouse/directive_resolvers/method_resolver.rb +19 -25
- data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +219 -0
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +70 -128
- data/lib/lighthouse/directive_resolvers/rename_resolver.rb +18 -24
- data/lib/lighthouse/directive_resolvers/resolution.rb +31 -27
- data/lib/lighthouse/directive_resolvers/validator_resolver.rb +193 -0
- data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +164 -163
- data/lib/lighthouse/directives/arguments/base.rb +23 -31
- data/lib/lighthouse/directives/arguments/filters.rb +113 -93
- data/lib/lighthouse/directives/arguments/order_by.rb +41 -41
- data/lib/lighthouse/directives/pagination_info_types.rb +69 -0
- data/lib/lighthouse/directives/pagination_manipulator.rb +176 -0
- data/lib/lighthouse/directives/registry.rb +210 -141
- data/lib/lighthouse/directives/where_conditions_directive.rb +236 -200
- data/lib/lighthouse/errors.rb +27 -0
- data/lib/lighthouse/federation_directives.rb +53 -0
- data/lib/lighthouse/field_resolver_stack.rb +56 -0
- data/lib/lighthouse/generated_resolvers.rb +82 -0
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/graphql_ruby_patches.rb +18 -0
- data/lib/lighthouse/ide_helper.rb +98 -0
- data/lib/lighthouse/namespaces.rb +21 -0
- data/lib/lighthouse/nested_mutations/belongs_to_operations.rb +50 -0
- data/lib/lighthouse/nested_mutations/executor.rb +149 -0
- data/lib/lighthouse/nested_mutations/has_operations.rb +66 -0
- data/lib/lighthouse/nested_mutations/many_to_many_operations.rb +44 -0
- data/lib/lighthouse/nested_mutations/partitioner.rb +125 -0
- data/lib/lighthouse/nested_mutations.rb +210 -0
- data/lib/lighthouse/pagination.rb +221 -0
- data/lib/lighthouse/persisted_queries.rb +167 -0
- data/lib/lighthouse/railtie.rb +26 -0
- data/lib/lighthouse/rb_lighthouse.rb +50 -29
- data/lib/lighthouse/reference_resolver.rb +13 -15
- data/lib/lighthouse/relation_batch.rb +156 -0
- data/lib/lighthouse/resolver_dispatch.rb +26 -0
- data/lib/lighthouse/schema_cache.rb +120 -0
- data/lib/lighthouse/schema_factory.rb +27 -37
- data/lib/lighthouse/schema_generator.rb +196 -239
- data/lib/lighthouse/schema_implementation.rb +51 -48
- data/lib/lighthouse/sdl_loader.rb +17 -20
- data/lib/lighthouse/support/arguments.rb +119 -0
- data/lib/lighthouse/support/authorization.rb +18 -19
- data/lib/lighthouse/support/batching.rb +16 -0
- data/lib/lighthouse/support/directive_args.rb +26 -24
- data/lib/lighthouse/support/model_resolver.rb +56 -56
- data/lib/lighthouse/support/naming.rb +55 -59
- data/lib/lighthouse/tasks/lighthouse.rake +33 -0
- data/lib/lighthouse/where_conditions/operator_map.rb +25 -29
- metadata +52 -64
- data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +0 -56
- data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +0 -47
- data/lib/lighthouse/directive_resolvers/can_resolver.rb +0 -86
- data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +0 -109
- data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +0 -43
- data/lib/lighthouse/directives/relation_directive.rb +0 -332
|
@@ -1,244 +1,201 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module Lighthouse
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
impl =
|
|
17
|
-
if defined?(::SchemaImplementation)
|
|
18
|
-
::SchemaImplementation.new
|
|
19
|
-
else
|
|
20
|
-
# Minimal fallback when SchemaImplementation isn't available.
|
|
21
|
-
# Keep the same dispatch semantics so directive resolvers work.
|
|
22
|
-
Class.new do
|
|
23
|
-
attr_accessor :schema
|
|
24
|
-
|
|
25
|
-
def call(type, field, obj, args, ctx)
|
|
26
|
-
field_name = field.respond_to?(:graphql_name) ? field.graphql_name.to_s : field.name.to_s
|
|
27
|
-
owner = schema && schema.types[type.respond_to?(:graphql_name) ? type.graphql_name : type.name.to_s]
|
|
28
|
-
|
|
29
|
-
if owner && owner.respond_to?("resolve_field_#{field_name}")
|
|
30
|
-
resolver = owner.method("resolve_field_#{field_name}")
|
|
31
|
-
param_count = resolver.parameters.length
|
|
32
|
-
return resolver.call(obj, args) if param_count == 2
|
|
33
|
-
|
|
34
|
-
return resolver.call(obj, args, ctx)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
return field.resolve_proc.call(obj, args, ctx) if field.respond_to?(:resolve_proc) && field.resolve_proc
|
|
38
|
-
|
|
39
|
-
obj.respond_to?(field_name) ? obj.public_send(field_name) : nil
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def resolve_type(_abstract_type, _obj, _ctx)
|
|
43
|
-
nil
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def coerce_input(_type, value, _ctx)
|
|
47
|
-
value
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def coerce_result(_type, value, _ctx)
|
|
51
|
-
value
|
|
52
|
-
end
|
|
53
|
-
end.new
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
final_sdl = sdl_string
|
|
57
|
-
base_types = opts.delete(:base_types) || {}
|
|
58
|
-
using = opts.delete(:using) || {}
|
|
59
|
-
schema_class = opts.delete(:schema_class) || ::GraphQL::Schema
|
|
60
|
-
|
|
61
|
-
# Build using the provided base class so federation / custom schema behavior
|
|
62
|
-
# is preserved through class hierarchy.
|
|
63
|
-
schema = schema_class.from_definition(final_sdl, default_resolve: impl, base_types: base_types, using: using)
|
|
64
|
-
|
|
65
|
-
# Make to_definition delegate to federation_sdl when available.
|
|
66
|
-
if schema.respond_to?(:federation_sdl)
|
|
67
|
-
schema.define_singleton_method(:to_definition) do
|
|
68
|
-
federation_sdl
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
# GraphQL-Ruby caches field resolution procs during schema build; rather
|
|
72
|
-
# than trying to override per-field `resolve_<field>` methods post-hoc,
|
|
73
|
-
# we install a single dispatcher here that prefers our generated
|
|
74
|
-
# `resolve_field_<name>` singletons.
|
|
75
|
-
if schema.respond_to?(:definition_default_resolve=) && schema.respond_to?(:definition_default_resolve)
|
|
76
|
-
previous_default = schema.definition_default_resolve
|
|
77
|
-
schema.definition_default_resolve = proc do |type, field, obj, arguments, ctx|
|
|
78
|
-
owner =
|
|
79
|
-
if type.respond_to?(:graphql_name)
|
|
80
|
-
schema.types[type.graphql_name]
|
|
81
|
-
elsif type.respond_to?(:name)
|
|
82
|
-
schema.types[type.name.to_s]
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
field_name = field.respond_to?(:graphql_name) ? field.graphql_name.to_s : field.name.to_s
|
|
86
|
-
|
|
87
|
-
if owner && owner.respond_to?("resolve_field_#{field_name}")
|
|
88
|
-
resolver = owner.method("resolve_field_#{field_name}")
|
|
89
|
-
source_file, = resolver.source_location
|
|
90
|
-
# GraphQL-Ruby's SDL builder also defines `resolve_field_*` methods
|
|
91
|
-
# internally. Only invoke ours (defined under `lib/lighthouse/`),
|
|
92
|
-
# otherwise we may call an internal resolver with the wrong arity.
|
|
93
|
-
if source_file && source_file.include?('/lib/lighthouse/')
|
|
94
|
-
param_count = resolver.parameters.length
|
|
95
|
-
next resolver.call(obj, arguments) if param_count == 2
|
|
96
|
-
|
|
97
|
-
next resolver.call(obj, arguments, ctx)
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
previous_default.call(type, field, obj, arguments, ctx)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Give the implementation object access to the built schema so its
|
|
106
|
-
# runtime dispatcher can look up generated singleton resolvers.
|
|
107
|
-
begin
|
|
108
|
-
impl.schema = schema if impl.respond_to?(:schema=)
|
|
109
|
-
rescue StandardError
|
|
110
|
-
# ignore
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
post_process(schema, impl, **opts)
|
|
114
|
-
|
|
115
|
-
schema
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def post_process(schema, impl, **_opts)
|
|
119
|
-
# Inspect types and fields for directives and attach generated resolvers
|
|
120
|
-
schema.types.each_value do |t|
|
|
121
|
-
next unless t.kind.object?
|
|
122
|
-
|
|
123
|
-
# Apply object-level federation directives to generated types so the
|
|
124
|
-
# Apollo Federation helper can detect entities and add _entities.
|
|
125
|
-
if t.ast_node && t.respond_to?(:key)
|
|
126
|
-
t.ast_node.directives.each do |dnode|
|
|
127
|
-
case dnode.name
|
|
128
|
-
when 'key'
|
|
129
|
-
arg = dnode.arguments.find { |a| a.name == 'fields' }
|
|
130
|
-
t.key(fields: arg.value) if arg && arg.respond_to?(:value)
|
|
131
|
-
|
|
132
|
-
# Setup federation resolve_reference: prefer a custom resolver
|
|
133
|
-
# registered with ReferenceResolver, but fall back to the base
|
|
134
|
-
# type's own resolve_reference (e.g. BaseObject's default model
|
|
135
|
-
# lookup) so direct-model entities resolve without a custom
|
|
136
|
-
# handler. Capture the inherited implementation before shadowing.
|
|
137
|
-
default_resolve_reference =
|
|
138
|
-
t.respond_to?(:resolve_reference) ? t.method(:resolve_reference) : nil
|
|
139
|
-
|
|
140
|
-
t.define_singleton_method(:resolve_reference) do |reference, context|
|
|
141
|
-
resolved = ::Lighthouse::ReferenceResolver.resolve(t.graphql_name, reference, context)
|
|
142
|
-
next resolved unless resolved.nil?
|
|
143
|
-
|
|
144
|
-
default_resolve_reference ? default_resolve_reference.call(reference, context) : nil
|
|
145
|
-
end
|
|
146
|
-
when 'extends'
|
|
147
|
-
t.extend_type if t.respond_to?(:extend_type)
|
|
148
|
-
when 'shareable'
|
|
149
|
-
t.shareable if t.respond_to?(:shareable)
|
|
150
|
-
when 'inaccessible'
|
|
151
|
-
t.inaccessible if t.respond_to?(:inaccessible)
|
|
152
|
-
when 'interfaceObject'
|
|
153
|
-
t.interface_object if t.respond_to?(:interface_object)
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
t.fields.each do |fname, field_def|
|
|
159
|
-
next unless field_def.ast_node
|
|
160
|
-
|
|
161
|
-
directives = field_def.ast_node.directives || []
|
|
162
|
-
registry = Lighthouse::DirectiveRegistry.default
|
|
163
|
-
# Apply wrapping directives in resolution order: data resolvers first
|
|
164
|
-
# (innermost), then @can (wraps to authorize the result), then guards
|
|
165
|
-
# (outermost, so authentication runs before anything else).
|
|
166
|
-
sorted = directives.sort_by { |d| registry.resolution_order(d.name) }
|
|
167
|
-
|
|
168
|
-
# Dispatch each directive to its registered resolver class. The
|
|
169
|
-
# registry replaces the previous hardcoded `case dnode.name` so that
|
|
170
|
-
# adding a directive is "register a class", not "edit the generator".
|
|
171
|
-
sorted.each do |dnode|
|
|
172
|
-
resolver_class = registry.resolution_class(dnode.name)
|
|
173
|
-
next unless resolver_class
|
|
174
|
-
|
|
175
|
-
resolver_class.new(field_def, t, dnode).define_resolver(t)
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
# GraphQL-Ruby's SDL builder defines `resolve_<field>` methods that call
|
|
179
|
-
# the schema's `definition_default_resolve`. Override `resolve_<field>`
|
|
180
|
-
# to delegate to our generated `resolve_field_<field>` when present, so
|
|
181
|
-
# runtime resolution consistently reaches our directive resolvers.
|
|
182
|
-
begin
|
|
183
|
-
method_name = "resolve_field_#{field_def.name}"
|
|
184
|
-
runtime_name = "resolve_#{field_def.name}"
|
|
185
|
-
if t.respond_to?(method_name)
|
|
186
|
-
t.define_singleton_method(runtime_name) do |obj, arguments, ctx|
|
|
187
|
-
public_send(method_name, obj, arguments, ctx)
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
rescue StandardError
|
|
191
|
-
# ignore override failures
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
# Ensure GraphQL-Ruby's ConnectionExtension is applied for SDL-built
|
|
195
|
-
# connection fields so the runtime will wrap returned values into
|
|
196
|
-
# Connection objects (populate_connection will be invoked).
|
|
197
|
-
begin
|
|
198
|
-
# Avoid adding the ConnectionExtension if pagination args were
|
|
199
|
-
# already added by SDL augmentation (to prevent duplicate arg
|
|
200
|
-
# collisions).
|
|
201
|
-
if field_def.connection? && !(field_def.extensions.any? do |e|
|
|
202
|
-
e.is_a?(::GraphQL::Schema::Field::ConnectionExtension)
|
|
203
|
-
end) && !((field_def.arguments || {}).key?(:after) || (field_def.arguments || {}).key?('after'))
|
|
204
|
-
field_def.extension(::GraphQL::Schema::Field::ConnectionExtension,
|
|
205
|
-
call_after_define: false)
|
|
206
|
-
end
|
|
207
|
-
rescue StandardError
|
|
208
|
-
# ignore failures during post-processing
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
# return schema for convenience
|
|
214
|
-
schema
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
# Scan SDL string for list-typed fields that carry relationship directives
|
|
218
|
-
# and produce Connection/Edge types where missing. This is a lightweight
|
|
219
|
-
# transformation that mirrors Lighthouse behavior: clients can declare
|
|
220
|
-
# `roles: [Role!]! @belongsToMany` and the builder will expose a
|
|
221
|
-
# `RoleConnection` type and swap the field to return that connection.
|
|
222
|
-
def augment_sdl_with_connections(sdl)
|
|
223
|
-
# Drive SDL manipulation through the registry so each directive owns its
|
|
224
|
-
# manipulation behavior and new manipulators can be added by registering
|
|
225
|
-
# a class (no edits here).
|
|
226
|
-
Lighthouse::DirectiveRegistry.default.manipulate_sdl(sdl)
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
# Prepend `directive @name(...) on ...` definitions for registered,
|
|
230
|
-
# self-describing directives that the SDL uses but has not declared.
|
|
231
|
-
# Already-declared directives are left untouched (no duplicates).
|
|
232
|
-
def inject_directive_definitions(sdl)
|
|
233
|
-
return sdl unless sdl.is_a?(String)
|
|
234
|
-
|
|
235
|
-
declared = sdl.scan(/directive\s+@(\w+)/).flatten.to_set
|
|
236
|
-
missing = Lighthouse::DirectiveRegistry.default.directive_definitions
|
|
237
|
-
.reject { |name, _| declared.include?(name) }
|
|
238
|
-
return sdl if missing.empty?
|
|
239
|
-
|
|
240
|
-
"#{missing.values.join("\n")}\n\n#{sdl}"
|
|
241
|
-
end
|
|
3
|
+
module Lighthouse::GraphQL::SchemaGenerator
|
|
4
|
+
# Minimal fallback when SchemaImplementation isn't available.
|
|
5
|
+
# Keep the same dispatch semantics so directive resolvers work.
|
|
6
|
+
class FallbackImplementation
|
|
7
|
+
attr_accessor :schema
|
|
8
|
+
|
|
9
|
+
def call(type, field, obj, args, ctx)
|
|
10
|
+
field_name = Lighthouse::GraphQL::GeneratedResolvers.field_name(field)
|
|
11
|
+
resolver = generated_resolver(type, field_name)
|
|
12
|
+
return Lighthouse::GraphQL::GeneratedResolvers.invoke(resolver, obj, args, ctx) if resolver
|
|
13
|
+
return field.resolve_proc.call(obj, args, ctx) if field.respond_to?(:resolve_proc) && field.resolve_proc
|
|
14
|
+
|
|
15
|
+
obj.respond_to?(field_name) ? obj.public_send(field_name) : nil
|
|
242
16
|
end
|
|
17
|
+
|
|
18
|
+
def resolve_type(_abstract_type, _obj, _ctx)
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def coerce_input(_type, value, _ctx)
|
|
23
|
+
value
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def coerce_result(_type, value, _ctx)
|
|
27
|
+
value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def generated_resolver(type, field_name)
|
|
33
|
+
owner = schema && schema.types[type.respond_to?(:graphql_name) ? type.graphql_name : type.name.to_s]
|
|
34
|
+
method_name = Lighthouse::GraphQL::GeneratedResolvers.method_name(field_name)
|
|
35
|
+
owner.method(method_name) if owner.respond_to?(method_name)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
def build(sdl_string, **opts)
|
|
42
|
+
# Decided on the app's SDL: once definitions are injected, every schema mentions it.
|
|
43
|
+
uses_cache_control = sdl_string.to_s.gsub(/directive\s+@cacheControl/, '').include?('@cacheControl')
|
|
44
|
+
# The manipulated document — from the schema cache when it is enabled
|
|
45
|
+
# and fresh (see SchemaCache), else built now.
|
|
46
|
+
document = Lighthouse::GraphQL::SchemaCache.document(sdl_string) { |sdl| manipulate(sdl) }
|
|
47
|
+
impl = default_implementation
|
|
48
|
+
schema = from_document(document, impl, opts)
|
|
49
|
+
|
|
50
|
+
delegate_to_federation_sdl(schema)
|
|
51
|
+
Lighthouse::GraphQL::GeneratedResolvers.install_dispatcher(schema)
|
|
52
|
+
attach_schema(impl, schema)
|
|
53
|
+
post_process(schema, impl, **opts)
|
|
54
|
+
|
|
55
|
+
# @cacheControl hints become the response's Cache-Control header,
|
|
56
|
+
# computed per query from the fields it selects.
|
|
57
|
+
schema.query_analyzer(Lighthouse::GraphQL::CacheControl::Analyzer) if uses_cache_control
|
|
58
|
+
|
|
59
|
+
schema
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The runtime resolver graphql-ruby calls for every field: SchemaImplementation
|
|
63
|
+
# (the library's, or the app's own) when loaded, else the minimal fallback.
|
|
64
|
+
def default_implementation
|
|
65
|
+
defined?(::SchemaImplementation) ? ::SchemaImplementation.new : FallbackImplementation.new
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Build using the provided base class so federation / custom schema behavior
|
|
69
|
+
# is preserved through class hierarchy.
|
|
70
|
+
# The document is handed over through `parser:`, graphql-ruby's public
|
|
71
|
+
# hook, so it is not parsed again. Takes the options it uses out of `opts`;
|
|
72
|
+
# the rest are post_process's.
|
|
73
|
+
def from_document(document, impl, opts)
|
|
74
|
+
base_types = opts.delete(:base_types) || {}
|
|
75
|
+
using = opts.delete(:using) || {}
|
|
76
|
+
schema_class = opts.delete(:schema_class) || ::GraphQL::Schema
|
|
77
|
+
|
|
78
|
+
schema_class.from_definition('', parser: prebuilt_parser(document), default_resolve: impl,
|
|
79
|
+
base_types: base_types, using: using)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Make to_definition delegate to federation_sdl when available.
|
|
83
|
+
def delegate_to_federation_sdl(schema)
|
|
84
|
+
return unless schema.respond_to?(:federation_sdl)
|
|
85
|
+
|
|
86
|
+
schema.define_singleton_method(:to_definition) do
|
|
87
|
+
federation_sdl
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Give the implementation object access to the built schema so its
|
|
92
|
+
# runtime dispatcher can look up generated singleton resolvers.
|
|
93
|
+
def attach_schema(impl, schema)
|
|
94
|
+
impl.schema = schema if impl.respond_to?(:schema=)
|
|
95
|
+
rescue StandardError
|
|
96
|
+
# ignore
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A "parser" returning a document that is already parsed. A module, not
|
|
100
|
+
# an object: graphql-ruby checks `defined?(parser::SchemaParser)`, which on
|
|
101
|
+
# a plain object builds a TypeError message by inspecting it — the whole
|
|
102
|
+
# document, quadratically slow on a large schema.
|
|
103
|
+
def prebuilt_parser(document)
|
|
104
|
+
Module.new { define_singleton_method(:parse) { |*| document } }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The SDL as the schema is built from it: directive definitions injected
|
|
108
|
+
# (so apps don't hand-declare Lighthouse's directives), then every
|
|
109
|
+
# registered manipulator run (pagination types, whereConditions inputs, ...).
|
|
110
|
+
def manipulate(sdl)
|
|
111
|
+
augment_sdl_with_connections(inject_directive_definitions(sdl))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def post_process(schema, _impl, **_opts)
|
|
115
|
+
root_kinds = { schema.query&.graphql_name => :queries, schema.mutation&.graphql_name => :mutations }.reject { |name, _| name.nil? }
|
|
116
|
+
|
|
117
|
+
# Inspect types and fields for directives and attach generated resolvers
|
|
118
|
+
schema.types.each_value do |type|
|
|
119
|
+
next unless type.kind.object?
|
|
120
|
+
|
|
121
|
+
# Apply object-level federation directives to generated types so the
|
|
122
|
+
# Apollo Federation helper can detect entities and add _entities.
|
|
123
|
+
Lighthouse::GraphQL::FederationDirectives.apply(type)
|
|
124
|
+
type.fields.each_value { |field_def| process_field(type, field_def, root_kinds[type.graphql_name]) }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# return schema for convenience
|
|
128
|
+
schema
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# A field's generated resolvers, then the runtime hooks that make
|
|
132
|
+
# graphql-ruby reach them.
|
|
133
|
+
def process_field(type, field_def, root_kind)
|
|
134
|
+
return unless field_def.ast_node
|
|
135
|
+
|
|
136
|
+
apply_field_resolvers(type, field_def, field_def.ast_node.directives || [], root_kind)
|
|
137
|
+
Lighthouse::GraphQL::GeneratedResolvers.route_runtime_resolver(type, field_def)
|
|
138
|
+
ensure_connection_extension(field_def)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Ensure GraphQL-Ruby's ConnectionExtension is applied for SDL-built
|
|
142
|
+
# connection fields so the runtime will wrap returned values into
|
|
143
|
+
# Connection objects (populate_connection will be invoked).
|
|
144
|
+
def ensure_connection_extension(field_def)
|
|
145
|
+
return unless field_def.connection? && field_def.extensions.none?(::GraphQL::Schema::Field::ConnectionExtension)
|
|
146
|
+
|
|
147
|
+
# Avoid adding the ConnectionExtension if pagination args were
|
|
148
|
+
# already added by SDL augmentation (to prevent duplicate arg
|
|
149
|
+
# collisions).
|
|
150
|
+
return if (field_def.arguments || {}).then { |arguments| arguments.key?(:after) || arguments.key?('after') }
|
|
151
|
+
|
|
152
|
+
field_def.extension(::GraphQL::Schema::Field::ConnectionExtension, call_after_define: false)
|
|
153
|
+
rescue StandardError
|
|
154
|
+
# ignore failures during post-processing
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# A field's resolvers, installed in resolution order (see FieldResolverStack).
|
|
158
|
+
def apply_field_resolvers(type, field_def, directives, root_kind)
|
|
159
|
+
Lighthouse::GraphQL::FieldResolverStack.apply(type, field_def, directives, root_kind)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Scan SDL string for list-typed fields that carry relationship directives
|
|
163
|
+
# and produce Connection/Edge types where missing. This is a lightweight
|
|
164
|
+
# transformation that mirrors Lighthouse behavior: clients can declare
|
|
165
|
+
# `roles: [Role!]! @belongsToMany` and the builder will expose a
|
|
166
|
+
# `RoleConnection` type and swap the field to return that connection.
|
|
167
|
+
def augment_sdl_with_connections(sdl)
|
|
168
|
+
# Drive SDL manipulation through the registry so each directive owns its
|
|
169
|
+
# manipulation behavior and new manipulators can be added by registering
|
|
170
|
+
# a class (no edits here).
|
|
171
|
+
Lighthouse::DirectiveRegistry.default.manipulate_sdl(sdl)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Prepend `directive @name(...) on ...` definitions for registered,
|
|
175
|
+
# self-describing directives that the SDL uses but has not declared.
|
|
176
|
+
# Already-declared directives are left untouched (no duplicates).
|
|
177
|
+
def inject_directive_definitions(sdl)
|
|
178
|
+
return sdl unless sdl.is_a?(String)
|
|
179
|
+
|
|
180
|
+
missing = missing_directive_definitions(sdl)
|
|
181
|
+
return sdl if missing.empty?
|
|
182
|
+
|
|
183
|
+
definitions = missing.values + missing_type_definitions(sdl, missing.values).values
|
|
184
|
+
"#{definitions.join("\n")}\n\n#{sdl}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Types the given directive definitions refer to (`CanAction`,
|
|
188
|
+
# `ClearCacheIdSource`, ...) that the SDL does not define itself.
|
|
189
|
+
def missing_type_definitions(sdl, directive_definitions, registry: Lighthouse::DirectiveRegistry.default)
|
|
190
|
+
declared = sdl.to_s.scan(/^\s*(?:scalar|enum|input|type|interface|union)\s+(\w+)/).flatten.to_set
|
|
191
|
+
text = directive_definitions.join("\n")
|
|
192
|
+
registry.type_definitions.select { |name, _| declared.exclude?(name) && text.match?(/\b#{name}\b/) }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# The registry's directive definitions minus those the SDL declares itself.
|
|
196
|
+
# Returns { "name" => "directive @name(...) on ..." }.
|
|
197
|
+
def missing_directive_definitions(sdl, registry: Lighthouse::DirectiveRegistry.default)
|
|
198
|
+
declared = sdl.to_s.scan(/directive\s+@(\w+)/).flatten.to_set
|
|
199
|
+
registry.directive_definitions.except(*declared)
|
|
243
200
|
end
|
|
244
201
|
end
|
|
@@ -16,57 +16,18 @@ class SchemaImplementation
|
|
|
16
16
|
def call(type, field, obj, args, ctx)
|
|
17
17
|
field_name = extract_field_name(field)
|
|
18
18
|
|
|
19
|
-
# Prefer singleton resolver methods attached during post_process
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
owner_candidates << type.metadata[:type_class]
|
|
23
|
-
end
|
|
24
|
-
owner_candidates << lookup_owner_type(type)
|
|
25
|
-
|
|
26
|
-
resolver_method_name = "resolve_field_#{field_name}"
|
|
27
|
-
owner = owner_candidates.compact.find do |cand|
|
|
28
|
-
next false unless cand.respond_to?(resolver_method_name)
|
|
29
|
-
m = cand.method(resolver_method_name)
|
|
30
|
-
source_file, = m.source_location
|
|
31
|
-
source_file && source_file.include?('/lib/lighthouse/')
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
if owner
|
|
35
|
-
resolver = owner.method(resolver_method_name)
|
|
36
|
-
param_count = resolver.parameters.length
|
|
37
|
-
return resolver.call(obj, args) if param_count == 2
|
|
38
|
-
resolver.call(obj, args, ctx)
|
|
39
|
-
end
|
|
19
|
+
# Prefer singleton resolver methods attached during post_process.
|
|
20
|
+
resolver = generated_resolver(type, field_name)
|
|
21
|
+
return Lighthouse::GraphQL::GeneratedResolvers.invoke(resolver, obj, args, ctx) if resolver
|
|
40
22
|
|
|
41
23
|
# Use field-level resolve proc if available
|
|
42
|
-
if field.respond_to?(:resolve_proc) && field.resolve_proc
|
|
43
|
-
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
if obj.is_a?(Hash)
|
|
47
|
-
return obj[field_name.to_s] if obj.key?(field_name.to_s)
|
|
48
|
-
return obj[field_name.to_sym] if obj.key?(field_name.to_sym)
|
|
49
|
-
|
|
50
|
-
snake = naming_attribute(field_name)
|
|
51
|
-
return obj[snake] if obj.key?(snake)
|
|
52
|
-
return obj[snake.to_sym] if obj.key?(snake.to_sym)
|
|
53
|
-
|
|
54
|
-
return nil
|
|
55
|
-
end
|
|
24
|
+
return field.resolve_proc.call(obj, args, ctx) if field.respond_to?(:resolve_proc) && field.resolve_proc
|
|
25
|
+
return read_hash(obj, field_name) if obj.is_a?(Hash)
|
|
56
26
|
|
|
57
|
-
|
|
58
|
-
# field name (camelCase) to the Rails attribute (snake_case) when the object
|
|
59
|
-
# does not respond to the literal name, so e.g. `customAttributes` resolves
|
|
60
|
-
# to `custom_attributes` without a per-field resolver.
|
|
61
|
-
return obj.public_send(field_name) if obj.respond_to?(field_name)
|
|
62
|
-
|
|
63
|
-
snake = naming_attribute(field_name)
|
|
64
|
-
return obj.public_send(snake) if snake != field_name.to_s && obj.respond_to?(snake)
|
|
65
|
-
|
|
66
|
-
nil
|
|
27
|
+
read_attribute(obj, field_name)
|
|
67
28
|
end
|
|
68
29
|
|
|
69
|
-
def resolve_type(abstract_type,
|
|
30
|
+
def resolve_type(abstract_type, _obj, _ctx)
|
|
70
31
|
# Try to resolve to a Ruby type class under Types::<Name>Type
|
|
71
32
|
type_name = abstract_type.respond_to?(:name) ? abstract_type.name : abstract_type.to_s
|
|
72
33
|
begin
|
|
@@ -76,16 +37,57 @@ class SchemaImplementation
|
|
|
76
37
|
end
|
|
77
38
|
end
|
|
78
39
|
|
|
79
|
-
def coerce_input(
|
|
40
|
+
def coerce_input(_type, value, _ctx)
|
|
80
41
|
value
|
|
81
42
|
end
|
|
82
43
|
|
|
83
|
-
def coerce_result(
|
|
44
|
+
def coerce_result(_type, value, _ctx)
|
|
84
45
|
value
|
|
85
46
|
end
|
|
86
47
|
|
|
87
48
|
private
|
|
88
49
|
|
|
50
|
+
# Our generated `resolve_field_<name>` on the first owner that has one: the
|
|
51
|
+
# type itself, its type class, or the schema's type of the same name.
|
|
52
|
+
def generated_resolver(type, field_name)
|
|
53
|
+
method_name = Lighthouse::GraphQL::GeneratedResolvers.method_name(field_name)
|
|
54
|
+
owner_candidates(type).each do |candidate|
|
|
55
|
+
next unless candidate.respond_to?(method_name)
|
|
56
|
+
|
|
57
|
+
resolver = candidate.method(method_name)
|
|
58
|
+
return resolver if Lighthouse::GraphQL::GeneratedResolvers.generated?(resolver)
|
|
59
|
+
end
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def owner_candidates(type)
|
|
64
|
+
candidates = [type]
|
|
65
|
+
candidates << type.metadata[:type_class] if type.respond_to?(:metadata) && type.metadata.is_a?(Hash) && type.metadata[:type_class]
|
|
66
|
+
candidates << lookup_owner_type(type)
|
|
67
|
+
candidates.compact
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# A Hash result is read by the field name as given, then in snake_case,
|
|
71
|
+
# each as a String or Symbol key.
|
|
72
|
+
def read_hash(hash, field_name)
|
|
73
|
+
snake = naming_attribute(field_name)
|
|
74
|
+
key = [field_name.to_s, field_name.to_sym, snake, snake.to_sym].find { |candidate| hash.key?(candidate) }
|
|
75
|
+
key.nil? ? nil : hash[key]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Fallback to reading the attribute off the Ruby object. Map the GraphQL
|
|
79
|
+
# field name (camelCase) to the Rails attribute (snake_case) when the object
|
|
80
|
+
# does not respond to the literal name, so e.g. `customAttributes` resolves
|
|
81
|
+
# to `custom_attributes` without a per-field resolver.
|
|
82
|
+
def read_attribute(obj, field_name)
|
|
83
|
+
return obj.public_send(field_name) if obj.respond_to?(field_name)
|
|
84
|
+
|
|
85
|
+
snake = naming_attribute(field_name)
|
|
86
|
+
return obj.public_send(snake) if snake != field_name.to_s && obj.respond_to?(snake)
|
|
87
|
+
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
|
|
89
91
|
def naming_attribute(field_name)
|
|
90
92
|
if defined?(Lighthouse::Support::Naming)
|
|
91
93
|
Lighthouse::Support::Naming.attribute(field_name)
|
|
@@ -106,6 +108,7 @@ class SchemaImplementation
|
|
|
106
108
|
|
|
107
109
|
def lookup_owner_type(type)
|
|
108
110
|
return nil unless @schema
|
|
111
|
+
|
|
109
112
|
type_name = if type.respond_to?(:graphql_name)
|
|
110
113
|
type.graphql_name
|
|
111
114
|
elsif type.respond_to?(:name)
|
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module Lighthouse
|
|
4
|
-
|
|
5
|
-
module SDLLoader
|
|
6
|
-
module_function
|
|
3
|
+
module Lighthouse::GraphQL::SDLLoader
|
|
4
|
+
module_function
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return ''
|
|
16
|
-
end
|
|
6
|
+
def load_from_folder(folder)
|
|
7
|
+
files = Dir[File.join(folder, '**', '*.graphql')]
|
|
8
|
+
if files.empty?
|
|
9
|
+
# fallback to the monolithic schema.graphql in apps/chatwoot
|
|
10
|
+
# schema.graphql is located at apps/chatwoot/schema.graphql
|
|
11
|
+
default = File.expand_path('../../..//schema.graphql', __dir__)
|
|
12
|
+
return File.read(default) if File.exist?(default)
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
files.join("\n\n")
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def load_from_file(path)
|
|
23
|
-
File.read(path)
|
|
24
|
-
end
|
|
14
|
+
return ''
|
|
25
15
|
end
|
|
16
|
+
|
|
17
|
+
files.map! { |f| File.read(f) }
|
|
18
|
+
files.join("\n\n")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def load_from_file(path)
|
|
22
|
+
File.read(path)
|
|
26
23
|
end
|
|
27
24
|
end
|