lighthouse-graphql 0.1.1 → 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 +154 -1
- data/README.md +42 -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 +51 -63
- data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +0 -56
- data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +0 -47
- data/lib/lighthouse/directive_resolvers/can_resolver.rb +0 -86
- data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +0 -109
- data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +0 -43
- data/lib/lighthouse/directives/relation_directive.rb +0 -332
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Executable schema built by Lighthouse from the SDL in <%= sdl_folder %>/. The
|
|
4
|
+
# Dataloader batches relationship loads (@hasMany, @belongsTo, ...) to avoid N+1.
|
|
5
|
+
<%= schema_name %> = Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
6
|
+
options: {
|
|
7
|
+
using: { GraphQL::Dataloader => {} }
|
|
8
|
+
# base_types: { object: Types::BaseObject } # your graphql-ruby base classes
|
|
9
|
+
}
|
|
10
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'class_generator'
|
|
4
|
+
|
|
5
|
+
# `rails generate lighthouse:mutation PublishPost` — a GraphQL::Schema::Mutation
|
|
6
|
+
# that `Mutation.publishPost` resolves to by convention.
|
|
7
|
+
class Lighthouse::Generators::MutationGenerator < Lighthouse::Generators::ClassGenerator
|
|
8
|
+
desc 'Generate a mutation class (GraphQL::Schema::Mutation) in app/graphql/mutations.'
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def kind = :mutations
|
|
13
|
+
|
|
14
|
+
def body
|
|
15
|
+
<<~RUBY
|
|
16
|
+
# Resolves Mutation.#{class_basename.camelize(:lower)}. Keyword arguments are the field's
|
|
17
|
+
# arguments in snake_case; `object` and `context` are available. Raise
|
|
18
|
+
# GraphQL::ExecutionError for failures the client should see.
|
|
19
|
+
class #{class_basename} < GraphQL::Schema::Mutation
|
|
20
|
+
def resolve(**args)
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
RUBY
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def usage
|
|
28
|
+
"Mutation.#{class_basename.camelize(:lower)} resolves to #{full_class_name} — add the field to your SDL without a resolving directive."
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'class_generator'
|
|
4
|
+
|
|
5
|
+
# `rails generate lighthouse:query LatestPosts` — a GraphQL::Schema::Resolver
|
|
6
|
+
# that `Query.latestPosts` resolves to by convention.
|
|
7
|
+
class Lighthouse::Generators::QueryGenerator < Lighthouse::Generators::ClassGenerator
|
|
8
|
+
desc 'Generate a query class (GraphQL::Schema::Resolver) in app/graphql/queries.'
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def kind = :queries
|
|
13
|
+
|
|
14
|
+
def body
|
|
15
|
+
<<~RUBY
|
|
16
|
+
# Resolves Query.#{class_basename.camelize(:lower)}. Keyword arguments are the field's
|
|
17
|
+
# arguments in snake_case; `object` and `context` are available.
|
|
18
|
+
class #{class_basename} < GraphQL::Schema::Resolver
|
|
19
|
+
def resolve(**args)
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
RUBY
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def usage
|
|
27
|
+
"Query.#{class_basename.camelize(:lower)} resolves to #{full_class_name} — add the field to your SDL without a resolving directive."
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'class_generator'
|
|
4
|
+
|
|
5
|
+
# `rails generate lighthouse:validator CreatePostInput` — a
|
|
6
|
+
# Dry::Validation::Contract for @validator. Name it after an input type
|
|
7
|
+
# (CreatePostInputValidator) or a field (Mutation::CreatePostValidator).
|
|
8
|
+
class Lighthouse::Generators::ValidatorGenerator < Lighthouse::Generators::ClassGenerator
|
|
9
|
+
desc 'Generate a validator (Dry::Validation::Contract) for @validator in app/graphql/validators.'
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def kind = :validators
|
|
14
|
+
|
|
15
|
+
def class_basename
|
|
16
|
+
base = super
|
|
17
|
+
base.end_with?('Validator') ? base : "#{base}Validator"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def body
|
|
21
|
+
<<~RUBY
|
|
22
|
+
# Arguments arrive with snake_case keys. Remove `option :graphql_context`
|
|
23
|
+
# if the rules don't need the GraphQL context.
|
|
24
|
+
class #{class_basename} < Dry::Validation::Contract
|
|
25
|
+
option :graphql_context, optional: true
|
|
26
|
+
|
|
27
|
+
params do
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
RUBY
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def usage
|
|
34
|
+
'Needs the dry-validation gem. Add @validator to the input type or field it is named after.'
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# @cacheControl — compute the HTTP Cache-Control header a response may carry
|
|
4
|
+
# from the fields a query selects, as PHP Lighthouse (after Apollo) does:
|
|
5
|
+
#
|
|
6
|
+
# - max-age is the lowest maxAge among the contributing fields; 0 means
|
|
7
|
+
# `no-cache`.
|
|
8
|
+
# - The response is `private` if any selected field's scope is PRIVATE.
|
|
9
|
+
# - Defaults: a root field is maxAge 0 and PRIVATE; any other field returning
|
|
10
|
+
# an object, interface or union is maxAge 0 unless its type carries
|
|
11
|
+
# @cacheControl; scalar fields inherit their parent and do not contribute.
|
|
12
|
+
# A directive on the field wins over one on its type, and its omitted
|
|
13
|
+
# arguments take the directive defaults (maxAge 0, PUBLIC).
|
|
14
|
+
# - `inheritMaxAge: true` uses the parent field's maxAge.
|
|
15
|
+
# - A mutation is never cacheable.
|
|
16
|
+
#
|
|
17
|
+
# The schema runs Analyzer on every query when its SDL uses @cacheControl;
|
|
18
|
+
# read the result with CacheControl.header(result) and set it on the
|
|
19
|
+
# response (see `apply`).
|
|
20
|
+
module Lighthouse::GraphQL::CacheControl
|
|
21
|
+
DEFINITION = 'directive @cacheControl(maxAge: Int! = 0, scope: CacheControlScope! = PUBLIC, ' \
|
|
22
|
+
'inheritMaxAge: Boolean! = false) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION'
|
|
23
|
+
|
|
24
|
+
TYPE_DEFINITIONS = {
|
|
25
|
+
'CacheControlScope' => <<~GRAPHQL.chomp
|
|
26
|
+
"Who a cached response may be shared with."
|
|
27
|
+
enum CacheControlScope {
|
|
28
|
+
"The same for every user: shared caches (CDNs, proxies) may store it."
|
|
29
|
+
PUBLIC
|
|
30
|
+
"Specific to the user: only the user's own client may store it."
|
|
31
|
+
PRIVATE
|
|
32
|
+
}
|
|
33
|
+
GRAPHQL
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
CONTEXT_KEY = :lighthouse_cache_control
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
# The Cache-Control value for an executed query (a GraphQL::Query::Result
|
|
41
|
+
# or GraphQL::Query), or nil when the schema does not use @cacheControl.
|
|
42
|
+
def header(result)
|
|
43
|
+
query = result.respond_to?(:query) ? result.query : result
|
|
44
|
+
query.respond_to?(:context) ? query.context[CONTEXT_KEY] : nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Set the header on a Rails (or Rack) response; a no-op without one.
|
|
48
|
+
def apply(response, result)
|
|
49
|
+
value = header(result)
|
|
50
|
+
response.headers['Cache-Control'] = value if value
|
|
51
|
+
value
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_header(max_age, private_scope)
|
|
55
|
+
visibility = private_scope ? 'private' : 'public'
|
|
56
|
+
max_age.positive? ? "max-age=#{max_age}, #{visibility}" : "no-cache#{', private' if private_scope}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class Analyzer < ::GraphQL::Analysis::Analyzer
|
|
60
|
+
def initialize(subject)
|
|
61
|
+
super
|
|
62
|
+
@parents = []
|
|
63
|
+
@max_age = nil
|
|
64
|
+
@private = false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def on_enter_field(node, _parent, visitor)
|
|
68
|
+
field = visitor.field_definition
|
|
69
|
+
return @parents.push(@parents.last) if field.nil? || visitor.skipping?
|
|
70
|
+
|
|
71
|
+
max_age, scope = hint_for(field)
|
|
72
|
+
unless max_age.nil?
|
|
73
|
+
@max_age = [@max_age, max_age].compact.min
|
|
74
|
+
@private ||= scope == 'PRIVATE'
|
|
75
|
+
end
|
|
76
|
+
@parents.push(max_age.nil? ? @parents.last : max_age)
|
|
77
|
+
node
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def on_leave_field(_node, _parent, _visitor)
|
|
81
|
+
@parents.pop
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def result
|
|
85
|
+
value =
|
|
86
|
+
if query.mutation?
|
|
87
|
+
'no-cache'
|
|
88
|
+
else
|
|
89
|
+
Lighthouse::GraphQL::CacheControl.to_header(@max_age || 0, @private)
|
|
90
|
+
end
|
|
91
|
+
query.context[CONTEXT_KEY] = value
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def root?
|
|
97
|
+
@parents.empty?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# [maxAge, scope] the field contributes, or [nil, nil] to inherit.
|
|
101
|
+
def hint_for(field)
|
|
102
|
+
directive = cache_control(field.ast_node)
|
|
103
|
+
return from_directive(directive) if directive
|
|
104
|
+
return [0, 'PRIVATE'] if root?
|
|
105
|
+
|
|
106
|
+
return_type = field.type.unwrap
|
|
107
|
+
return [nil, nil] if return_type.kind.leaf?
|
|
108
|
+
|
|
109
|
+
type_directive = cache_control(return_type.ast_node)
|
|
110
|
+
type_directive ? from_directive(type_directive) : [0, 'PUBLIC']
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def from_directive(directive)
|
|
114
|
+
scope = Lighthouse::Support::DirectiveArgs.literal(directive, 'scope') || 'PUBLIC'
|
|
115
|
+
if Lighthouse::Support::DirectiveArgs.literal(directive, 'inheritMaxAge')
|
|
116
|
+
[@parents.last || 0, scope]
|
|
117
|
+
else
|
|
118
|
+
[Lighthouse::Support::DirectiveArgs.literal(directive, 'maxAge') || 0, scope]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def cache_control(ast_node)
|
|
123
|
+
ast_node&.directives&.find { |d| d.name == 'cacheControl' }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
|
|
7
|
+
# Keys and invalidation for @cache / @clearCache over Config.cache_store
|
|
8
|
+
# (Rails.cache by default).
|
|
9
|
+
#
|
|
10
|
+
# PHP Lighthouse invalidates with cache tags, which Rails cache stores do not
|
|
11
|
+
# have (and `delete_matched` is not universal). Instead every entry key embeds
|
|
12
|
+
# four version tokens — the type, the type + parent id, the type + field, and
|
|
13
|
+
# the type + parent id + field — and @clearCache replaces the matching token.
|
|
14
|
+
# Old entries are never read again and expire on their own. A token that
|
|
15
|
+
# was evicted is re-created with a fresh value, so eviction only ever causes
|
|
16
|
+
# misses, never stale reads.
|
|
17
|
+
module Lighthouse::GraphQL::Caching
|
|
18
|
+
PREFIX = 'lighthouse'
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def store
|
|
23
|
+
Lighthouse::Config.cache_store
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def entry_key(type, id, field, args, user_key)
|
|
27
|
+
tokens = versions(version_keys(type, id, field))
|
|
28
|
+
[PREFIX, type, id, field, digest(args), user_key, *tokens].join(':')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @clearCache: invalidate everything cached for `type`, narrowed to one
|
|
32
|
+
# parent `id` and/or one `field` when given.
|
|
33
|
+
def clear(type, id: nil, field: nil)
|
|
34
|
+
key = version_key(type, id, field)
|
|
35
|
+
store.write(key, SecureRandom.hex(8))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def version_keys(type, id, field)
|
|
39
|
+
[version_key(type, nil, nil), version_key(type, id, nil), version_key(type, nil, field), version_key(type, id, field)]
|
|
40
|
+
.uniq
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def version_key(type, id, field)
|
|
44
|
+
[PREFIX, 'version', type, id, field].join(':')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def versions(keys)
|
|
48
|
+
found = store.read_multi(*keys)
|
|
49
|
+
keys.map do |key|
|
|
50
|
+
found[key] || begin
|
|
51
|
+
store.write(key, SecureRandom.hex(8), unless_exist: true)
|
|
52
|
+
store.read(key)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def digest(args)
|
|
58
|
+
Digest::SHA256.hexdigest(JSON.generate(canonical(Lighthouse::Support::Arguments.plain(args))))[0, 32]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def canonical(value)
|
|
62
|
+
case value
|
|
63
|
+
when Hash then value.map { |k, v| [k.to_s, canonical(v)] }.sort_by(&:first)
|
|
64
|
+
when Array then value.map { |v| canonical(v) }
|
|
65
|
+
else value
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/lighthouse/config.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'active_support/cache'
|
|
4
|
+
|
|
3
5
|
module Lighthouse
|
|
4
6
|
# Convenience entrypoint: `Lighthouse.configure { |c| c.resolver_namespaces = [...] }`.
|
|
5
7
|
def self.configure(&block)
|
|
@@ -7,12 +9,30 @@ module Lighthouse
|
|
|
7
9
|
end
|
|
8
10
|
|
|
9
11
|
module Config
|
|
12
|
+
# Where each kind of class a schema refers to by name is looked up. Query and
|
|
13
|
+
# mutation fields resolve to GraphQL::Schema::Resolver / Mutation classes,
|
|
14
|
+
# builders compose onto a relation, validators check arguments: distinct
|
|
15
|
+
# contracts, so distinct namespaces (app/graphql/queries, .../mutations, ...).
|
|
16
|
+
DEFAULT_NAMESPACES = {
|
|
17
|
+
queries: ['Queries'],
|
|
18
|
+
mutations: ['Mutations'],
|
|
19
|
+
builders: ['Builders'],
|
|
20
|
+
validators: ['Validators']
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
# Every setting reset! forgets, so the next read falls back to its default.
|
|
24
|
+
SETTINGS = %i[
|
|
25
|
+
sdl_folder resolver_namespaces namespaces cache_store transactional_mutations pagination_default_count pagination_max_count
|
|
26
|
+
schema_cache schema_cache_path watch_sdl persisted_queries persisted_queries_store persisted_queries_ttl
|
|
27
|
+
persisted_queries_manifest persisted_queries_only on_error directive_map authenticated_user policy_user authorizer
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
10
30
|
class << self
|
|
11
|
-
attr_writer :sdl_folder, :resolver_namespaces, :on_error,
|
|
12
|
-
:
|
|
31
|
+
attr_writer :sdl_folder, :resolver_namespaces, :on_error, :authenticated_user, :policy_user, :authorizer, :cache_store,
|
|
32
|
+
:transactional_mutations, :schema_cache, :watch_sdl, :persisted_queries, :persisted_queries_store, :persisted_queries_only
|
|
13
33
|
|
|
14
34
|
def sdl_folder
|
|
15
|
-
@sdl_folder ||= (defined?(Rails) && Rails.respond_to?(:root) ?
|
|
35
|
+
@sdl_folder ||= (defined?(Rails) && Rails.respond_to?(:root) ? Rails.root.join('graphql').to_s : nil)
|
|
16
36
|
end
|
|
17
37
|
|
|
18
38
|
def directive_map
|
|
@@ -27,6 +47,57 @@ module Lighthouse
|
|
|
27
47
|
@resolver_namespaces ||= ['App::Graphql']
|
|
28
48
|
end
|
|
29
49
|
|
|
50
|
+
# { queries:, mutations:, builders:, validators: } => [namespace, ...].
|
|
51
|
+
# Assigning a partial hash overrides only the kinds it names.
|
|
52
|
+
def namespaces
|
|
53
|
+
@namespaces ||= DEFAULT_NAMESPACES.transform_values(&:dup)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def namespaces=(overrides)
|
|
57
|
+
@namespaces = namespaces.merge(overrides.to_h.transform_keys(&:to_sym).transform_values { |v| Array(v) })
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Store backing @cache and @clearCache. Defaults to Rails.cache.
|
|
61
|
+
def cache_store
|
|
62
|
+
@cache_store ||=
|
|
63
|
+
if defined?(Rails) && Rails.respond_to?(:cache) && Rails.cache
|
|
64
|
+
Rails.cache
|
|
65
|
+
else
|
|
66
|
+
ActiveSupport::Cache::MemoryStore.new
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# --- Scale options: all off by default ----------------------------------
|
|
71
|
+
#
|
|
72
|
+
# schema_cache cache the built schema document in tmp/cache (SchemaCache)
|
|
73
|
+
# schema_cache_path where; default Rails.root/tmp/cache/lighthouse
|
|
74
|
+
# watch_sdl reload the app (and so the schema) when a .graphql file changes
|
|
75
|
+
# persisted_queries accept Apollo's Automatic Persisted Queries (PersistedQueries)
|
|
76
|
+
# persisted_queries_store where APQ keeps queries; default cache_store
|
|
77
|
+
# persisted_queries_ttl how long they live there (seconds; nil: until evicted)
|
|
78
|
+
# persisted_queries_manifest a manifest of trusted operations, served by hash
|
|
79
|
+
# persisted_queries_only reject any operation not in the manifest
|
|
80
|
+
attr_accessor :schema_cache_path, :persisted_queries_ttl, :persisted_queries_manifest
|
|
81
|
+
|
|
82
|
+
def schema_cache = @schema_cache || false
|
|
83
|
+
def watch_sdl = @watch_sdl || false
|
|
84
|
+
def persisted_queries = @persisted_queries || false
|
|
85
|
+
def persisted_queries_only = @persisted_queries_only || false
|
|
86
|
+
|
|
87
|
+
def persisted_queries_store
|
|
88
|
+
@persisted_queries_store || cache_store
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Pagination defaults, overridden by a directive's defaultCount / maxCount.
|
|
92
|
+
# Without a default count, paginated fields require `first`.
|
|
93
|
+
attr_accessor :pagination_default_count, :pagination_max_count
|
|
94
|
+
|
|
95
|
+
# Run @create/@update/... in a database transaction, so a failing record
|
|
96
|
+
# (e.g. the third of five in @createMany) leaves nothing behind.
|
|
97
|
+
def transactional_mutations
|
|
98
|
+
@transactional_mutations.nil? || @transactional_mutations
|
|
99
|
+
end
|
|
100
|
+
|
|
30
101
|
# Hook invoked when a directive/resolver swallows an error. Defaults to
|
|
31
102
|
# re-raising in test (so failures surface) and logging otherwise. Override
|
|
32
103
|
# via `Lighthouse.configure { |c| c.on_error = ->(e, ctx) { ... } }`.
|
|
@@ -56,8 +127,10 @@ module Lighthouse
|
|
|
56
127
|
end
|
|
57
128
|
end
|
|
58
129
|
|
|
59
|
-
# Checks an ability: `->(user, record, ability) { true/false }`.
|
|
60
|
-
#
|
|
130
|
+
# Checks an ability: `->(user, record, ability, args = []) { true/false }`.
|
|
131
|
+
# `record` may be a model class (@canModel). `args` carries the @can*
|
|
132
|
+
# `args:` values and, with `injectArgs: true`, the field's arguments first.
|
|
133
|
+
# Defaults to Pundit: `Pundit.policy!(user, record).<ability>?(*args)`.
|
|
61
134
|
def authorizer
|
|
62
135
|
@authorizer ||= default_authorizer
|
|
63
136
|
end
|
|
@@ -66,29 +139,22 @@ module Lighthouse
|
|
|
66
139
|
yield(self) if block_given?
|
|
67
140
|
end
|
|
68
141
|
|
|
69
|
-
def [](
|
|
70
|
-
public_send(
|
|
142
|
+
def [](key)
|
|
143
|
+
public_send(key)
|
|
71
144
|
end
|
|
72
145
|
|
|
73
146
|
def reset!
|
|
74
|
-
|
|
75
|
-
@resolver_namespaces = nil
|
|
76
|
-
@on_error = nil
|
|
77
|
-
@directive_map = nil
|
|
78
|
-
@authenticated_user = nil
|
|
79
|
-
@policy_user = nil
|
|
80
|
-
@authorizer = nil
|
|
147
|
+
SETTINGS.each { |setting| instance_variable_set("@#{setting}", nil) }
|
|
81
148
|
end
|
|
82
149
|
|
|
83
150
|
private
|
|
84
151
|
|
|
85
152
|
def default_authorizer
|
|
86
|
-
lambda do |user, record, ability|
|
|
87
|
-
unless defined?(::Pundit)
|
|
88
|
-
raise 'Lighthouse: configure `Config.authorizer` to use @can (Pundit not detected).'
|
|
89
|
-
end
|
|
153
|
+
lambda do |user, record, ability, args = []|
|
|
154
|
+
raise 'Lighthouse: configure `Config.authorizer` to use the @can* directives (Pundit not detected).' unless defined?(::Pundit)
|
|
90
155
|
|
|
91
|
-
::Pundit.policy!(user, record).
|
|
156
|
+
check = ::Pundit.policy!(user, record).method("#{ability}?")
|
|
157
|
+
check.arity.zero? ? check.call : check.call(*args)
|
|
92
158
|
end
|
|
93
159
|
end
|
|
94
160
|
|
|
@@ -2,16 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'directive'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
module ArgBuilder
|
|
14
|
-
include Directive
|
|
15
|
-
end
|
|
16
|
-
end
|
|
5
|
+
# A directive on a field ARGUMENT that composes a constraint onto an
|
|
6
|
+
# ActiveRecord relation at resolve time, e.g. @eq, @neq, @in, @notIn, @like,
|
|
7
|
+
# @where, @orderBy. These compose in SDL argument order via the
|
|
8
|
+
# ArgBuilderPipeline.
|
|
9
|
+
#
|
|
10
|
+
# Implementors provide `#handle_builder(relation, value)` returning a relation.
|
|
11
|
+
module Lighthouse::Contracts::ArgBuilder
|
|
12
|
+
include Lighthouse::Contracts::Directive
|
|
17
13
|
end
|
|
@@ -2,16 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'directive'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
module ArgManipulator
|
|
14
|
-
include Directive
|
|
15
|
-
end
|
|
16
|
-
end
|
|
5
|
+
# A directive on a field ARGUMENT that mutates the schema AST before build,
|
|
6
|
+
# e.g. @whereConditions / @whereHasConditions generating input/enum types and
|
|
7
|
+
# swapping the `_` placeholder argument type.
|
|
8
|
+
#
|
|
9
|
+
# Implementors provide (at the class level)
|
|
10
|
+
# `manipulate_argument_definition(manipulation_context, parent, field, arg, directive)`.
|
|
11
|
+
module Lighthouse::Contracts::ArgManipulator
|
|
12
|
+
include Lighthouse::Contracts::Directive
|
|
17
13
|
end
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# - `definition` -> String? the SDL `directive @name(...) on ...` line
|
|
14
|
-
module Directive
|
|
15
|
-
end
|
|
16
|
-
end
|
|
3
|
+
# Contracts are duck-typed marker modules that mirror the PHP Lighthouse
|
|
4
|
+
# directive interfaces. A directive class includes whichever contracts it
|
|
5
|
+
# fulfils; the DirectiveRegistry and SchemaGenerator detect capabilities via
|
|
6
|
+
# `klass.include?(Contracts::X)` and invoke the corresponding hook.
|
|
7
|
+
# Base marker. Every Lighthouse directive class includes this.
|
|
8
|
+
#
|
|
9
|
+
# Implementors are expected to expose (at the class level):
|
|
10
|
+
# - `directive_name` -> String the SDL name without the leading '@'
|
|
11
|
+
# - `definition` -> String? the SDL `directive @name(...) on ...` line
|
|
12
|
+
module Lighthouse::Contracts::Directive
|
|
17
13
|
end
|
|
@@ -2,16 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'directive'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
module FieldManipulator
|
|
14
|
-
include Directive
|
|
15
|
-
end
|
|
16
|
-
end
|
|
5
|
+
# A directive that mutates the schema AST before it is built, e.g. @paginate
|
|
6
|
+
# and the relationship directives injecting Connection/Paginator types, or
|
|
7
|
+
# @orderBy generating its column enum.
|
|
8
|
+
#
|
|
9
|
+
# Implementors provide (at the class level)
|
|
10
|
+
# `manipulate_field_definition(manipulation_context, parent, field, directive)`.
|
|
11
|
+
module Lighthouse::Contracts::FieldManipulator
|
|
12
|
+
include Lighthouse::Contracts::Directive
|
|
17
13
|
end
|
|
@@ -2,15 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'directive'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
module FieldMiddleware
|
|
13
|
-
include Directive
|
|
14
|
-
end
|
|
15
|
-
end
|
|
5
|
+
# A directive that wraps an existing field resolver, e.g. @auth wrapping the
|
|
6
|
+
# underlying resolver to enforce authentication.
|
|
7
|
+
#
|
|
8
|
+
# Implementors provide `#wrap_resolver(next_resolver)` returning a new
|
|
9
|
+
# `#call(obj, args, ctx)` lambda that may invoke `next_resolver`.
|
|
10
|
+
module Lighthouse::Contracts::FieldMiddleware
|
|
11
|
+
include Lighthouse::Contracts::Directive
|
|
16
12
|
end
|
|
@@ -2,17 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'directive'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
module FieldResolver
|
|
15
|
-
include Directive
|
|
16
|
-
end
|
|
17
|
-
end
|
|
5
|
+
# A directive that provides the runtime resolver for a field, e.g. @all,
|
|
6
|
+
# @find, @paginate, @hasMany, @belongsTo, @field, @auth, @aggregate.
|
|
7
|
+
#
|
|
8
|
+
# During the resolution phase the SchemaGenerator instantiates the directive
|
|
9
|
+
# and installs its resolver onto the field. Legacy resolvers fulfil this via
|
|
10
|
+
# `#define_resolver(target_type)`; clean directives implement `#resolve_field`
|
|
11
|
+
# returning a `#call(obj, args, ctx)` lambda.
|
|
12
|
+
module Lighthouse::Contracts::FieldResolver
|
|
13
|
+
include Lighthouse::Contracts::Directive
|
|
18
14
|
end
|
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
# Loads one association for many parents in a single query per batch, with
|
|
4
|
+
# ActiveRecord's preloader — the N+1 fix behind @hasMany, @belongsTo, ...
|
|
5
|
+
class Lighthouse::GraphQL::DataloaderSources::AssociationLoader < GraphQL::Dataloader::Source
|
|
6
|
+
def initialize(model_class, association_name)
|
|
7
|
+
super()
|
|
8
|
+
@model_class = model_class
|
|
9
|
+
@association_name = association_name.to_s
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# keys: the parent records
|
|
13
|
+
def fetch(parents)
|
|
14
|
+
preload(parents.select { |parent| association?(parent) })
|
|
15
|
+
parents.map { |parent| parent.respond_to?(@association_name) ? parent.public_send(@association_name) : nil }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def association?(parent)
|
|
21
|
+
parent.class.respond_to?(:reflect_on_association) && parent.class.reflect_on_association(@association_name)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def preload(records)
|
|
25
|
+
return if records.empty?
|
|
11
26
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
else
|
|
18
|
-
nil
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
27
|
+
preloader = ::ActiveRecord::Associations::Preloader
|
|
28
|
+
if preloader.method_defined?(:call)
|
|
29
|
+
preloader.new(records: records, associations: @association_name.to_sym).call # Rails >= 7
|
|
30
|
+
else
|
|
31
|
+
preloader.new.preload(records, @association_name.to_sym) # Rails 6
|
|
23
32
|
end
|
|
24
33
|
end
|
|
25
34
|
end
|