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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +168 -1
  3. data/README.md +46 -2
  4. data/docs/authorization.md +116 -57
  5. data/docs/best-practices.md +16 -3
  6. data/docs/caching.md +161 -0
  7. data/docs/configuration.md +83 -4
  8. data/docs/custom-directives.md +5 -1
  9. data/docs/directives.md +117 -38
  10. data/docs/getting-started.md +84 -0
  11. data/docs/mutations.md +260 -0
  12. data/docs/performance.md +138 -0
  13. data/docs/relationships.md +80 -33
  14. data/docs/resolvers.md +179 -0
  15. data/docs/validation.md +132 -0
  16. data/lib/generators/lighthouse/builder_generator.rb +30 -0
  17. data/lib/generators/lighthouse/class_generator.rb +61 -0
  18. data/lib/generators/lighthouse/install/install_generator.rb +127 -0
  19. data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +8 -0
  20. data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +57 -0
  21. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +67 -0
  22. data/lib/generators/lighthouse/install/templates/schema.graphql.tt +19 -0
  23. data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -0
  24. data/lib/generators/lighthouse/mutation_generator.rb +30 -0
  25. data/lib/generators/lighthouse/query_generator.rb +29 -0
  26. data/lib/generators/lighthouse/validator_generator.rb +36 -0
  27. data/lib/lighthouse/cache_control.rb +126 -0
  28. data/lib/lighthouse/caching.rb +68 -0
  29. data/lib/lighthouse/config.rb +85 -19
  30. data/lib/lighthouse/contracts/arg_builder.rb +8 -12
  31. data/lib/lighthouse/contracts/arg_manipulator.rb +8 -12
  32. data/lib/lighthouse/contracts/directive.rb +10 -14
  33. data/lib/lighthouse/contracts/field_manipulator.rb +8 -12
  34. data/lib/lighthouse/contracts/field_middleware.rb +7 -11
  35. data/lib/lighthouse/contracts/field_resolver.rb +9 -13
  36. data/lib/lighthouse/dataloader_sources/association_loader.rb +28 -19
  37. data/lib/lighthouse/dataloader_sources/model_loader.rb +10 -14
  38. data/lib/lighthouse/dataloader_sources/relation_count_loader.rb +16 -0
  39. data/lib/lighthouse/dataloader_sources/relation_page_loader.rb +18 -0
  40. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +57 -69
  41. data/lib/lighthouse/directive_resolvers/all_resolver.rb +26 -57
  42. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +161 -0
  43. data/lib/lighthouse/directive_resolvers/auth_resolver.rb +41 -38
  44. data/lib/lighthouse/directive_resolvers/base.rb +142 -86
  45. data/lib/lighthouse/directive_resolvers/builder_applier.rb +58 -56
  46. data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +133 -0
  47. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +177 -0
  48. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +70 -0
  49. data/lib/lighthouse/directive_resolvers/count_resolver.rb +42 -48
  50. data/lib/lighthouse/directive_resolvers/field_resolver.rb +27 -68
  51. data/lib/lighthouse/directive_resolvers/find_resolver.rb +42 -52
  52. data/lib/lighthouse/directive_resolvers/first_resolver.rb +39 -45
  53. data/lib/lighthouse/directive_resolvers/guard_resolver.rb +16 -22
  54. data/lib/lighthouse/directive_resolvers/method_resolver.rb +19 -25
  55. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +219 -0
  56. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +70 -128
  57. data/lib/lighthouse/directive_resolvers/rename_resolver.rb +18 -24
  58. data/lib/lighthouse/directive_resolvers/resolution.rb +31 -27
  59. data/lib/lighthouse/directive_resolvers/validator_resolver.rb +193 -0
  60. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +164 -163
  61. data/lib/lighthouse/directives/arguments/base.rb +23 -31
  62. data/lib/lighthouse/directives/arguments/filters.rb +113 -93
  63. data/lib/lighthouse/directives/arguments/order_by.rb +41 -41
  64. data/lib/lighthouse/directives/pagination_info_types.rb +69 -0
  65. data/lib/lighthouse/directives/pagination_manipulator.rb +176 -0
  66. data/lib/lighthouse/directives/registry.rb +210 -141
  67. data/lib/lighthouse/directives/where_conditions_directive.rb +236 -200
  68. data/lib/lighthouse/errors.rb +27 -0
  69. data/lib/lighthouse/federation_directives.rb +53 -0
  70. data/lib/lighthouse/field_resolver_stack.rb +56 -0
  71. data/lib/lighthouse/generated_resolvers.rb +82 -0
  72. data/lib/lighthouse/graphql/version.rb +1 -1
  73. data/lib/lighthouse/graphql_ruby_patches.rb +18 -0
  74. data/lib/lighthouse/ide_helper.rb +98 -0
  75. data/lib/lighthouse/namespaces.rb +21 -0
  76. data/lib/lighthouse/nested_mutations/belongs_to_operations.rb +50 -0
  77. data/lib/lighthouse/nested_mutations/executor.rb +149 -0
  78. data/lib/lighthouse/nested_mutations/has_operations.rb +66 -0
  79. data/lib/lighthouse/nested_mutations/many_to_many_operations.rb +44 -0
  80. data/lib/lighthouse/nested_mutations/partitioner.rb +125 -0
  81. data/lib/lighthouse/nested_mutations.rb +210 -0
  82. data/lib/lighthouse/pagination.rb +221 -0
  83. data/lib/lighthouse/persisted_queries.rb +167 -0
  84. data/lib/lighthouse/railtie.rb +26 -0
  85. data/lib/lighthouse/rb_lighthouse.rb +50 -29
  86. data/lib/lighthouse/reference_resolver.rb +13 -15
  87. data/lib/lighthouse/relation_batch.rb +156 -0
  88. data/lib/lighthouse/resolver_dispatch.rb +26 -0
  89. data/lib/lighthouse/schema_cache.rb +120 -0
  90. data/lib/lighthouse/schema_factory.rb +27 -37
  91. data/lib/lighthouse/schema_generator.rb +196 -239
  92. data/lib/lighthouse/schema_implementation.rb +51 -48
  93. data/lib/lighthouse/sdl_loader.rb +17 -20
  94. data/lib/lighthouse/support/arguments.rb +119 -0
  95. data/lib/lighthouse/support/authorization.rb +18 -19
  96. data/lib/lighthouse/support/batching.rb +16 -0
  97. data/lib/lighthouse/support/directive_args.rb +26 -24
  98. data/lib/lighthouse/support/model_resolver.rb +56 -56
  99. data/lib/lighthouse/support/naming.rb +55 -59
  100. data/lib/lighthouse/tasks/lighthouse.rake +33 -0
  101. data/lib/lighthouse/where_conditions/operator_map.rb +25 -29
  102. metadata +52 -64
  103. data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +0 -56
  104. data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +0 -47
  105. data/lib/lighthouse/directive_resolvers/can_resolver.rb +0 -86
  106. data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +0 -109
  107. data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +0 -43
  108. 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
- module GraphQL
5
- module SchemaGenerator
6
- module_function
7
-
8
- def build(sdl_string, **opts)
9
- # Contribute SDL definitions for any registered directive the schema uses
10
- # but hasn't declared, so the app doesn't hand-declare every directive.
11
- sdl_string = inject_directive_definitions(sdl_string)
12
- # Augment SDL: convert list-typed fields with relationship directives
13
- # (e.g. `roles: [Role!]! @belongsToMany`) into connection types
14
- # (`roles: RoleConnection!`) and generate missing Connection/Edge types.
15
- sdl_string = augment_sdl_with_connections(sdl_string)
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
- owner_candidates = [type]
21
- if type.respond_to?(:metadata) && type.metadata.is_a?(Hash) && type.metadata[:type_class]
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
- return field.resolve_proc.call(obj, args, ctx)
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
- # Fallback to reading the attribute off the Ruby object. Map the GraphQL
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, obj, ctx)
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(type, value, ctx)
40
+ def coerce_input(_type, value, _ctx)
80
41
  value
81
42
  end
82
43
 
83
- def coerce_result(type, value, ctx)
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
- module GraphQL
5
- module SDLLoader
6
- module_function
3
+ module Lighthouse::GraphQL::SDLLoader
4
+ module_function
7
5
 
8
- def load_from_folder(folder)
9
- files = Dir[File.join(folder, '**', '*.graphql')].sort
10
- if files.empty?
11
- # fallback to the monolithic schema.graphql in apps/chatwoot
12
- # schema.graphql is located at apps/chatwoot/schema.graphql
13
- default = File.expand_path('../../..//schema.graphql', __dir__)
14
- return File.read(default) if File.exist?(default)
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
- files.map! { |f| File.read(f) }
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