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
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
relation = relation.offset(offset) if offset > 0
|
|
88
|
-
relation = relation.limit(limit) if limit
|
|
48
|
+
protected
|
|
89
49
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
66
|
+
decorate_relation(obj.association(name.to_sym).scope, args, ctx)
|
|
67
|
+
end
|
|
122
68
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
16
|
+
resolver = lambda do |obj, _arguments, _ctx|
|
|
17
|
+
next nil if obj.nil?
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|