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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +175 -1
  3. data/README.md +47 -3
  4. data/docs/authorization.md +116 -57
  5. data/docs/best-practices.md +16 -3
  6. data/docs/caching.md +161 -0
  7. data/docs/configuration.md +83 -4
  8. data/docs/custom-directives.md +5 -1
  9. data/docs/directives.md +117 -38
  10. data/docs/getting-started.md +84 -0
  11. data/docs/motivation.md +184 -0
  12. data/docs/mutations.md +260 -0
  13. data/docs/performance.md +138 -0
  14. data/docs/relationships.md +80 -33
  15. data/docs/resolvers.md +189 -0
  16. data/docs/validation.md +132 -0
  17. data/lib/generators/lighthouse/builder_generator.rb +30 -0
  18. data/lib/generators/lighthouse/class_generator.rb +61 -0
  19. data/lib/generators/lighthouse/install/install_generator.rb +127 -0
  20. data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +8 -0
  21. data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +57 -0
  22. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +67 -0
  23. data/lib/generators/lighthouse/install/templates/schema.graphql.tt +19 -0
  24. data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -0
  25. data/lib/generators/lighthouse/mutation_generator.rb +30 -0
  26. data/lib/generators/lighthouse/query_generator.rb +29 -0
  27. data/lib/generators/lighthouse/validator_generator.rb +36 -0
  28. data/lib/lighthouse/cache_control.rb +126 -0
  29. data/lib/lighthouse/caching.rb +68 -0
  30. data/lib/lighthouse/config.rb +85 -19
  31. data/lib/lighthouse/contracts/arg_builder.rb +8 -12
  32. data/lib/lighthouse/contracts/arg_manipulator.rb +8 -12
  33. data/lib/lighthouse/contracts/directive.rb +10 -14
  34. data/lib/lighthouse/contracts/field_manipulator.rb +8 -12
  35. data/lib/lighthouse/contracts/field_middleware.rb +7 -11
  36. data/lib/lighthouse/contracts/field_resolver.rb +9 -13
  37. data/lib/lighthouse/dataloader_sources/association_loader.rb +28 -19
  38. data/lib/lighthouse/dataloader_sources/model_loader.rb +10 -14
  39. data/lib/lighthouse/dataloader_sources/relation_count_loader.rb +16 -0
  40. data/lib/lighthouse/dataloader_sources/relation_page_loader.rb +18 -0
  41. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +57 -69
  42. data/lib/lighthouse/directive_resolvers/all_resolver.rb +26 -57
  43. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +161 -0
  44. data/lib/lighthouse/directive_resolvers/auth_resolver.rb +41 -38
  45. data/lib/lighthouse/directive_resolvers/base.rb +142 -86
  46. data/lib/lighthouse/directive_resolvers/builder_applier.rb +58 -56
  47. data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +133 -0
  48. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +177 -0
  49. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +70 -0
  50. data/lib/lighthouse/directive_resolvers/count_resolver.rb +42 -48
  51. data/lib/lighthouse/directive_resolvers/field_resolver.rb +27 -68
  52. data/lib/lighthouse/directive_resolvers/find_resolver.rb +42 -52
  53. data/lib/lighthouse/directive_resolvers/first_resolver.rb +39 -45
  54. data/lib/lighthouse/directive_resolvers/guard_resolver.rb +16 -22
  55. data/lib/lighthouse/directive_resolvers/method_resolver.rb +19 -25
  56. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +219 -0
  57. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +70 -128
  58. data/lib/lighthouse/directive_resolvers/rename_resolver.rb +18 -24
  59. data/lib/lighthouse/directive_resolvers/resolution.rb +31 -27
  60. data/lib/lighthouse/directive_resolvers/validator_resolver.rb +193 -0
  61. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +164 -163
  62. data/lib/lighthouse/directives/arguments/base.rb +23 -31
  63. data/lib/lighthouse/directives/arguments/filters.rb +113 -93
  64. data/lib/lighthouse/directives/arguments/order_by.rb +41 -41
  65. data/lib/lighthouse/directives/pagination_info_types.rb +69 -0
  66. data/lib/lighthouse/directives/pagination_manipulator.rb +176 -0
  67. data/lib/lighthouse/directives/registry.rb +210 -141
  68. data/lib/lighthouse/directives/where_conditions_directive.rb +236 -200
  69. data/lib/lighthouse/errors.rb +27 -0
  70. data/lib/lighthouse/federation_directives.rb +53 -0
  71. data/lib/lighthouse/field_resolver_stack.rb +56 -0
  72. data/lib/lighthouse/generated_resolvers.rb +82 -0
  73. data/lib/lighthouse/graphql/version.rb +1 -1
  74. data/lib/lighthouse/graphql_ruby_patches.rb +18 -0
  75. data/lib/lighthouse/ide_helper.rb +98 -0
  76. data/lib/lighthouse/namespaces.rb +21 -0
  77. data/lib/lighthouse/nested_mutations/belongs_to_operations.rb +50 -0
  78. data/lib/lighthouse/nested_mutations/executor.rb +149 -0
  79. data/lib/lighthouse/nested_mutations/has_operations.rb +66 -0
  80. data/lib/lighthouse/nested_mutations/many_to_many_operations.rb +44 -0
  81. data/lib/lighthouse/nested_mutations/partitioner.rb +125 -0
  82. data/lib/lighthouse/nested_mutations.rb +210 -0
  83. data/lib/lighthouse/pagination.rb +221 -0
  84. data/lib/lighthouse/persisted_queries.rb +167 -0
  85. data/lib/lighthouse/railtie.rb +26 -0
  86. data/lib/lighthouse/rb_lighthouse.rb +50 -29
  87. data/lib/lighthouse/reference_resolver.rb +13 -15
  88. data/lib/lighthouse/relation_batch.rb +156 -0
  89. data/lib/lighthouse/resolver_dispatch.rb +26 -0
  90. data/lib/lighthouse/schema_cache.rb +120 -0
  91. data/lib/lighthouse/schema_factory.rb +27 -37
  92. data/lib/lighthouse/schema_generator.rb +196 -239
  93. data/lib/lighthouse/schema_implementation.rb +51 -48
  94. data/lib/lighthouse/sdl_loader.rb +17 -20
  95. data/lib/lighthouse/support/arguments.rb +119 -0
  96. data/lib/lighthouse/support/authorization.rb +18 -19
  97. data/lib/lighthouse/support/batching.rb +16 -0
  98. data/lib/lighthouse/support/directive_args.rb +26 -24
  99. data/lib/lighthouse/support/model_resolver.rb +56 -56
  100. data/lib/lighthouse/support/naming.rb +55 -59
  101. data/lib/lighthouse/tasks/lighthouse.rake +33 -0
  102. data/lib/lighthouse/where_conditions/operator_map.rb +25 -29
  103. data/sig/lighthouse/directives.rbs +158 -0
  104. data/sig/lighthouse/graphql.rbs +78 -0
  105. data/sig/lighthouse.rbs +142 -0
  106. metadata +56 -64
  107. data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +0 -56
  108. data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +0 -47
  109. data/lib/lighthouse/directive_resolvers/can_resolver.rb +0 -86
  110. data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +0 -109
  111. data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +0 -43
  112. data/lib/lighthouse/directives/relation_directive.rb +0 -332
@@ -2,62 +2,64 @@
2
2
 
3
3
  require_relative 'base'
4
4
 
5
- module Lighthouse
6
- module GraphQL
7
- module DirectiveResolvers
8
- class BuilderApplier
9
- def self.apply(relation, field_def_ast_node, args, ctx, resolver_instance)
10
- return relation unless field_def_ast_node && field_def_ast_node.directives
11
-
12
- builder_directives = field_def_ast_node.directives.select { |d| d.name == 'builder' }
13
- return relation if builder_directives.empty?
14
-
15
- builder_directives.each do |directive_node|
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
- module Lighthouse
7
- module GraphQL
8
- module DirectiveResolvers
9
- # @count(model: "Model", relation: "assoc") — return the number of records.
10
- # Counts `Model.all` when `model:` is given, otherwise the named relation on
11
- # the parent object (or a relation matching the field name). Honors @builder
12
- # and where/argument filters before counting.
13
- class CountDirectiveResolver < BaseDirectiveResolver
14
- def self.directive_name = 'count'
15
- def self.definition = 'directive @count(model: String, relation: String, scopes: [String!]) on FIELD_DEFINITION'
16
-
17
- def define_resolver(target_type_class)
18
- field_name = @field_def.name
19
- method_name = "resolve_field_#{field_name}"
20
- model_name = directive_arg('model')
21
- relation_name = directive_arg('relation')
22
- resolver_instance = self
23
- field_ast_node = @field_def.ast_node
24
-
25
- resolver = lambda do |obj, arguments, ctx|
26
- args_h = arguments.respond_to?(:to_h) ? arguments.to_h : {}
27
- relation =
28
- if model_name
29
- model_class = resolver_instance.constantize_model(model_name)
30
- model_class&.all
31
- elsif relation_name && obj.respond_to?(relation_name)
32
- obj.public_send(relation_name)
33
- elsif obj.respond_to?(field_name)
34
- obj.public_send(field_name)
35
- end
36
-
37
- next 0 unless relation.respond_to?(:count)
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