lighthouse-graphql 0.2.0 → 0.3.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -1
  3. data/README.md +16 -1
  4. data/docs/caching.md +1 -1
  5. data/docs/configuration.md +68 -0
  6. data/docs/custom-directives.md +11 -1
  7. data/docs/directives.md +306 -0
  8. data/docs/events.md +201 -0
  9. data/docs/getting-started.md +38 -9
  10. data/docs/motivation.md +184 -0
  11. data/docs/mutations.md +15 -0
  12. data/docs/resolvers.md +13 -1
  13. data/docs/subscriptions.md +423 -0
  14. data/lib/generators/lighthouse/builder_generator.rb +1 -1
  15. data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +7 -45
  16. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +31 -3
  17. data/lib/generators/lighthouse/subscription_generator.rb +95 -0
  18. data/lib/lighthouse/broadcast_job.rb +14 -0
  19. data/lib/lighthouse/broadcasting.rb +143 -0
  20. data/lib/lighthouse/cache_control.rb +5 -2
  21. data/lib/lighthouse/config.rb +53 -12
  22. data/lib/lighthouse/controller.rb +131 -0
  23. data/lib/lighthouse/directive_resolvers/broadcast_resolver.rb +31 -0
  24. data/lib/lighthouse/directive_resolvers/convert_empty_strings_resolver.rb +134 -0
  25. data/lib/lighthouse/directive_resolvers/event_resolver.rb +37 -0
  26. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +28 -13
  27. data/lib/lighthouse/directive_resolvers/soft_delete_resolvers.rb +66 -0
  28. data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +68 -0
  29. data/lib/lighthouse/directives/arguments/soft_deletes.rb +112 -0
  30. data/lib/lighthouse/directives/registry.rb +14 -6
  31. data/lib/lighthouse/directives/soft_deletes_directive.rb +123 -0
  32. data/lib/lighthouse/events/execution_trace.rb +51 -0
  33. data/lib/lighthouse/events/lifecycle.rb +146 -0
  34. data/lib/lighthouse/events/schema_build.rb +44 -0
  35. data/lib/lighthouse/events.rb +76 -0
  36. data/lib/lighthouse/feature_directive.rb +137 -0
  37. data/lib/lighthouse/field_resolver_stack.rb +22 -2
  38. data/lib/lighthouse/graphql/version.rb +1 -1
  39. data/lib/lighthouse/namespaces.rb +1 -0
  40. data/lib/lighthouse/persisted_queries/batch.rb +34 -0
  41. data/lib/lighthouse/persisted_queries.rb +17 -4
  42. data/lib/lighthouse/rb_lighthouse.rb +20 -0
  43. data/lib/lighthouse/schema_cache.rb +12 -5
  44. data/lib/lighthouse/schema_generator.rb +27 -2
  45. data/lib/lighthouse/subscription.rb +120 -0
  46. data/lib/lighthouse/support/soft_deletes.rb +164 -0
  47. data/lib/lighthouse/tasks/lighthouse.rake +10 -0
  48. data/sig/lighthouse/controller.rbs +30 -0
  49. data/sig/lighthouse/directives.rbs +158 -0
  50. data/sig/lighthouse/events.rbs +122 -0
  51. data/sig/lighthouse/graphql.rbs +111 -0
  52. data/sig/lighthouse.rbs +193 -0
  53. metadata +29 -2
@@ -43,7 +43,7 @@ module Lighthouse::GraphQL::SchemaGenerator
43
43
  uses_cache_control = sdl_string.to_s.gsub(/directive\s+@cacheControl/, '').include?('@cacheControl')
44
44
  # The manipulated document — from the schema cache when it is enabled
45
45
  # and fresh (see SchemaCache), else built now.
46
- document = Lighthouse::GraphQL::SchemaCache.document(sdl_string) { |sdl| manipulate(sdl) }
46
+ document = build_document(sdl_string)
47
47
  impl = default_implementation
48
48
  schema = from_document(document, impl, opts)
49
49
 
@@ -51,6 +51,7 @@ module Lighthouse::GraphQL::SchemaGenerator
51
51
  Lighthouse::GraphQL::GeneratedResolvers.install_dispatcher(schema)
52
52
  attach_schema(impl, schema)
53
53
  post_process(schema, impl, **opts)
54
+ schema.trace_with(Lighthouse::Events::ExecutionTrace)
54
55
 
55
56
  # @cacheControl hints become the response's Cache-Control header,
56
57
  # computed per query from the fields it selects.
@@ -104,6 +105,18 @@ module Lighthouse::GraphQL::SchemaGenerator
104
105
  Module.new { define_singleton_method(:parse) { |*| document } }
105
106
  end
106
107
 
108
+ # The manipulated schema document for the app's SDL, from the schema cache
109
+ # when it is enabled and fresh. The build events fire around the phases:
110
+ # RegisterDirectiveNamespaces on every build (the cache key covers the
111
+ # registered directives), BuildSchemaString and ManipulateAST only when the
112
+ # document is built, i.e. on a cache miss.
113
+ def build_document(sdl, force: false)
114
+ Lighthouse::Events::SchemaBuild.register_directive_namespaces
115
+ Lighthouse::GraphQL::SchemaCache.document(sdl, force: force, finalize: Lighthouse::Events::SchemaBuild.method(:manipulate_ast)) do |source|
116
+ manipulate(Lighthouse::Events::SchemaBuild.schema_string(source))
117
+ end
118
+ end
119
+
107
120
  # The SDL as the schema is built from it: directive definitions injected
108
121
  # (so apps don't hand-declare Lighthouse's directives), then every
109
122
  # registered manipulator run (pagination types, whereConditions inputs, ...).
@@ -112,7 +125,7 @@ module Lighthouse::GraphQL::SchemaGenerator
112
125
  end
113
126
 
114
127
  def post_process(schema, _impl, **_opts)
115
- root_kinds = { schema.query&.graphql_name => :queries, schema.mutation&.graphql_name => :mutations }.reject { |name, _| name.nil? }
128
+ root_kinds = root_kinds(schema)
116
129
 
117
130
  # Inspect types and fields for directives and attach generated resolvers
118
131
  schema.types.each_value do |type|
@@ -124,10 +137,22 @@ module Lighthouse::GraphQL::SchemaGenerator
124
137
  type.fields.each_value { |field_def| process_field(type, field_def, root_kinds[type.graphql_name]) }
125
138
  end
126
139
 
140
+ # @feature hides a field or type per query, through graphql-ruby's visibility.
141
+ Lighthouse::GraphQL::FeatureDirective.apply(schema)
142
+
143
+ # Every @broadcast must name a field of the Subscription type.
144
+ Lighthouse::GraphQL::Broadcasting.validate!(schema)
145
+
127
146
  # return schema for convenience
128
147
  schema
129
148
  end
130
149
 
150
+ # { type name => :queries | :mutations | :subscriptions } for the schema's root types.
151
+ def root_kinds(schema)
152
+ { schema.query => :queries, schema.mutation => :mutations, schema.subscription => :subscriptions }
153
+ .reject { |type, _| type.nil? }.transform_keys(&:graphql_name)
154
+ end
155
+
131
156
  # A field's generated resolvers, then the runtime hooks that make
132
157
  # graphql-ruby reach them.
133
158
  def process_field(type, field_def, root_kind)
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+ require 'active_support/core_ext/class/attribute'
5
+
6
+ # PHP Lighthouse's subscription model, as a concern a plain graphql-ruby
7
+ # subscription includes. The class is written as graphql-ruby's guide has it,
8
+ # and gains the hooks of PHP Lighthouse's GraphQLSubscription, as
9
+ # `include Lighthouse::GraphQL::Controller` gives a controller its `execute`:
10
+ #
11
+ # class Subscriptions::PostUpdated < GraphQL::Schema::Subscription
12
+ # include Lighthouse::GraphQL::Subscription
13
+ #
14
+ # argument :author_id, ID, required: false
15
+ #
16
+ # def filter?(post, author_id: nil) = author_id.nil? || post.author_id.to_s == author_id
17
+ # end
18
+ #
19
+ # PHP Lighthouse here graphql-ruby
20
+ # authorize(Subscriber, Request) authorize?(**args) authorized? (initial request only)
21
+ # filter(Subscriber, $root) filter?(root, **args) update returns NO_UPDATE when false
22
+ # resolve($root, $args, ...) resolve_payload(root, **args) update's return value
23
+ # encodeTopic / decodeTopic self.topic_for(arguments:, field:, scope:)
24
+ #
25
+ # `object`, `context` and `field` are available in every hook, and `context`
26
+ # is the subscriber's: graphql-ruby stores it with the subscription and
27
+ # replays it for each update, so `context[:current_user]` is the subscriber.
28
+ #
29
+ # Precedence: the concern sits between the class and GraphQL::Schema::Subscription,
30
+ # so a class that defines `update` or `authorized?` itself wins, and its
31
+ # `filter?`, `resolve_payload` or `authorize?` are not called unless it calls
32
+ # `super`. Everything else graphql-ruby offers (`subscribe`, `unsubscribe`,
33
+ # `subscription_scope`, `ready?`) is untouched.
34
+ #
35
+ # Unlike a plain subscription, every subscriber of the field shares one topic
36
+ # (the field name and its scope, not its arguments), which is PHP Lighthouse's
37
+ # model: a trigger reaches all of them and `filter?` decides who gets it, from
38
+ # the subscriber's own arguments. `topic_by :arguments` turns graphql-ruby's
39
+ # exact-argument matching back on.
40
+ module Lighthouse::GraphQL::Subscription
41
+ extend ActiveSupport::Concern
42
+
43
+ included do
44
+ unless self < ::GraphQL::Schema::Subscription
45
+ raise Lighthouse::DefinitionError,
46
+ "#{self} includes Lighthouse::GraphQL::Subscription, which is for subclasses of GraphQL::Schema::Subscription " \
47
+ '(class Subscriptions::PostUpdated < GraphQL::Schema::Subscription).'
48
+ end
49
+
50
+ class_attribute :lighthouse_topic_by, instance_accessor: false, instance_predicate: false, default: :field
51
+ end
52
+
53
+ class_methods do
54
+ # :field (the default) or :arguments. See `topic_for`. Inherited by subclasses.
55
+ def topic_by(mode = nil)
56
+ return lighthouse_topic_by unless mode
57
+
58
+ raise ArgumentError, "topic_by takes :field or :arguments, not #{mode.inspect}" unless %i[field arguments].include?(mode)
59
+
60
+ self.lighthouse_topic_by = mode
61
+ end
62
+
63
+ # The stream a subscription belongs to, and the one a trigger addresses.
64
+ # Only the scope and the field by default: every subscriber is updated, and
65
+ # `filter?` narrows it down. With `topic_by :arguments` it is
66
+ # graphql-ruby's: the trigger's arguments must equal the subscriber's.
67
+ def topic_for(arguments:, field:, scope:)
68
+ return super if topic_by == :arguments
69
+
70
+ ::GraphQL::Subscriptions::Serialize.dump_recursive([scope, field.graphql_name])
71
+ end
72
+
73
+ # The arguments @broadcast triggers this subscription with, for the
74
+ # `root` a mutation returned. Matter only with `topic_by :arguments`,
75
+ # where they must equal the subscribers' arguments.
76
+ def trigger_arguments(_root, _context)
77
+ {}
78
+ end
79
+
80
+ # The `scope:` @broadcast triggers this subscription with. Needed when the
81
+ # class declares `subscription_scope`.
82
+ def trigger_scope(_root, _context)
83
+ nil
84
+ end
85
+ end
86
+
87
+ # May this client subscribe? Return false (or raise a GraphQL::ExecutionError)
88
+ # to refuse. Called once, with the subscription request, not for each update.
89
+ def authorize?(**_args)
90
+ true
91
+ end
92
+
93
+ # Should this subscriber receive the broadcast of `root`? `args` are the
94
+ # arguments the client subscribed with; `context` is theirs.
95
+ def filter?(_root, **_args)
96
+ true
97
+ end
98
+
99
+ # The value an update resolves the field with; the broadcast root by default.
100
+ def resolve_payload(root, **_args)
101
+ root
102
+ end
103
+
104
+ def authorized?(**args)
105
+ return super if context.query.subscription_update? || authorize?(**args)
106
+
107
+ [false, Lighthouse::AuthorizationError.new('Unauthorized subscription request.')]
108
+ end
109
+
110
+ def update(**args)
111
+ filter?(object, **args) ? resolve_payload(object, **args) : ::GraphQL::Schema::Subscription::NO_UPDATE
112
+ end
113
+ end
114
+
115
+ # What a Subscription field with no class of its own resolves to: graphql-ruby's
116
+ # subscription with the concern, so every subscriber gets every broadcast
117
+ # unchanged.
118
+ Lighthouse::GraphQL::Subscription::Default = Class.new(GraphQL::Schema::Subscription) do
119
+ include Lighthouse::GraphQL::Subscription
120
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Soft deletes behind one seam, for @softDeletes / @trashed, @restore,
4
+ # @forceDelete and @withoutGlobalScopes. Rails has no built-in soft deletes, so
5
+ # the model's strategy is detected, in this order:
6
+ #
7
+ # - paranoia (`acts_as_paranoid`): a default scope hides trashed records.
8
+ # - discard (`include Discard::Model`): nothing is hidden unless the model
9
+ # adds `default_scope -> { kept }`.
10
+ # - a plain `deleted_at` column (Column), with no gem.
11
+ #
12
+ # `Config.soft_deletes` replaces the detection with an adapter of the app's
13
+ # own: any object answering `soft_deletes?(model)`, `only_trashed(relation)`,
14
+ # `with_trashed(relation)`, `without_trashed(relation)`, `restore(record)` and
15
+ # `force_delete(record)` (the last two answering whether they succeeded).
16
+ # `Column.new(:archived_at)` is a ready-made one for another column name.
17
+ module Lighthouse::Support::SoftDeletes
18
+ module_function
19
+
20
+ # The adapter in use: Config.soft_deletes, else the detecting one.
21
+ def adapter
22
+ Lighthouse::Config.soft_deletes || (@default ||= Lighthouse::Support::SoftDeletes::Auto.new)
23
+ end
24
+
25
+ def supported?(model)
26
+ adapter.soft_deletes?(model)
27
+ end
28
+
29
+ # A client-facing error: the schema asks for soft deletes on a model without them.
30
+ def require_support!(model)
31
+ return if supported?(model)
32
+
33
+ raise ::GraphQL::ExecutionError,
34
+ "Model #{model.name} does not support soft deletes (no paranoia, discard or deleted_at column; see Config.soft_deletes)."
35
+ end
36
+
37
+ def only_trashed(relation)
38
+ require_support!(relation.klass)
39
+ adapter.only_trashed(relation)
40
+ end
41
+
42
+ def with_trashed(relation)
43
+ require_support!(relation.klass)
44
+ adapter.with_trashed(relation)
45
+ end
46
+
47
+ def without_trashed(relation)
48
+ require_support!(relation.klass)
49
+ adapter.without_trashed(relation)
50
+ end
51
+
52
+ # The relation for a `Trashed` enum value: ONLY, WITH or WITHOUT.
53
+ def filter(relation, trashed)
54
+ case trashed.to_s.upcase
55
+ when 'ONLY' then only_trashed(relation)
56
+ when 'WITH' then with_trashed(relation)
57
+ when 'WITHOUT' then without_trashed(relation)
58
+ else raise ::GraphQL::ExecutionError, "Unknown trashed value #{trashed.inspect}, expected ONLY, WITH or WITHOUT."
59
+ end
60
+ end
61
+
62
+ def restore(record)
63
+ require_support!(record.class)
64
+ adapter.restore(record)
65
+ end
66
+
67
+ def force_delete(record)
68
+ require_support!(record.class)
69
+ adapter.force_delete(record)
70
+ end
71
+
72
+ # paranoia: `with_deleted` / `only_deleted` / `#restore` / `#really_destroy!`.
73
+ class Paranoia
74
+ def soft_deletes?(model)
75
+ return false unless defined?(::Paranoia)
76
+
77
+ model.respond_to?(:paranoid?) && model.paranoid?
78
+ end
79
+
80
+ def only_trashed(relation) = relation.only_deleted
81
+ def with_trashed(relation) = relation.with_deleted
82
+
83
+ def without_trashed(relation)
84
+ model = relation.klass
85
+ relation.with_deleted.where(model.paranoia_column => model.paranoia_sentinel_value)
86
+ end
87
+
88
+ def restore(record) = record.restore
89
+
90
+ def force_delete(record) = record.really_destroy!
91
+ end
92
+
93
+ # discard: `kept` / `discarded` scopes, `#undiscard`; `#destroy` really deletes.
94
+ class Discard
95
+ def soft_deletes?(model)
96
+ return false unless defined?(::Discard::Model)
97
+
98
+ model.include?(::Discard::Model)
99
+ end
100
+
101
+ def only_trashed(relation) = with_trashed(relation).discarded
102
+ def with_trashed(relation) = relation.unscope(where: relation.klass.discard_column)
103
+ def without_trashed(relation) = with_trashed(relation).kept
104
+
105
+ def restore(record) = record.undiscard
106
+
107
+ def force_delete(record) = record.destroy
108
+ end
109
+
110
+ # A nullable timestamp column (`deleted_at` by default) and no gem. It marks
111
+ # trashed records with a value and may or may not be hidden by a default scope:
112
+ # every filter unscopes the column first, so both work. Restoring sets it to
113
+ # null (skipping callbacks and validations); force deleting is `#destroy`.
114
+ class Column
115
+ DEFAULT_COLUMN = 'deleted_at'
116
+
117
+ def initialize(column = DEFAULT_COLUMN)
118
+ @column = column.to_s
119
+ end
120
+
121
+ def soft_deletes?(model)
122
+ model.respond_to?(:column_names) && model.table_exists? && model.column_names.include?(@column)
123
+ rescue ::ActiveRecord::ActiveRecordError
124
+ false
125
+ end
126
+
127
+ def only_trashed(relation) = with_trashed(relation).where.not(@column => nil)
128
+ def with_trashed(relation) = relation.unscope(where: @column)
129
+ def without_trashed(relation) = with_trashed(relation).where(@column => nil)
130
+
131
+ def restore(record)
132
+ record.update_columns(@column => nil) # rubocop:disable Rails/SkipsModelValidations -- a trashed record may no longer validate.
133
+ end
134
+
135
+ def force_delete(record) = record.destroy
136
+ end
137
+
138
+ # The default adapter: asks each strategy, in order, whether the model uses it.
139
+ class Auto
140
+ def initialize(strategies = nil)
141
+ @strategies = strategies || [Paranoia.new, Discard.new, Column.new]
142
+ end
143
+
144
+ def soft_deletes?(model)
145
+ !strategy(model).nil?
146
+ end
147
+
148
+ def only_trashed(relation) = strategy!(relation.klass).only_trashed(relation)
149
+ def with_trashed(relation) = strategy!(relation.klass).with_trashed(relation)
150
+ def without_trashed(relation) = strategy!(relation.klass).without_trashed(relation)
151
+ def restore(record) = strategy!(record.class).restore(record)
152
+ def force_delete(record) = strategy!(record.class).force_delete(record)
153
+
154
+ private
155
+
156
+ def strategy(model)
157
+ @strategies.find { |strategy| strategy.soft_deletes?(model) }
158
+ end
159
+
160
+ def strategy!(model)
161
+ strategy(model) || raise(::GraphQL::ExecutionError, "Model #{model.name} does not support soft deletes.")
162
+ end
163
+ end
164
+ end
@@ -18,6 +18,16 @@ namespace :lighthouse do
18
18
  puts "Cleared #{Lighthouse::GraphQL::SchemaCache.directory}"
19
19
  end
20
20
 
21
+ desc 'Build the schema, dispatch the ValidateSchema event and check graphql-ruby accepts it (SCHEMA=AppSchema)'
22
+ task validate_schema: :environment do
23
+ schema = Object.const_get(ENV.fetch('SCHEMA', 'AppSchema'))
24
+ Lighthouse::Events.dispatch(Lighthouse::Events::ValidateSchema.new(schema))
25
+ schema.to_definition # builds every type: unresolvable references and duplicate names raise
26
+ puts "#{schema} is valid."
27
+ rescue StandardError => e
28
+ abort("#{schema || 'Schema'} is invalid: #{e.message}")
29
+ end
30
+
21
31
  namespace :persisted_queries do
22
32
  desc 'Validate every operation of the persisted query manifest against the schema (SCHEMA=AppSchema)'
23
33
  task verify: :environment do
@@ -0,0 +1,30 @@
1
+ # The Rails controller concern that serves a schema. See docs/getting-started.md.
2
+ module Lighthouse
3
+ module GraphQL
4
+ module Controller
5
+ module ClassMethods
6
+ # The schema `execute` runs: a schema class, its name (constantized per
7
+ # request) or a block (evaluated in the controller, per request).
8
+ def lighthouse_schema: (?(Class | String | Symbol)? schema) ?{ () -> untyped } -> untyped
9
+ end
10
+
11
+ # The action: dispatches StartRequest, runs the operation (or a batch),
12
+ # sets Cache-Control, renders JSON, dispatches EndRequest.
13
+ def execute: () -> untyped
14
+
15
+ private
16
+
17
+ # Dispatch StartRequest, yield, dispatch EndRequest; only the outermost call does.
18
+ def lighthouse_request: [T] () { () -> T } -> T
19
+
20
+ # The operation's context; { current_user: current_user } when the controller has one.
21
+ def graphql_context: () -> Hash[Symbol, untyped]
22
+
23
+ # The schema class `lighthouse_schema` names.
24
+ def graphql_schema: () -> untyped
25
+
26
+ # The `variables` parameter as a Hash: from a JSON String, a Hash or parameters.
27
+ def graphql_variables: (untyped param) -> Hash[String, untyped]
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,158 @@
1
+ # The extension points for custom directives: the contracts a directive class
2
+ # includes, the registry it is registered in, and the base classes it can
3
+ # subclass. See docs/custom-directives.md.
4
+ module Lighthouse
5
+ # What the registry asks a self-describing directive class: its SDL name
6
+ # (without the @) and its `directive @name(...) on ...` definition.
7
+ interface _DirectiveClass
8
+ def directive_name: () -> String
9
+ def definition: () -> String?
10
+ end
11
+
12
+ # Marker modules: the registry detects a class's capabilities by which of
13
+ # them it includes. Only FieldResolver and ArgBuilder change how a class is
14
+ # registered; the others document intent.
15
+ module Contracts
16
+ module Directive
17
+ end
18
+
19
+ # Its instances, built with (field_def, type_def, directive_node, opts),
20
+ # install the field's resolver in `define_resolver(target_type)`.
21
+ module FieldResolver
22
+ include Directive
23
+ end
24
+
25
+ # Its instances, built with (arg_node, directive_node), answer
26
+ # `handle_builder(relation, value)` with the constrained relation.
27
+ module ArgBuilder
28
+ include Directive
29
+ end
30
+
31
+ module FieldMiddleware
32
+ include Directive
33
+ end
34
+
35
+ module FieldManipulator
36
+ include Directive
37
+ end
38
+
39
+ module ArgManipulator
40
+ include Directive
41
+ end
42
+ end
43
+
44
+ class DirectiveRegistry
45
+ RESOLUTION_ORDER: Hash[String, Integer]
46
+ NAMED_RESOLVERS: Hash[String, Symbol]
47
+ SELF_DESCRIBING_RESOLVERS: Array[Symbol]
48
+ ARG_BUILDERS: Array[Symbol]
49
+ PASSIVE_DEFINITIONS: Hash[String, String]
50
+
51
+ def self.default: () -> DirectiveRegistry
52
+
53
+ # register(MyDirective) when the class answers `directive_name`, else
54
+ # register('name', MyDirective).
55
+ def self.register: (Class | String | Symbol name_or_klass, ?Class? klass) -> DirectiveRegistry
56
+
57
+ def initialize: () -> void
58
+ def register: (Class | String | Symbol name_or_klass, ?Class? klass) -> self
59
+ def register_defaults: () -> self
60
+ def register_definition: (String | Symbol name, String definition) -> self
61
+ def register_type_definition: (String | Symbol name, String definition) -> self
62
+ def register_manipulator: (_SdlManipulator manipulator) -> self
63
+
64
+ def resolution_class: (String | Symbol name) -> Class?
65
+ def resolution_order: (String | Symbol name) -> Integer
66
+ def wrapper?: (String | Symbol name) -> bool
67
+ def resolution_names: () -> Array[String]
68
+ def arg_builder_class: (String | Symbol name) -> Class?
69
+ def arg_builder_names: () -> Array[String]
70
+
71
+ # { "name" => "directive @name(...) on ..." }
72
+ def directive_definitions: () -> Hash[String, String]
73
+ # { "Name" => "enum Name { ... }" }
74
+ def type_definitions: () -> Hash[String, String]
75
+
76
+ attr_reader sdl_manipulators: Array[_SdlManipulator]
77
+ def manipulate_sdl: (String sdl) -> String
78
+ end
79
+
80
+ # Writes the SDL an IDE needs next to the app's own: `bin/rails lighthouse:ide_helper`.
81
+ module IdeHelper
82
+ DIRECTIVES_FILE: String
83
+ PROGRAMMATIC_TYPES_FILE: String
84
+
85
+ # Writes both files into `output_dir` and returns their paths.
86
+ def self?.write: (output_dir: path, ?sdl_folder: path?, ?registry: DirectiveRegistry) -> Array[String]
87
+ def self?.directives_sdl: (?String sdl, ?registry: DirectiveRegistry) -> String
88
+ def self?.programmatic_types_sdl: (String sdl, ?registry: DirectiveRegistry) -> String
89
+ end
90
+
91
+ module GraphQL
92
+ module DirectiveResolvers
93
+ # The base of the built-in field directives, and of a custom one:
94
+ # subclass it and implement `define_resolver`.
95
+ class BaseDirectiveResolver
96
+ # graphql-ruby's field and type definitions, and the directive's AST node.
97
+ @field_def: untyped
98
+ @type_def: untyped
99
+ @directive_node: untyped
100
+ @opts: Hash[Symbol, untyped]
101
+
102
+ def initialize: (untyped field_def, untyped type_def, untyped directive_node, ?Hash[Symbol, untyped] opts) -> void
103
+
104
+ # Install the field's resolver on `target_type_class` (a graphql-ruby type class).
105
+ def define_resolver: (untyped target_type_class) -> untyped
106
+
107
+ def apply_where_filters: (relation relation, untyped args) -> relation
108
+ def apply_arg_builders: (relation relation, arguments args_h) -> relation
109
+ def extract_relation_name_from_directive: (untyped arg_node) -> String?
110
+ def constantize_model: (String? name) -> Class?
111
+ def apply_scopes: (relation relation) -> relation
112
+
113
+ # The literal value of an argument of this directive.
114
+ def directive_arg: (String name) -> untyped
115
+ def resolver_method_name: () -> String
116
+ # The field's resolver so far, for a directive that wraps it.
117
+ def capture_inner: (untyped target_type_class) -> untyped
118
+ def install: (untyped target_type_class, untyped callable) -> untyped
119
+ # The model the directive acts on; raises DefinitionError when there is none.
120
+ def model_class: () -> Class
121
+ end
122
+
123
+ # Applies the field's @builder directives, in SDL order, to its relation.
124
+ class BuilderApplier
125
+ DEFINITION: String
126
+
127
+ def self.apply: (relation relation, untyped field_def_ast_node, arguments args, context ctx, untyped resolver_instance) -> relation
128
+ def self.apply_directive: (relation relation, untyped directive_node, arguments args, context ctx, untyped resolver_instance) -> relation
129
+ def self.extract_directive_arg: (untyped directive_node, String name) -> untyped
130
+ end
131
+ end
132
+
133
+ module Directives
134
+ module Arguments
135
+ # The base of the argument filters (@eq, @in, @orderBy, ...), and of a
136
+ # custom one: subclass it and implement `handle_builder`.
137
+ class Base
138
+ include Contracts::ArgBuilder
139
+
140
+ # graphql-ruby's AST nodes for the argument and the directive on it.
141
+ @arg_node: untyped
142
+ @directive_node: untyped
143
+
144
+ def initialize: (untyped arg_node, untyped directive_node) -> void
145
+
146
+ # Constrain the relation by the value the client gave the argument.
147
+ def handle_builder: (relation relation, untyped value) -> relation
148
+
149
+ private
150
+
151
+ # The directive's `key:`, else the argument's name in snake_case.
152
+ def column: () -> String
153
+ def arel_column: (relation relation) -> untyped
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,122 @@
1
+ # The events Lighthouse dispatches and the dispatcher an app listens through.
2
+ # See docs/events.md.
3
+ module Lighthouse
4
+ module Events
5
+ # Anything that responds to `call(event)`; what it returns means something
6
+ # to some events (BuildSchemaString, RegisterDirectiveNamespaces,
7
+ # BuildExtensionsResponse), and is ignored by the rest.
8
+ interface _Listener
9
+ def call: (untyped event) -> untyped
10
+ end
11
+
12
+ # Register a listener (a callable or a block) for an event class and its
13
+ # subclasses; answers the listener, for `forget`.
14
+ def self.listen: (Class event_class, ?_Listener? callable) ?{ (untyped event) -> untyped } -> _Listener
15
+
16
+ def self.forget: (_Listener listener) -> nil
17
+ def self.reset!: () -> nil
18
+
19
+ # Call the listeners of the event's class and ancestors in registration
20
+ # order; answers their non-nil return values.
21
+ def self.dispatch: (untyped event) -> Array[untyped]
22
+
23
+ # Whether dispatching an `event_class` event would reach a listener or an
24
+ # ActiveSupport::Notifications subscriber.
25
+ def self.listening?: (Class event_class) -> bool
26
+
27
+ # The ActiveSupport::Notifications name an event class is instrumented as.
28
+ def self.notification_name: (Class event_class) -> String
29
+
30
+ # The base of the events; a listener on it hears all of them.
31
+ class Event
32
+ attr_reader moment: Time
33
+
34
+ def initialize: () -> void
35
+ end
36
+
37
+ class StartRequest < Event
38
+ attr_reader request: untyped
39
+
40
+ def initialize: (untyped request) -> void
41
+ end
42
+
43
+ class StartOperationOrOperations < Event
44
+ attr_reader operation_or_operations: Hash[Symbol, untyped] | Array[Hash[Symbol, untyped]]
45
+
46
+ def initialize: (Hash[Symbol, untyped] | Array[Hash[Symbol, untyped]] operation_or_operations) -> void
47
+ end
48
+
49
+ # A listener may answer SDL (a String) to append to the user schema.
50
+ class BuildSchemaString < Event
51
+ attr_reader user_schema: String
52
+
53
+ def initialize: (String user_schema) -> void
54
+ end
55
+
56
+ # A listener may answer a namespace (a Module or its name), or an Array of them.
57
+ class RegisterDirectiveNamespaces < Event
58
+ end
59
+
60
+ class ManipulateAST < Event
61
+ attr_accessor document_ast: untyped
62
+
63
+ def initialize: (untyped document_ast) -> void
64
+ end
65
+
66
+ class StartExecution < Event
67
+ attr_reader schema: untyped
68
+ attr_reader query: untyped
69
+ attr_reader variables: Hash[String, untyped]?
70
+ attr_reader operation_name: String?
71
+ attr_reader context: untyped
72
+
73
+ def initialize: (untyped schema, untyped query, Hash[String, untyped]? variables, String? operation_name, untyped context) -> void
74
+ end
75
+
76
+ # A listener may answer an ExtensionsResponse, or an Array of them.
77
+ class BuildExtensionsResponse < Event
78
+ attr_reader result: Hash[String, untyped]
79
+ attr_reader query_complexity: Integer?
80
+
81
+ def initialize: (Hash[String, untyped] result, Integer? query_complexity) -> void
82
+ end
83
+
84
+ class ManipulateResult < Event
85
+ attr_accessor result: Hash[String, untyped]
86
+
87
+ def initialize: (Hash[String, untyped] result) -> void
88
+ end
89
+
90
+ class EndExecution < Event
91
+ attr_reader result: Hash[String, untyped]
92
+
93
+ def initialize: (Hash[String, untyped] result) -> void
94
+ end
95
+
96
+ class EndOperationOrOperations < Event
97
+ attr_reader result_or_results: untyped
98
+
99
+ def initialize: (untyped result_or_results) -> void
100
+ end
101
+
102
+ class EndRequest < Event
103
+ attr_reader response: untyped
104
+
105
+ def initialize: (untyped response) -> void
106
+ end
107
+
108
+ class ValidateSchema < Event
109
+ attr_reader schema: untyped
110
+
111
+ def initialize: (untyped schema) -> void
112
+ end
113
+
114
+ # A key and content merged into the "extensions" of a result.
115
+ class ExtensionsResponse
116
+ attr_reader key: String | Symbol
117
+ attr_reader content: untyped
118
+
119
+ def initialize: (String | Symbol key, untyped content) -> void
120
+ end
121
+ end
122
+ end