lighthouse-graphql 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +175 -1
- data/README.md +47 -3
- data/docs/authorization.md +116 -57
- data/docs/best-practices.md +16 -3
- data/docs/caching.md +161 -0
- data/docs/configuration.md +83 -4
- data/docs/custom-directives.md +5 -1
- data/docs/directives.md +117 -38
- data/docs/getting-started.md +84 -0
- data/docs/motivation.md +184 -0
- data/docs/mutations.md +260 -0
- data/docs/performance.md +138 -0
- data/docs/relationships.md +80 -33
- data/docs/resolvers.md +189 -0
- data/docs/validation.md +132 -0
- data/lib/generators/lighthouse/builder_generator.rb +30 -0
- data/lib/generators/lighthouse/class_generator.rb +61 -0
- data/lib/generators/lighthouse/install/install_generator.rb +127 -0
- data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +8 -0
- data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +57 -0
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +67 -0
- data/lib/generators/lighthouse/install/templates/schema.graphql.tt +19 -0
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -0
- data/lib/generators/lighthouse/mutation_generator.rb +30 -0
- data/lib/generators/lighthouse/query_generator.rb +29 -0
- data/lib/generators/lighthouse/validator_generator.rb +36 -0
- data/lib/lighthouse/cache_control.rb +126 -0
- data/lib/lighthouse/caching.rb +68 -0
- data/lib/lighthouse/config.rb +85 -19
- data/lib/lighthouse/contracts/arg_builder.rb +8 -12
- data/lib/lighthouse/contracts/arg_manipulator.rb +8 -12
- data/lib/lighthouse/contracts/directive.rb +10 -14
- data/lib/lighthouse/contracts/field_manipulator.rb +8 -12
- data/lib/lighthouse/contracts/field_middleware.rb +7 -11
- data/lib/lighthouse/contracts/field_resolver.rb +9 -13
- data/lib/lighthouse/dataloader_sources/association_loader.rb +28 -19
- data/lib/lighthouse/dataloader_sources/model_loader.rb +10 -14
- data/lib/lighthouse/dataloader_sources/relation_count_loader.rb +16 -0
- data/lib/lighthouse/dataloader_sources/relation_page_loader.rb +18 -0
- data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +57 -69
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +26 -57
- data/lib/lighthouse/directive_resolvers/association_resolvers.rb +161 -0
- data/lib/lighthouse/directive_resolvers/auth_resolver.rb +41 -38
- data/lib/lighthouse/directive_resolvers/base.rb +142 -86
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +58 -56
- data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +133 -0
- data/lib/lighthouse/directive_resolvers/can_resolvers.rb +177 -0
- data/lib/lighthouse/directive_resolvers/convention_resolver.rb +70 -0
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +42 -48
- data/lib/lighthouse/directive_resolvers/field_resolver.rb +27 -68
- data/lib/lighthouse/directive_resolvers/find_resolver.rb +42 -52
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +39 -45
- data/lib/lighthouse/directive_resolvers/guard_resolver.rb +16 -22
- data/lib/lighthouse/directive_resolvers/method_resolver.rb +19 -25
- data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +219 -0
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +70 -128
- data/lib/lighthouse/directive_resolvers/rename_resolver.rb +18 -24
- data/lib/lighthouse/directive_resolvers/resolution.rb +31 -27
- data/lib/lighthouse/directive_resolvers/validator_resolver.rb +193 -0
- data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +164 -163
- data/lib/lighthouse/directives/arguments/base.rb +23 -31
- data/lib/lighthouse/directives/arguments/filters.rb +113 -93
- data/lib/lighthouse/directives/arguments/order_by.rb +41 -41
- data/lib/lighthouse/directives/pagination_info_types.rb +69 -0
- data/lib/lighthouse/directives/pagination_manipulator.rb +176 -0
- data/lib/lighthouse/directives/registry.rb +210 -141
- data/lib/lighthouse/directives/where_conditions_directive.rb +236 -200
- data/lib/lighthouse/errors.rb +27 -0
- data/lib/lighthouse/federation_directives.rb +53 -0
- data/lib/lighthouse/field_resolver_stack.rb +56 -0
- data/lib/lighthouse/generated_resolvers.rb +82 -0
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/graphql_ruby_patches.rb +18 -0
- data/lib/lighthouse/ide_helper.rb +98 -0
- data/lib/lighthouse/namespaces.rb +21 -0
- data/lib/lighthouse/nested_mutations/belongs_to_operations.rb +50 -0
- data/lib/lighthouse/nested_mutations/executor.rb +149 -0
- data/lib/lighthouse/nested_mutations/has_operations.rb +66 -0
- data/lib/lighthouse/nested_mutations/many_to_many_operations.rb +44 -0
- data/lib/lighthouse/nested_mutations/partitioner.rb +125 -0
- data/lib/lighthouse/nested_mutations.rb +210 -0
- data/lib/lighthouse/pagination.rb +221 -0
- data/lib/lighthouse/persisted_queries.rb +167 -0
- data/lib/lighthouse/railtie.rb +26 -0
- data/lib/lighthouse/rb_lighthouse.rb +50 -29
- data/lib/lighthouse/reference_resolver.rb +13 -15
- data/lib/lighthouse/relation_batch.rb +156 -0
- data/lib/lighthouse/resolver_dispatch.rb +26 -0
- data/lib/lighthouse/schema_cache.rb +120 -0
- data/lib/lighthouse/schema_factory.rb +27 -37
- data/lib/lighthouse/schema_generator.rb +196 -239
- data/lib/lighthouse/schema_implementation.rb +51 -48
- data/lib/lighthouse/sdl_loader.rb +17 -20
- data/lib/lighthouse/support/arguments.rb +119 -0
- data/lib/lighthouse/support/authorization.rb +18 -19
- data/lib/lighthouse/support/batching.rb +16 -0
- data/lib/lighthouse/support/directive_args.rb +26 -24
- data/lib/lighthouse/support/model_resolver.rb +56 -56
- data/lib/lighthouse/support/naming.rb +55 -59
- data/lib/lighthouse/tasks/lighthouse.rake +33 -0
- data/lib/lighthouse/where_conditions/operator_map.rb +25 -29
- data/sig/lighthouse/directives.rbs +158 -0
- data/sig/lighthouse/graphql.rbs +78 -0
- data/sig/lighthouse.rbs +142 -0
- metadata +56 -64
- data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +0 -56
- data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +0 -47
- data/lib/lighthouse/directive_resolvers/can_resolver.rb +0 -86
- data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +0 -109
- data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +0 -43
- data/lib/lighthouse/directives/relation_directive.rb +0 -332
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
|
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
39
|
+
private
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|