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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +56 -1
- data/README.md +11 -0
- data/docs/caching.md +1 -1
- data/docs/configuration.md +68 -0
- data/docs/custom-directives.md +11 -1
- data/docs/directives.md +306 -0
- data/docs/events.md +201 -0
- data/docs/getting-started.md +38 -9
- data/docs/mutations.md +15 -0
- data/docs/resolvers.md +3 -1
- data/docs/subscriptions.md +423 -0
- data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +7 -45
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +31 -3
- data/lib/generators/lighthouse/subscription_generator.rb +95 -0
- data/lib/lighthouse/broadcast_job.rb +14 -0
- data/lib/lighthouse/broadcasting.rb +143 -0
- data/lib/lighthouse/cache_control.rb +5 -2
- data/lib/lighthouse/config.rb +53 -12
- data/lib/lighthouse/controller.rb +131 -0
- data/lib/lighthouse/directive_resolvers/broadcast_resolver.rb +31 -0
- data/lib/lighthouse/directive_resolvers/convert_empty_strings_resolver.rb +134 -0
- data/lib/lighthouse/directive_resolvers/event_resolver.rb +37 -0
- data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +28 -13
- data/lib/lighthouse/directive_resolvers/soft_delete_resolvers.rb +66 -0
- data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +68 -0
- data/lib/lighthouse/directives/arguments/soft_deletes.rb +112 -0
- data/lib/lighthouse/directives/registry.rb +14 -6
- data/lib/lighthouse/directives/soft_deletes_directive.rb +123 -0
- data/lib/lighthouse/events/execution_trace.rb +51 -0
- data/lib/lighthouse/events/lifecycle.rb +146 -0
- data/lib/lighthouse/events/schema_build.rb +44 -0
- data/lib/lighthouse/events.rb +76 -0
- data/lib/lighthouse/feature_directive.rb +137 -0
- data/lib/lighthouse/field_resolver_stack.rb +22 -2
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/namespaces.rb +1 -0
- data/lib/lighthouse/persisted_queries/batch.rb +34 -0
- data/lib/lighthouse/persisted_queries.rb +17 -4
- data/lib/lighthouse/rb_lighthouse.rb +20 -0
- data/lib/lighthouse/schema_cache.rb +12 -5
- data/lib/lighthouse/schema_generator.rb +27 -2
- data/lib/lighthouse/subscription.rb +120 -0
- data/lib/lighthouse/support/soft_deletes.rb +164 -0
- data/lib/lighthouse/tasks/lighthouse.rake +10 -0
- data/sig/lighthouse/controller.rbs +30 -0
- data/sig/lighthouse/events.rbs +122 -0
- data/sig/lighthouse/graphql.rbs +33 -0
- data/sig/lighthouse.rbs +52 -1
- 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
|
-
|
|
42
|
-
|
|
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
|
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|