lighthouse-graphql 0.1.1 → 0.2.1

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