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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +154 -1
  3. data/README.md +42 -2
  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/mutations.md +260 -0
  12. data/docs/performance.md +138 -0
  13. data/docs/relationships.md +80 -33
  14. data/docs/resolvers.md +179 -0
  15. data/docs/validation.md +132 -0
  16. data/lib/generators/lighthouse/builder_generator.rb +30 -0
  17. data/lib/generators/lighthouse/class_generator.rb +61 -0
  18. data/lib/generators/lighthouse/install/install_generator.rb +127 -0
  19. data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +8 -0
  20. data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +57 -0
  21. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +67 -0
  22. data/lib/generators/lighthouse/install/templates/schema.graphql.tt +19 -0
  23. data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -0
  24. data/lib/generators/lighthouse/mutation_generator.rb +30 -0
  25. data/lib/generators/lighthouse/query_generator.rb +29 -0
  26. data/lib/generators/lighthouse/validator_generator.rb +36 -0
  27. data/lib/lighthouse/cache_control.rb +126 -0
  28. data/lib/lighthouse/caching.rb +68 -0
  29. data/lib/lighthouse/config.rb +85 -19
  30. data/lib/lighthouse/contracts/arg_builder.rb +8 -12
  31. data/lib/lighthouse/contracts/arg_manipulator.rb +8 -12
  32. data/lib/lighthouse/contracts/directive.rb +10 -14
  33. data/lib/lighthouse/contracts/field_manipulator.rb +8 -12
  34. data/lib/lighthouse/contracts/field_middleware.rb +7 -11
  35. data/lib/lighthouse/contracts/field_resolver.rb +9 -13
  36. data/lib/lighthouse/dataloader_sources/association_loader.rb +28 -19
  37. data/lib/lighthouse/dataloader_sources/model_loader.rb +10 -14
  38. data/lib/lighthouse/dataloader_sources/relation_count_loader.rb +16 -0
  39. data/lib/lighthouse/dataloader_sources/relation_page_loader.rb +18 -0
  40. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +57 -69
  41. data/lib/lighthouse/directive_resolvers/all_resolver.rb +26 -57
  42. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +161 -0
  43. data/lib/lighthouse/directive_resolvers/auth_resolver.rb +41 -38
  44. data/lib/lighthouse/directive_resolvers/base.rb +142 -86
  45. data/lib/lighthouse/directive_resolvers/builder_applier.rb +58 -56
  46. data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +133 -0
  47. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +177 -0
  48. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +70 -0
  49. data/lib/lighthouse/directive_resolvers/count_resolver.rb +42 -48
  50. data/lib/lighthouse/directive_resolvers/field_resolver.rb +27 -68
  51. data/lib/lighthouse/directive_resolvers/find_resolver.rb +42 -52
  52. data/lib/lighthouse/directive_resolvers/first_resolver.rb +39 -45
  53. data/lib/lighthouse/directive_resolvers/guard_resolver.rb +16 -22
  54. data/lib/lighthouse/directive_resolvers/method_resolver.rb +19 -25
  55. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +219 -0
  56. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +70 -128
  57. data/lib/lighthouse/directive_resolvers/rename_resolver.rb +18 -24
  58. data/lib/lighthouse/directive_resolvers/resolution.rb +31 -27
  59. data/lib/lighthouse/directive_resolvers/validator_resolver.rb +193 -0
  60. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +164 -163
  61. data/lib/lighthouse/directives/arguments/base.rb +23 -31
  62. data/lib/lighthouse/directives/arguments/filters.rb +113 -93
  63. data/lib/lighthouse/directives/arguments/order_by.rb +41 -41
  64. data/lib/lighthouse/directives/pagination_info_types.rb +69 -0
  65. data/lib/lighthouse/directives/pagination_manipulator.rb +176 -0
  66. data/lib/lighthouse/directives/registry.rb +210 -141
  67. data/lib/lighthouse/directives/where_conditions_directive.rb +236 -200
  68. data/lib/lighthouse/errors.rb +27 -0
  69. data/lib/lighthouse/federation_directives.rb +53 -0
  70. data/lib/lighthouse/field_resolver_stack.rb +56 -0
  71. data/lib/lighthouse/generated_resolvers.rb +82 -0
  72. data/lib/lighthouse/graphql/version.rb +1 -1
  73. data/lib/lighthouse/graphql_ruby_patches.rb +18 -0
  74. data/lib/lighthouse/ide_helper.rb +98 -0
  75. data/lib/lighthouse/namespaces.rb +21 -0
  76. data/lib/lighthouse/nested_mutations/belongs_to_operations.rb +50 -0
  77. data/lib/lighthouse/nested_mutations/executor.rb +149 -0
  78. data/lib/lighthouse/nested_mutations/has_operations.rb +66 -0
  79. data/lib/lighthouse/nested_mutations/many_to_many_operations.rb +44 -0
  80. data/lib/lighthouse/nested_mutations/partitioner.rb +125 -0
  81. data/lib/lighthouse/nested_mutations.rb +210 -0
  82. data/lib/lighthouse/pagination.rb +221 -0
  83. data/lib/lighthouse/persisted_queries.rb +167 -0
  84. data/lib/lighthouse/railtie.rb +26 -0
  85. data/lib/lighthouse/rb_lighthouse.rb +50 -29
  86. data/lib/lighthouse/reference_resolver.rb +13 -15
  87. data/lib/lighthouse/relation_batch.rb +156 -0
  88. data/lib/lighthouse/resolver_dispatch.rb +26 -0
  89. data/lib/lighthouse/schema_cache.rb +120 -0
  90. data/lib/lighthouse/schema_factory.rb +27 -37
  91. data/lib/lighthouse/schema_generator.rb +196 -239
  92. data/lib/lighthouse/schema_implementation.rb +51 -48
  93. data/lib/lighthouse/sdl_loader.rb +17 -20
  94. data/lib/lighthouse/support/arguments.rb +119 -0
  95. data/lib/lighthouse/support/authorization.rb +18 -19
  96. data/lib/lighthouse/support/batching.rb +16 -0
  97. data/lib/lighthouse/support/directive_args.rb +26 -24
  98. data/lib/lighthouse/support/model_resolver.rb +56 -56
  99. data/lib/lighthouse/support/naming.rb +55 -59
  100. data/lib/lighthouse/tasks/lighthouse.rake +33 -0
  101. data/lib/lighthouse/where_conditions/operator_map.rb +25 -29
  102. metadata +51 -63
  103. data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +0 -56
  104. data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +0 -47
  105. data/lib/lighthouse/directive_resolvers/can_resolver.rb +0 -86
  106. data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +0 -109
  107. data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +0 -43
  108. 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
@@ -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
- :authenticated_user, :policy_user, :authorizer
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) ? File.join(Rails.root, 'graphql') : nil)
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
- # Defaults to Pundit: `Pundit.policy!(user, record).<ability>?`.
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 [](k)
70
- public_send(k)
142
+ def [](key)
143
+ public_send(key)
71
144
  end
72
145
 
73
146
  def reset!
74
- @sdl_folder = nil
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).public_send("#{ability}?")
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
- module Lighthouse
6
- module Contracts
7
- # A directive on a field ARGUMENT that composes a constraint onto an
8
- # ActiveRecord relation at resolve time, e.g. @eq, @neq, @in, @notIn, @like,
9
- # @where, @orderBy. These compose in SDL argument order via the
10
- # ArgBuilderPipeline.
11
- #
12
- # Implementors provide `#handle_builder(relation, value)` returning a relation.
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
- module Lighthouse
6
- module Contracts
7
- # A directive on a field ARGUMENT that mutates the schema AST before build,
8
- # e.g. @whereConditions / @whereHasConditions generating input/enum types and
9
- # swapping the `_` placeholder argument type.
10
- #
11
- # Implementors provide (at the class level)
12
- # `manipulate_argument_definition(manipulation_context, parent, field, arg, directive)`.
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
- module Lighthouse
4
- # Contracts are duck-typed marker modules that mirror the PHP Lighthouse
5
- # directive interfaces. A directive class includes whichever contracts it
6
- # fulfils; the DirectiveRegistry and SchemaGenerator detect capabilities via
7
- # `klass.include?(Contracts::X)` and invoke the corresponding hook.
8
- module Contracts
9
- # Base marker. Every Lighthouse directive class includes this.
10
- #
11
- # Implementors are expected to expose (at the class level):
12
- # - `directive_name` -> String the SDL name without the leading '@'
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
- module Lighthouse
6
- module Contracts
7
- # A directive that mutates the schema AST before it is built, e.g. @paginate
8
- # and the relationship directives injecting Connection/Paginator types, or
9
- # @orderBy generating its column enum.
10
- #
11
- # Implementors provide (at the class level)
12
- # `manipulate_field_definition(manipulation_context, parent, field, directive)`.
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
- module Lighthouse
6
- module Contracts
7
- # A directive that wraps an existing field resolver, e.g. @auth wrapping the
8
- # underlying resolver to enforce authentication.
9
- #
10
- # Implementors provide `#wrap_resolver(next_resolver)` returning a new
11
- # `#call(obj, args, ctx)` lambda that may invoke `next_resolver`.
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
- module Lighthouse
6
- module Contracts
7
- # A directive that provides the runtime resolver for a field, e.g. @all,
8
- # @find, @paginate, @hasMany, @belongsTo, @field, @auth, @aggregate.
9
- #
10
- # During the resolution phase the SchemaGenerator instantiates the directive
11
- # and installs its resolver onto the field. Legacy resolvers fulfil this via
12
- # `#define_resolver(target_type)`; clean directives implement `#resolve_field`
13
- # returning a `#call(obj, args, ctx)` lambda.
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
- module Lighthouse
4
- module GraphQL
5
- module DataloaderSources
6
- class AssociationLoader < ::GraphQL::Dataloader::Source
7
- def initialize(model_class, association_name)
8
- @model_class = model_class
9
- @association_name = association_name.to_s
10
- end
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
- # keys: array of parent objects
13
- def fetch(keys)
14
- keys.map do |parent|
15
- if parent.respond_to?(@association_name)
16
- parent.public_send(@association_name)
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