lighthouse-graphql 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +168 -1
- data/README.md +46 -2
- data/docs/authorization.md +116 -57
- data/docs/best-practices.md +16 -3
- data/docs/caching.md +161 -0
- data/docs/configuration.md +83 -4
- data/docs/custom-directives.md +5 -1
- data/docs/directives.md +117 -38
- data/docs/getting-started.md +84 -0
- data/docs/mutations.md +260 -0
- data/docs/performance.md +138 -0
- data/docs/relationships.md +80 -33
- data/docs/resolvers.md +179 -0
- data/docs/validation.md +132 -0
- data/lib/generators/lighthouse/builder_generator.rb +30 -0
- data/lib/generators/lighthouse/class_generator.rb +61 -0
- data/lib/generators/lighthouse/install/install_generator.rb +127 -0
- data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +8 -0
- data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +57 -0
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +67 -0
- data/lib/generators/lighthouse/install/templates/schema.graphql.tt +19 -0
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -0
- data/lib/generators/lighthouse/mutation_generator.rb +30 -0
- data/lib/generators/lighthouse/query_generator.rb +29 -0
- data/lib/generators/lighthouse/validator_generator.rb +36 -0
- data/lib/lighthouse/cache_control.rb +126 -0
- data/lib/lighthouse/caching.rb +68 -0
- data/lib/lighthouse/config.rb +85 -19
- data/lib/lighthouse/contracts/arg_builder.rb +8 -12
- data/lib/lighthouse/contracts/arg_manipulator.rb +8 -12
- data/lib/lighthouse/contracts/directive.rb +10 -14
- data/lib/lighthouse/contracts/field_manipulator.rb +8 -12
- data/lib/lighthouse/contracts/field_middleware.rb +7 -11
- data/lib/lighthouse/contracts/field_resolver.rb +9 -13
- data/lib/lighthouse/dataloader_sources/association_loader.rb +28 -19
- data/lib/lighthouse/dataloader_sources/model_loader.rb +10 -14
- data/lib/lighthouse/dataloader_sources/relation_count_loader.rb +16 -0
- data/lib/lighthouse/dataloader_sources/relation_page_loader.rb +18 -0
- data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +57 -69
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +26 -57
- data/lib/lighthouse/directive_resolvers/association_resolvers.rb +161 -0
- data/lib/lighthouse/directive_resolvers/auth_resolver.rb +41 -38
- data/lib/lighthouse/directive_resolvers/base.rb +142 -86
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +58 -56
- data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +133 -0
- data/lib/lighthouse/directive_resolvers/can_resolvers.rb +177 -0
- data/lib/lighthouse/directive_resolvers/convention_resolver.rb +70 -0
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +42 -48
- data/lib/lighthouse/directive_resolvers/field_resolver.rb +27 -68
- data/lib/lighthouse/directive_resolvers/find_resolver.rb +42 -52
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +39 -45
- data/lib/lighthouse/directive_resolvers/guard_resolver.rb +16 -22
- data/lib/lighthouse/directive_resolvers/method_resolver.rb +19 -25
- data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +219 -0
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +70 -128
- data/lib/lighthouse/directive_resolvers/rename_resolver.rb +18 -24
- data/lib/lighthouse/directive_resolvers/resolution.rb +31 -27
- data/lib/lighthouse/directive_resolvers/validator_resolver.rb +193 -0
- data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +164 -163
- data/lib/lighthouse/directives/arguments/base.rb +23 -31
- data/lib/lighthouse/directives/arguments/filters.rb +113 -93
- data/lib/lighthouse/directives/arguments/order_by.rb +41 -41
- data/lib/lighthouse/directives/pagination_info_types.rb +69 -0
- data/lib/lighthouse/directives/pagination_manipulator.rb +176 -0
- data/lib/lighthouse/directives/registry.rb +210 -141
- data/lib/lighthouse/directives/where_conditions_directive.rb +236 -200
- data/lib/lighthouse/errors.rb +27 -0
- data/lib/lighthouse/federation_directives.rb +53 -0
- data/lib/lighthouse/field_resolver_stack.rb +56 -0
- data/lib/lighthouse/generated_resolvers.rb +82 -0
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/graphql_ruby_patches.rb +18 -0
- data/lib/lighthouse/ide_helper.rb +98 -0
- data/lib/lighthouse/namespaces.rb +21 -0
- data/lib/lighthouse/nested_mutations/belongs_to_operations.rb +50 -0
- data/lib/lighthouse/nested_mutations/executor.rb +149 -0
- data/lib/lighthouse/nested_mutations/has_operations.rb +66 -0
- data/lib/lighthouse/nested_mutations/many_to_many_operations.rb +44 -0
- data/lib/lighthouse/nested_mutations/partitioner.rb +125 -0
- data/lib/lighthouse/nested_mutations.rb +210 -0
- data/lib/lighthouse/pagination.rb +221 -0
- data/lib/lighthouse/persisted_queries.rb +167 -0
- data/lib/lighthouse/railtie.rb +26 -0
- data/lib/lighthouse/rb_lighthouse.rb +50 -29
- data/lib/lighthouse/reference_resolver.rb +13 -15
- data/lib/lighthouse/relation_batch.rb +156 -0
- data/lib/lighthouse/resolver_dispatch.rb +26 -0
- data/lib/lighthouse/schema_cache.rb +120 -0
- data/lib/lighthouse/schema_factory.rb +27 -37
- data/lib/lighthouse/schema_generator.rb +196 -239
- data/lib/lighthouse/schema_implementation.rb +51 -48
- data/lib/lighthouse/sdl_loader.rb +17 -20
- data/lib/lighthouse/support/arguments.rb +119 -0
- data/lib/lighthouse/support/authorization.rb +18 -19
- data/lib/lighthouse/support/batching.rb +16 -0
- data/lib/lighthouse/support/directive_args.rb +26 -24
- data/lib/lighthouse/support/model_resolver.rb +56 -56
- data/lib/lighthouse/support/naming.rb +55 -59
- data/lib/lighthouse/tasks/lighthouse.rake +33 -0
- data/lib/lighthouse/where_conditions/operator_map.rb +25 -29
- metadata +52 -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
|
@@ -2,100 +2,120 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'base'
|
|
4
4
|
|
|
5
|
-
module Lighthouse
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
5
|
+
module Lighthouse::GraphQL::Directives::Arguments
|
|
6
|
+
# @eq — equality filter (the default). `field(status: String @eq)`
|
|
7
|
+
class Eq < Base
|
|
8
|
+
def self.directive_name = 'eq'
|
|
9
|
+
def self.definition = 'directive @eq(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
|
|
10
|
+
|
|
11
|
+
def handle_builder(relation, value)
|
|
12
|
+
return relation if value.nil?
|
|
13
|
+
|
|
14
|
+
relation.where(column => value)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @whereKey — filter by the model's primary key, whatever its column is
|
|
19
|
+
# named; a list matches any of its values. `deletePosts(ids: [ID!]! @whereKey)`
|
|
20
|
+
class WhereKey < Base
|
|
21
|
+
def self.directive_name = 'whereKey'
|
|
22
|
+
def self.definition = 'directive @whereKey on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
|
|
23
|
+
|
|
24
|
+
def handle_builder(relation, value)
|
|
25
|
+
return relation if value.nil?
|
|
26
|
+
|
|
27
|
+
relation.where(relation.klass.primary_key => value)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @neq — inequality filter.
|
|
32
|
+
class Neq < Base
|
|
33
|
+
def self.directive_name = 'neq'
|
|
34
|
+
def self.definition = 'directive @neq(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
|
|
35
|
+
|
|
36
|
+
def handle_builder(relation, value)
|
|
37
|
+
return relation if value.nil?
|
|
38
|
+
|
|
39
|
+
relation.where.not(column => value)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @in — value is one of a list.
|
|
44
|
+
class In < Base
|
|
45
|
+
def self.directive_name = 'in'
|
|
46
|
+
def self.definition = 'directive @in(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
|
|
47
|
+
|
|
48
|
+
def handle_builder(relation, value)
|
|
49
|
+
return relation if value.nil?
|
|
50
|
+
|
|
51
|
+
relation.where(column => Array(value))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @notIn — value is none of a list.
|
|
56
|
+
class NotIn < Base
|
|
57
|
+
def self.directive_name = 'notIn'
|
|
58
|
+
def self.definition = 'directive @notIn(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
|
|
59
|
+
|
|
60
|
+
def handle_builder(relation, value)
|
|
61
|
+
return relation if value.nil?
|
|
62
|
+
|
|
63
|
+
relation.where.not(column => Array(value))
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @like — SQL LIKE with the value wrapped in wildcards.
|
|
68
|
+
class Like < Base
|
|
69
|
+
def self.directive_name = 'like'
|
|
70
|
+
def self.definition = 'directive @like(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
|
|
71
|
+
|
|
72
|
+
def handle_builder(relation, value)
|
|
73
|
+
return relation if value.nil?
|
|
74
|
+
|
|
75
|
+
relation.where(arel_column(relation).matches("%#{value}%"))
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @where — generic operator filter, sharing the SQLOperator semantics with
|
|
80
|
+
# the @whereConditions extension. `field(age: Int @where(operator: "GT"))`
|
|
81
|
+
class Where < Base
|
|
82
|
+
def self.directive_name = 'where'
|
|
83
|
+
def self.definition = 'directive @where(key: String, operator: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
|
|
84
|
+
|
|
85
|
+
# LIKE pattern of each wildcard operator, built from the client value.
|
|
86
|
+
LIKE_PATTERNS = {
|
|
87
|
+
'CONTAINS' => ->(value) { "%#{value}%" },
|
|
88
|
+
'STARTS_WITH' => ->(value) { "#{value}%" },
|
|
89
|
+
'ENDS_WITH' => ->(value) { "%#{value}" }
|
|
90
|
+
}.freeze
|
|
91
|
+
|
|
92
|
+
def handle_builder(relation, value)
|
|
93
|
+
return relation if value.nil?
|
|
94
|
+
|
|
95
|
+
operator = sql_operator
|
|
96
|
+
case operator
|
|
97
|
+
when 'IN' then relation.where(column => Array(value))
|
|
98
|
+
when 'NIN' then relation.where.not(column => Array(value))
|
|
99
|
+
when 'IS_NULL' then relation.where(column => nil)
|
|
100
|
+
when 'IS_NOT_NULL' then relation.where.not(column => nil)
|
|
101
|
+
when *LIKE_PATTERNS.keys then relation.where(arel_column(relation).matches(LIKE_PATTERNS[operator].call(value)))
|
|
102
|
+
else compare(relation, operator, value)
|
|
98
103
|
end
|
|
99
104
|
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def sql_operator
|
|
109
|
+
(Lighthouse::Support::DirectiveArgs.literal(@directive_node, 'operator') || 'EQ').to_s.upcase
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Comparison operators map onto Arel predicates; an unknown one falls
|
|
113
|
+
# back to equality.
|
|
114
|
+
def compare(relation, operator, value)
|
|
115
|
+
arel_method = Lighthouse::WhereConditions::OperatorMap[operator]
|
|
116
|
+
return relation.where(column => value) unless arel_method
|
|
117
|
+
|
|
118
|
+
relation.where(arel_column(relation).public_send(arel_method, value))
|
|
119
|
+
end
|
|
100
120
|
end
|
|
101
121
|
end
|
|
@@ -2,47 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'base'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
next rel if col.nil? || col.empty?
|
|
23
|
-
|
|
24
|
-
direction = clause[:order].to_s.downcase == 'desc' ? :desc : :asc
|
|
25
|
-
rel.order(col => direction)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
def normalize(value)
|
|
32
|
-
list = value.is_a?(Array) ? value : [value]
|
|
33
|
-
list.filter_map do |item|
|
|
34
|
-
hash = item.respond_to?(:to_h) ? item.to_h : item
|
|
35
|
-
next nil unless hash.is_a?(Hash)
|
|
36
|
-
|
|
37
|
-
symbolized = hash.transform_keys { |k| k.to_s.to_sym }
|
|
38
|
-
col = symbolized[:column] || symbolized[:field]
|
|
39
|
-
next nil if col.nil?
|
|
40
|
-
|
|
41
|
-
{ column: col, order: symbolized[:order] || symbolized[:direction] || 'ASC' }
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
5
|
+
# @orderBy — sort the result by one or more columns.
|
|
6
|
+
#
|
|
7
|
+
# The client value is a list of clauses, each `{ column:, order: }`
|
|
8
|
+
# (also accepts `{ field:, direction: }`). Clauses apply in order, so
|
|
9
|
+
# `[{column: NAME, order: ASC}, {column: AGE, order: DESC}]` sorts by
|
|
10
|
+
# name then age.
|
|
11
|
+
class Lighthouse::GraphQL::Directives::Arguments::OrderBy < Lighthouse::GraphQL::Directives::Arguments::Base
|
|
12
|
+
def self.directive_name = 'orderBy'
|
|
13
|
+
def self.definition = 'directive @orderBy(columns: [String!], relations: [String!]) on ARGUMENT_DEFINITION | FIELD_DEFINITION'
|
|
14
|
+
|
|
15
|
+
def handle_builder(relation, value)
|
|
16
|
+
normalize(value).reduce(relation) do |rel, clause|
|
|
17
|
+
col = Lighthouse::Support::Naming.column(clause[:column], rel)
|
|
18
|
+
next rel if col.blank?
|
|
19
|
+
|
|
20
|
+
direction = clause[:order].to_s.downcase == 'desc' ? :desc : :asc
|
|
21
|
+
rel.order(col => direction)
|
|
46
22
|
end
|
|
47
23
|
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def normalize(value)
|
|
28
|
+
list = value.is_a?(Array) ? value : [value]
|
|
29
|
+
list.filter_map { |item| clause(item) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# One `{ column:, order: }` clause, or nil when the item names no column.
|
|
33
|
+
def clause(item)
|
|
34
|
+
symbolized = symbolized_hash(item)
|
|
35
|
+
return nil unless symbolized
|
|
36
|
+
|
|
37
|
+
col = symbolized[:column] || symbolized[:field]
|
|
38
|
+
return nil if col.nil?
|
|
39
|
+
|
|
40
|
+
{ column: col, order: symbolized[:order] || symbolized[:direction] || 'ASC' }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The item as a symbol-keyed hash, or nil when it is not hash-like.
|
|
44
|
+
def symbolized_hash(item)
|
|
45
|
+
hash = item.respond_to?(:to_h) ? item.to_h : item
|
|
46
|
+
hash.transform_keys { |k| k.to_s.to_sym } if hash.is_a?(Hash)
|
|
47
|
+
end
|
|
48
48
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SDL of the info types the generated paginator and connection types report
|
|
4
|
+
# through (`paginatorInfo: PaginatorInfo!`, `pageInfo: PageInfo!`), keyed by
|
|
5
|
+
# type name. Each is emitted once by PaginationManipulator, and only when the
|
|
6
|
+
# schema does not declare it itself.
|
|
7
|
+
module Lighthouse::GraphQL::Directives::PaginationInfoTypes
|
|
8
|
+
DEFINITIONS = {
|
|
9
|
+
'PaginatorInfo' => <<~GRAPHQL,
|
|
10
|
+
"Information about pagination using a fully featured paginator."
|
|
11
|
+
type PaginatorInfo {
|
|
12
|
+
"Number of items in the current page."
|
|
13
|
+
count: Int!
|
|
14
|
+
"Index of the current page."
|
|
15
|
+
currentPage: Int!
|
|
16
|
+
"Index of the first item in the current page."
|
|
17
|
+
firstItem: Int
|
|
18
|
+
"Are there more pages after this one?"
|
|
19
|
+
hasMorePages: Boolean!
|
|
20
|
+
"Index of the last item in the current page."
|
|
21
|
+
lastItem: Int
|
|
22
|
+
"Index of the last available page."
|
|
23
|
+
lastPage: Int!
|
|
24
|
+
"Number of items per page."
|
|
25
|
+
perPage: Int!
|
|
26
|
+
"Number of total available items."
|
|
27
|
+
total: Int!
|
|
28
|
+
}
|
|
29
|
+
GRAPHQL
|
|
30
|
+
'SimplePaginatorInfo' => <<~GRAPHQL,
|
|
31
|
+
"Information about pagination using a simple paginator."
|
|
32
|
+
type SimplePaginatorInfo {
|
|
33
|
+
"Number of items in the current page."
|
|
34
|
+
count: Int!
|
|
35
|
+
"Index of the current page."
|
|
36
|
+
currentPage: Int!
|
|
37
|
+
"Index of the first item in the current page."
|
|
38
|
+
firstItem: Int
|
|
39
|
+
"Index of the last item in the current page."
|
|
40
|
+
lastItem: Int
|
|
41
|
+
"Number of items per page."
|
|
42
|
+
perPage: Int!
|
|
43
|
+
"Are there more pages after this one?"
|
|
44
|
+
hasMorePages: Boolean!
|
|
45
|
+
}
|
|
46
|
+
GRAPHQL
|
|
47
|
+
'PageInfo' => <<~GRAPHQL
|
|
48
|
+
"Information about pagination using a Relay style cursor connection."
|
|
49
|
+
type PageInfo {
|
|
50
|
+
"When paginating forwards, are there more items?"
|
|
51
|
+
hasNextPage: Boolean!
|
|
52
|
+
"When paginating backwards, are there more items?"
|
|
53
|
+
hasPreviousPage: Boolean!
|
|
54
|
+
"The cursor to continue paginating backwards."
|
|
55
|
+
startCursor: String
|
|
56
|
+
"The cursor to continue paginating forwards."
|
|
57
|
+
endCursor: String
|
|
58
|
+
"Total number of nodes in the paginated connection."
|
|
59
|
+
total: Int!
|
|
60
|
+
"Number of nodes in the current page."
|
|
61
|
+
count: Int!
|
|
62
|
+
"Index of the current page."
|
|
63
|
+
currentPage: Int!
|
|
64
|
+
"Index of the last available page."
|
|
65
|
+
lastPage: Int!
|
|
66
|
+
}
|
|
67
|
+
GRAPHQL
|
|
68
|
+
}.freeze
|
|
69
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'pagination_info_types'
|
|
4
|
+
|
|
5
|
+
# Schema side of pagination, as PHP Lighthouse does it: a list field with
|
|
6
|
+
# @paginate — or a relation directive given `type:` — becomes a paginator
|
|
7
|
+
# or connection type, and gains its pagination arguments.
|
|
8
|
+
#
|
|
9
|
+
# users: [User!]! @paginate -> users(first: Int!, page: Int): UserPaginator!
|
|
10
|
+
# users: [User!]! @paginate(type: SIMPLE) -> users(first: Int!, page: Int): UserSimplePaginator!
|
|
11
|
+
# users: [User!]! @paginate(type: CONNECTION) -> users(first: Int!, after: String): UserConnection!
|
|
12
|
+
# posts: [Post!]! @hasMany(type: PAGINATOR) -> posts(first: Int!, page: Int): PostPaginator!
|
|
13
|
+
#
|
|
14
|
+
# `first` defaults to the directive's defaultCount, else
|
|
15
|
+
# Config.pagination_default_count; without either it is required. A plain
|
|
16
|
+
# relation directive (no `type:`) and @all keep returning the list.
|
|
17
|
+
# The runtime side is Lighthouse::GraphQL::Pagination.
|
|
18
|
+
class Lighthouse::GraphQL::Directives::PaginationManipulator
|
|
19
|
+
RELATION_DIRECTIVES = %w[hasMany belongsToMany morphMany morphToMany].freeze
|
|
20
|
+
OBJECT_DEFINITIONS = [
|
|
21
|
+
::GraphQL::Language::Nodes::ObjectTypeDefinition,
|
|
22
|
+
::GraphQL::Language::Nodes::ObjectTypeExtension
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
# Shared by every generated paginator/connection of the same kind.
|
|
26
|
+
INFO_TYPES = Lighthouse::GraphQL::Directives::PaginationInfoTypes::DEFINITIONS
|
|
27
|
+
|
|
28
|
+
def self.manipulate(sdl)
|
|
29
|
+
return sdl unless sdl.is_a?(String) && sdl.match?(/@(paginate|#{RELATION_DIRECTIVES.join('|')})\b/)
|
|
30
|
+
|
|
31
|
+
document = parse(sdl)
|
|
32
|
+
return sdl unless document
|
|
33
|
+
|
|
34
|
+
new(document, cache_control: sdl.gsub(/directive\s+@cacheControl/, '').include?('@cacheControl')).manipulate
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.parse(sdl)
|
|
38
|
+
::GraphQL::Language::Parser.parse(sdl)
|
|
39
|
+
rescue ::GraphQL::ParseError => e
|
|
40
|
+
warn("Lighthouse pagination: parse failed: #{e.message}")
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
private_class_method :parse
|
|
44
|
+
|
|
45
|
+
# With @cacheControl in use, the generated wrappers (paginatorInfo, data,
|
|
46
|
+
# pageInfo, edges, node) take their parent's maxAge, as in PHP Lighthouse:
|
|
47
|
+
# they are not entities of their own, so they must not turn a cacheable
|
|
48
|
+
# page into `no-cache`.
|
|
49
|
+
def initialize(document, cache_control: false)
|
|
50
|
+
@document = document
|
|
51
|
+
@inherit = cache_control ? ' @cacheControl(inheritMaxAge: true)' : ''
|
|
52
|
+
@defined = document.definitions.filter_map { |d| d.name if d.respond_to?(:name) && !d.class.name.end_with?('Extension') }
|
|
53
|
+
@generated = {}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def manipulate
|
|
57
|
+
definitions = @document.definitions.map do |definition|
|
|
58
|
+
next definition unless OBJECT_DEFINITIONS.any? { |kind| definition.is_a?(kind) }
|
|
59
|
+
|
|
60
|
+
definition.merge(fields: definition.fields.map { |field| paginate_field(field) })
|
|
61
|
+
end
|
|
62
|
+
printed = ::GraphQL::Language::Printer.new.print(@document.merge(definitions: definitions))
|
|
63
|
+
extra = @generated.except(*@defined).values
|
|
64
|
+
([printed] + extra).join("\n\n")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def paginate_field(field)
|
|
70
|
+
type = pagination_type(field.directives)
|
|
71
|
+
item = type && list_item_type(field.type)
|
|
72
|
+
return field unless item
|
|
73
|
+
|
|
74
|
+
first = first_argument(field.directives)
|
|
75
|
+
|
|
76
|
+
case type
|
|
77
|
+
when 'CONNECTION'
|
|
78
|
+
generate("#{item}Connection", connection_type(item))
|
|
79
|
+
generate("#{item}Edge", edge_type(item))
|
|
80
|
+
generate('PageInfo', INFO_TYPES['PageInfo'])
|
|
81
|
+
rewrite(field, "#{item}Connection", first, 'after: String', 'A cursor after which elements are returned.')
|
|
82
|
+
when 'SIMPLE'
|
|
83
|
+
generate("#{item}SimplePaginator", paginator_type(item, 'SimplePaginator', 'SimplePaginatorInfo'))
|
|
84
|
+
generate('SimplePaginatorInfo', INFO_TYPES['SimplePaginatorInfo'])
|
|
85
|
+
rewrite(field, "#{item}SimplePaginator", first, 'page: Int', 'The offset from which items are returned.')
|
|
86
|
+
else
|
|
87
|
+
generate("#{item}Paginator", paginator_type(item, 'Paginator', 'PaginatorInfo'))
|
|
88
|
+
generate('PaginatorInfo', INFO_TYPES['PaginatorInfo'])
|
|
89
|
+
rewrite(field, "#{item}Paginator", first, 'page: Int', 'The offset from which items are returned.')
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# `first` defaults to the directive's defaultCount, else the configured
|
|
94
|
+
# default; with neither it is required.
|
|
95
|
+
def first_argument(directives)
|
|
96
|
+
default_count = int_arg(directives, 'defaultCount') || Lighthouse::Config.pagination_default_count
|
|
97
|
+
"first: Int!#{" = #{default_count}" if default_count}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The field returning `type_name!`, plus `first` and the page/cursor
|
|
101
|
+
# argument, unless the field declares them itself.
|
|
102
|
+
def rewrite(field, type_name, first, second, second_description)
|
|
103
|
+
snippet = %(type T { f("Limits number of fetched items." #{first}, "#{second_description}" #{second}): #{type_name}! })
|
|
104
|
+
generated = ::GraphQL::Language::Parser.parse(snippet).definitions.first.fields.first
|
|
105
|
+
declared = field.arguments.map(&:name)
|
|
106
|
+
arguments = field.arguments + generated.arguments.reject { |argument| declared.include?(argument.name) }
|
|
107
|
+
field.merge(type: generated.type, arguments: arguments)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def generate(name, sdl)
|
|
111
|
+
@generated[name] ||= sdl.strip
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def paginator_type(item, suffix, info)
|
|
115
|
+
<<~GRAPHQL
|
|
116
|
+
"A paginated list of #{item} items."
|
|
117
|
+
type #{item}#{suffix} {
|
|
118
|
+
"Pagination information about the list of items."
|
|
119
|
+
paginatorInfo: #{info}!#{@inherit}
|
|
120
|
+
"A list of #{item} items."
|
|
121
|
+
data: [#{item}!]!#{@inherit}
|
|
122
|
+
}
|
|
123
|
+
GRAPHQL
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def connection_type(item)
|
|
127
|
+
<<~GRAPHQL
|
|
128
|
+
"A paginated list of #{item} edges."
|
|
129
|
+
type #{item}Connection {
|
|
130
|
+
"Pagination information about the list of edges."
|
|
131
|
+
pageInfo: PageInfo!#{@inherit}
|
|
132
|
+
"A list of #{item} edges."
|
|
133
|
+
edges: [#{item}Edge!]!#{@inherit}
|
|
134
|
+
}
|
|
135
|
+
GRAPHQL
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def edge_type(item)
|
|
139
|
+
<<~GRAPHQL
|
|
140
|
+
"An edge that contains a node of type #{item} and a cursor."
|
|
141
|
+
type #{item}Edge {
|
|
142
|
+
"The #{item} node."
|
|
143
|
+
node: #{item}!#{@inherit}
|
|
144
|
+
"A unique cursor that can be used for pagination."
|
|
145
|
+
cursor: String!
|
|
146
|
+
}
|
|
147
|
+
GRAPHQL
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# PAGINATOR / SIMPLE / CONNECTION, or nil when the field is not paginated.
|
|
151
|
+
def pagination_type(directives)
|
|
152
|
+
paginate = directives.find { |d| d.name == 'paginate' }
|
|
153
|
+
return (Lighthouse::Support::DirectiveArgs.literal(paginate, 'type') || 'PAGINATOR').to_s.upcase if paginate
|
|
154
|
+
|
|
155
|
+
relation_pagination_type(directives.find { |d| RELATION_DIRECTIVES.include?(d.name) })
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# A relation directive paginates only when given `type:`.
|
|
159
|
+
def relation_pagination_type(relation)
|
|
160
|
+
relation && Lighthouse::Support::DirectiveArgs.literal(relation, 'type')&.to_s&.upcase
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def int_arg(directives, name)
|
|
164
|
+
directive = directives.find { |d| d.name == 'paginate' || RELATION_DIRECTIVES.include?(d.name) }
|
|
165
|
+
directive && Lighthouse::Support::DirectiveArgs.literal(directive, name)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def list_item_type(type_node)
|
|
169
|
+
type_node = type_node.of_type if type_node.is_a?(::GraphQL::Language::Nodes::NonNullType)
|
|
170
|
+
return nil unless type_node.is_a?(::GraphQL::Language::Nodes::ListType)
|
|
171
|
+
|
|
172
|
+
inner = type_node.of_type
|
|
173
|
+
inner = inner.of_type while inner.respond_to?(:of_type)
|
|
174
|
+
inner.name
|
|
175
|
+
end
|
|
176
|
+
end
|