lighthouse-graphql 0.2.1 → 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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -1
  3. data/README.md +11 -0
  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/mutations.md +15 -0
  11. data/docs/resolvers.md +3 -1
  12. data/docs/subscriptions.md +423 -0
  13. data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +7 -45
  14. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +31 -3
  15. data/lib/generators/lighthouse/subscription_generator.rb +95 -0
  16. data/lib/lighthouse/broadcast_job.rb +14 -0
  17. data/lib/lighthouse/broadcasting.rb +143 -0
  18. data/lib/lighthouse/cache_control.rb +5 -2
  19. data/lib/lighthouse/config.rb +53 -12
  20. data/lib/lighthouse/controller.rb +131 -0
  21. data/lib/lighthouse/directive_resolvers/broadcast_resolver.rb +31 -0
  22. data/lib/lighthouse/directive_resolvers/convert_empty_strings_resolver.rb +134 -0
  23. data/lib/lighthouse/directive_resolvers/event_resolver.rb +37 -0
  24. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +28 -13
  25. data/lib/lighthouse/directive_resolvers/soft_delete_resolvers.rb +66 -0
  26. data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +68 -0
  27. data/lib/lighthouse/directives/arguments/soft_deletes.rb +112 -0
  28. data/lib/lighthouse/directives/registry.rb +14 -6
  29. data/lib/lighthouse/directives/soft_deletes_directive.rb +123 -0
  30. data/lib/lighthouse/events/execution_trace.rb +51 -0
  31. data/lib/lighthouse/events/lifecycle.rb +146 -0
  32. data/lib/lighthouse/events/schema_build.rb +44 -0
  33. data/lib/lighthouse/events.rb +76 -0
  34. data/lib/lighthouse/feature_directive.rb +137 -0
  35. data/lib/lighthouse/field_resolver_stack.rb +22 -2
  36. data/lib/lighthouse/graphql/version.rb +1 -1
  37. data/lib/lighthouse/namespaces.rb +1 -0
  38. data/lib/lighthouse/persisted_queries/batch.rb +34 -0
  39. data/lib/lighthouse/persisted_queries.rb +17 -4
  40. data/lib/lighthouse/rb_lighthouse.rb +20 -0
  41. data/lib/lighthouse/schema_cache.rb +12 -5
  42. data/lib/lighthouse/schema_generator.rb +27 -2
  43. data/lib/lighthouse/subscription.rb +120 -0
  44. data/lib/lighthouse/support/soft_deletes.rb +164 -0
  45. data/lib/lighthouse/tasks/lighthouse.rake +10 -0
  46. data/sig/lighthouse/controller.rbs +30 -0
  47. data/sig/lighthouse/events.rbs +122 -0
  48. data/sig/lighthouse/graphql.rbs +33 -0
  49. data/sig/lighthouse.rbs +52 -1
  50. metadata +24 -1
@@ -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,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
@@ -43,6 +43,10 @@ module Lighthouse
43
43
  # `schema.execute` with the query the request names: its result, or a
44
44
  # protocol error as a ready-to-render { "errors" => [...] } Hash.
45
45
  def self?.execute: (schema schema, query: String?, ?extensions: untyped, **untyped options) -> untyped
46
+ # A batch: `schema.multiplex` with the query each operation (a Hash of
47
+ # query, extensions, variables, operation_name, context) names. One result
48
+ # per operation, in order; a protocol error is a ready-to-render Hash.
49
+ def self?.multiplex: (schema schema, Array[Hash[Symbol | String, untyped]] operations, **untyped options) -> Array[untyped]
46
50
  # The query text to run for a request's `query` and `extensions`.
47
51
  def self?.resolve: (String? query, untyped extensions) -> String?
48
52
  def self?.enabled?: () -> bool
@@ -51,6 +55,35 @@ module Lighthouse
51
55
  def self?.register: (String query) -> String
52
56
  end
53
57
 
58
+ # A concern for a GraphQL::Schema::Subscription (graphql-ruby ships no RBS, so
59
+ # the class it is included in is not declared): PHP Lighthouse's hooks and
60
+ # topic. All optional. `object` is the broadcast root and `context` the
61
+ # subscriber's. Including it elsewhere raises Lighthouse::DefinitionError.
62
+ module Subscription
63
+ # May this client subscribe? Called with the subscription request only.
64
+ def authorize?: (**untyped args) -> boolish
65
+ # Should this subscriber receive the broadcast of `root`? `args` are the
66
+ # arguments it subscribed with.
67
+ def filter?: (untyped root, **untyped args) -> boolish
68
+ # The value an update resolves the field with.
69
+ def resolve_payload: (untyped root, **untyped args) -> untyped
70
+
71
+ # The class methods the concern adds to its includer.
72
+ module ClassMethods
73
+ # :field (default): every subscriber shares the field's topic and filter?
74
+ # decides; :arguments: graphql-ruby's, subscribers match the trigger's arguments.
75
+ def topic_by: (?(:field | :arguments)? mode) -> (:field | :arguments)
76
+ def topic_for: (arguments: untyped, field: untyped, scope: untyped) -> String
77
+ # What @broadcast triggers the subscription with, for the root a mutation returned.
78
+ def trigger_arguments: (untyped root, context context) -> Hash[Symbol | String, untyped]
79
+ def trigger_scope: (untyped root, context context) -> untyped
80
+ end
81
+
82
+ # What a Subscription field with no class of its own resolves to.
83
+ class Default
84
+ end
85
+ end
86
+
54
87
  # The Cache-Control header the selected fields' @cacheControl hints allow.
55
88
  module CacheControl
56
89
  DEFINITION: String
data/sig/lighthouse.rbs CHANGED
@@ -51,6 +51,25 @@ module Lighthouse
51
51
  def call: (untyped user, untyped record, String | Symbol ability, ?Array[untyped] args) -> boolish
52
52
  end
53
53
 
54
+ # Config.feature_checker: is the feature `name` (as written in @feature) on
55
+ # for `actor`? The default asks Flipper.
56
+ interface _FeatureChecker
57
+ def call: (String name, untyped actor, context? context) -> boolish
58
+ end
59
+
60
+ # Config.soft_deletes: what "trashed" means for @softDeletes, @trashed,
61
+ # @restore, @forceDelete and @withoutGlobalScopes. The filters take a model's
62
+ # relation and answer the constrained one; `restore` and `force_delete` act
63
+ # on a record and answer whether they succeeded.
64
+ interface _SoftDeletes
65
+ def soft_deletes?: (Class model) -> boolish
66
+ def only_trashed: (relation relation) -> relation
67
+ def with_trashed: (relation relation) -> relation
68
+ def without_trashed: (relation relation) -> relation
69
+ def restore: (untyped record) -> boolish
70
+ def force_delete: (untyped record) -> boolish
71
+ end
72
+
54
73
  # What DirectiveRegistry#register_manipulator takes: it transforms the SDL
55
74
  # before the schema is built.
56
75
  interface _SdlManipulator
@@ -70,7 +89,7 @@ module Lighthouse
70
89
  def self.sdl_folder: () -> path?
71
90
  def self.sdl_folder=: (path? folder) -> path?
72
91
 
73
- # { queries:, mutations:, builders:, validators: } => [namespace, ...].
92
+ # { queries:, mutations:, subscriptions:, builders:, validators:, events: } => [namespace, ...].
74
93
  # Assigning a partial hash overrides only the kinds it names.
75
94
  def self.namespaces: () -> Hash[Symbol, Array[String]]
76
95
  def self.namespaces=: (Hash[Symbol | String, String | Array[String]] overrides) -> Hash[Symbol | String, String | Array[String]]
@@ -110,6 +129,16 @@ module Lighthouse
110
129
  def self.persisted_queries_only: () -> bool
111
130
  def self.persisted_queries_only=: (bool? enabled) -> bool?
112
131
 
132
+ def self.convert_empty_strings_to_null: () -> bool
133
+ def self.convert_empty_strings_to_null=: (bool? enabled) -> bool?
134
+
135
+ # Whether @broadcast triggers in an ActiveJob (Lighthouse::GraphQL::BroadcastJob)
136
+ # unless the directive says shouldQueue; and the job's queue (nil: ActiveJob's default).
137
+ def self.queue_broadcasts: () -> bool
138
+ def self.queue_broadcasts=: (bool? enabled) -> bool?
139
+ def self.broadcast_queue: () -> (String | Symbol)?
140
+ def self.broadcast_queue=: ((String | Symbol)? queue) -> (String | Symbol)?
141
+
113
142
  def self.on_error: () -> _ErrorHandler
114
143
  def self.on_error=: (_ErrorHandler? handler) -> _ErrorHandler?
115
144
  def self.handle_error: (StandardError error, ?context? context) -> void
@@ -120,6 +149,28 @@ module Lighthouse
120
149
  def self.policy_user=: (_UserReader? reader) -> _UserReader?
121
150
  def self.authorizer: () -> _Authorizer
122
151
  def self.authorizer=: (_Authorizer? authorizer) -> _Authorizer?
152
+
153
+ def self.soft_deletes: () -> _SoftDeletes?
154
+ def self.soft_deletes=: (_SoftDeletes? adapter) -> _SoftDeletes?
155
+
156
+ def self.feature_checker: () -> _FeatureChecker
157
+ def self.feature_checker=: (_FeatureChecker? checker) -> _FeatureChecker?
158
+ def self.feature_actor: () -> _UserReader
159
+ def self.feature_actor=: (_UserReader? reader) -> _UserReader?
160
+ end
161
+
162
+ module Support
163
+ module SoftDeletes
164
+ # A nullable timestamp column marking trashed records, with no gem:
165
+ # `Config.soft_deletes = Support::SoftDeletes::Column.new(:archived_at)`.
166
+ class Column
167
+ include _SoftDeletes
168
+
169
+ DEFAULT_COLUMN: String
170
+
171
+ def initialize: (?String | Symbol column) -> void
172
+ end
173
+ end
123
174
  end
124
175
 
125
176
  # The schema refers to something that does not exist or breaks a contract.