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,77 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'base'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
instance = resolver_class.new
|
|
25
|
-
if instance.respond_to?(:call)
|
|
26
|
-
result = instance.call(obj, args, ctx)
|
|
27
|
-
elsif instance.respond_to?(:resolve)
|
|
28
|
-
result = instance.resolve(obj, args, ctx)
|
|
29
|
-
else
|
|
30
|
-
result = nil
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
result
|
|
35
|
-
rescue StandardError => e
|
|
36
|
-
raise e
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
target_type_class.define_singleton_method(method_name, &resolver)
|
|
41
|
-
|
|
42
|
-
if @field_def.respond_to?(:resolve_proc=)
|
|
43
|
-
@field_def.resolve_proc = resolver
|
|
44
|
-
end
|
|
45
|
-
end
|
|
5
|
+
class Lighthouse::GraphQL::DirectiveResolvers::FieldDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
6
|
+
def self.directive_name = 'field'
|
|
7
|
+
def self.definition = 'directive @field(resolver: String, class: String) on FIELD_DEFINITION'
|
|
8
|
+
|
|
9
|
+
# On Query and Mutation the class is looked up in the queries / mutations
|
|
10
|
+
# namespaces; on any other type, in Config.resolver_namespaces.
|
|
11
|
+
def define_resolver(target_type_class)
|
|
12
|
+
field_def = @field_def
|
|
13
|
+
class_name = extract_directive_arg('resolver') || extract_directive_arg('class')
|
|
14
|
+
kind = root_kind(target_type_class)
|
|
15
|
+
|
|
16
|
+
install(target_type_class, lambda do |obj, args, ctx|
|
|
17
|
+
namespaces = kind ? Lighthouse::Config.namespaces[kind] : Lighthouse::Config.resolver_namespaces
|
|
18
|
+
klass = Lighthouse::Support::ModelResolver.constantize(class_name, namespaces: namespaces)
|
|
19
|
+
next nil unless klass
|
|
20
|
+
|
|
21
|
+
Lighthouse::GraphQL::ResolverDispatch.call(klass, field_def, obj, args, ctx)
|
|
22
|
+
end)
|
|
23
|
+
end
|
|
46
24
|
|
|
47
|
-
|
|
25
|
+
private
|
|
48
26
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
arg.value.respond_to?(:value) ? arg.value.value : arg.value
|
|
53
|
-
end
|
|
27
|
+
def extract_directive_arg(name)
|
|
28
|
+
arg = @directive_node.arguments&.find { |a| a.name == name }
|
|
29
|
+
return nil unless arg
|
|
54
30
|
|
|
55
|
-
|
|
56
|
-
|
|
31
|
+
arg.value.respond_to?(:value) ? arg.value.value : arg.value
|
|
32
|
+
end
|
|
57
33
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
elsif resolver_class.respond_to?(:new)
|
|
61
|
-
instance = resolver_class.new
|
|
62
|
-
if instance.respond_to?(:call)
|
|
63
|
-
instance.call(obj, args, ctx)
|
|
64
|
-
elsif instance.respond_to?(:resolve)
|
|
65
|
-
instance.resolve(obj, args, ctx)
|
|
66
|
-
else
|
|
67
|
-
nil
|
|
68
|
-
end
|
|
69
|
-
else
|
|
70
|
-
nil
|
|
71
|
-
end
|
|
72
|
-
rescue StandardError => e
|
|
73
|
-
raise e
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
34
|
+
def root_kind(target_type_class)
|
|
35
|
+
@opts[:root_kind] || Lighthouse::GraphQL::DirectiveResolvers::ConventionResolver.root_kind_of(target_type_class)
|
|
77
36
|
end
|
|
78
37
|
end
|
|
@@ -2,64 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'base'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
5
|
+
# @find — the single record the field's argument filters select:
|
|
6
|
+
#
|
|
7
|
+
# user(id: ID! @eq): User @find
|
|
8
|
+
# userByEmail(email: String! @eq): User @find
|
|
9
|
+
#
|
|
10
|
+
# A lookup by `id` alone is batched through the Dataloader, and an `id`
|
|
11
|
+
# argument without a filter directive selects by primary key. More than one
|
|
12
|
+
# match is an error, as in PHP Lighthouse; none returns null.
|
|
13
|
+
class Lighthouse::GraphQL::DirectiveResolvers::FindDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
14
|
+
def self.directive_name = 'find'
|
|
15
|
+
def self.definition = 'directive @find(model: String, scopes: [String!]) on FIELD_DEFINITION'
|
|
16
|
+
|
|
17
|
+
def define_resolver(target_type_class)
|
|
18
|
+
me = self
|
|
19
|
+
install(target_type_class, lambda do |_obj, args, ctx|
|
|
20
|
+
me.send(:find, Lighthouse::Support::Arguments.shallow(args), ctx)
|
|
21
|
+
end)
|
|
22
|
+
end
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
loader = ctx.dataloader.with(Lighthouse::GraphQL::DataloaderSources::ModelLoader, model_class)
|
|
25
|
-
return loader.load(id)
|
|
26
|
-
end
|
|
24
|
+
protected
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
def find(args, ctx)
|
|
27
|
+
model = model_class
|
|
28
|
+
dataloader = Lighthouse::Support::Batching.dataloader(ctx)
|
|
29
|
+
return dataloader.with(Lighthouse::GraphQL::DataloaderSources::ModelLoader, model).load(args[:id]) if dataloader && batchable?(args)
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
args = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
34
|
-
id = args[:id] || args['id']
|
|
31
|
+
records = find_relation(model, args).limit(2).to_a
|
|
32
|
+
raise ::GraphQL::ExecutionError, 'The query returned more than one result.' if records.size > 1
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
nil
|
|
39
|
-
elsif id && ctx&.dataloader
|
|
40
|
-
loader = ctx.dataloader.with(Lighthouse::GraphQL::DataloaderSources::ModelLoader, model_class)
|
|
41
|
-
loader.load(id)
|
|
42
|
-
else
|
|
43
|
-
model_class.find_by(id: id)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
34
|
+
records.first
|
|
35
|
+
end
|
|
48
36
|
|
|
49
|
-
|
|
37
|
+
# Only a lookup by `id` alone, with no scopes to apply, can be batched.
|
|
38
|
+
def batchable?(args)
|
|
39
|
+
args.keys == [:id] && !directive_arg('scopes')
|
|
40
|
+
end
|
|
50
41
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
42
|
+
def find_relation(model, args)
|
|
43
|
+
relation = apply_scopes(model.all)
|
|
44
|
+
relation = relation.where(model.primary_key => args[:id]) if args.key?(:id) && !filtered?('id')
|
|
45
|
+
apply_where_filters(relation, args)
|
|
46
|
+
end
|
|
56
47
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
end
|
|
48
|
+
# Does the argument carry a filter directive (@eq, @where, ...)? An `id`
|
|
49
|
+
# without one still selects by primary key.
|
|
50
|
+
def filtered?(name)
|
|
51
|
+
argument = (@field_def.ast_node&.arguments || []).find { |a| a.name == name }
|
|
52
|
+
registry = Lighthouse::DirectiveRegistry.default
|
|
53
|
+
argument&.directives&.any? { |d| registry.arg_builder_class(d.name) }
|
|
64
54
|
end
|
|
65
55
|
end
|
|
@@ -3,51 +3,45 @@
|
|
|
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
|
-
relation = DirectiveResolvers::BuilderApplier.apply(relation, field_ast_node, args_h, ctx, resolver_instance)
|
|
29
|
-
relation = resolver_instance.apply_where_filters(relation, args_h)
|
|
30
|
-
relation.first
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
target_type_class.define_singleton_method(method_name, &resolver)
|
|
34
|
-
@field_def.resolve_proc = resolver if @field_def.respond_to?(:resolve_proc=)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
def directive_arg(name)
|
|
40
|
-
Lighthouse::Support::DirectiveArgs.literal(@directive_node, name)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def field_type_name
|
|
44
|
-
unwrapped = @field_def.type.respond_to?(:unwrap) ? @field_def.type.unwrap : @field_def.type
|
|
45
|
-
return unwrapped.graphql_name if unwrapped.respond_to?(:graphql_name) && unwrapped.graphql_name
|
|
46
|
-
return unwrapped.name if unwrapped.respond_to?(:name) && unwrapped.name
|
|
47
|
-
|
|
48
|
-
@field_def.type.to_type_signature.to_s.gsub(/[!\[\]]/, '')
|
|
49
|
-
end
|
|
50
|
-
end
|
|
6
|
+
# @first(model: "Model") — return a single record (the first match). Like
|
|
7
|
+
# @all but `.first`, honoring `scopes:`, @builder and where/argument filters so it can
|
|
8
|
+
# be combined with @eq/@where to fetch one record by criteria.
|
|
9
|
+
class Lighthouse::GraphQL::DirectiveResolvers::FirstDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
10
|
+
def self.directive_name = 'first'
|
|
11
|
+
def self.definition = 'directive @first(model: String, scopes: [String!]) on FIELD_DEFINITION'
|
|
12
|
+
|
|
13
|
+
def define_resolver(target_type_class)
|
|
14
|
+
method_name = "resolve_field_#{@field_def.name}"
|
|
15
|
+
model_name_static = directive_arg('model') || field_type_name
|
|
16
|
+
resolver_instance = self
|
|
17
|
+
field_ast_node = @field_def.ast_node
|
|
18
|
+
|
|
19
|
+
resolver = lambda do |_obj, arguments, ctx|
|
|
20
|
+
model_class = resolver_instance.constantize_model(model_name_static)
|
|
21
|
+
next nil unless model_class
|
|
22
|
+
|
|
23
|
+
args_h = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
24
|
+
relation = resolver_instance.apply_scopes(model_class.all)
|
|
25
|
+
relation = Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply(relation, field_ast_node, args_h, ctx, resolver_instance)
|
|
26
|
+
relation = resolver_instance.apply_where_filters(relation, args_h)
|
|
27
|
+
relation.first
|
|
51
28
|
end
|
|
29
|
+
|
|
30
|
+
target_type_class.define_singleton_method(method_name, &resolver)
|
|
31
|
+
@field_def.resolve_proc = resolver if @field_def.respond_to?(:resolve_proc=)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def directive_arg(name)
|
|
37
|
+
Lighthouse::Support::DirectiveArgs.literal(@directive_node, name)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def field_type_name
|
|
41
|
+
unwrapped = @field_def.type.respond_to?(:unwrap) ? @field_def.type.unwrap : @field_def.type
|
|
42
|
+
return unwrapped.graphql_name if unwrapped.respond_to?(:graphql_name) && unwrapped.graphql_name
|
|
43
|
+
return unwrapped.name if unwrapped.respond_to?(:name) && unwrapped.name
|
|
44
|
+
|
|
45
|
+
@field_def.type.to_type_signature.to_s.gsub(/[!\[\]]/, '')
|
|
52
46
|
end
|
|
53
47
|
end
|
|
@@ -2,31 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'base'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class GuardDirectiveResolver < BaseDirectiveResolver
|
|
12
|
-
def self.directive_name = 'guard'
|
|
13
|
-
def self.definition = 'directive @guard on FIELD_DEFINITION'
|
|
5
|
+
# @guard — require an authenticated user before the field resolves.
|
|
6
|
+
# Wraps the field's resolver; raises "Unauthenticated." when no user is
|
|
7
|
+
# present (per Config.authenticated_user). Mirrors PHP Lighthouse's @guard.
|
|
8
|
+
class Lighthouse::GraphQL::DirectiveResolvers::GuardDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
9
|
+
def self.directive_name = 'guard'
|
|
10
|
+
def self.definition = 'directive @guard on FIELD_DEFINITION'
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
def define_resolver(target_type_class)
|
|
13
|
+
field_name = @field_def.name
|
|
14
|
+
method_name = "resolve_field_#{field_name}"
|
|
15
|
+
inner = Lighthouse::GraphQL::DirectiveResolvers::Resolution.capture(target_type_class, method_name, @field_def, field_name)
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
wrapper = lambda do |obj, arguments, ctx|
|
|
18
|
+
raise(::GraphQL::ExecutionError, 'Unauthenticated.') unless Lighthouse::Support::Authorization.authenticated?(ctx)
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
target_type_class.define_singleton_method(method_name, &wrapper)
|
|
27
|
-
@field_def.resolve_proc = wrapper if @field_def.respond_to?(:resolve_proc=)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
20
|
+
inner.call(obj, arguments, ctx)
|
|
30
21
|
end
|
|
22
|
+
|
|
23
|
+
target_type_class.define_singleton_method(method_name, &wrapper)
|
|
24
|
+
@field_def.resolve_proc = wrapper if @field_def.respond_to?(:resolve_proc=)
|
|
31
25
|
end
|
|
32
26
|
end
|
|
@@ -2,34 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'base'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class MethodDirectiveResolver < BaseDirectiveResolver
|
|
15
|
-
def self.directive_name = 'method'
|
|
16
|
-
def self.definition = 'directive @method(name: String) on FIELD_DEFINITION'
|
|
5
|
+
# @method(name: "someMethod") — resolve the field by calling a method on the
|
|
6
|
+
# parent object. Mirrors PHP Lighthouse's @method. Defaults to the field's
|
|
7
|
+
# own (snake_cased) name when `name` is omitted.
|
|
8
|
+
#
|
|
9
|
+
# labels: [String!]! @method(name: "label_list")
|
|
10
|
+
# canReply: Boolean @method(name: "can_reply?")
|
|
11
|
+
class Lighthouse::GraphQL::DirectiveResolvers::MethodDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
12
|
+
def self.directive_name = 'method'
|
|
13
|
+
def self.definition = 'directive @method(name: String) on FIELD_DEFINITION'
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
def define_resolver(target_type_class)
|
|
16
|
+
resolver_method = "resolve_field_#{@field_def.name}"
|
|
17
|
+
target_method = (Lighthouse::Support::DirectiveArgs.literal(@directive_node, 'name') ||
|
|
18
|
+
Lighthouse::Support::Naming.attribute(@field_def.name)).to_s
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
resolver = lambda do |obj, _arguments, _ctx|
|
|
21
|
+
next nil if obj.nil?
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
target_type_class.define_singleton_method(resolver_method, &resolver)
|
|
30
|
-
@field_def.resolve_proc = resolver if @field_def.respond_to?(:resolve_proc=)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
23
|
+
obj.respond_to?(target_method) ? obj.public_send(target_method) : nil
|
|
33
24
|
end
|
|
25
|
+
|
|
26
|
+
target_type_class.define_singleton_method(resolver_method, &resolver)
|
|
27
|
+
@field_def.resolve_proc = resolver if @field_def.respond_to?(:resolve_proc=)
|
|
34
28
|
end
|
|
35
29
|
end
|
|
@@ -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
|