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
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Lighthouse::GraphQL::DirectiveResolvers
6
+ # Shared by @create, @createMany, @update, @updateMany, @upsert and
7
+ # @upsertMany: saves the model a field returns through ActiveRecord, so the
8
+ # model's own validations decide what is valid — the Rails way — and nested
9
+ # input on its associations runs as nested mutations (see NestedMutations).
10
+ # Failures raise a Lighthouse::ValidationError keyed by argument path. The
11
+ # whole field runs in one transaction (Config.transactional_mutations).
12
+ #
13
+ # Arguments map to attributes by their snake_case name, or @rename's
14
+ # `attribute:`; @spread merges an input object into its parent; @inject
15
+ # adds server-side values. On an argument, these directives take
16
+ # `relation:` and act on that association of the parent instead.
17
+ class ModelMutationResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
18
+ ARGUMENTS = 'model: String, relation: String'
19
+ LOCATIONS = 'FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
20
+
21
+ def define_resolver(target_type_class)
22
+ me = self
23
+ install(target_type_class, lambda do |obj, args, ctx|
24
+ model = me.send(:model_class)
25
+ me.send(:transaction, model) { me.send(:mutate, model, obj, args, ctx) }
26
+ end)
27
+ end
28
+
29
+ protected
30
+
31
+ def mutate(_model, _obj, _args, _ctx)
32
+ raise NotImplementedError
33
+ end
34
+
35
+ def executor
36
+ Lighthouse::GraphQL::NestedMutations::Executor.new(@field_def.name)
37
+ end
38
+
39
+ def transaction(model, &block)
40
+ Lighthouse::Config.transactional_mutations ? model.transaction(&block) : yield
41
+ end
42
+
43
+ def field_input(args, ctx)
44
+ values = Lighthouse::Support::Arguments.inject(@field_def.ast_node, Lighthouse::Support::Arguments.shallow(args), ctx)
45
+ Lighthouse::GraphQL::NestedMutations::Input.new(values, @field_def.arguments, [])
46
+ end
47
+
48
+ NOT_EXACTLY_ONE_ARGUMENT = '@*Many directives must ensure that clients pass exactly one field argument.'
49
+ ARGUMENT_NOT_LIST = '@*Many directives must ensure that the single argument value is a list.'
50
+ LIST_ITEM_NOT_INPUT_OBJECT = '@*Many directives must ensure that the list items of its single argument value are input objects.'
51
+
52
+ # The inputs of the single list-of-inputs argument of the *Many directives.
53
+ def item_inputs(args)
54
+ argument = list_argument
55
+ items = Array(Lighthouse::Support::Arguments.shallow(args)[argument.keyword])
56
+ items.each_with_index.map do |item, index|
57
+ Lighthouse::GraphQL::NestedMutations::Input.new(Lighthouse::Support::Arguments.shallow(item), argument.type.unwrap.arguments,
58
+ [argument.graphql_name, index.to_s])
59
+ end
60
+ end
61
+
62
+ # The field's single argument, which must be a list of input objects.
63
+ def list_argument
64
+ argument = @field_def.arguments.values.first
65
+ raise Lighthouse::DefinitionError, NOT_EXACTLY_ONE_ARGUMENT unless @field_def.arguments.size == 1
66
+ raise Lighthouse::DefinitionError, ARGUMENT_NOT_LIST unless argument.type.list?
67
+ raise Lighthouse::DefinitionError, LIST_ITEM_NOT_INPUT_OBJECT unless argument.type.unwrap.kind.input_object?
68
+
69
+ argument
70
+ end
71
+
72
+ # Save every item, then raise once with all their errors (the
73
+ # transaction rolls the saved ones back).
74
+ def each_item(inputs)
75
+ messages = {}
76
+ records = inputs.map do |input|
77
+ yield input
78
+ rescue Lighthouse::ValidationError => e
79
+ messages.merge!(e.messages) { |_, a, b| a + b }
80
+ nil
81
+ end
82
+ raise Lighthouse::ValidationError.new(@field_def.name, messages) unless messages.empty?
83
+
84
+ records
85
+ end
86
+
87
+ def identifying_columns
88
+ return nil unless @directive_node.arguments.any? { |a| a.name == 'identifyingColumns' }
89
+
90
+ Array(directive_arg('identifyingColumns'))
91
+ end
92
+
93
+ # An empty list would match any record; refuse it when the schema is built.
94
+ def validate_identifying_columns!
95
+ return unless identifying_columns && identifying_columns.empty?
96
+
97
+ raise Lighthouse::DefinitionError,
98
+ "@#{@directive_node.name} on #{@type_def.graphql_name}.#{@field_def.name}: identifyingColumns must not be empty."
99
+ end
100
+ end
101
+
102
+ # @create — create a model from the field's arguments.
103
+ class CreateDirectiveResolver < ModelMutationResolver
104
+ def self.directive_name = 'create'
105
+ def self.definition = "directive @create(#{ARGUMENTS}) on #{LOCATIONS}"
106
+
107
+ protected
108
+
109
+ def mutate(model, _obj, args, ctx) = executor.create(model, field_input(args, ctx))
110
+ end
111
+
112
+ # @createMany — create one model per input in the field's single list argument.
113
+ class CreateManyDirectiveResolver < ModelMutationResolver
114
+ def self.directive_name = 'createMany'
115
+ def self.definition = "directive @createMany(#{ARGUMENTS}) on #{LOCATIONS}"
116
+
117
+ protected
118
+
119
+ def mutate(model, _obj, args, _ctx)
120
+ each_item(item_inputs(args)) { |input| executor.create(model, input) }
121
+ end
122
+ end
123
+
124
+ # @update — find a model by its primary key argument and update it.
125
+ class UpdateDirectiveResolver < ModelMutationResolver
126
+ def self.directive_name = 'update'
127
+ def self.definition = "directive @update(#{ARGUMENTS}) on #{LOCATIONS}"
128
+
129
+ protected
130
+
131
+ def mutate(model, _obj, args, ctx) = executor.update(model, field_input(args, ctx))
132
+ end
133
+
134
+ # @updateMany — update one model per input (each carrying its primary key).
135
+ class UpdateManyDirectiveResolver < ModelMutationResolver
136
+ def self.directive_name = 'updateMany'
137
+ def self.definition = "directive @updateMany(#{ARGUMENTS}) on #{LOCATIONS}"
138
+
139
+ protected
140
+
141
+ def mutate(model, _obj, args, _ctx)
142
+ each_item(item_inputs(args)) { |input| executor.update(model, input) }
143
+ end
144
+ end
145
+
146
+ # @upsert — update the model the arguments identify (by `identifyingColumns`,
147
+ # else primary key), or create it.
148
+ class UpsertDirectiveResolver < ModelMutationResolver
149
+ def self.directive_name = 'upsert'
150
+ def self.definition = "directive @upsert(#{ARGUMENTS}, identifyingColumns: [String!]) on #{LOCATIONS}"
151
+
152
+ def define_resolver(target_type_class)
153
+ validate_identifying_columns!
154
+ super
155
+ end
156
+
157
+ protected
158
+
159
+ def mutate(model, _obj, args, ctx)
160
+ # rubocop:disable-next Rails/SkipsModelValidations -- Executor#upsert, not ActiveRecord's: it saves through validations.
161
+ executor.upsert(model, field_input(args, ctx), identifying: identifying_columns)
162
+ end
163
+ end
164
+
165
+ # @upsertMany — @upsert for each input in the field's single list argument.
166
+ class UpsertManyDirectiveResolver < ModelMutationResolver
167
+ def self.directive_name = 'upsertMany'
168
+ def self.definition = "directive @upsertMany(#{ARGUMENTS}, identifyingColumns: [String!]) on #{LOCATIONS}"
169
+
170
+ def define_resolver(target_type_class)
171
+ validate_identifying_columns!
172
+ super
173
+ end
174
+
175
+ protected
176
+
177
+ def mutate(model, _obj, args, _ctx)
178
+ columns = identifying_columns
179
+ # rubocop:disable-next Rails/SkipsModelValidations -- Executor#upsert, not ActiveRecord's: it saves through validations.
180
+ each_item(item_inputs(args)) { |input| executor.upsert(model, input, identifying: columns) }
181
+ end
182
+ end
183
+
184
+ # @delete — destroy the models the field's argument filters select
185
+ # (@whereKey, @eq, ...), running their callbacks, and return them: a list
186
+ # for a list field, else the first or null. Refuses to run without a
187
+ # filter argument, which would delete every row.
188
+ class DeleteDirectiveResolver < ModelMutationResolver
189
+ def self.directive_name = 'delete'
190
+ def self.definition = "directive @delete(#{ARGUMENTS}, scopes: [String!]) on #{LOCATIONS}"
191
+
192
+ WOULD_MODIFY_ALL = 'Would modify all models, use an argument to filter.'
193
+
194
+ protected
195
+
196
+ # A record whose callbacks refuse the destroy is reported as an error next
197
+ # to the data, as PHP Lighthouse does; the others are still deleted.
198
+ def mutate(model, _obj, args, ctx)
199
+ args = Lighthouse::Support::Arguments.shallow(args)
200
+ raise ::GraphQL::ExecutionError, WOULD_MODIFY_ALL unless filtered?(args)
201
+
202
+ relation = Array(directive_arg('scopes')).reduce(model.all) { |acc, scope| acc.public_send(scope) }
203
+ records = apply_where_filters(relation, args).to_a
204
+ records.each do |record|
205
+ next if record.destroy
206
+
207
+ ctx.add_error(::GraphQL::ExecutionError.new("Could not modify model #{model.name} with ID #{record.id}."))
208
+ end
209
+ @field_def.type.list? ? records : records.first
210
+ end
211
+
212
+ def filtered?(args)
213
+ registry = Lighthouse::DirectiveRegistry.default
214
+ (@field_def.ast_node.arguments || []).any? do |argument|
215
+ args.key?(argument.name.underscore.to_sym) && argument.directives.any? { |d| registry.arg_builder_class(d.name) }
216
+ end
217
+ end
218
+ end
219
+ end
@@ -1,147 +1,89 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ostruct'
4
3
  require_relative 'base'
5
4
  require_relative 'builder_applier'
6
5
 
7
- module Lighthouse
8
- module GraphQL
9
- module DirectiveResolvers
10
- class PaginateDirectiveResolver < BaseDirectiveResolver
11
- class PaginatorResult
12
- attr_reader :paginator_info, :data
13
-
14
- def initialize(paginator_info:, data:)
15
- @paginator_info = paginator_info
16
- @data = data
17
- end
18
-
19
- def paginatorInfo
20
- @paginator_info
21
- end
22
- end
23
-
24
- def define_resolver(target_type_class)
25
- field_name = @field_def.name
26
- method_name = "resolve_field_#{field_name}"
27
-
28
- # Resolve model name now, but constantize lazily at call time so
29
- # Rails autoloading (Zeitwerk) can load the model after startup.
30
- model_name_static = extract_model_name || extract_model_name_from_field_type
31
- # If the field was transformed to a Paginator type (e.g. InboxPaginator),
32
- # derive the underlying model name by stripping a trailing "Paginator".
33
- model_name_static = model_name_static.to_s.sub(/Paginator\z/, '')
34
- # Closure to lookup the model class lazily
35
- model_lookup = proc { constantize_model(model_name_static) }
36
-
37
-
38
-
39
- # Determine whether this paginate directive requested a cursor/connection
40
- type_arg = extract_type_name
41
- is_connection_type = type_arg && %w[CONNECTION CURSOR].include?(type_arg.to_s.upcase)
42
-
43
- # Paginator object builder (responds to both `paginatorInfo` and `paginator_info`)
44
- paginator_builder = proc do |pinfo, data|
45
- paginator_info_object = OpenStruct.new(pinfo)
46
- # Ensure both camelCase and underscored-methods can be resolved by fallback
47
- if pinfo.is_a?(Hash)
48
- underscored = {}
49
- pinfo.each_key do |k|
50
- key = k.to_s
51
- snake = key.underscore
52
- underscored[snake] = pinfo[k] unless pinfo.key?(snake)
53
- end
54
- underscored.each { |kk, vv| paginator_info_object[kk] = vv }
55
- end
56
-
57
- PaginatorResult.new(paginator_info: paginator_info_object, data: data)
58
- end
59
-
60
- # Define owner singleton method to support graphql-ruby's execution
61
- resolver_instance = self
62
- field_ast_node = @field_def.ast_node
63
- target_type_class.define_singleton_method(method_name) do |obj, arguments, ctx|
64
- model_class = model_lookup.call
65
- return [] unless model_class
66
-
67
- args_h = arguments.respond_to?(:to_h) ? arguments.to_h : {}
68
-
69
- limit = args_h[:first]
70
- offset = 0
71
- if args_h[:after]
72
- begin
73
- decoded = Base64.decode64(args_h[:after])
74
- if decoded.start_with?('offset:')
75
- offset = decoded.split(':', 2)[1].to_i
76
- end
77
- rescue StandardError
78
- offset = 0
79
- end
80
- end
6
+ # @paginate — paginate a model query, as PHP Lighthouse does. On a nested
7
+ # field it pages the parent's association of the same name.
8
+ #
9
+ # users(name: String @like): [User!]! @paginate(defaultCount: 25)
10
+ #
11
+ # `type:` picks PAGINATOR (default), SIMPLE or CONNECTION (see
12
+ # Directives::PaginationManipulator for the generated schema). The query is
13
+ # the model's (`model:`, else the return type), narrowed by `scopes:`,
14
+ # @builder and the field's argument filters (@eq, @where, @orderBy,
15
+ # @whereConditions, ...). `maxCount` caps `first` (null: unlimited),
16
+ # overriding Config.pagination_max_count.
17
+ class Lighthouse::GraphQL::DirectiveResolvers::PaginateDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
18
+ def self.directive_name = 'paginate'
19
+
20
+ def self.definition
21
+ 'directive @paginate(type: PaginateType = PAGINATOR, model: String, scopes: [String!], ' \
22
+ 'defaultCount: Int, maxCount: Int) on FIELD_DEFINITION'
23
+ end
81
24
 
82
- relation = model_class.all
25
+ def self.type_definitions
26
+ {
27
+ 'PaginateType' => <<~GRAPHQL.chomp
28
+ "The type of pagination a field uses."
29
+ enum PaginateType {
30
+ "Offset-based pagination with page numbers and a total, like Laravel's LengthAwarePaginator."
31
+ PAGINATOR
32
+ "Offset-based pagination without a total count, which saves a query."
33
+ SIMPLE
34
+ "Cursor-based pagination, compatible with the Relay specification."
35
+ CONNECTION
36
+ }
37
+ GRAPHQL
38
+ }
39
+ end
83
40
 
84
- relation = DirectiveResolvers::BuilderApplier.apply(relation, field_ast_node, args_h, ctx, resolver_instance)
41
+ def define_resolver(target_type_class)
42
+ me = self
43
+ install(target_type_class, lambda do |obj, args, ctx|
44
+ me.send(:resolve_page, obj, Lighthouse::Support::Arguments.shallow(args), ctx)
45
+ end)
46
+ end
85
47
 
86
- relation = DirectiveResolvers::WhereConditionsApplier.apply(relation, args_h[:where] || args_h['where'])
87
- relation = relation.offset(offset) if offset > 0
88
- relation = relation.limit(limit) if limit
48
+ protected
89
49
 
90
- if is_connection_type
91
- ::GraphQL::Pagination::ArrayConnection.new(relation, context: ctx, parent: nil, field: nil, arguments: {})
92
- else
93
- data = relation.to_a
94
- per_page = limit
95
- total = relation.count
96
- paginator_info = {
97
- count: data.size,
98
- currentPage: 1,
99
- firstItem: data.any? ? (offset + 1) : nil,
100
- hasMorePages: (offset + data.size) < total,
101
- lastItem: data.any? ? (offset + data.size) : nil,
102
- lastPage: per_page && per_page > 0 ? (total.to_f / per_page).ceil : 1,
103
- perPage: per_page || data.size,
104
- total: total
105
- }
50
+ def resolve_page(obj, args, ctx)
51
+ Lighthouse::GraphQL::Pagination.paginate(page_list(obj, args, ctx), (directive_arg('type') || 'PAGINATOR').to_s.upcase, args, limits)
52
+ end
106
53
 
107
- paginator_builder.call(paginator_info, data)
108
- end
109
- end
54
+ # On a nested field, the parent's association of that name (as in PHP
55
+ # Lighthouse, `User.posts @paginate` pages the user's posts), batched with
56
+ # the other parents' when the Dataloader can; otherwise, or with `model:`,
57
+ # the model's whole table.
58
+ def page_list(obj, args, ctx)
59
+ name = Lighthouse::Support::Naming.attribute(@field_def.name)
60
+ return decorate_relation(model_class.all, args, ctx) unless nested?(obj, name)
110
61
 
111
- # Attach a field-level resolve_proc that delegates to the generated
112
- # singleton resolver. This keeps runtime dispatch consistent with
113
- # SchemaImplementation (which checks for resolve_field_* first).
114
- if @field_def.respond_to?(:resolve_proc=)
115
- @field_def.resolve_proc = ->(obj, arguments, ctx) do
116
- target_type_class.public_send(method_name, obj, arguments, ctx)
117
- end
118
- end
119
- end
62
+ dataloader = Lighthouse::Support::Batching.dataloader(ctx)
63
+ batch = dataloader && Lighthouse::GraphQL::RelationBatch.for(obj, name, args, self, ctx)
64
+ return Lighthouse::GraphQL::Pagination::BatchedList.new(batch, obj, dataloader) if batch
120
65
 
121
- private
66
+ decorate_relation(obj.association(name.to_sym).scope, args, ctx)
67
+ end
122
68
 
123
- def extract_model_name
124
- arg = @directive_node.arguments&.find { |a| a.name == 'model' }
125
- return nil unless arg
126
- arg.value.respond_to?(:value) ? arg.value.value : arg.value
127
- end
69
+ def nested?(obj, name)
70
+ !directive_arg('model') && obj.respond_to?(:association) && obj.class.reflect_on_association(name.to_sym)
71
+ end
128
72
 
73
+ public
129
74
 
130
- def extract_type_name
131
- arg = @directive_node.arguments&.find { |a| a.name == 'type' }
132
- return nil unless arg
133
- arg.value.respond_to?(:value) ? arg.value.value : arg.value
134
- end
75
+ # `scopes:`, @builder and the argument filters, on one relation or a RelationBatch.
76
+ def decorate_relation(relation, args, ctx)
77
+ relation = Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply(apply_scopes(relation), @field_def.ast_node, args, ctx, self)
78
+ apply_where_filters(relation, args)
79
+ end
135
80
 
136
- def extract_model_name_from_field_type
137
- unwrapped = @field_def.type.respond_to?(:unwrap) ? @field_def.type.unwrap : @field_def.type
138
- return unwrapped.graphql_name if unwrapped.respond_to?(:graphql_name) && unwrapped.graphql_name
139
- return unwrapped.name if unwrapped.respond_to?(:name) && unwrapped.name
81
+ protected
140
82
 
141
- # fallback for late-bound signatures like "InboxPaginator!"
142
- @field_def.type.to_type_signature.to_s.gsub(/[!\[\]]/, '')
143
- end
144
- end
145
- end
83
+ def limits
84
+ limits = { default_count: directive_arg('defaultCount') }
85
+ given = @directive_node.arguments.any? { |a| a.name == 'maxCount' }
86
+ limits[:max_count] = directive_arg('maxCount') || :unlimited if given
87
+ limits
146
88
  end
147
89
  end
@@ -2,34 +2,28 @@
2
2
 
3
3
  require_relative 'base'
4
4
 
5
- module Lighthouse
6
- module GraphQL
7
- module DirectiveResolvers
8
- # @rename(attribute: "snake_case_name") — explicit override of the default
9
- # field reader. Reads the named attribute off the parent object regardless
10
- # of the GraphQL field name.
11
- class RenameDirectiveResolver < BaseDirectiveResolver
12
- def self.directive_name = 'rename'
13
- def self.definition = 'directive @rename(attribute: String!) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION'
5
+ # @rename(attribute: "snake_case_name") — explicit override of the default
6
+ # field reader. Reads the named attribute off the parent object regardless
7
+ # of the GraphQL field name.
8
+ class Lighthouse::GraphQL::DirectiveResolvers::RenameDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
9
+ def self.directive_name = 'rename'
10
+ def self.definition = 'directive @rename(attribute: String!) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
14
11
 
15
- def define_resolver(target_type_class)
16
- method_name = "resolve_field_#{@field_def.name}"
17
- attribute = Lighthouse::Support::DirectiveArgs.literal(@directive_node, 'attribute').to_s
12
+ def define_resolver(target_type_class)
13
+ method_name = "resolve_field_#{@field_def.name}"
14
+ attribute = Lighthouse::Support::DirectiveArgs.literal(@directive_node, 'attribute').to_s
18
15
 
19
- resolver = lambda do |obj, _arguments, _ctx|
20
- next nil if obj.nil?
16
+ resolver = lambda do |obj, _arguments, _ctx|
17
+ next nil if obj.nil?
21
18
 
22
- if obj.is_a?(Hash)
23
- obj[attribute] || obj[attribute.to_sym]
24
- elsif obj.respond_to?(attribute)
25
- obj.public_send(attribute)
26
- end
27
- end
28
-
29
- target_type_class.define_singleton_method(method_name, &resolver)
30
- @field_def.resolve_proc = resolver if @field_def.respond_to?(:resolve_proc=)
31
- end
19
+ if obj.is_a?(Hash)
20
+ obj[attribute] || obj[attribute.to_sym]
21
+ elsif obj.respond_to?(attribute)
22
+ obj.public_send(attribute)
32
23
  end
33
24
  end
25
+
26
+ target_type_class.define_singleton_method(method_name, &resolver)
27
+ @field_def.resolve_proc = resolver if @field_def.respond_to?(:resolve_proc=)
34
28
  end
35
29
  end
@@ -1,35 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Lighthouse
4
- module GraphQL
5
- module DirectiveResolvers
6
- # Helper for "middleware" directives (@guard, @can) that need to wrap a
7
- # field's existing resolver.
8
- module Resolution
9
- module_function
3
+ # Helper for "middleware" directives (@guard, @can) that need to wrap a
4
+ # field's existing resolver.
5
+ module Lighthouse::GraphQL::DirectiveResolvers::Resolution
6
+ module_function
10
7
 
11
- # Capture the field's current resolver (generated singleton method,
12
- # resolve_proc, or the default attribute reader) as a callable
13
- # (obj, args, ctx) -> value, so it can be wrapped. The returned callable
14
- # is unaffected by subsequently redefining the singleton method.
15
- def capture(target_type_class, method_name, field_def, field_name)
16
- original_singleton = target_type_class.method(method_name) if target_type_class.respond_to?(method_name)
17
- original_proc = field_def.resolve_proc if field_def.respond_to?(:resolve_proc) && field_def.resolve_proc
8
+ # Capture the field's current resolver (generated singleton method,
9
+ # resolve_proc, or the default attribute reader) as a callable
10
+ # (obj, args, ctx) -> value, so it can be wrapped. The returned callable
11
+ # is unaffected by subsequently redefining the singleton method.
12
+ def capture(target_type_class, method_name, field_def, field_name)
13
+ original_singleton = lighthouse_singleton(target_type_class, method_name)
14
+ original_proc = field_def.resolve_proc if field_def.respond_to?(:resolve_proc) && field_def.resolve_proc
18
15
 
19
- lambda do |obj, arguments, ctx|
20
- if original_singleton
21
- original_singleton.call(obj, arguments, ctx)
22
- elsif original_proc
23
- original_proc.call(obj, arguments, ctx)
24
- elsif obj.respond_to?(field_name)
25
- obj.public_send(field_name)
26
- else
27
- snake = Lighthouse::Support::Naming.attribute(field_name)
28
- obj.respond_to?(snake) ? obj.public_send(snake) : nil
29
- end
30
- end
31
- end
16
+ lambda do |obj, arguments, ctx|
17
+ if original_singleton
18
+ original_singleton.call(obj, arguments, ctx)
19
+ elsif original_proc
20
+ original_proc.call(obj, arguments, ctx)
21
+ elsif obj.respond_to?(field_name)
22
+ obj.public_send(field_name)
23
+ else
24
+ snake = Lighthouse::Support::Naming.attribute(field_name)
25
+ obj.respond_to?(snake) ? obj.public_send(snake) : nil
32
26
  end
33
27
  end
34
28
  end
29
+
30
+ # graphql-ruby's SDL builder defines `resolve_field_*` methods of its own
31
+ # (with a different arity); only ours, defined under lib/lighthouse/, count.
32
+ def lighthouse_singleton(target_type_class, method_name)
33
+ return nil unless target_type_class.respond_to?(method_name)
34
+
35
+ method = target_type_class.method(method_name)
36
+ source_file, = method.source_location
37
+ method if source_file&.include?('/lib/lighthouse/')
38
+ end
35
39
  end