lighthouse-graphql 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +168 -1
- data/README.md +46 -2
- data/docs/authorization.md +116 -57
- data/docs/best-practices.md +16 -3
- data/docs/caching.md +161 -0
- data/docs/configuration.md +83 -4
- data/docs/custom-directives.md +5 -1
- data/docs/directives.md +117 -38
- data/docs/getting-started.md +84 -0
- data/docs/mutations.md +260 -0
- data/docs/performance.md +138 -0
- data/docs/relationships.md +80 -33
- data/docs/resolvers.md +179 -0
- data/docs/validation.md +132 -0
- data/lib/generators/lighthouse/builder_generator.rb +30 -0
- data/lib/generators/lighthouse/class_generator.rb +61 -0
- data/lib/generators/lighthouse/install/install_generator.rb +127 -0
- data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +8 -0
- data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +57 -0
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +67 -0
- data/lib/generators/lighthouse/install/templates/schema.graphql.tt +19 -0
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -0
- data/lib/generators/lighthouse/mutation_generator.rb +30 -0
- data/lib/generators/lighthouse/query_generator.rb +29 -0
- data/lib/generators/lighthouse/validator_generator.rb +36 -0
- data/lib/lighthouse/cache_control.rb +126 -0
- data/lib/lighthouse/caching.rb +68 -0
- data/lib/lighthouse/config.rb +85 -19
- data/lib/lighthouse/contracts/arg_builder.rb +8 -12
- data/lib/lighthouse/contracts/arg_manipulator.rb +8 -12
- data/lib/lighthouse/contracts/directive.rb +10 -14
- data/lib/lighthouse/contracts/field_manipulator.rb +8 -12
- data/lib/lighthouse/contracts/field_middleware.rb +7 -11
- data/lib/lighthouse/contracts/field_resolver.rb +9 -13
- data/lib/lighthouse/dataloader_sources/association_loader.rb +28 -19
- data/lib/lighthouse/dataloader_sources/model_loader.rb +10 -14
- data/lib/lighthouse/dataloader_sources/relation_count_loader.rb +16 -0
- data/lib/lighthouse/dataloader_sources/relation_page_loader.rb +18 -0
- data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +57 -69
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +26 -57
- data/lib/lighthouse/directive_resolvers/association_resolvers.rb +161 -0
- data/lib/lighthouse/directive_resolvers/auth_resolver.rb +41 -38
- data/lib/lighthouse/directive_resolvers/base.rb +142 -86
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +58 -56
- data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +133 -0
- data/lib/lighthouse/directive_resolvers/can_resolvers.rb +177 -0
- data/lib/lighthouse/directive_resolvers/convention_resolver.rb +70 -0
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +42 -48
- data/lib/lighthouse/directive_resolvers/field_resolver.rb +27 -68
- data/lib/lighthouse/directive_resolvers/find_resolver.rb +42 -52
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +39 -45
- data/lib/lighthouse/directive_resolvers/guard_resolver.rb +16 -22
- data/lib/lighthouse/directive_resolvers/method_resolver.rb +19 -25
- data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +219 -0
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +70 -128
- data/lib/lighthouse/directive_resolvers/rename_resolver.rb +18 -24
- data/lib/lighthouse/directive_resolvers/resolution.rb +31 -27
- data/lib/lighthouse/directive_resolvers/validator_resolver.rb +193 -0
- data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +164 -163
- data/lib/lighthouse/directives/arguments/base.rb +23 -31
- data/lib/lighthouse/directives/arguments/filters.rb +113 -93
- data/lib/lighthouse/directives/arguments/order_by.rb +41 -41
- data/lib/lighthouse/directives/pagination_info_types.rb +69 -0
- data/lib/lighthouse/directives/pagination_manipulator.rb +176 -0
- data/lib/lighthouse/directives/registry.rb +210 -141
- data/lib/lighthouse/directives/where_conditions_directive.rb +236 -200
- data/lib/lighthouse/errors.rb +27 -0
- data/lib/lighthouse/federation_directives.rb +53 -0
- data/lib/lighthouse/field_resolver_stack.rb +56 -0
- data/lib/lighthouse/generated_resolvers.rb +82 -0
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/graphql_ruby_patches.rb +18 -0
- data/lib/lighthouse/ide_helper.rb +98 -0
- data/lib/lighthouse/namespaces.rb +21 -0
- data/lib/lighthouse/nested_mutations/belongs_to_operations.rb +50 -0
- data/lib/lighthouse/nested_mutations/executor.rb +149 -0
- data/lib/lighthouse/nested_mutations/has_operations.rb +66 -0
- data/lib/lighthouse/nested_mutations/many_to_many_operations.rb +44 -0
- data/lib/lighthouse/nested_mutations/partitioner.rb +125 -0
- data/lib/lighthouse/nested_mutations.rb +210 -0
- data/lib/lighthouse/pagination.rb +221 -0
- data/lib/lighthouse/persisted_queries.rb +167 -0
- data/lib/lighthouse/railtie.rb +26 -0
- data/lib/lighthouse/rb_lighthouse.rb +50 -29
- data/lib/lighthouse/reference_resolver.rb +13 -15
- data/lib/lighthouse/relation_batch.rb +156 -0
- data/lib/lighthouse/resolver_dispatch.rb +26 -0
- data/lib/lighthouse/schema_cache.rb +120 -0
- data/lib/lighthouse/schema_factory.rb +27 -37
- data/lib/lighthouse/schema_generator.rb +196 -239
- data/lib/lighthouse/schema_implementation.rb +51 -48
- data/lib/lighthouse/sdl_loader.rb +17 -20
- data/lib/lighthouse/support/arguments.rb +119 -0
- data/lib/lighthouse/support/authorization.rb +18 -19
- data/lib/lighthouse/support/batching.rb +16 -0
- data/lib/lighthouse/support/directive_args.rb +26 -24
- data/lib/lighthouse/support/model_resolver.rb +56 -56
- data/lib/lighthouse/support/naming.rb +55 -59
- data/lib/lighthouse/tasks/lighthouse.rake +33 -0
- data/lib/lighthouse/where_conditions/operator_map.rb +25 -29
- metadata +52 -64
- data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +0 -56
- data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +0 -47
- data/lib/lighthouse/directive_resolvers/can_resolver.rb +0 -86
- data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +0 -109
- data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +0 -43
- data/lib/lighthouse/directives/relation_directive.rb +0 -332
|
@@ -1,91 +1,147 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
3
|
+
class Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
4
|
+
def initialize(field_def, type_def, directive_node, opts = {})
|
|
5
|
+
@field_def = field_def
|
|
6
|
+
@type_def = type_def
|
|
7
|
+
@directive_node = directive_node
|
|
8
|
+
@opts = opts
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def define_resolver(target_type_class)
|
|
12
|
+
raise NotImplementedError
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def apply_where_filters(relation, args)
|
|
16
|
+
return relation unless relation.respond_to?(:where)
|
|
17
|
+
|
|
18
|
+
args_h = args.respond_to?(:to_h) ? args.to_h : (args || {})
|
|
19
|
+
|
|
20
|
+
relation = apply_where_conditions(relation, args_h)
|
|
21
|
+
@field_def.ast_node.arguments.each do |arg_node|
|
|
22
|
+
relation = apply_where_has_conditions(relation, arg_node, args_h)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
apply_arg_builders(relation, args_h)
|
|
26
|
+
rescue ::GraphQL::ExecutionError
|
|
27
|
+
raise
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
Lighthouse::Config.handle_error(e)
|
|
30
|
+
relation
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Compose argument-builder directives (@eq, @neq, @in, @notIn, @like,
|
|
34
|
+
# @where, @orderBy) onto the relation, in SDL argument order, for each
|
|
35
|
+
# field argument that carries one and that the client supplied a value
|
|
36
|
+
# for.
|
|
37
|
+
def apply_arg_builders(relation, args_h)
|
|
38
|
+
return relation unless relation.respond_to?(:where)
|
|
39
|
+
return relation unless @field_def.ast_node
|
|
40
|
+
|
|
41
|
+
arg_builders.each do |arg_node, dnode, builder_class|
|
|
42
|
+
value = Lighthouse::Support::Naming.fetch_arg(args_h, arg_node.name)
|
|
43
|
+
next if value.nil?
|
|
44
|
+
|
|
45
|
+
relation = builder_class.new(arg_node, dnode).handle_builder(relation, value)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
relation
|
|
49
|
+
rescue ::GraphQL::ExecutionError
|
|
50
|
+
raise
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
Lighthouse::Config.handle_error(e)
|
|
53
|
+
relation
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def extract_relation_name_from_directive(arg_node)
|
|
57
|
+
directive = arg_node.directives.find { |dir| dir.name == 'whereHasConditions' }
|
|
58
|
+
return nil unless directive
|
|
59
|
+
|
|
60
|
+
relation_arg = directive.arguments.find { |arg| arg.name == 'relation' }
|
|
61
|
+
return nil unless relation_arg
|
|
62
|
+
|
|
63
|
+
relation_arg.value.respond_to?(:value) ? relation_arg.value.value : relation_arg.value
|
|
64
|
+
rescue StandardError
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def constantize_model(name)
|
|
69
|
+
Lighthouse::Support::ModelResolver.constantize(name)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The model scopes the directive's `scopes:` argument names, in order.
|
|
73
|
+
def apply_scopes(relation)
|
|
74
|
+
Array(directive_arg('scopes')).reduce(relation) { |acc, scope| acc.public_send(scope) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
protected
|
|
78
|
+
|
|
79
|
+
def directive_arg(name)
|
|
80
|
+
Lighthouse::Support::DirectiveArgs.literal(@directive_node, name)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def resolver_method_name
|
|
84
|
+
"resolve_field_#{@field_def.name}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The field's resolver so far, for a directive that wraps it.
|
|
88
|
+
def capture_inner(target_type_class)
|
|
89
|
+
Lighthouse::GraphQL::DirectiveResolvers::Resolution.capture(target_type_class, resolver_method_name, @field_def, @field_def.name)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def install(target_type_class, callable)
|
|
93
|
+
target_type_class.define_singleton_method(resolver_method_name, &callable)
|
|
94
|
+
@field_def.resolve_proc = callable if @field_def.respond_to?(:resolve_proc=)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The model a directive acts on: its `model:` argument, the `model:` of
|
|
98
|
+
# another directive on the field (`@count(model: "Post") @canQuery`), or
|
|
99
|
+
# the field's return type without a generated Paginator/Connection suffix.
|
|
100
|
+
def model_class
|
|
101
|
+
name = directive_arg('model') || sibling_model_name || return_type_model_name
|
|
102
|
+
constantize_model(name) || raise(Lighthouse::DefinitionError,
|
|
103
|
+
"@#{@directive_node&.name} on #{@type_def.graphql_name}.#{@field_def.name}: no model #{name}")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def sibling_model_name
|
|
109
|
+
(@field_def.ast_node&.directives || []).filter_map do |d|
|
|
110
|
+
Lighthouse::Support::DirectiveArgs.literal(d, 'model')
|
|
111
|
+
end.first
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def return_type_model_name
|
|
115
|
+
@field_def.type.unwrap.graphql_name.sub(/(SimplePaginator|Paginator|Connection)\z/, '')
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The `where:` argument (@whereConditions), when the client supplied one.
|
|
119
|
+
def apply_where_conditions(relation, args_h)
|
|
120
|
+
where_condition = args_h[:where] || args_h['where']
|
|
121
|
+
return relation unless where_condition
|
|
122
|
+
|
|
123
|
+
Lighthouse::GraphQL::DirectiveResolvers::WhereConditionsApplier.apply(relation, where_condition)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# A `whereHasConditions` argument the client supplied filters the
|
|
127
|
+
# relation by the related records matching its condition.
|
|
128
|
+
def apply_where_has_conditions(relation, arg_node, args_h)
|
|
129
|
+
return relation unless arg_node.directives.any? { |dir| dir.name == 'whereHasConditions' }
|
|
130
|
+
|
|
131
|
+
arg_value = Lighthouse::Support::Naming.fetch_arg(args_h, arg_node.name)
|
|
132
|
+
return relation unless arg_value
|
|
133
|
+
|
|
134
|
+
relation_name = extract_relation_name_from_directive(arg_node) || Lighthouse::Support::Naming.derive_relation_name(arg_node.name)
|
|
135
|
+
Lighthouse::GraphQL::DirectiveResolvers::WhereConditionsApplier.apply_has_argument(relation, arg_node.name, arg_value, relation_name)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Every (argument, directive, builder class) on the field, in SDL order.
|
|
139
|
+
def arg_builders
|
|
140
|
+
registry = Lighthouse::DirectiveRegistry.default
|
|
141
|
+
(@field_def.ast_node.arguments || []).flat_map do |arg_node|
|
|
142
|
+
(arg_node.directives || []).filter_map do |dnode|
|
|
143
|
+
builder_class = registry.arg_builder_class(dnode.name)
|
|
144
|
+
[arg_node, dnode, builder_class] if builder_class
|
|
89
145
|
end
|
|
90
146
|
end
|
|
91
147
|
end
|
|
@@ -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
|