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
@@ -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
@@ -1,19 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Lighthouse
4
- module GraphQL
5
- module DataloaderSources
6
- class ModelLoader < ::GraphQL::Dataloader::Source
7
- def initialize(model_class)
8
- @model_class = model_class
9
- end
3
+ class Lighthouse::GraphQL::DataloaderSources::ModelLoader < GraphQL::Dataloader::Source
4
+ def initialize(model_class)
5
+ super()
6
+ @model_class = model_class
7
+ end
10
8
 
11
- # keys are ids
12
- def fetch(keys)
13
- records = @model_class.where(id: keys).index_by(&:id)
14
- keys.map { |k| records[k] }
15
- end
16
- end
17
- end
9
+ # Keys are ids. GraphQL `ID` arguments arrive as strings while records
10
+ # carry their column type (usually Integer), so match on the string form.
11
+ def fetch(keys)
12
+ records = @model_class.where(id: keys).index_by { |record| record.id.to_s }
13
+ keys.map { |k| records[k.to_s] }
18
14
  end
19
15
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The total of a paginated relation for every parent the Dataloader collected,
4
+ # in one GROUP BY query. Only asked for when the client selects a total.
5
+ class Lighthouse::GraphQL::DataloaderSources::RelationCountLoader < GraphQL::Dataloader::Source
6
+ def initialize(batch)
7
+ super()
8
+ @batch = batch
9
+ end
10
+
11
+ # keys: the parent records
12
+ def fetch(owners)
13
+ counts = @batch.counts(owners)
14
+ owners.map { |owner| counts.fetch(@batch.owner_key(owner), 0) }
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # One page of a paginated relation for every parent the Dataloader collected,
4
+ # in one query (see Lighthouse::GraphQL::RelationBatch).
5
+ class Lighthouse::GraphQL::DataloaderSources::RelationPageLoader < GraphQL::Dataloader::Source
6
+ def initialize(batch, offset, limit)
7
+ super()
8
+ @batch = batch
9
+ @offset = offset
10
+ @limit = limit
11
+ end
12
+
13
+ # keys: the parent records
14
+ def fetch(owners)
15
+ pages = @batch.pages(owners, @offset, @limit)
16
+ owners.map { |owner| pages.fetch(@batch.owner_key(owner), []) }
17
+ end
18
+ end
@@ -3,84 +3,72 @@
3
3
  require_relative 'base'
4
4
  require_relative 'builder_applier'
5
5
 
6
- module Lighthouse
7
- module GraphQL
8
- module DirectiveResolvers
9
- class AggregateDirectiveResolver < BaseDirectiveResolver
10
- def define_resolver(target_type_class)
11
- field_name = @field_def.name
12
- method_name = "resolve_field_#{field_name}"
6
+ class Lighthouse::GraphQL::DirectiveResolvers::AggregateDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
7
+ def self.directive_name = 'aggregate'
13
8
 
14
- model_name = extract_directive_arg('model')
15
- relation_name = extract_directive_arg('relation')
16
- column_name = extract_directive_arg('column')
17
- function_name = extract_directive_arg('function')
18
- resolver_instance = self
19
- field_ast_node = @field_def.ast_node
9
+ # An unknown function aggregates to null.
10
+ AGGREGATIONS = {
11
+ 'SUM' => ->(relation, column) { relation.sum(column) },
12
+ 'AVG' => ->(relation, column) { relation.average(column)&.to_f },
13
+ 'MIN' => ->(relation, column) { relation.minimum(column) },
14
+ 'MAX' => ->(relation, column) { relation.maximum(column) },
15
+ 'COUNT' => ->(relation, _column) { relation.count }
16
+ }.freeze
20
17
 
21
- resolver_lambda = lambda do |obj, args, ctx|
22
- relation = if model_name
23
- model_class = resolver_instance.send(:constantize_model, model_name)
24
- model_class ? model_class.all : nil
25
- elsif relation_name && obj.respond_to?(relation_name)
26
- obj.public_send(relation_name)
27
- else
28
- nil
29
- end
18
+ def self.definition
19
+ 'directive @aggregate(function: String!, column: String, model: String, relation: String) on FIELD_DEFINITION'
20
+ end
30
21
 
31
- return nil unless relation
22
+ def define_resolver(target_type_class)
23
+ model_name = extract_directive_arg('model')
24
+ relation_name = extract_directive_arg('relation')
25
+ column_name = extract_directive_arg('column')
26
+ function_name = extract_directive_arg('function')
27
+ resolver_instance = self
28
+ field_ast_node = @field_def.ast_node
32
29
 
33
- relation = DirectiveResolvers::BuilderApplier.apply(relation, field_ast_node, args, ctx, resolver_instance)
34
- resolver_instance.send(:compute_aggregation, relation, column_name, function_name)
35
- end
30
+ install(target_type_class, lambda do |obj, args, ctx|
31
+ relation = resolver_instance.send(:aggregate_source, obj, model_name, relation_name)
32
+ return nil unless relation
36
33
 
37
- target_type_class.define_singleton_method(method_name, &resolver_lambda)
38
- @field_def.resolve_proc = resolver_lambda if @field_def.respond_to?(:resolve_proc=)
39
- end
34
+ relation = Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply(relation, field_ast_node, args, ctx, resolver_instance)
35
+ resolver_instance.send(:compute_aggregation, relation, column_name, function_name)
36
+ end)
37
+ end
40
38
 
41
- private
39
+ private
42
40
 
43
- def extract_directive_arg(name)
44
- arg = @directive_node.arguments&.find { |a| a.name == name }
45
- return nil unless arg
46
- v = arg.value.respond_to?(:value) ? arg.value.value : arg.value
47
- v.is_a?(::GraphQL::Language::Nodes::Enum) ? v.name : v
48
- end
41
+ def extract_directive_arg(name)
42
+ arg = @directive_node.arguments&.find { |a| a.name == name }
43
+ return nil unless arg
49
44
 
50
- def compute_aggregation(relation, column, function_name)
51
- return nil unless relation.respond_to?(:any?)
52
- column = column.to_s
53
- case function_name.to_s.upcase
54
- when 'SUM'
55
- relation.sum(column)
56
- when 'AVG'
57
- relation.average(column)&.to_f
58
- when 'MIN'
59
- relation.minimum(column)
60
- when 'MAX'
61
- relation.maximum(column)
62
- when 'COUNT'
63
- relation.count
64
- else
65
- nil
66
- end
67
- rescue StandardError
68
- nil
69
- end
45
+ v = arg.value.respond_to?(:value) ? arg.value.value : arg.value
46
+ v.is_a?(::GraphQL::Language::Nodes::Enum) ? v.name : v
47
+ end
70
48
 
71
- def set_resolve_proc(method_name)
72
- if @field_def.respond_to?(:resolve_proc=)
73
- @field_def.resolve_proc = ->(obj, arguments, ctx) do
74
- target_type_class = self.class
75
- if target_type_class.respond_to?(method_name)
76
- target_type_class.public_send(method_name, obj, arguments, ctx)
77
- else
78
- nil
79
- end
80
- end
81
- end
82
- end
83
- end
49
+ # The model's table when `model:` is given, else the parent's relation.
50
+ def aggregate_source(obj, model_name, relation_name)
51
+ if model_name
52
+ constantize_model(model_name)&.all
53
+ elsif relation_name && obj.respond_to?(relation_name)
54
+ obj.public_send(relation_name)
84
55
  end
85
56
  end
57
+
58
+ def compute_aggregation(relation, column, function_name)
59
+ return nil unless relation.respond_to?(:any?)
60
+
61
+ AGGREGATIONS[function_name.to_s.upcase]&.call(relation, column.to_s)
62
+ rescue StandardError
63
+ nil
64
+ end
65
+
66
+ def install_resolve_proc(method_name)
67
+ return unless @field_def.respond_to?(:resolve_proc=)
68
+
69
+ @field_def.resolve_proc = lambda { |obj, arguments, ctx|
70
+ target_type_class = self.class
71
+ target_type_class.public_send(method_name, obj, arguments, ctx) if target_type_class.respond_to?(method_name)
72
+ }
73
+ end
86
74
  end