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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +47 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +134 -0
  5. data/docs/authorization.md +109 -0
  6. data/docs/best-practices.md +75 -0
  7. data/docs/configuration.md +70 -0
  8. data/docs/custom-directives.md +129 -0
  9. data/docs/directives.md +298 -0
  10. data/docs/federation.md +108 -0
  11. data/docs/filtering-and-ordering.md +128 -0
  12. data/docs/getting-started.md +152 -0
  13. data/docs/relationships.md +95 -0
  14. data/lib/lighthouse/config.rb +112 -0
  15. data/lib/lighthouse/contracts/arg_builder.rb +17 -0
  16. data/lib/lighthouse/contracts/arg_manipulator.rb +17 -0
  17. data/lib/lighthouse/contracts/directive.rb +17 -0
  18. data/lib/lighthouse/contracts/field_manipulator.rb +17 -0
  19. data/lib/lighthouse/contracts/field_middleware.rb +16 -0
  20. data/lib/lighthouse/contracts/field_resolver.rb +18 -0
  21. data/lib/lighthouse/dataloader_sources/association_loader.rb +25 -0
  22. data/lib/lighthouse/dataloader_sources/model_loader.rb +19 -0
  23. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +86 -0
  24. data/lib/lighthouse/directive_resolvers/all_resolver.rb +73 -0
  25. data/lib/lighthouse/directive_resolvers/auth_resolver.rb +44 -0
  26. data/lib/lighthouse/directive_resolvers/base.rb +92 -0
  27. data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +56 -0
  28. data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +47 -0
  29. data/lib/lighthouse/directive_resolvers/builder_applier.rb +63 -0
  30. data/lib/lighthouse/directive_resolvers/can_resolver.rb +86 -0
  31. data/lib/lighthouse/directive_resolvers/count_resolver.rb +56 -0
  32. data/lib/lighthouse/directive_resolvers/field_resolver.rb +78 -0
  33. data/lib/lighthouse/directive_resolvers/find_resolver.rb +65 -0
  34. data/lib/lighthouse/directive_resolvers/first_resolver.rb +53 -0
  35. data/lib/lighthouse/directive_resolvers/guard_resolver.rb +32 -0
  36. data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +109 -0
  37. data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +43 -0
  38. data/lib/lighthouse/directive_resolvers/method_resolver.rb +35 -0
  39. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +147 -0
  40. data/lib/lighthouse/directive_resolvers/rename_resolver.rb +35 -0
  41. data/lib/lighthouse/directive_resolvers/resolution.rb +35 -0
  42. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +169 -0
  43. data/lib/lighthouse/directives/arguments/base.rb +40 -0
  44. data/lib/lighthouse/directives/arguments/filters.rb +101 -0
  45. data/lib/lighthouse/directives/arguments/order_by.rb +48 -0
  46. data/lib/lighthouse/directives/registry.rb +168 -0
  47. data/lib/lighthouse/directives/relation_directive.rb +332 -0
  48. data/lib/lighthouse/directives/where_conditions_directive.rb +208 -0
  49. data/lib/lighthouse/graphql/version.rb +7 -0
  50. data/lib/lighthouse/graphql.rb +8 -0
  51. data/lib/lighthouse/rb_lighthouse.rb +88 -0
  52. data/lib/lighthouse/reference_resolver.rb +24 -0
  53. data/lib/lighthouse/schema_factory.rb +46 -0
  54. data/lib/lighthouse/schema_generator.rb +244 -0
  55. data/lib/lighthouse/schema_implementation.rb +119 -0
  56. data/lib/lighthouse/sdl_loader.rb +27 -0
  57. data/lib/lighthouse/support/authorization.rb +26 -0
  58. data/lib/lighthouse/support/directive_args.rb +33 -0
  59. data/lib/lighthouse/support/model_resolver.rb +61 -0
  60. data/lib/lighthouse/support/naming.rb +71 -0
  61. data/lib/lighthouse/where_conditions/operator_map.rb +35 -0
  62. data/lib/lighthouse-graphql.rb +10 -0
  63. metadata +195 -0
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lighthouse
4
+ # Maps directive names to the classes that implement them, so the
5
+ # SchemaGenerator can dispatch by iterating the registry instead of a
6
+ # hardcoded `case` statement. Adding a directive is "write one class and
7
+ # register it" — no generator edits.
8
+ #
9
+ # Two registration styles are supported during the migration to clean,
10
+ # self-describing directive classes:
11
+ #
12
+ # register('hasMany', SomeLegacyResolverClass) # explicit name
13
+ # register(MyDirectiveClass) # class responds to .directive_name
14
+ #
15
+ # A registered class is classified as a field resolver when it includes
16
+ # Contracts::FieldResolver OR responds to the legacy `#define_resolver`
17
+ # instance method.
18
+ class DirectiveRegistry
19
+ class << self
20
+ def default
21
+ @default ||= new.tap(&:register_defaults)
22
+ end
23
+
24
+ # Allow the host app to extend the default registry from an initializer.
25
+ def register(name_or_klass, klass = nil)
26
+ default.register(name_or_klass, klass)
27
+ end
28
+ end
29
+
30
+ def initialize
31
+ @resolution = {}
32
+ @arg_builders = {}
33
+ @sdl_manipulators = []
34
+ end
35
+
36
+ def register(name_or_klass, klass = nil)
37
+ if klass.nil? && name_or_klass.respond_to?(:directive_name)
38
+ klass = name_or_klass
39
+ name = klass.directive_name
40
+ else
41
+ name = name_or_klass
42
+ end
43
+
44
+ if arg_builder?(klass)
45
+ @arg_builders[name.to_s] = klass
46
+ elsif resolver?(klass)
47
+ @resolution[name.to_s] = klass
48
+ end
49
+ self
50
+ end
51
+
52
+ def resolution_class(name)
53
+ @resolution[name.to_s]
54
+ end
55
+
56
+ # Application order for wrapping/resolution directives on a field. Lower is
57
+ # applied first (innermost, closest to the data); higher is applied last
58
+ # (outermost, so it executes first). Authentication guards run outermost;
59
+ # @can wraps the resolver to authorize its result.
60
+ def resolution_order(name)
61
+ case name.to_s
62
+ when 'guard', 'auth' then 2
63
+ when 'can' then 1
64
+ else 0
65
+ end
66
+ end
67
+
68
+ def resolution_names
69
+ @resolution.keys
70
+ end
71
+
72
+ # Argument-builder directives (@eq, @neq, @in, @notIn, @like, @where,
73
+ # @orderBy) compose constraints onto a relation at resolve time.
74
+ def arg_builder_class(name)
75
+ @arg_builders[name.to_s]
76
+ end
77
+
78
+ def arg_builder_names
79
+ @arg_builders.keys
80
+ end
81
+
82
+ # SDL `directive @name(...) on ...` definitions contributed by registered,
83
+ # self-describing directive classes. The generator injects these so the app
84
+ # does not have to hand-declare every directive it uses.
85
+ # Returns { "name" => "directive @name(...) on ..." }.
86
+ def directive_definitions
87
+ definitions = {}
88
+ (@resolution.values + @arg_builders.values).uniq.each do |klass|
89
+ next unless klass.respond_to?(:directive_name) && klass.respond_to?(:definition)
90
+
91
+ text = safe_definition(klass)
92
+ definitions[klass.directive_name.to_s] = text if text
93
+ end
94
+ definitions
95
+ end
96
+
97
+ # Register an SDL manipulator: any object responding to `manipulate(sdl)` and
98
+ # returning the (possibly transformed) SDL string. These run before the
99
+ # schema is built to inject Connection/Paginator types, whereConditions
100
+ # inputs, orderBy enums, etc. Order of registration is preserved.
101
+ def register_manipulator(manipulator)
102
+ @sdl_manipulators << manipulator unless @sdl_manipulators.include?(manipulator)
103
+ self
104
+ end
105
+
106
+ def sdl_manipulators
107
+ @sdl_manipulators
108
+ end
109
+
110
+ # Fold every registered manipulator over the SDL string.
111
+ def manipulate_sdl(sdl)
112
+ @sdl_manipulators.reduce(sdl) { |acc, manipulator| manipulator.manipulate(acc) }
113
+ end
114
+
115
+ def register_defaults
116
+ dr = Lighthouse::GraphQL::DirectiveResolvers
117
+ {
118
+ 'hasMany' => dr::HasManyDirectiveResolver,
119
+ 'belongsTo' => dr::BelongsToDirectiveResolver,
120
+ 'belongsToMany' => dr::BelongsToManyDirectiveResolver,
121
+ 'hasOne' => dr::HasOneDirectiveResolver,
122
+ 'all' => dr::AllDirectiveResolver,
123
+ 'find' => dr::FindDirectiveResolver,
124
+ 'first' => dr::FirstDirectiveResolver,
125
+ 'paginate' => dr::PaginateDirectiveResolver,
126
+ 'aggregate' => dr::AggregateDirectiveResolver,
127
+ 'count' => dr::CountDirectiveResolver,
128
+ 'field' => dr::FieldDirectiveResolver,
129
+ 'rename' => dr::RenameDirectiveResolver,
130
+ 'method' => dr::MethodDirectiveResolver,
131
+ 'auth' => dr::AuthDirectiveResolver,
132
+ 'guard' => dr::GuardDirectiveResolver,
133
+ 'can' => dr::CanDirectiveResolver
134
+ }.each { |name, k| register(name, k) }
135
+
136
+ # Argument-builder filter/sort directives.
137
+ args = Lighthouse::GraphQL::Directives::Arguments
138
+ [args::Eq, args::Neq, args::In, args::NotIn, args::Like, args::Where, args::OrderBy]
139
+ .each { |k| register(k) }
140
+
141
+ # SDL manipulators run in registration order during the manipulation phase.
142
+ register_manipulator(Lighthouse::GraphQL::Directives::RelationDirective)
143
+ register_manipulator(Lighthouse::GraphQL::Directives::WhereConditionsDirective)
144
+
145
+ self
146
+ end
147
+
148
+ private
149
+
150
+ def safe_definition(klass)
151
+ klass.definition
152
+ rescue StandardError
153
+ nil
154
+ end
155
+
156
+ def arg_builder?(klass)
157
+ klass.respond_to?(:include?) && klass.include?(Lighthouse::Contracts::ArgBuilder)
158
+ end
159
+
160
+ def resolver?(klass)
161
+ return false unless klass
162
+
163
+ (klass.respond_to?(:include?) && klass.include?(Lighthouse::Contracts::FieldResolver)) ||
164
+ klass.method_defined?(:define_resolver) ||
165
+ klass.method_defined?(:resolve_field)
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,332 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lighthouse
4
+ module GraphQL
5
+ module Directives
6
+ # AST transformer that converts list-typed relationship fields annotated
7
+ # with relationship directives (e.g. @hasMany, @paginate) into
8
+ # Connection/Paginator return types and injects pagination arguments.
9
+ class RelationDirective
10
+ DIRECTIVE_NAMES = %w[hasMany belongsToMany hasManyThrough paginate all].freeze
11
+
12
+ def self.manipulate(sdl)
13
+ return sdl unless sdl.is_a?(String) && !sdl.empty?
14
+
15
+ begin
16
+ document = ::GraphQL::Language::Parser.parse(sdl)
17
+ rescue StandardError => e
18
+ warn("Lighthouse::GraphQL::Directives::RelationDirective: parse failed: #{e.class}: #{e.message}")
19
+ return sdl
20
+ end
21
+
22
+ existing_def_names = document.definitions.filter_map { |d| d.respond_to?(:name) ? d.name : nil }
23
+ produced_inner_types = []
24
+
25
+ document.definitions.each do |definition|
26
+ next unless definition.is_a?(::GraphQL::Language::Nodes::ObjectTypeDefinition)
27
+
28
+ definition.fields.each do |field|
29
+ next unless field.type
30
+ next if field.directives.nil? || field.directives.empty?
31
+ next unless field.directives.any? { |d| DIRECTIVE_NAMES.include?(d.name) }
32
+
33
+ list_inner = list_inner_named_type(field.type)
34
+ next unless list_inner
35
+
36
+ produce_connection, produce_paginator = pagination_kind_for(field.directives)
37
+ next unless produce_connection || produce_paginator
38
+
39
+ produced_inner_types << list_inner unless produced_inner_types.include?(list_inner)
40
+
41
+ # Replace list type with Connection/Paginator return type, preserving outer nullability.
42
+ new_named_type =
43
+ if produce_connection
44
+ "#{list_inner}Connection"
45
+ else
46
+ "#{list_inner}Paginator"
47
+ end
48
+
49
+ field.instance_variable_set(:@type, replace_with_named_type(field.type, new_named_type))
50
+
51
+ if produce_connection
52
+ field.instance_variable_set(:@arguments, ensure_connection_arguments(field.arguments || []))
53
+ end
54
+ end
55
+ end
56
+
57
+ return sdl if produced_inner_types.empty?
58
+
59
+ # Add Connection/Edge/Paginator utility types where missing.
60
+ new_defs = []
61
+ produced_inner_types.each do |inner|
62
+ conn_name = "#{inner}Connection"
63
+ edge_name = "#{inner}Edge"
64
+ paginator_name = "#{inner}Paginator"
65
+
66
+ unless existing_def_names.include?(conn_name)
67
+ new_defs << build_connection_type(conn_name, edge_name, inner)
68
+ existing_def_names << conn_name
69
+ end
70
+
71
+ unless existing_def_names.include?(edge_name)
72
+ new_defs << build_edge_type(edge_name, inner)
73
+ existing_def_names << edge_name
74
+ end
75
+
76
+ unless existing_def_names.include?(paginator_name)
77
+ new_defs << build_paginator_type(paginator_name, inner)
78
+ existing_def_names << paginator_name
79
+ end
80
+ end
81
+
82
+ unless existing_def_names.include?('PaginatorInfo')
83
+ new_defs << build_paginator_info_type
84
+ existing_def_names << 'PaginatorInfo'
85
+ end
86
+
87
+ unless existing_def_names.include?('PageInfo')
88
+ new_defs << build_page_info_type
89
+ existing_def_names << 'PageInfo'
90
+ end
91
+
92
+ modified_sdl = ::GraphQL::Language::Printer.new.print(document)
93
+ modified_sdl + "\n\n" + new_defs.map { |d| ::GraphQL::Language::Printer.new.print(d) }.join("\n\n")
94
+ end
95
+
96
+ def self.pagination_kind_for(directives)
97
+ # For relation directives, always use Connection.
98
+ if directives.any? { |d| d.name != 'paginate' && DIRECTIVE_NAMES.include?(d.name) }
99
+ return [true, false]
100
+ end
101
+
102
+ paginate = directives.find { |d| d.name == 'paginate' }
103
+ return [false, false] unless paginate
104
+
105
+ type_arg = directive_arg_literal(paginate, 'type')&.to_s&.upcase
106
+ if %w[CONNECTION CURSOR].include?(type_arg)
107
+ [true, false]
108
+ else
109
+ [false, true]
110
+ end
111
+ end
112
+
113
+ def self.directive_arg_literal(directive, name)
114
+ arg = directive.arguments&.find { |a| a.name == name }
115
+ return nil unless arg
116
+ arg.value.respond_to?(:value) ? arg.value.value : arg.value
117
+ end
118
+
119
+ def self.list_inner_named_type(type_node)
120
+ case type_node
121
+ when ::GraphQL::Language::Nodes::NonNullType
122
+ list_inner_named_type(type_node.of_type)
123
+ when ::GraphQL::Language::Nodes::ListType
124
+ unwrap_named_type(type_node.of_type)
125
+ else
126
+ nil
127
+ end
128
+ end
129
+
130
+ def self.unwrap_named_type(type_node)
131
+ case type_node
132
+ when ::GraphQL::Language::Nodes::NonNullType
133
+ unwrap_named_type(type_node.of_type)
134
+ when ::GraphQL::Language::Nodes::TypeName
135
+ type_node.name
136
+ else
137
+ nil
138
+ end
139
+ end
140
+
141
+ def self.replace_with_named_type(type_node, new_name)
142
+ new_type = ::GraphQL::Language::Nodes::TypeName.new(name: new_name)
143
+ if type_node.is_a?(::GraphQL::Language::Nodes::NonNullType)
144
+ ::GraphQL::Language::Nodes::NonNullType.new(of_type: new_type)
145
+ else
146
+ new_type
147
+ end
148
+ end
149
+
150
+ def self.ensure_connection_arguments(existing_args)
151
+ existing_names = existing_args.map(&:name)
152
+ args = existing_args.map do |arg|
153
+ next arg unless %w[after before first last].include?(arg.name)
154
+ next arg if arg.description && !arg.description.empty?
155
+
156
+ ::GraphQL::Language::Nodes::InputValueDefinition.new(
157
+ name: arg.name,
158
+ type: arg.type,
159
+ directives: arg.directives || [],
160
+ default_value: arg.default_value,
161
+ description: connection_arg_description(arg.name)
162
+ )
163
+ end
164
+
165
+ %w[after before first last].each do |name|
166
+ next if existing_names.include?(name)
167
+
168
+ type =
169
+ if %w[first last].include?(name)
170
+ ::GraphQL::Language::Nodes::TypeName.new(name: 'Int')
171
+ else
172
+ ::GraphQL::Language::Nodes::TypeName.new(name: 'String')
173
+ end
174
+
175
+ args << ::GraphQL::Language::Nodes::InputValueDefinition.new(
176
+ name: name,
177
+ type: type,
178
+ directives: [],
179
+ default_value: nil,
180
+ description: connection_arg_description(name)
181
+ )
182
+ end
183
+
184
+ args
185
+ end
186
+
187
+ def self.build_connection_type(conn_name, edge_name, node_name)
188
+ ::GraphQL::Language::Nodes::ObjectTypeDefinition.new(
189
+ name: conn_name,
190
+ interfaces: [],
191
+ directives: [],
192
+ description: "The connection type for #{node_name}.",
193
+ fields: [
194
+ ::GraphQL::Language::Nodes::FieldDefinition.new(
195
+ name: 'edges',
196
+ type: ::GraphQL::Language::Nodes::ListType.new(of_type: ::GraphQL::Language::Nodes::TypeName.new(name: edge_name)),
197
+ arguments: [],
198
+ directives: [],
199
+ description: 'A list of edges.'
200
+ ),
201
+ ::GraphQL::Language::Nodes::FieldDefinition.new(
202
+ name: 'nodes',
203
+ type: ::GraphQL::Language::Nodes::ListType.new(of_type: ::GraphQL::Language::Nodes::TypeName.new(name: node_name)),
204
+ arguments: [],
205
+ directives: [],
206
+ description: 'A list of nodes.'
207
+ ),
208
+ ::GraphQL::Language::Nodes::FieldDefinition.new(
209
+ name: 'pageInfo',
210
+ type: ::GraphQL::Language::Nodes::NonNullType.new(of_type: ::GraphQL::Language::Nodes::TypeName.new(name: 'PageInfo')),
211
+ arguments: [],
212
+ directives: [],
213
+ description: 'Information to aid in pagination.'
214
+ )
215
+ ]
216
+ )
217
+ end
218
+
219
+ def self.build_edge_type(edge_name, node_name)
220
+ ::GraphQL::Language::Nodes::ObjectTypeDefinition.new(
221
+ name: edge_name,
222
+ interfaces: [],
223
+ directives: [],
224
+ description: 'An edge in a connection.',
225
+ fields: [
226
+ ::GraphQL::Language::Nodes::FieldDefinition.new(
227
+ name: 'cursor',
228
+ type: ::GraphQL::Language::Nodes::NonNullType.new(of_type: ::GraphQL::Language::Nodes::TypeName.new(name: 'String')),
229
+ arguments: [],
230
+ directives: [],
231
+ description: 'A cursor for use in pagination.'
232
+ ),
233
+ ::GraphQL::Language::Nodes::FieldDefinition.new(
234
+ name: 'node',
235
+ type: ::GraphQL::Language::Nodes::TypeName.new(name: node_name),
236
+ arguments: [],
237
+ directives: [],
238
+ description: 'The item at the end of the edge.'
239
+ )
240
+ ]
241
+ )
242
+ end
243
+
244
+ def self.build_paginator_type(paginator_name, inner_name)
245
+ ::GraphQL::Language::Nodes::ObjectTypeDefinition.new(
246
+ name: paginator_name,
247
+ interfaces: [],
248
+ directives: [],
249
+ description: "A paginated list of #{inner_name} items.",
250
+ fields: [
251
+ ::GraphQL::Language::Nodes::FieldDefinition.new(
252
+ name: 'paginatorInfo',
253
+ type: ::GraphQL::Language::Nodes::NonNullType.new(of_type: ::GraphQL::Language::Nodes::TypeName.new(name: 'PaginatorInfo')),
254
+ arguments: [],
255
+ directives: [],
256
+ description: 'Pagination information about the list of items.'
257
+ ),
258
+ ::GraphQL::Language::Nodes::FieldDefinition.new(
259
+ name: 'data',
260
+ type: ::GraphQL::Language::Nodes::ListType.new(of_type: ::GraphQL::Language::Nodes::TypeName.new(name: inner_name)),
261
+ arguments: [],
262
+ directives: [],
263
+ description: "A list of #{inner_name} items."
264
+ )
265
+ ]
266
+ )
267
+ end
268
+
269
+ def self.build_paginator_info_type
270
+ ::GraphQL::Language::Nodes::ObjectTypeDefinition.new(
271
+ name: 'PaginatorInfo',
272
+ interfaces: [],
273
+ directives: [],
274
+ description: 'Information about pagination in a paginator.',
275
+ fields: [
276
+ field_def('count', 'Int', non_null: true),
277
+ field_def('currentPage', 'Int', non_null: true),
278
+ field_def('firstItem', 'Int'),
279
+ field_def('hasMorePages', 'Boolean', non_null: true),
280
+ field_def('lastItem', 'Int'),
281
+ field_def('lastPage', 'Int', non_null: true),
282
+ field_def('perPage', 'Int', non_null: true),
283
+ field_def('total', 'Int', non_null: true)
284
+ ]
285
+ )
286
+ end
287
+
288
+ def self.build_page_info_type
289
+ # Minimal Relay PageInfo. GraphQL-Ruby can also supply this via Relay,
290
+ # but SDL-first apps may not include it.
291
+ ::GraphQL::Language::Nodes::ObjectTypeDefinition.new(
292
+ name: 'PageInfo',
293
+ interfaces: [],
294
+ directives: [],
295
+ description: nil,
296
+ fields: [
297
+ field_def('hasNextPage', 'Boolean', non_null: true),
298
+ field_def('hasPreviousPage', 'Boolean', non_null: true),
299
+ field_def('startCursor', 'String'),
300
+ field_def('endCursor', 'String')
301
+ ]
302
+ )
303
+ end
304
+
305
+ def self.field_def(name, type_name, non_null: false)
306
+ type = ::GraphQL::Language::Nodes::TypeName.new(name: type_name)
307
+ type = ::GraphQL::Language::Nodes::NonNullType.new(of_type: type) if non_null
308
+ ::GraphQL::Language::Nodes::FieldDefinition.new(
309
+ name: name,
310
+ type: type,
311
+ arguments: [],
312
+ directives: [],
313
+ description: nil
314
+ )
315
+ end
316
+
317
+ def self.connection_arg_description(name)
318
+ case name
319
+ when 'after'
320
+ 'Returns the elements in the list that come after the specified cursor.'
321
+ when 'before'
322
+ 'Returns the elements in the list that come before the specified cursor.'
323
+ when 'first'
324
+ 'Returns the first _n_ elements from the list.'
325
+ when 'last'
326
+ 'Returns the last _n_ elements from the list.'
327
+ end
328
+ end
329
+ end
330
+ end
331
+ end
332
+ end