lighthouse-graphql 0.1.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 +7 -0
- data/CHANGELOG.md +47 -0
- data/LICENSE.txt +21 -0
- data/README.md +134 -0
- data/docs/authorization.md +109 -0
- data/docs/best-practices.md +75 -0
- data/docs/configuration.md +70 -0
- data/docs/custom-directives.md +129 -0
- data/docs/directives.md +298 -0
- data/docs/federation.md +108 -0
- data/docs/filtering-and-ordering.md +128 -0
- data/docs/getting-started.md +152 -0
- data/docs/relationships.md +95 -0
- data/lib/lighthouse/config.rb +112 -0
- data/lib/lighthouse/contracts/arg_builder.rb +17 -0
- data/lib/lighthouse/contracts/arg_manipulator.rb +17 -0
- data/lib/lighthouse/contracts/directive.rb +17 -0
- data/lib/lighthouse/contracts/field_manipulator.rb +17 -0
- data/lib/lighthouse/contracts/field_middleware.rb +16 -0
- data/lib/lighthouse/contracts/field_resolver.rb +18 -0
- data/lib/lighthouse/dataloader_sources/association_loader.rb +25 -0
- data/lib/lighthouse/dataloader_sources/model_loader.rb +19 -0
- data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +86 -0
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +73 -0
- data/lib/lighthouse/directive_resolvers/auth_resolver.rb +44 -0
- data/lib/lighthouse/directive_resolvers/base.rb +92 -0
- data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +56 -0
- data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +47 -0
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +63 -0
- data/lib/lighthouse/directive_resolvers/can_resolver.rb +86 -0
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +56 -0
- data/lib/lighthouse/directive_resolvers/field_resolver.rb +78 -0
- data/lib/lighthouse/directive_resolvers/find_resolver.rb +65 -0
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +53 -0
- data/lib/lighthouse/directive_resolvers/guard_resolver.rb +32 -0
- data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +109 -0
- data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +43 -0
- data/lib/lighthouse/directive_resolvers/method_resolver.rb +35 -0
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +147 -0
- data/lib/lighthouse/directive_resolvers/rename_resolver.rb +35 -0
- data/lib/lighthouse/directive_resolvers/resolution.rb +35 -0
- data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +169 -0
- data/lib/lighthouse/directives/arguments/base.rb +40 -0
- data/lib/lighthouse/directives/arguments/filters.rb +101 -0
- data/lib/lighthouse/directives/arguments/order_by.rb +48 -0
- data/lib/lighthouse/directives/registry.rb +168 -0
- data/lib/lighthouse/directives/relation_directive.rb +332 -0
- data/lib/lighthouse/directives/where_conditions_directive.rb +208 -0
- data/lib/lighthouse/graphql/version.rb +7 -0
- data/lib/lighthouse/graphql.rb +8 -0
- data/lib/lighthouse/rb_lighthouse.rb +88 -0
- data/lib/lighthouse/reference_resolver.rb +24 -0
- data/lib/lighthouse/schema_factory.rb +46 -0
- data/lib/lighthouse/schema_generator.rb +244 -0
- data/lib/lighthouse/schema_implementation.rb +119 -0
- data/lib/lighthouse/sdl_loader.rb +27 -0
- data/lib/lighthouse/support/authorization.rb +26 -0
- data/lib/lighthouse/support/directive_args.rb +33 -0
- data/lib/lighthouse/support/model_resolver.rb +61 -0
- data/lib/lighthouse/support/naming.rb +71 -0
- data/lib/lighthouse/where_conditions/operator_map.rb +35 -0
- data/lib/lighthouse-graphql.rb +10 -0
- metadata +195 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Relationships
|
|
2
|
+
|
|
3
|
+
Just like in Rails, you can resolve associations directly from your schema. Add a
|
|
4
|
+
field named after the association and annotate it with the matching directive.
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
class Post < ApplicationRecord
|
|
8
|
+
belongs_to :author, class_name: 'User'
|
|
9
|
+
has_many :comments
|
|
10
|
+
end
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```graphql
|
|
14
|
+
type Post {
|
|
15
|
+
author: User! @belongsTo(relation: "author")
|
|
16
|
+
comments: [Comment!]! @hasMany
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Avoiding the N+1 problem
|
|
21
|
+
|
|
22
|
+
Resolving relationships naively triggers one query per parent record. Lighthouse's
|
|
23
|
+
relationship directives load through graphql-ruby's **Dataloader**, batching all
|
|
24
|
+
sibling loads into a single query. Enable it on your schema:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
class AppSchema < BaseAppSchema
|
|
28
|
+
use GraphQL::Dataloader
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
With the Dataloader in place, a query for 100 posts and their authors issues two
|
|
33
|
+
queries, not 101.
|
|
34
|
+
|
|
35
|
+
## One-to-many — `@hasMany`
|
|
36
|
+
|
|
37
|
+
```graphql
|
|
38
|
+
type User {
|
|
39
|
+
posts: [Post!]! @hasMany
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`@hasMany` returns a **connection** type, so it supports pagination arguments and
|
|
44
|
+
`edges { node { ... } }` / `nodes { ... }` selections out of the box. The inverse
|
|
45
|
+
is `@belongsTo`.
|
|
46
|
+
|
|
47
|
+
## One-to-one — `@hasOne`
|
|
48
|
+
|
|
49
|
+
```graphql
|
|
50
|
+
type User {
|
|
51
|
+
profile: Profile @hasOne
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Inverse — `@belongsTo`
|
|
56
|
+
|
|
57
|
+
```graphql
|
|
58
|
+
type Post {
|
|
59
|
+
author: User! @belongsTo(relation: "author")
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Many-to-many — `@belongsToMany`
|
|
64
|
+
|
|
65
|
+
```graphql
|
|
66
|
+
type User {
|
|
67
|
+
roles: [Role!]! @belongsToMany
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Renaming relations
|
|
72
|
+
|
|
73
|
+
By default the relation name equals the field name (snake_cased). When they
|
|
74
|
+
differ, pass `relation:`:
|
|
75
|
+
|
|
76
|
+
```graphql
|
|
77
|
+
type Post {
|
|
78
|
+
writtenBy: User! @belongsTo(relation: "author")
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Filtering a relationship
|
|
83
|
+
|
|
84
|
+
Relationship fields accept the same [argument filters](filtering-and-ordering.md)
|
|
85
|
+
and [`@whereConditions`](filtering-and-ordering.md#complex-where-conditions) as
|
|
86
|
+
root queries, so you can scope the related records:
|
|
87
|
+
|
|
88
|
+
```graphql
|
|
89
|
+
type User {
|
|
90
|
+
publishedPosts(
|
|
91
|
+
status: String @eq
|
|
92
|
+
orderBy: [OrderByClause!] @orderBy
|
|
93
|
+
): [Post!]! @hasMany(relation: "posts")
|
|
94
|
+
}
|
|
95
|
+
```
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lighthouse
|
|
4
|
+
# Convenience entrypoint: `Lighthouse.configure { |c| c.resolver_namespaces = [...] }`.
|
|
5
|
+
def self.configure(&block)
|
|
6
|
+
Config.configure(&block)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module Config
|
|
10
|
+
class << self
|
|
11
|
+
attr_writer :sdl_folder, :resolver_namespaces, :on_error,
|
|
12
|
+
:authenticated_user, :policy_user, :authorizer
|
|
13
|
+
|
|
14
|
+
def sdl_folder
|
|
15
|
+
@sdl_folder ||= (defined?(Rails) && Rails.respond_to?(:root) ? File.join(Rails.root, 'graphql') : nil)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def directive_map
|
|
19
|
+
@directive_map ||= {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Namespaces tried (in order) when resolving a class name from a directive
|
|
23
|
+
# argument, e.g. `@field(resolver: "ContactPerson")` or `@all(model: "X")`.
|
|
24
|
+
# Keeping this in config means the library holds no app-specific namespace
|
|
25
|
+
# and can be extracted into a gem as-is.
|
|
26
|
+
def resolver_namespaces
|
|
27
|
+
@resolver_namespaces ||= ['App::Graphql']
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Hook invoked when a directive/resolver swallows an error. Defaults to
|
|
31
|
+
# re-raising in test (so failures surface) and logging otherwise. Override
|
|
32
|
+
# via `Lighthouse.configure { |c| c.on_error = ->(e, ctx) { ... } }`.
|
|
33
|
+
def on_error
|
|
34
|
+
@on_error ||= default_on_error
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def handle_error(error, context = nil)
|
|
38
|
+
on_error.call(error, context)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# --- Authorization seams (used by @guard / @can) -------------------------
|
|
42
|
+
# The library stays framework-agnostic: how to read the authenticated user
|
|
43
|
+
# and how to check an ability are both configurable. The defaults work with
|
|
44
|
+
# a `current_user`/`pundit_user` context and Pundit.
|
|
45
|
+
|
|
46
|
+
# Returns the authenticated user from the GraphQL context (nil if none).
|
|
47
|
+
def authenticated_user
|
|
48
|
+
@authenticated_user ||= ->(ctx) { ctx && (ctx[:current_user] || ctx['current_user']) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The "user" object passed to the authorizer (e.g. Pundit's user context).
|
|
52
|
+
# Defaults to ctx[:pundit_user], falling back to the authenticated user.
|
|
53
|
+
def policy_user
|
|
54
|
+
@policy_user ||= lambda do |ctx|
|
|
55
|
+
(ctx && (ctx[:pundit_user] || ctx['pundit_user'])) || authenticated_user.call(ctx)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Checks an ability: `->(user, record, ability) { true/false }`.
|
|
60
|
+
# Defaults to Pundit: `Pundit.policy!(user, record).<ability>?`.
|
|
61
|
+
def authorizer
|
|
62
|
+
@authorizer ||= default_authorizer
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def configure
|
|
66
|
+
yield(self) if block_given?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def [](k)
|
|
70
|
+
public_send(k)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
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
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
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
|
|
90
|
+
|
|
91
|
+
::Pundit.policy!(user, record).public_send("#{ability}?")
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def default_on_error
|
|
96
|
+
lambda do |error, _context|
|
|
97
|
+
if defined?(Rails) && Rails.respond_to?(:env) && Rails.env.test?
|
|
98
|
+
raise error
|
|
99
|
+
elsif defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
100
|
+
Rails.logger.warn("[Lighthouse] #{error.class}: #{error.message}")
|
|
101
|
+
else
|
|
102
|
+
warn("[Lighthouse] #{error.class}: #{error.message}")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
module GraphQL
|
|
110
|
+
Config = Lighthouse::Config unless const_defined?(:Config, false)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'directive'
|
|
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
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'directive'
|
|
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
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
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
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'directive'
|
|
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
|
|
17
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'directive'
|
|
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
|
|
16
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'directive'
|
|
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
|
|
18
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
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
|
|
11
|
+
|
|
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
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
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
|
|
10
|
+
|
|
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
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'builder_applier'
|
|
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}"
|
|
13
|
+
|
|
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
|
|
20
|
+
|
|
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
|
|
30
|
+
|
|
31
|
+
return nil unless relation
|
|
32
|
+
|
|
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
|
|
36
|
+
|
|
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
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
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
|
|
49
|
+
|
|
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
|
|
70
|
+
|
|
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
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'builder_applier'
|
|
5
|
+
|
|
6
|
+
module Lighthouse
|
|
7
|
+
module GraphQL
|
|
8
|
+
module DirectiveResolvers
|
|
9
|
+
class AllDirectiveResolver < BaseDirectiveResolver
|
|
10
|
+
def define_resolver(target_type_class)
|
|
11
|
+
field_name = @field_def.name
|
|
12
|
+
|
|
13
|
+
resolver_instance = self
|
|
14
|
+
field_ast_node = @field_def.ast_node
|
|
15
|
+
|
|
16
|
+
method_name = "resolve_field_#{field_name}"
|
|
17
|
+
|
|
18
|
+
model_name_static = extract_model_name || @field_def.type.unwrap.name
|
|
19
|
+
model_class_static = constantize_model(model_name_static)
|
|
20
|
+
|
|
21
|
+
target_type_class.define_singleton_method(method_name) do |_obj, arguments, ctx|
|
|
22
|
+
model_class = model_class_static
|
|
23
|
+
return [] unless model_class
|
|
24
|
+
|
|
25
|
+
# apply simple pagination args if present
|
|
26
|
+
args = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
27
|
+
relation = model_class.all
|
|
28
|
+
|
|
29
|
+
relation = DirectiveResolvers::BuilderApplier.apply(relation, field_ast_node, args, ctx, resolver_instance)
|
|
30
|
+
|
|
31
|
+
relation = resolver_instance.apply_where_filters(relation, args)
|
|
32
|
+
relation = relation.limit(args[:first]) if args[:first]
|
|
33
|
+
relation = relation.offset(args[:offset]) if args[:offset]
|
|
34
|
+
relation
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if @field_def.respond_to?(:resolve_proc=)
|
|
38
|
+
@field_def.resolve_proc = ->(_obj, arguments, ctx) do
|
|
39
|
+
model_class = model_class_static
|
|
40
|
+
if model_class.nil?
|
|
41
|
+
[]
|
|
42
|
+
else
|
|
43
|
+
args = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
44
|
+
relation = model_class.all
|
|
45
|
+
|
|
46
|
+
relation = DirectiveResolvers::BuilderApplier.apply(relation, field_ast_node, args, ctx, resolver_instance)
|
|
47
|
+
|
|
48
|
+
relation = resolver_instance.apply_where_filters(relation, args)
|
|
49
|
+
relation = relation.limit(args[:first]) if args[:first]
|
|
50
|
+
relation = relation.offset(args[:offset]) if args[:offset]
|
|
51
|
+
relation
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def extract_model_name
|
|
60
|
+
arg = @directive_node.arguments&.find { |a| a.name == 'model' }
|
|
61
|
+
return nil unless arg
|
|
62
|
+
arg.value.respond_to?(:value) ? arg.value.value : arg.value
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def constantize_model(name)
|
|
66
|
+
return nil unless name
|
|
67
|
+
name = name.to_s.split('::').last
|
|
68
|
+
Object.const_get(name) rescue nil
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lighthouse
|
|
4
|
+
module GraphQL
|
|
5
|
+
module DirectiveResolvers
|
|
6
|
+
class AuthDirectiveResolver < BaseDirectiveResolver
|
|
7
|
+
def define_resolver(target_type_class)
|
|
8
|
+
field_name = @field_def.name
|
|
9
|
+
method_name = "resolve_field_#{field_name}"
|
|
10
|
+
|
|
11
|
+
original_singleton =
|
|
12
|
+
if target_type_class.respond_to?(method_name)
|
|
13
|
+
target_type_class.method(method_name)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
original_proc =
|
|
17
|
+
if @field_def.respond_to?(:resolve_proc)
|
|
18
|
+
@field_def.resolve_proc
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
wrapper = lambda do |obj, arguments, ctx|
|
|
22
|
+
user = ctx && (ctx[:current_user] || ctx['current_user'])
|
|
23
|
+
raise(::GraphQL::ExecutionError, 'Unauthenticated.') unless user
|
|
24
|
+
|
|
25
|
+
if original_singleton
|
|
26
|
+
original_singleton.call(obj, arguments, ctx)
|
|
27
|
+
elsif original_proc
|
|
28
|
+
original_proc.call(obj, arguments, ctx)
|
|
29
|
+
elsif obj.respond_to?(field_name)
|
|
30
|
+
obj.public_send(field_name)
|
|
31
|
+
else
|
|
32
|
+
snake = field_name.to_s.underscore
|
|
33
|
+
obj.respond_to?(snake) ? obj.public_send(snake) : nil
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
target_type_class.define_singleton_method(method_name, &wrapper)
|
|
38
|
+
@field_def.resolve_proc = wrapper if @field_def.respond_to?(:resolve_proc=)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lighthouse
|
|
4
|
+
module GraphQL
|
|
5
|
+
module DirectiveResolvers
|
|
6
|
+
class BaseDirectiveResolver
|
|
7
|
+
def initialize(field_def, type_def, directive_node, opts = {})
|
|
8
|
+
@field_def = field_def
|
|
9
|
+
@type_def = type_def
|
|
10
|
+
@directive_node = directive_node
|
|
11
|
+
@opts = opts
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def define_resolver(target_type_class)
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def apply_where_filters(relation, args)
|
|
19
|
+
return relation unless relation.respond_to?(:where)
|
|
20
|
+
|
|
21
|
+
args_h = args.respond_to?(:to_h) ? args.to_h : (args || {})
|
|
22
|
+
|
|
23
|
+
where_condition = args_h[:where] || args_h['where']
|
|
24
|
+
relation = DirectiveResolvers::WhereConditionsApplier.apply(relation, where_condition) if where_condition
|
|
25
|
+
|
|
26
|
+
@field_def.ast_node.arguments.each do |arg_node|
|
|
27
|
+
next unless arg_node.directives.any? { |dir| dir.name == 'whereHasConditions' }
|
|
28
|
+
|
|
29
|
+
arg_value = Lighthouse::Support::Naming.fetch_arg(args_h, arg_node.name)
|
|
30
|
+
next unless arg_value
|
|
31
|
+
|
|
32
|
+
relation_name = extract_relation_name_from_directive(arg_node) || Lighthouse::Support::Naming.derive_relation_name(arg_node.name)
|
|
33
|
+
relation = DirectiveResolvers::WhereConditionsApplier.apply_has_argument(relation, arg_node.name, arg_value, relation_name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
apply_arg_builders(relation, args_h)
|
|
37
|
+
rescue ::GraphQL::ExecutionError
|
|
38
|
+
raise
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
Lighthouse::Config.handle_error(e)
|
|
41
|
+
relation
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Compose argument-builder directives (@eq, @neq, @in, @notIn, @like,
|
|
45
|
+
# @where, @orderBy) onto the relation, in SDL argument order, for each
|
|
46
|
+
# field argument that carries one and that the client supplied a value
|
|
47
|
+
# for.
|
|
48
|
+
def apply_arg_builders(relation, args_h)
|
|
49
|
+
return relation unless relation.respond_to?(:where)
|
|
50
|
+
return relation unless @field_def.ast_node
|
|
51
|
+
|
|
52
|
+
registry = Lighthouse::DirectiveRegistry.default
|
|
53
|
+
|
|
54
|
+
(@field_def.ast_node.arguments || []).each do |arg_node|
|
|
55
|
+
(arg_node.directives || []).each do |dnode|
|
|
56
|
+
builder_class = registry.arg_builder_class(dnode.name)
|
|
57
|
+
next unless builder_class
|
|
58
|
+
|
|
59
|
+
value = Lighthouse::Support::Naming.fetch_arg(args_h, arg_node.name)
|
|
60
|
+
next if value.nil?
|
|
61
|
+
|
|
62
|
+
relation = builder_class.new(arg_node, dnode).handle_builder(relation, value)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
relation
|
|
67
|
+
rescue ::GraphQL::ExecutionError
|
|
68
|
+
raise
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
Lighthouse::Config.handle_error(e)
|
|
71
|
+
relation
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def extract_relation_name_from_directive(arg_node)
|
|
75
|
+
directive = arg_node.directives.find { |dir| dir.name == 'whereHasConditions' }
|
|
76
|
+
return nil unless directive
|
|
77
|
+
|
|
78
|
+
relation_arg = directive.arguments.find { |arg| arg.name == 'relation' }
|
|
79
|
+
return nil unless relation_arg
|
|
80
|
+
|
|
81
|
+
relation_arg.value.respond_to?(:value) ? relation_arg.value.value : relation_arg.value
|
|
82
|
+
rescue StandardError
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def constantize_model(name)
|
|
87
|
+
Lighthouse::Support::ModelResolver.constantize(name)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|