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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +168 -1
  3. data/README.md +46 -2
  4. data/docs/authorization.md +116 -57
  5. data/docs/best-practices.md +16 -3
  6. data/docs/caching.md +161 -0
  7. data/docs/configuration.md +83 -4
  8. data/docs/custom-directives.md +5 -1
  9. data/docs/directives.md +117 -38
  10. data/docs/getting-started.md +84 -0
  11. data/docs/mutations.md +260 -0
  12. data/docs/performance.md +138 -0
  13. data/docs/relationships.md +80 -33
  14. data/docs/resolvers.md +179 -0
  15. data/docs/validation.md +132 -0
  16. data/lib/generators/lighthouse/builder_generator.rb +30 -0
  17. data/lib/generators/lighthouse/class_generator.rb +61 -0
  18. data/lib/generators/lighthouse/install/install_generator.rb +127 -0
  19. data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +8 -0
  20. data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +57 -0
  21. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +67 -0
  22. data/lib/generators/lighthouse/install/templates/schema.graphql.tt +19 -0
  23. data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -0
  24. data/lib/generators/lighthouse/mutation_generator.rb +30 -0
  25. data/lib/generators/lighthouse/query_generator.rb +29 -0
  26. data/lib/generators/lighthouse/validator_generator.rb +36 -0
  27. data/lib/lighthouse/cache_control.rb +126 -0
  28. data/lib/lighthouse/caching.rb +68 -0
  29. data/lib/lighthouse/config.rb +85 -19
  30. data/lib/lighthouse/contracts/arg_builder.rb +8 -12
  31. data/lib/lighthouse/contracts/arg_manipulator.rb +8 -12
  32. data/lib/lighthouse/contracts/directive.rb +10 -14
  33. data/lib/lighthouse/contracts/field_manipulator.rb +8 -12
  34. data/lib/lighthouse/contracts/field_middleware.rb +7 -11
  35. data/lib/lighthouse/contracts/field_resolver.rb +9 -13
  36. data/lib/lighthouse/dataloader_sources/association_loader.rb +28 -19
  37. data/lib/lighthouse/dataloader_sources/model_loader.rb +10 -14
  38. data/lib/lighthouse/dataloader_sources/relation_count_loader.rb +16 -0
  39. data/lib/lighthouse/dataloader_sources/relation_page_loader.rb +18 -0
  40. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +57 -69
  41. data/lib/lighthouse/directive_resolvers/all_resolver.rb +26 -57
  42. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +161 -0
  43. data/lib/lighthouse/directive_resolvers/auth_resolver.rb +41 -38
  44. data/lib/lighthouse/directive_resolvers/base.rb +142 -86
  45. data/lib/lighthouse/directive_resolvers/builder_applier.rb +58 -56
  46. data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +133 -0
  47. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +177 -0
  48. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +70 -0
  49. data/lib/lighthouse/directive_resolvers/count_resolver.rb +42 -48
  50. data/lib/lighthouse/directive_resolvers/field_resolver.rb +27 -68
  51. data/lib/lighthouse/directive_resolvers/find_resolver.rb +42 -52
  52. data/lib/lighthouse/directive_resolvers/first_resolver.rb +39 -45
  53. data/lib/lighthouse/directive_resolvers/guard_resolver.rb +16 -22
  54. data/lib/lighthouse/directive_resolvers/method_resolver.rb +19 -25
  55. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +219 -0
  56. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +70 -128
  57. data/lib/lighthouse/directive_resolvers/rename_resolver.rb +18 -24
  58. data/lib/lighthouse/directive_resolvers/resolution.rb +31 -27
  59. data/lib/lighthouse/directive_resolvers/validator_resolver.rb +193 -0
  60. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +164 -163
  61. data/lib/lighthouse/directives/arguments/base.rb +23 -31
  62. data/lib/lighthouse/directives/arguments/filters.rb +113 -93
  63. data/lib/lighthouse/directives/arguments/order_by.rb +41 -41
  64. data/lib/lighthouse/directives/pagination_info_types.rb +69 -0
  65. data/lib/lighthouse/directives/pagination_manipulator.rb +176 -0
  66. data/lib/lighthouse/directives/registry.rb +210 -141
  67. data/lib/lighthouse/directives/where_conditions_directive.rb +236 -200
  68. data/lib/lighthouse/errors.rb +27 -0
  69. data/lib/lighthouse/federation_directives.rb +53 -0
  70. data/lib/lighthouse/field_resolver_stack.rb +56 -0
  71. data/lib/lighthouse/generated_resolvers.rb +82 -0
  72. data/lib/lighthouse/graphql/version.rb +1 -1
  73. data/lib/lighthouse/graphql_ruby_patches.rb +18 -0
  74. data/lib/lighthouse/ide_helper.rb +98 -0
  75. data/lib/lighthouse/namespaces.rb +21 -0
  76. data/lib/lighthouse/nested_mutations/belongs_to_operations.rb +50 -0
  77. data/lib/lighthouse/nested_mutations/executor.rb +149 -0
  78. data/lib/lighthouse/nested_mutations/has_operations.rb +66 -0
  79. data/lib/lighthouse/nested_mutations/many_to_many_operations.rb +44 -0
  80. data/lib/lighthouse/nested_mutations/partitioner.rb +125 -0
  81. data/lib/lighthouse/nested_mutations.rb +210 -0
  82. data/lib/lighthouse/pagination.rb +221 -0
  83. data/lib/lighthouse/persisted_queries.rb +167 -0
  84. data/lib/lighthouse/railtie.rb +26 -0
  85. data/lib/lighthouse/rb_lighthouse.rb +50 -29
  86. data/lib/lighthouse/reference_resolver.rb +13 -15
  87. data/lib/lighthouse/relation_batch.rb +156 -0
  88. data/lib/lighthouse/resolver_dispatch.rb +26 -0
  89. data/lib/lighthouse/schema_cache.rb +120 -0
  90. data/lib/lighthouse/schema_factory.rb +27 -37
  91. data/lib/lighthouse/schema_generator.rb +196 -239
  92. data/lib/lighthouse/schema_implementation.rb +51 -48
  93. data/lib/lighthouse/sdl_loader.rb +17 -20
  94. data/lib/lighthouse/support/arguments.rb +119 -0
  95. data/lib/lighthouse/support/authorization.rb +18 -19
  96. data/lib/lighthouse/support/batching.rb +16 -0
  97. data/lib/lighthouse/support/directive_args.rb +26 -24
  98. data/lib/lighthouse/support/model_resolver.rb +56 -56
  99. data/lib/lighthouse/support/naming.rb +55 -59
  100. data/lib/lighthouse/tasks/lighthouse.rake +33 -0
  101. data/lib/lighthouse/where_conditions/operator_map.rb +25 -29
  102. metadata +52 -64
  103. data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +0 -56
  104. data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +0 -47
  105. data/lib/lighthouse/directive_resolvers/can_resolver.rb +0 -86
  106. data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +0 -109
  107. data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +0 -43
  108. data/lib/lighthouse/directives/relation_directive.rb +0 -332
@@ -2,100 +2,120 @@
2
2
 
3
3
  require_relative 'base'
4
4
 
5
- module Lighthouse
6
- module GraphQL
7
- module Directives
8
- module Arguments
9
- # @eq — equality filter (the default). `field(status: String @eq)`
10
- class Eq < Base
11
- def self.directive_name = 'eq'
12
- def self.definition = 'directive @eq(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
13
-
14
- def handle_builder(relation, value)
15
- return relation if value.nil?
16
-
17
- relation.where(column => value)
18
- end
19
- end
20
-
21
- # @neq — inequality filter.
22
- class Neq < Base
23
- def self.directive_name = 'neq'
24
- def self.definition = 'directive @neq(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
25
-
26
- def handle_builder(relation, value)
27
- return relation if value.nil?
28
-
29
- relation.where.not(column => value)
30
- end
31
- end
32
-
33
- # @in — value is one of a list.
34
- class In < Base
35
- def self.directive_name = 'in'
36
- def self.definition = 'directive @in(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
37
-
38
- def handle_builder(relation, value)
39
- return relation if value.nil?
40
-
41
- relation.where(column => Array(value))
42
- end
43
- end
44
-
45
- # @notIn — value is none of a list.
46
- class NotIn < Base
47
- def self.directive_name = 'notIn'
48
- def self.definition = 'directive @notIn(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
49
-
50
- def handle_builder(relation, value)
51
- return relation if value.nil?
52
-
53
- relation.where.not(column => Array(value))
54
- end
55
- end
56
-
57
- # @like — SQL LIKE with the value wrapped in wildcards.
58
- class Like < Base
59
- def self.directive_name = 'like'
60
- def self.definition = 'directive @like(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
61
-
62
- def handle_builder(relation, value)
63
- return relation if value.nil?
64
-
65
- relation.where(arel_column(relation).matches("%#{value}%"))
66
- end
67
- end
68
-
69
- # @where — generic operator filter, sharing the SQLOperator semantics with
70
- # the @whereConditions extension. `field(age: Int @where(operator: "GT"))`
71
- class Where < Base
72
- def self.directive_name = 'where'
73
- def self.definition = 'directive @where(key: String, operator: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
74
-
75
- def handle_builder(relation, value)
76
- return relation if value.nil?
77
-
78
- op = (Lighthouse::Support::DirectiveArgs.literal(@directive_node, 'operator') || 'EQ').to_s.upcase
79
-
80
- case op
81
- when 'IN' then relation.where(column => Array(value))
82
- when 'NIN' then relation.where.not(column => Array(value))
83
- when 'IS_NULL' then relation.where(column => nil)
84
- when 'IS_NOT_NULL' then relation.where.not(column => nil)
85
- when 'CONTAINS' then relation.where(arel_column(relation).matches("%#{value}%"))
86
- when 'STARTS_WITH' then relation.where(arel_column(relation).matches("#{value}%"))
87
- when 'ENDS_WITH' then relation.where(arel_column(relation).matches("%#{value}"))
88
- else
89
- arel_method = Lighthouse::WhereConditions::OperatorMap[op]
90
- if arel_method
91
- relation.where(arel_column(relation).public_send(arel_method, value))
92
- else
93
- relation.where(column => value)
94
- end
95
- end
96
- end
97
- end
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
- module Lighthouse
6
- module GraphQL
7
- module Directives
8
- module Arguments
9
- # @orderBy — sort the result by one or more columns.
10
- #
11
- # The client value is a list of clauses, each `{ column:, order: }`
12
- # (also accepts `{ field:, direction: }`). Clauses apply in order, so
13
- # `[{column: NAME, order: ASC}, {column: AGE, order: DESC}]` sorts by
14
- # name then age.
15
- class OrderBy < Base
16
- def self.directive_name = 'orderBy'
17
- def self.definition = 'directive @orderBy(columns: [String!], relations: [String!]) on ARGUMENT_DEFINITION | FIELD_DEFINITION'
18
-
19
- def handle_builder(relation, value)
20
- normalize(value).reduce(relation) do |rel, clause|
21
- col = Lighthouse::Support::Naming.column(clause[:column], rel)
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