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
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The events Lighthouse dispatches, in the order a request meets them (see
4
+ # docs/events.md). All inherit Events::Event, so `moment` is there when a
5
+ # class says so.
6
+ module Lighthouse::Events
7
+ # A request reached the controller. `request` is the Rack/Rails request.
8
+ class StartRequest < Event
9
+ attr_reader :request
10
+
11
+ def initialize(request)
12
+ super()
13
+ @request = request
14
+ end
15
+ end
16
+
17
+ # The operation(s) of a request were read, before they run: one Hash
18
+ # (query, variables, operation_name, extensions) or, for a batch, an Array of them.
19
+ class StartOperationOrOperations < Event
20
+ attr_reader :operation_or_operations
21
+
22
+ def initialize(operation_or_operations)
23
+ super()
24
+ @operation_or_operations = operation_or_operations
25
+ end
26
+ end
27
+
28
+ # The schema is being built from `user_schema`, the SDL of the app. A
29
+ # listener may return SDL (a String) to append to it.
30
+ class BuildSchemaString < Event
31
+ attr_reader :user_schema
32
+
33
+ def initialize(user_schema)
34
+ super()
35
+ @user_schema = user_schema
36
+ end
37
+ end
38
+
39
+ # The directive classes are being registered. A listener may return a
40
+ # namespace (a Module or its name) or an Array of them; every directive class
41
+ # under them is registered.
42
+ class RegisterDirectiveNamespaces < Event
43
+ end
44
+
45
+ # The schema document was built and manipulated, before the schema is built
46
+ # from it. graphql-ruby's nodes are immutable: a listener sets `document_ast`
47
+ # to the document to build from.
48
+ class ManipulateAST < Event
49
+ attr_accessor :document_ast
50
+
51
+ def initialize(document_ast)
52
+ super()
53
+ @document_ast = document_ast
54
+ end
55
+ end
56
+
57
+ # A query is about to run. `query` is its parsed GraphQL::Language::Nodes::Document
58
+ # (nil if it does not parse). Fires once per operation of a multiplex.
59
+ class StartExecution < Event
60
+ attr_reader :schema, :query, :variables, :operation_name, :context
61
+
62
+ def initialize(schema, query, variables, operation_name, context)
63
+ super()
64
+ @schema = schema
65
+ @query = query
66
+ @variables = variables
67
+ @operation_name = operation_name
68
+ @context = context
69
+ end
70
+ end
71
+
72
+ # A query ran. A listener may return an Events::ExtensionsResponse (or an
73
+ # Array of them), which is merged into the result's "extensions".
74
+ # `query_complexity` is nil unless it was computed.
75
+ class BuildExtensionsResponse < Event
76
+ attr_reader :result, :query_complexity
77
+
78
+ def initialize(result, query_complexity)
79
+ super()
80
+ @result = result
81
+ @query_complexity = query_complexity
82
+ end
83
+ end
84
+
85
+ # The result Hash ("data", "errors", "extensions") of a query, before it is
86
+ # returned. A listener may change it in place, or set `result` to replace it.
87
+ class ManipulateResult < Event
88
+ attr_accessor :result
89
+
90
+ def initialize(result)
91
+ super()
92
+ @result = result
93
+ end
94
+ end
95
+
96
+ # A query ran and its result is final.
97
+ class EndExecution < Event
98
+ attr_reader :result
99
+
100
+ def initialize(result)
101
+ super()
102
+ @result = result
103
+ end
104
+ end
105
+
106
+ # The operation(s) ran: the result, or for a batch an Array of results.
107
+ class EndOperationOrOperations < Event
108
+ attr_reader :result_or_results
109
+
110
+ def initialize(result_or_results)
111
+ super()
112
+ @result_or_results = result_or_results
113
+ end
114
+ end
115
+
116
+ # The response is rendered. `response` is the Rack/Rails response.
117
+ class EndRequest < Event
118
+ attr_reader :response
119
+
120
+ def initialize(response)
121
+ super()
122
+ @response = response
123
+ end
124
+ end
125
+
126
+ # The schema is being validated (`rake lighthouse:validate_schema`).
127
+ class ValidateSchema < Event
128
+ attr_reader :schema
129
+
130
+ def initialize(schema)
131
+ super()
132
+ @schema = schema
133
+ end
134
+ end
135
+
136
+ # A key and its content for the "extensions" of a result, as a listener of
137
+ # BuildExtensionsResponse returns it.
138
+ class ExtensionsResponse
139
+ attr_reader :key, :content
140
+
141
+ def initialize(key, content)
142
+ @key = key
143
+ @content = content
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The events of the schema build, as SchemaGenerator.build_document meets them.
4
+ module Lighthouse::Events::SchemaBuild
5
+ module_function
6
+
7
+ # Dispatch RegisterDirectiveNamespaces and register every directive class
8
+ # under the namespaces the listeners return (a Module, its name, or an
9
+ # Array of them), in the default registry.
10
+ def register_directive_namespaces(registry = Lighthouse::DirectiveRegistry.default)
11
+ namespaces = Lighthouse::Events.dispatch(Lighthouse::Events::RegisterDirectiveNamespaces.new).flatten
12
+ namespaces = namespaces.select { |namespace| namespace.is_a?(String) || namespace.is_a?(Module) }
13
+ namespaces.each { |namespace| register_namespace(resolve_namespace(namespace), registry) }
14
+ end
15
+
16
+ # The SDL with what the listeners of BuildSchemaString return appended.
17
+ def schema_string(sdl)
18
+ return sdl unless sdl.is_a?(String)
19
+
20
+ added = Lighthouse::Events.dispatch(Lighthouse::Events::BuildSchemaString.new(sdl)).flatten.grep(String)
21
+ added.empty? ? sdl : [sdl, *added].join("\n")
22
+ end
23
+
24
+ # The document the listeners of ManipulateAST leave on the event.
25
+ def manipulate_ast(document)
26
+ event = Lighthouse::Events::ManipulateAST.new(document)
27
+ Lighthouse::Events.dispatch(event)
28
+ event.document_ast
29
+ end
30
+
31
+ def resolve_namespace(namespace)
32
+ return namespace if namespace.is_a?(Module)
33
+
34
+ Lighthouse::Support::ModelResolver.constantize(namespace, namespaces: []) ||
35
+ raise(Lighthouse::DefinitionError, "RegisterDirectiveNamespaces: no namespace #{namespace}")
36
+ end
37
+
38
+ def register_namespace(namespace, registry)
39
+ namespace.constants(false).each do |name|
40
+ directive = namespace.const_get(name)
41
+ registry.register(directive) if directive.respond_to?(:directive_name)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/notifications'
4
+
5
+ # A small, framework-agnostic event dispatcher: PHP Lighthouse's events, which
6
+ # let an app hook into the request lifecycle and the schema build without
7
+ # patching either (see docs/events.md).
8
+ #
9
+ # Lighthouse::Events.listen(Lighthouse::Events::BuildSchemaString) do |event|
10
+ # 'extend type Query { version: String }'
11
+ # end
12
+ #
13
+ # A listener is anything that responds to `call(event)`. Every dispatch also
14
+ # goes through ActiveSupport::Notifications, as
15
+ # "<event name underscored>.lighthouse" (`start_execution.lighthouse`), with
16
+ # the event as the `:event` payload, so Rails tooling can observe them too.
17
+ module Lighthouse::Events
18
+ # Base class of the events Lighthouse dispatches; a listener on it hears all
19
+ # of them. Events that carry a time expose it as `moment`.
20
+ class Event
21
+ attr_reader :moment
22
+
23
+ def initialize
24
+ @moment = Time.now.utc
25
+ end
26
+ end
27
+
28
+ @listeners = [].freeze
29
+ @mutex = Mutex.new
30
+
31
+ class << self
32
+ # Register a listener (a callable or a block) for `event_class` and its
33
+ # subclasses. Returns the listener, to hand to `forget`.
34
+ def listen(event_class, callable = nil, &block)
35
+ listener = callable || block
36
+ raise ArgumentError, 'Lighthouse::Events.listen needs a callable or a block' unless listener.respond_to?(:call)
37
+
38
+ @mutex.synchronize { @listeners = [*@listeners, [event_class, listener]].freeze }
39
+ listener
40
+ end
41
+
42
+ # Unregister a listener (the value `listen` returned) from every event.
43
+ def forget(listener)
44
+ @mutex.synchronize { @listeners = @listeners.reject { |_, registered| registered.equal?(listener) }.freeze }
45
+ nil
46
+ end
47
+
48
+ # Drop every listener (between specs).
49
+ def reset!
50
+ @mutex.synchronize { @listeners = [].freeze }
51
+ nil
52
+ end
53
+
54
+ # Call the listeners of the event's class and its ancestors, in
55
+ # registration order. Returns their non-nil return values; what a
56
+ # return value means is up to the event.
57
+ def dispatch(event)
58
+ ::ActiveSupport::Notifications.instrument(notification_name(event.class), event: event) do
59
+ @listeners.filter_map { |klass, listener| listener.call(event) if event.is_a?(klass) }
60
+ end
61
+ end
62
+
63
+ # Whether dispatching an `event_class` event would reach anyone: a
64
+ # listener, or an ActiveSupport::Notifications subscriber. Hot paths ask
65
+ # first, to cost nothing when no one listens.
66
+ def listening?(event_class)
67
+ @listeners.any? { |klass, _| event_class <= klass } ||
68
+ ::ActiveSupport::Notifications.notifier.listening?(notification_name(event_class))
69
+ end
70
+
71
+ # The ActiveSupport::Notifications name an event class is instrumented as.
72
+ def notification_name(event_class)
73
+ "#{(event_class.name || 'event').demodulize.underscore}.lighthouse"
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @feature — include a field or an object type in the schema depending on a
4
+ # feature flag (PHP Lighthouse's Laravel Pennant integration, here backed by
5
+ # Flipper).
6
+ #
7
+ # Flags are per request and per actor while the schema is built once, so the
8
+ # element is not removed from the schema: it is hidden through graphql-ruby's
9
+ # per-query visibility. Its `visible?(context)` answers whether the flag's
10
+ # state matches `when` (ACTIVE by default). A hidden field disappears from
11
+ # introspection and a query selecting it fails validation, as if the SDL did
12
+ # not declare it; the fields returning a hidden type are hidden with it.
13
+ #
14
+ # The state is read through Config.feature_checker for the actor of
15
+ # Config.feature_actor, at most once per feature and query (memoized in the
16
+ # query's context), however many times graphql-ruby asks.
17
+ #
18
+ # Printing the schema (`to_definition`) is a query of its own: it shows the
19
+ # features as they stand with no actor. Only graphql-ruby's own null context
20
+ # (preloading a visibility profile) hides nothing.
21
+ module Lighthouse::GraphQL::FeatureDirective
22
+ # Arguments with a default are nullable, not `Type! = default` as in PHP: graphql-ruby
23
+ # 2.5 does not fill in a directive argument's default, so a non-null one would be
24
+ # refused wherever it is left out. Left out or null, it takes its default.
25
+ DEFINITION = 'directive @feature(name: String!, when: FeatureState = ACTIVE) on FIELD_DEFINITION | OBJECT'
26
+
27
+ TYPE_DEFINITIONS = {
28
+ 'FeatureState' => <<~GRAPHQL.chomp
29
+ "Options for the `when` argument of `@feature`."
30
+ enum FeatureState {
31
+ "Indicates an active feature."
32
+ ACTIVE
33
+ "Indicates an inactive feature."
34
+ INACTIVE
35
+ }
36
+ GRAPHQL
37
+ }.freeze
38
+
39
+ STATES = %w[ACTIVE INACTIVE].freeze
40
+ CONTEXT_KEY = :lighthouse_features
41
+
42
+ module_function
43
+
44
+ # Config.feature_checker's default: ask Flipper.
45
+ def default_checker
46
+ lambda do |name, actor, _context|
47
+ raise 'Lighthouse: configure `Config.feature_checker` to use @feature (Flipper not detected).' unless defined?(::Flipper)
48
+
49
+ ::Flipper.enabled?(name.to_sym, actor)
50
+ end
51
+ end
52
+
53
+ # Hide the types and fields of the schema that carry @feature.
54
+ def apply(schema)
55
+ used = false
56
+ schema.types.each_value do |type|
57
+ next unless type.kind.object? || type.kind.interface?
58
+
59
+ used |= hide_type(type)
60
+ type.fields.each_value { |field| used |= hide_field(type, field) }
61
+ end
62
+ silence_visibility_warning(schema) if used
63
+ schema
64
+ end
65
+
66
+ # Is the feature's state the one @feature asks for, in this query?
67
+ def visible?(name, state, context)
68
+ return true if context.nil? || context.is_a?(::GraphQL::Query::NullContext)
69
+
70
+ active = features(context).fetch(name) do
71
+ features(context)[name] = check(name, context)
72
+ end
73
+ (state == 'ACTIVE') == active
74
+ end
75
+
76
+ def check(name, context)
77
+ actor = Lighthouse::Config.feature_actor.call(context)
78
+ Lighthouse::Config.feature_checker.call(name, actor, context) ? true : false
79
+ end
80
+
81
+ def features(context)
82
+ context[CONTEXT_KEY] ||= {}
83
+ end
84
+
85
+ def hide_type(type)
86
+ feature = requirement(type, type.graphql_name)
87
+ return false unless feature
88
+
89
+ name, state = feature
90
+ original = type.method(:visible?)
91
+ type.define_singleton_method(:visible?) do |context|
92
+ original.call(context) && Lighthouse::GraphQL::FeatureDirective.visible?(name, state, context)
93
+ end
94
+ true
95
+ end
96
+
97
+ def hide_field(type, field)
98
+ feature = requirement(field, "#{type.graphql_name}.#{field.name}")
99
+ return false unless feature
100
+
101
+ name, state = feature
102
+ original = field.method(:visible?)
103
+ field.define_singleton_method(:visible?) do |context|
104
+ original.call(context) && Lighthouse::GraphQL::FeatureDirective.visible?(name, state, context)
105
+ end
106
+ true
107
+ end
108
+
109
+ # [feature name, required state] from the member's @feature, or nil.
110
+ def requirement(member, path)
111
+ node = member.ast_node&.directives&.find { |directive| directive.name == 'feature' }
112
+ return unless node
113
+
114
+ name = Lighthouse::Support::DirectiveArgs.literal(node, 'name').to_s
115
+ raise Lighthouse::DefinitionError, "@feature on #{path} needs a feature `name`" if name.strip.empty?
116
+
117
+ [name, required_state(node, path)]
118
+ end
119
+
120
+ def required_state(node, path)
121
+ state = Lighthouse::Support::DirectiveArgs.literal(node, 'when') || 'ACTIVE'
122
+ return state if STATES.include?(state)
123
+
124
+ raise Lighthouse::DefinitionError, "@feature on #{path}: `when` must be ACTIVE or INACTIVE, got #{state}"
125
+ end
126
+
127
+ # graphql-ruby 2.6 warns, once per query, when a member is hidden and the
128
+ # schema configured neither Visibility nor Warden. @feature is configured
129
+ # on purpose: keep the schema on the default (Warden) unless it chose.
130
+ def silence_visibility_warning(schema)
131
+ chosen = [::GraphQL::Schema::Warden, (::GraphQL::Schema::Visibility if defined?(::GraphQL::Schema::Visibility))]
132
+ return if schema.plugins.any? { |(plugin, _options)| chosen.include?(plugin) }
133
+ return unless ::GraphQL::Schema::Warden.respond_to?(:use)
134
+
135
+ schema.use(::GraphQL::Schema::Warden)
136
+ end
137
+ end
@@ -15,6 +15,7 @@ module Lighthouse::GraphQL::FieldResolverStack
15
15
  steps = directive_steps(type, field_def, directives, opts)
16
16
  steps << convention_step(type, field_def, root_kind) if root_kind && !resolving_directive?(directives)
17
17
  steps << validator_step(type, field_def, opts) if input_validation?(field_def, directives)
18
+ steps << empty_strings_step(type, field_def, opts) if empty_string_conversion?(field_def, directives)
18
19
 
19
20
  steps.each_with_index.sort_by { |(order, _), index| [order, index] }.each { |(_, step), _| step.call }
20
21
  end
@@ -38,8 +39,14 @@ module Lighthouse::GraphQL::FieldResolverStack
38
39
  end
39
40
 
40
41
  def convention_step(type, field_def, root_kind)
41
- resolver = Lighthouse::GraphQL::DirectiveResolvers::ConventionResolver
42
- [0, -> { resolver.new(field_def, type, root_kind).define_resolver(type) }]
42
+ resolvers = Lighthouse::GraphQL::DirectiveResolvers
43
+ resolver =
44
+ if root_kind == :subscriptions
45
+ resolvers::SubscriptionDirectiveResolver.new(field_def, type, nil, root_kind: root_kind)
46
+ else
47
+ resolvers::ConventionResolver.new(field_def, type, root_kind)
48
+ end
49
+ [0, -> { resolver.define_resolver(type) }]
43
50
  end
44
51
 
45
52
  # An explicit @validator on the field already validates its arguments.
@@ -53,4 +60,17 @@ module Lighthouse::GraphQL::FieldResolverStack
53
60
  [Lighthouse::DirectiveRegistry.default.resolution_order('validator'),
54
61
  -> { resolver.new(field_def, type, nil, opts).define_resolver(type) }]
55
62
  end
63
+
64
+ # Without @convertEmptyStringsToNull on the field itself, an argument or input
65
+ # type that carries it (or Config.convert_empty_strings_to_null) still converts.
66
+ def empty_string_conversion?(field_def, directives)
67
+ directives.none? { |d| d.name == 'convertEmptyStringsToNull' } &&
68
+ Lighthouse::GraphQL::DirectiveResolvers::ConvertEmptyStringsToNullDirectiveResolver.implicit?(field_def)
69
+ end
70
+
71
+ def empty_strings_step(type, field_def, opts)
72
+ resolver = Lighthouse::GraphQL::DirectiveResolvers::ConvertEmptyStringsToNullDirectiveResolver
73
+ [Lighthouse::DirectiveRegistry.default.resolution_order(resolver.directive_name),
74
+ -> { resolver.new(field_def, type, nil, opts).define_resolver(type) }]
75
+ end
56
76
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lighthouse
4
4
  module GraphQL
5
- VERSION = '0.2.1'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  end
@@ -6,6 +6,7 @@
6
6
  # here, first.
7
7
  module Lighthouse
8
8
  module Contracts; end
9
+ module Events; end
9
10
  module Generators; end
10
11
  module Support; end
11
12
  module WhereConditions; end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A batch of operations (a JSON array in the request) run in one go through
4
+ # `schema.multiplex`: the query each operation (a Hash of query, extensions,
5
+ # variables, operation_name and context) names, resolved as PersistedQueries.execute
6
+ # does. Answers one result per operation, in order; an operation the protocol
7
+ # rejects gets its ready-to-render error Hash. Dispatches the Operation(s)
8
+ # events with the Arrays of operations and results.
9
+ module Lighthouse::GraphQL::PersistedQueries::Batch
10
+ module_function
11
+
12
+ def run(schema, operations, **options)
13
+ operations = operations.map { |operation| operation.to_h.symbolize_keys }
14
+ Lighthouse::Events.dispatch(Lighthouse::Events::StartOperationOrOperations.new(operations))
15
+ rejected = {}
16
+ queries = operations.each_with_index.filter_map { |operation, index| query(operation, index, rejected) }
17
+ ran = schema.multiplex(queries, **options).to_a
18
+ results = operations.each_index.map { |index| rejected.fetch(index) { ran.shift } }
19
+ Lighthouse::Events.dispatch(Lighthouse::Events::EndOperationOrOperations.new(results))
20
+ results
21
+ end
22
+
23
+ # One operation as `schema.multiplex` takes it; a rejected one is noted
24
+ # under its index instead.
25
+ def query(operation, index, rejected)
26
+ {
27
+ query: Lighthouse::GraphQL::PersistedQueries.resolve(operation[:query], operation[:extensions]),
28
+ variables: operation[:variables] || {}, operation_name: operation[:operation_name], context: operation[:context] || {}
29
+ }
30
+ rescue Lighthouse::GraphQL::PersistedQueries::Error => e
31
+ rejected[index] = e.to_h
32
+ nil
33
+ end
34
+ end
@@ -49,11 +49,24 @@ module Lighthouse::GraphQL::PersistedQueries
49
49
  module_function
50
50
 
51
51
  # `schema.execute` with the query the request names; a protocol error
52
- # comes back as a ready-to-render { "errors" => [...] } Hash.
52
+ # comes back as a ready-to-render { "errors" => [...] } Hash. Dispatches
53
+ # StartOperationOrOperations before and EndOperationOrOperations after.
53
54
  def execute(schema, query:, extensions: nil, **options)
54
- schema.execute(resolve(query, extensions), **options)
55
- rescue Error => e
56
- e.to_h
55
+ operation = { query: query, variables: options[:variables], operation_name: options[:operation_name], extensions: extensions }
56
+ Lighthouse::Events.dispatch(Lighthouse::Events::StartOperationOrOperations.new(operation))
57
+ result =
58
+ begin
59
+ schema.execute(resolve(query, extensions), **options)
60
+ rescue Error => e
61
+ e.to_h
62
+ end
63
+ Lighthouse::Events.dispatch(Lighthouse::Events::EndOperationOrOperations.new(result))
64
+ result
65
+ end
66
+
67
+ # A batch (a JSON array in the request) through `schema.multiplex`; see Batch.
68
+ def multiplex(schema, operations, **options)
69
+ Lighthouse::GraphQL::PersistedQueries::Batch.run(schema, operations, **options)
57
70
  end
58
71
 
59
72
  # The query text to run for a request's `query` and `extensions`.
@@ -28,6 +28,7 @@ require_relative 'namespaces'
28
28
  support/authorization
29
29
  support/arguments
30
30
  support/batching
31
+ support/soft_deletes
31
32
  where_conditions/operator_map
32
33
  contracts/directive
33
34
  contracts/field_resolver
@@ -39,19 +40,29 @@ require_relative 'namespaces'
39
40
  generated_resolvers
40
41
  federation_directives
41
42
  field_resolver_stack
43
+ events
44
+ events/lifecycle
45
+ events/execution_trace
46
+ events/schema_build
42
47
  schema_generator
43
48
  schema_cache
44
49
  persisted_queries
50
+ persisted_queries/batch
51
+ controller
45
52
  schema_factory
46
53
  schema_implementation
47
54
  resolver_dispatch
48
55
  caching
49
56
  cache_control
57
+ feature_directive
58
+ broadcasting
59
+ subscription
50
60
  pagination
51
61
  relation_batch
52
62
  nested_mutations
53
63
  directives/pagination_manipulator
54
64
  directives/where_conditions_directive
65
+ directives/soft_deletes_directive
55
66
  directive_resolvers/aggregate_resolver
56
67
  directive_resolvers/field_resolver
57
68
  directive_resolvers/where_conditions_applier
@@ -70,11 +81,17 @@ require_relative 'namespaces'
70
81
  directive_resolvers/can_resolvers
71
82
  directive_resolvers/convention_resolver
72
83
  directive_resolvers/model_mutation_resolver
84
+ directive_resolvers/soft_delete_resolvers
73
85
  directive_resolvers/cache_resolvers
74
86
  directive_resolvers/validator_resolver
87
+ directive_resolvers/subscription_resolver
88
+ directive_resolvers/broadcast_resolver
89
+ directive_resolvers/event_resolver
90
+ directive_resolvers/convert_empty_strings_resolver
75
91
  directives/arguments/base
76
92
  directives/arguments/filters
77
93
  directives/arguments/order_by
94
+ directives/arguments/soft_deletes
78
95
  directives/registry
79
96
  dataloader_sources/association_loader
80
97
  dataloader_sources/model_loader
@@ -87,6 +104,9 @@ rescue LoadError => e
87
104
  warn("Lighthouse: could not require #{rel}: #{e.message}")
88
105
  end
89
106
 
107
+ # The activejob gem is optional: the job that queues @broadcast loads on first use.
108
+ Lighthouse::GraphQL.autoload(:BroadcastJob, File.expand_path('broadcast_job.rb', __dir__))
109
+
90
110
  require_relative 'railtie' if defined?(Rails::Railtie)
91
111
 
92
112
  module Lighthouse::GraphQL::RbLightHouse
@@ -40,15 +40,22 @@ module Lighthouse::GraphQL::SchemaCache
40
40
 
41
41
  # The parsed document for `sdl` after manipulation. `build` turns the SDL
42
42
  # into the manipulated SDL string; it only runs on a cache miss.
43
- # `force:` caches even when the cache is off (warm!).
44
- def document(sdl, force: false, &build)
45
- return parse(yield(sdl)) unless force || enabled?
43
+ # `force:` caches even when the cache is off (warm!). `finalize:` (a callable
44
+ # taking and returning the document) runs on the parsed document of a miss,
45
+ # before it is cached.
46
+ def document(sdl, force: false, finalize: nil, &build)
47
+ return built(sdl, finalize, &build) unless force || enabled?
46
48
 
47
49
  path = File.join(directory.to_s, "#{PREFIX}#{key(sdl)}.bin")
48
50
  cached = read(path)
49
51
  return cached if cached
50
52
 
51
- parse(yield(sdl)).tap { |document| write(path, document) }
53
+ built(sdl, finalize, &build).tap { |document| write(path, document) }
54
+ end
55
+
56
+ def built(sdl, finalize)
57
+ document = parse(yield(sdl))
58
+ finalize ? finalize.call(document) : document
52
59
  end
53
60
 
54
61
  def key(sdl)
@@ -73,7 +80,7 @@ module Lighthouse::GraphQL::SchemaCache
73
80
  # and return the cache file's path.
74
81
  def warm!(sdl = nil)
75
82
  sdl ||= Lighthouse::GraphQL::SDLLoader.load_from_folder(Lighthouse::Config.sdl_folder.to_s)
76
- document(sdl, force: true) { |source| Lighthouse::GraphQL::SchemaGenerator.manipulate(source) }
83
+ Lighthouse::GraphQL::SchemaGenerator.build_document(sdl, force: true)
77
84
  File.join(directory.to_s, "#{PREFIX}#{key(sdl)}.bin")
78
85
  end
79
86