lighthouse-graphql 0.1.1 → 0.2.1
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 +175 -1
- data/README.md +47 -3
- 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/motivation.md +184 -0
- data/docs/mutations.md +260 -0
- data/docs/performance.md +138 -0
- data/docs/relationships.md +80 -33
- data/docs/resolvers.md +189 -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
- data/sig/lighthouse/directives.rbs +158 -0
- data/sig/lighthouse/graphql.rbs +78 -0
- data/sig/lighthouse.rbs +142 -0
- metadata +56 -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
|
@@ -2,62 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'base'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
relation = apply_directive(relation, directive_node, args, ctx, resolver_instance)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
relation
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def self.apply_directive(relation, directive_node, args, ctx, resolver_instance)
|
|
23
|
-
class_name = extract_directive_arg(directive_node, 'class')
|
|
24
|
-
method_name = extract_directive_arg(directive_node, 'method') || 'call'
|
|
25
|
-
|
|
26
|
-
return relation unless class_name
|
|
27
|
-
|
|
28
|
-
builder_class = resolver_instance.send(:constantize_model, class_name)
|
|
29
|
-
return relation unless builder_class
|
|
30
|
-
|
|
31
|
-
if builder_class.respond_to?(method_name)
|
|
32
|
-
result = builder_class.public_send(method_name, relation, args, ctx)
|
|
33
|
-
result || relation
|
|
34
|
-
elsif builder_class.is_a?(Class)
|
|
35
|
-
instance = builder_class.new
|
|
36
|
-
if instance.respond_to?(method_name)
|
|
37
|
-
result = instance.public_send(method_name, relation, args, ctx)
|
|
38
|
-
result || relation
|
|
39
|
-
else
|
|
40
|
-
relation
|
|
41
|
-
end
|
|
42
|
-
else
|
|
43
|
-
relation
|
|
44
|
-
end
|
|
45
|
-
rescue ::GraphQL::ExecutionError
|
|
46
|
-
# Client-facing errors (e.g. authorization / scoping guards raised by a
|
|
47
|
-
# builder) MUST propagate — never silently fall back to the unscoped
|
|
48
|
-
# relation, or a builder that enforces tenancy could be bypassed.
|
|
49
|
-
raise
|
|
50
|
-
rescue StandardError => e
|
|
51
|
-
Lighthouse::Config.handle_error(e)
|
|
52
|
-
relation
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def self.extract_directive_arg(directive_node, name)
|
|
56
|
-
arg = directive_node.arguments&.find { |a| a.name == name }
|
|
57
|
-
return nil unless arg
|
|
58
|
-
arg.value.respond_to?(:value) ? arg.value.value : arg.value
|
|
59
|
-
end
|
|
60
|
-
end
|
|
5
|
+
class Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier
|
|
6
|
+
DEFINITION = 'directive @builder(class: String!, method: String) repeatable on FIELD_DEFINITION'
|
|
7
|
+
|
|
8
|
+
def self.apply(relation, field_def_ast_node, args, ctx, resolver_instance)
|
|
9
|
+
return relation unless field_def_ast_node&.directives
|
|
10
|
+
|
|
11
|
+
builder_directives = field_def_ast_node.directives.select { |d| d.name == 'builder' }
|
|
12
|
+
return relation if builder_directives.empty?
|
|
13
|
+
|
|
14
|
+
builder_directives.each do |directive_node|
|
|
15
|
+
relation = apply_directive(relation, directive_node, args, ctx, resolver_instance)
|
|
61
16
|
end
|
|
17
|
+
|
|
18
|
+
relation
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.apply_directive(relation, directive_node, args, ctx, _resolver_instance)
|
|
22
|
+
class_name = extract_directive_arg(directive_node, 'class')
|
|
23
|
+
method_name = extract_directive_arg(directive_node, 'method') || 'call'
|
|
24
|
+
|
|
25
|
+
return relation unless class_name
|
|
26
|
+
|
|
27
|
+
# Builders are not resolvers: they compose onto a relation, and live in
|
|
28
|
+
# their own namespace (Config.namespaces[:builders], app/graphql/builders).
|
|
29
|
+
builder_class = Lighthouse::Support::ModelResolver.constantize(
|
|
30
|
+
class_name, namespaces: Lighthouse::Config.namespaces[:builders]
|
|
31
|
+
)
|
|
32
|
+
return relation unless builder_class
|
|
33
|
+
|
|
34
|
+
receiver = builder_receiver(builder_class, method_name)
|
|
35
|
+
return relation unless receiver
|
|
36
|
+
|
|
37
|
+
receiver.public_send(method_name, relation, args, ctx) || relation
|
|
38
|
+
rescue ::GraphQL::ExecutionError
|
|
39
|
+
# Client-facing errors (e.g. authorization / scoping guards raised by a
|
|
40
|
+
# builder) MUST propagate — never silently fall back to the unscoped
|
|
41
|
+
# relation, or a builder that enforces tenancy could be bypassed.
|
|
42
|
+
raise
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
Lighthouse::Config.handle_error(e)
|
|
45
|
+
relation
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.extract_directive_arg(directive_node, name)
|
|
49
|
+
arg = directive_node.arguments&.find { |a| a.name == name }
|
|
50
|
+
return nil unless arg
|
|
51
|
+
|
|
52
|
+
arg.value.respond_to?(:value) ? arg.value.value : arg.value
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# What answers the builder method: the class itself, else an instance
|
|
56
|
+
# of it; nil when neither does.
|
|
57
|
+
def self.builder_receiver(builder_class, method_name)
|
|
58
|
+
return builder_class if builder_class.respond_to?(method_name)
|
|
59
|
+
return nil unless builder_class.is_a?(Class)
|
|
60
|
+
|
|
61
|
+
instance = builder_class.new
|
|
62
|
+
instance if instance.respond_to?(method_name)
|
|
62
63
|
end
|
|
64
|
+
private_class_method :builder_receiver
|
|
63
65
|
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Lighthouse::GraphQL::DirectiveResolvers
|
|
6
|
+
# @cache — keep a field's resolved value in Config.cache_store (Rails.cache).
|
|
7
|
+
#
|
|
8
|
+
# The key covers the parent type and its key (the field marked @cacheKey,
|
|
9
|
+
# else the first `ID` field; none on Query/Mutation), the field, and its
|
|
10
|
+
# arguments; `private: true` adds the authenticated user, so each user gets
|
|
11
|
+
# their own entry (guests share one). `maxAge` is in seconds; without it
|
|
12
|
+
# entries live until @clearCache or eviction. Lists and relations are
|
|
13
|
+
# cached as arrays; a value the store cannot serialize (e.g. a connection)
|
|
14
|
+
# is reported through Config.on_error and returned uncached.
|
|
15
|
+
class CacheDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
16
|
+
def self.directive_name = 'cache'
|
|
17
|
+
def self.definition = 'directive @cache(maxAge: Int, private: Boolean = false) on FIELD_DEFINITION'
|
|
18
|
+
|
|
19
|
+
ROOT_TYPES = %w[Query Mutation Subscription].freeze
|
|
20
|
+
|
|
21
|
+
def define_resolver(target_type_class)
|
|
22
|
+
type_name = @type_def.graphql_name
|
|
23
|
+
field_name = @field_def.name
|
|
24
|
+
max_age = directive_arg('maxAge')
|
|
25
|
+
per_user = directive_arg('private') == true
|
|
26
|
+
parent_key = parent_key_reader
|
|
27
|
+
inner = capture_inner(target_type_class)
|
|
28
|
+
|
|
29
|
+
install(target_type_class, lambda do |obj, args, ctx|
|
|
30
|
+
user_key = per_user ? CacheDirectiveResolver.user_key(ctx) : nil
|
|
31
|
+
key = Lighthouse::GraphQL::Caching.entry_key(type_name, parent_key&.call(obj), field_name, args, user_key)
|
|
32
|
+
cached = Lighthouse::GraphQL::Caching.store.read(key)
|
|
33
|
+
next cached[:value] if cached.is_a?(Hash) && cached.key?(:value)
|
|
34
|
+
|
|
35
|
+
value = CacheDirectiveResolver.materialize(inner.call(obj, args, ctx))
|
|
36
|
+
CacheDirectiveResolver.write(key, value, max_age, ctx)
|
|
37
|
+
value
|
|
38
|
+
end)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.user_key(ctx)
|
|
42
|
+
user = Lighthouse::Support::Authorization.authenticated_user(ctx)
|
|
43
|
+
return nil if user.nil?
|
|
44
|
+
|
|
45
|
+
"user:#{user.respond_to?(:id) ? user.id : user}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.materialize(value)
|
|
49
|
+
Lighthouse::Support::Arguments.list(value) || value
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.write(key, value, max_age, ctx)
|
|
53
|
+
Lighthouse::GraphQL::Caching.store.write(key, { value: value }, expires_in: max_age)
|
|
54
|
+
rescue TypeError, ArgumentError, NoMethodError => e
|
|
55
|
+
Lighthouse::Config.handle_error(e, ctx)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
# A callable reading the parent's cache key, or nil on root types.
|
|
61
|
+
def parent_key_reader
|
|
62
|
+
return nil if ROOT_TYPES.include?(@type_def.graphql_name)
|
|
63
|
+
|
|
64
|
+
key_field = cache_key_field
|
|
65
|
+
return nil unless key_field
|
|
66
|
+
|
|
67
|
+
name = key_field.name
|
|
68
|
+
->(obj) { obj.nil? ? nil : Lighthouse::Support::Arguments.read(obj, name) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The field marked @cacheKey, else the first `ID` field.
|
|
72
|
+
def cache_key_field
|
|
73
|
+
fields = @type_def.fields.values
|
|
74
|
+
fields.find { |f| cache_key?(f) } || fields.find { |f| f.type.unwrap.graphql_name == 'ID' }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def cache_key?(field)
|
|
78
|
+
field.ast_node&.directives&.any? { |d| d.name == 'cacheKey' }
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @clearCache — on a mutation, invalidate @cache entries once the field
|
|
83
|
+
# resolved successfully. `type` alone clears all of that type's entries;
|
|
84
|
+
# `idSource` narrows it to the ids found at an argument path
|
|
85
|
+
# (`{ argument: "input.id" }`) or a result path (`{ field: "*.id" }`), and
|
|
86
|
+
# `field` to a single field.
|
|
87
|
+
class ClearCacheDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
88
|
+
def self.directive_name = 'clearCache'
|
|
89
|
+
|
|
90
|
+
def self.definition
|
|
91
|
+
'directive @clearCache(type: String!, idSource: ClearCacheIdSource, field: String) ' \
|
|
92
|
+
'repeatable on FIELD_DEFINITION'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.type_definitions
|
|
96
|
+
{
|
|
97
|
+
'ClearCacheIdSource' => <<~GRAPHQL.chomp
|
|
98
|
+
"Where @clearCache finds the ids to clear. Exactly one of the fields must be given."
|
|
99
|
+
input ClearCacheIdSource {
|
|
100
|
+
"Path of an argument the field is applied to, e.g. \\"input.id\\"."
|
|
101
|
+
argument: String
|
|
102
|
+
"Path of a field in the result, e.g. \\"*.id\\" for a list."
|
|
103
|
+
field: String
|
|
104
|
+
}
|
|
105
|
+
GRAPHQL
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def define_resolver(target_type_class)
|
|
110
|
+
type = directive_arg('type')
|
|
111
|
+
field = directive_arg('field')
|
|
112
|
+
source = directive_arg('idSource') || {}
|
|
113
|
+
inner = capture_inner(target_type_class)
|
|
114
|
+
|
|
115
|
+
install(target_type_class, lambda do |obj, args, ctx|
|
|
116
|
+
result = inner.call(obj, args, ctx)
|
|
117
|
+
ids =
|
|
118
|
+
if source['argument']
|
|
119
|
+
Lighthouse::Support::Arguments.dig(Lighthouse::Support::Arguments.plain(args), source['argument'])
|
|
120
|
+
elsif source['field']
|
|
121
|
+
Lighthouse::Support::Arguments.dig(result, source['field'])
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if ids.nil?
|
|
125
|
+
Lighthouse::GraphQL::Caching.clear(type, field: field)
|
|
126
|
+
else
|
|
127
|
+
ids.flatten.uniq.each { |id| Lighthouse::GraphQL::Caching.clear(type, id: id, field: field) }
|
|
128
|
+
end
|
|
129
|
+
result
|
|
130
|
+
end)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Lighthouse::GraphQL::DirectiveResolvers
|
|
6
|
+
# The @can* family — PHP Lighthouse's authorization directives, checked
|
|
7
|
+
# with Pundit by default (Config.authorizer): `ability: "update"` calls
|
|
8
|
+
# `PostPolicy#update?`. Each directive differs only in WHAT it authorizes:
|
|
9
|
+
#
|
|
10
|
+
# @canFind the models whose primary keys an argument holds
|
|
11
|
+
# @canModel the model class (e.g. create? before a @create)
|
|
12
|
+
# @canQuery the models the field's query would return
|
|
13
|
+
# @canResolved whatever the field resolved to (each item of a list)
|
|
14
|
+
# @canRoot the parent object
|
|
15
|
+
#
|
|
16
|
+
# Shared arguments: `injectArgs` passes the field's arguments to the policy
|
|
17
|
+
# method, `args` passes static values after them, and `action` decides what
|
|
18
|
+
# a denial (or a @canFind miss) does — pass the error, replace it with a
|
|
19
|
+
# generic "This action is unauthorized.", or return `returnValue` instead.
|
|
20
|
+
# All are repeatable; they run after @guard and before validation.
|
|
21
|
+
class CanDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
22
|
+
COMMON_ARGUMENTS = 'ability: String!, injectArgs: Boolean = false, args: CanArgs, ' \
|
|
23
|
+
'action: CanAction = EXCEPTION_PASS, returnValue: CanArgs'
|
|
24
|
+
|
|
25
|
+
TYPE_DEFINITIONS = {
|
|
26
|
+
'CanArgs' => '"Any constant literal value: https://graphql.github.io/graphql-spec/draft/#sec-Input-Values"' \
|
|
27
|
+
"\nscalar CanArgs",
|
|
28
|
+
'CanAction' => <<~GRAPHQL.chomp
|
|
29
|
+
"What a @can* directive does when the policy denies the ability."
|
|
30
|
+
enum CanAction {
|
|
31
|
+
"Pass the error to the client."
|
|
32
|
+
EXCEPTION_PASS
|
|
33
|
+
"Throw a generic \\"This action is unauthorized.\\" error, concealing the real one."
|
|
34
|
+
EXCEPTION_NOT_AUTHORIZED
|
|
35
|
+
"Return the value of returnValue instead of resolving the field."
|
|
36
|
+
RETURN_VALUE
|
|
37
|
+
}
|
|
38
|
+
GRAPHQL
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
def self.type_definitions = TYPE_DEFINITIONS
|
|
42
|
+
|
|
43
|
+
def self.definition_with(extra = nil)
|
|
44
|
+
"directive @#{directive_name}(#{[COMMON_ARGUMENTS, extra].compact.join(', ')}) repeatable on FIELD_DEFINITION"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def define_resolver(target_type_class)
|
|
48
|
+
me = self
|
|
49
|
+
inner = capture_inner(target_type_class)
|
|
50
|
+
install(target_type_class, lambda do |obj, args, ctx|
|
|
51
|
+
catch(me) { me.send(:authorize_and_resolve, inner, obj, args, ctx) }
|
|
52
|
+
end)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
protected
|
|
56
|
+
|
|
57
|
+
def authorize_and_resolve(inner, obj, args, ctx)
|
|
58
|
+
check { authorize_before(obj, args, ctx) }
|
|
59
|
+
inner.call(obj, args, ctx)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def authorize_before(_obj, _args, _ctx)
|
|
63
|
+
raise NotImplementedError
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Run an authorization step, turning its failure into what `action:` asks for.
|
|
67
|
+
def check
|
|
68
|
+
yield
|
|
69
|
+
rescue ::GraphQL::ExecutionError
|
|
70
|
+
case directive_arg('action')
|
|
71
|
+
when 'EXCEPTION_NOT_AUTHORIZED' then raise Lighthouse::AuthorizationError
|
|
72
|
+
when 'RETURN_VALUE' then throw(self, directive_arg('returnValue'))
|
|
73
|
+
else raise
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def authorize!(ctx, record, args)
|
|
78
|
+
ability = directive_arg('ability')
|
|
79
|
+
return if Lighthouse::Support::Authorization.authorized?(ctx, record, ability, policy_args(args))
|
|
80
|
+
|
|
81
|
+
raise Lighthouse::AuthorizationError
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def authorize_each!(ctx, records, args)
|
|
85
|
+
records.each { |record| authorize!(ctx, record, args) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def policy_args(args)
|
|
89
|
+
values = []
|
|
90
|
+
values << Lighthouse::Support::Arguments.plain(args) if directive_arg('injectArgs')
|
|
91
|
+
static = directive_arg('args')
|
|
92
|
+
values.concat(static.is_a?(Array) ? static : [static]) unless static.nil?
|
|
93
|
+
values
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def scoped(relation)
|
|
97
|
+
Array(directive_arg('scopes')).reduce(relation) { |acc, scope| acc.public_send(scope) }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class CanFindDirectiveResolver < CanDirectiveResolver
|
|
102
|
+
def self.directive_name = 'canFind'
|
|
103
|
+
def self.definition = definition_with('find: String!, model: String, findOrFail: Boolean = true, scopes: [String!]')
|
|
104
|
+
|
|
105
|
+
protected
|
|
106
|
+
|
|
107
|
+
def authorize_before(_obj, args, ctx)
|
|
108
|
+
model = model_class
|
|
109
|
+
ids = Lighthouse::Support::Arguments.dig(Lighthouse::Support::Arguments.plain(args), directive_arg('find')).flatten.uniq
|
|
110
|
+
records = scoped(model.where(model.primary_key => ids)).to_a
|
|
111
|
+
if directive_arg('findOrFail') != false && records.size < ids.size
|
|
112
|
+
missing = ids.map(&:to_s) - records.map { |r| r.public_send(model.primary_key).to_s }
|
|
113
|
+
raise ::GraphQL::ExecutionError, "Couldn't find #{model.name} with '#{model.primary_key}'=#{missing.join(', ')}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
authorize_each!(ctx, records, args)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
class CanModelDirectiveResolver < CanDirectiveResolver
|
|
121
|
+
def self.directive_name = 'canModel'
|
|
122
|
+
def self.definition = definition_with('model: String')
|
|
123
|
+
|
|
124
|
+
protected
|
|
125
|
+
|
|
126
|
+
def authorize_before(_obj, args, ctx)
|
|
127
|
+
authorize!(ctx, model_class, args)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class CanQueryDirectiveResolver < CanDirectiveResolver
|
|
132
|
+
def self.directive_name = 'canQuery'
|
|
133
|
+
def self.definition = definition_with('model: String, scopes: [String!]')
|
|
134
|
+
|
|
135
|
+
protected
|
|
136
|
+
|
|
137
|
+
# The records the field's argument filters (@eq, @where, ...) select.
|
|
138
|
+
def authorize_before(_obj, args, ctx)
|
|
139
|
+
authorize_each!(ctx, apply_where_filters(scoped(model_class.all), args).to_a, args)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
class CanResolvedDirectiveResolver < CanDirectiveResolver
|
|
144
|
+
def self.directive_name = 'canResolved'
|
|
145
|
+
def self.definition = definition_with
|
|
146
|
+
|
|
147
|
+
protected
|
|
148
|
+
|
|
149
|
+
# The resolver runs first: only use it on fields that do not mutate data.
|
|
150
|
+
def authorize_and_resolve(inner, obj, args, ctx)
|
|
151
|
+
result = inner.call(obj, args, ctx)
|
|
152
|
+
check { authorize_each!(ctx, resolved_records(result), args) }
|
|
153
|
+
result
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def resolved_records(result)
|
|
157
|
+
case result
|
|
158
|
+
when nil then []
|
|
159
|
+
when Lighthouse::GraphQL::Pagination::Paginator then result.data
|
|
160
|
+
when Lighthouse::GraphQL::Pagination::Connection then result.edges.pluck('node')
|
|
161
|
+
when Hash then result.key?('data') ? Array(result['data']) : [result] # a simple paginator, or a record as a Hash
|
|
162
|
+
else Lighthouse::Support::Arguments.list(result) || [result]
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
class CanRootDirectiveResolver < CanDirectiveResolver
|
|
168
|
+
def self.directive_name = 'canRoot'
|
|
169
|
+
def self.definition = definition_with
|
|
170
|
+
|
|
171
|
+
protected
|
|
172
|
+
|
|
173
|
+
def authorize_before(obj, args, ctx)
|
|
174
|
+
authorize!(ctx, obj, args)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
# Resolves a Query or Mutation field that has no resolving directive by
|
|
6
|
+
# convention, as PHP Lighthouse does: `Query.latestPosts` resolves to
|
|
7
|
+
# `Queries::LatestPosts` (a GraphQL::Schema::Resolver), `Mutation.publishPost`
|
|
8
|
+
# to `Mutations::PublishPost` (a GraphQL::Schema::Mutation), looked up in
|
|
9
|
+
# Config.namespaces[:queries] / [:mutations] at call time so code reloading
|
|
10
|
+
# keeps working. Not a directive: SchemaGenerator installs it.
|
|
11
|
+
class Lighthouse::GraphQL::DirectiveResolvers::ConventionResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
12
|
+
def initialize(field_def, type_def, kind)
|
|
13
|
+
super(field_def, type_def, nil)
|
|
14
|
+
@kind = kind
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def define_resolver(target_type_class)
|
|
18
|
+
me = self
|
|
19
|
+
inner = capture_inner(target_type_class)
|
|
20
|
+
|
|
21
|
+
install(target_type_class, lambda do |obj, args, ctx|
|
|
22
|
+
me.send(:resolve_by_convention, inner, obj, args, ctx)
|
|
23
|
+
end)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
ROOT_KINDS = { 'Query' => :queries, 'Mutation' => :mutations }.freeze
|
|
27
|
+
|
|
28
|
+
# :queries / :mutations for the conventional root type names. The
|
|
29
|
+
# SchemaGenerator passes the schema's actual roots as `root_kind:`.
|
|
30
|
+
def self.root_kind_of(type)
|
|
31
|
+
ROOT_KINDS[type.respond_to?(:graphql_name) ? type.graphql_name : type.to_s]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def resolve_by_convention(inner, obj, args, ctx)
|
|
37
|
+
namespaces = Lighthouse::Config.namespaces[@kind]
|
|
38
|
+
klass = Lighthouse::Support::ModelResolver.constantize(class_name, namespaces: namespaces, fallback: false)
|
|
39
|
+
return missing(namespaces, inner, obj, args, ctx) unless klass
|
|
40
|
+
|
|
41
|
+
unless Lighthouse::GraphQL::ResolverDispatch.graphql_resolver?(klass)
|
|
42
|
+
raise Lighthouse::DefinitionError,
|
|
43
|
+
"#{klass} resolves #{location}, so it must inherit from GraphQL::Schema::Resolver " \
|
|
44
|
+
'(or GraphQL::Schema::Mutation).'
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
Lighthouse::GraphQL::ResolverDispatch.call(klass, @field_def, obj, args, ctx)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# No class: the root object may still answer (a custom root_value);
|
|
51
|
+
# otherwise report the misconfiguration through Config.on_error.
|
|
52
|
+
def missing(namespaces, inner, obj, args, ctx)
|
|
53
|
+
return inner.call(obj, args, ctx) if obj && !obj.is_a?(Hash) && obj.respond_to?(@field_def.name)
|
|
54
|
+
|
|
55
|
+
expected = Array(namespaces).map { |ns| "#{ns}::#{class_name}" }.join(' or ')
|
|
56
|
+
Lighthouse::Config.handle_error(
|
|
57
|
+
Lighthouse::DefinitionError.new("#{location} has no resolver: define #{expected}, or add a resolving directive."),
|
|
58
|
+
ctx
|
|
59
|
+
)
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def class_name
|
|
64
|
+
@class_name ||= @field_def.name.to_s.camelize
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def location
|
|
68
|
+
@location ||= "#{@type_def.graphql_name}.#{@field_def.name}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -3,54 +3,48 @@
|
|
|
3
3
|
require_relative 'base'
|
|
4
4
|
require_relative 'builder_applier'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
relation = DirectiveResolvers::BuilderApplier.apply(relation, field_ast_node, args_h, ctx, resolver_instance)
|
|
40
|
-
relation = resolver_instance.apply_where_filters(relation, args_h)
|
|
41
|
-
relation.count
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
target_type_class.define_singleton_method(method_name, &resolver)
|
|
45
|
-
@field_def.resolve_proc = resolver if @field_def.respond_to?(:resolve_proc=)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
private
|
|
49
|
-
|
|
50
|
-
def directive_arg(name)
|
|
51
|
-
Lighthouse::Support::DirectiveArgs.literal(@directive_node, name)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
6
|
+
# @count(model: "Model", relation: "assoc") — return the number of records.
|
|
7
|
+
# Counts `Model.all` when `model:` is given, otherwise the named relation on
|
|
8
|
+
# the parent object (or a relation matching the field name). Honors `scopes:`, @builder
|
|
9
|
+
# and where/argument filters before counting.
|
|
10
|
+
class Lighthouse::GraphQL::DirectiveResolvers::CountDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
11
|
+
def self.directive_name = 'count'
|
|
12
|
+
def self.definition = 'directive @count(model: String, relation: String, scopes: [String!]) on FIELD_DEFINITION'
|
|
13
|
+
|
|
14
|
+
def define_resolver(target_type_class)
|
|
15
|
+
model_name = directive_arg('model')
|
|
16
|
+
relation_name = directive_arg('relation')
|
|
17
|
+
me = self
|
|
18
|
+
|
|
19
|
+
install(target_type_class, lambda do |obj, arguments, ctx|
|
|
20
|
+
me.send(:count_records, me.send(:countable, obj, model_name, relation_name), arguments, ctx)
|
|
21
|
+
end)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def directive_arg(name)
|
|
27
|
+
Lighthouse::Support::DirectiveArgs.literal(@directive_node, name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def countable(obj, model_name, relation_name)
|
|
31
|
+
field_name = @field_def.name
|
|
32
|
+
if model_name
|
|
33
|
+
constantize_model(model_name)&.all
|
|
34
|
+
elsif relation_name && obj.respond_to?(relation_name)
|
|
35
|
+
obj.public_send(relation_name)
|
|
36
|
+
elsif obj.respond_to?(field_name)
|
|
37
|
+
obj.public_send(field_name)
|
|
54
38
|
end
|
|
55
39
|
end
|
|
40
|
+
|
|
41
|
+
def count_records(relation, arguments, ctx)
|
|
42
|
+
return 0 unless relation.respond_to?(:count)
|
|
43
|
+
|
|
44
|
+
args_h = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
45
|
+
relation = apply_scopes(relation)
|
|
46
|
+
relation = Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply(relation, @field_def.ast_node, args_h, ctx, self)
|
|
47
|
+
relation = apply_where_filters(relation, args_h)
|
|
48
|
+
relation.count
|
|
49
|
+
end
|
|
56
50
|
end
|