lighthouse-graphql 0.3.0 → 0.5.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 +182 -1
- data/README.md +18 -7
- data/docs/best-practices.md +16 -6
- data/docs/configuration.md +33 -8
- data/docs/directives.md +318 -10
- data/docs/events.md +3 -1
- data/docs/federation.md +3 -2
- data/docs/generators.md +82 -0
- data/docs/getting-started.md +66 -32
- data/docs/motivation.md +3 -2
- data/docs/relationships.md +42 -0
- data/docs/relay.md +137 -0
- data/docs/resolvers.md +338 -25
- data/docs/security.md +227 -0
- data/docs/subscriptions.md +60 -37
- data/docs/types.md +219 -0
- data/docs/uploads.md +80 -0
- data/lib/generators/lighthouse/base_classes.rb +70 -0
- data/lib/generators/lighthouse/class_generator.rb +19 -8
- data/lib/generators/lighthouse/install/install_generator.rb +96 -20
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +19 -10
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +21 -2
- data/lib/generators/lighthouse/mutation_generator.rb +5 -4
- data/lib/generators/lighthouse/query_generator.rb +5 -4
- data/lib/generators/lighthouse/subscription_generator.rb +21 -16
- data/lib/lighthouse/async_mutation_job.rb +17 -0
- data/lib/lighthouse/complexity.rb +175 -0
- data/lib/lighthouse/complexity_analyzer.rb +56 -0
- data/lib/lighthouse/config.rb +16 -4
- data/lib/lighthouse/controller.rb +27 -9
- data/lib/lighthouse/dataloader_sources/preload_loader.rb +16 -0
- data/lib/lighthouse/dataloader_sources/relation_counts_loader.rb +46 -0
- data/lib/lighthouse/default_resolve_dispatcher.rb +26 -0
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/argument_binding.rb +61 -0
- data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +220 -0
- data/lib/lighthouse/directive_resolvers/association_resolvers.rb +2 -2
- data/lib/lighthouse/directive_resolvers/async_resolver.rb +90 -0
- data/lib/lighthouse/directive_resolvers/base.rb +63 -14
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +89 -19
- data/lib/lighthouse/directive_resolvers/can_resolvers.rb +1 -1
- data/lib/lighthouse/directive_resolvers/convention_resolver.rb +16 -5
- data/lib/lighthouse/directive_resolvers/convert_empty_strings_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/field_resolver.rb +35 -7
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +57 -0
- data/lib/lighthouse/directive_resolvers/lazy_load_resolver.rb +61 -0
- data/lib/lighthouse/directive_resolvers/limit_resolver.rb +59 -0
- data/lib/lighthouse/directive_resolvers/method_resolver.rb +38 -3
- data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/namespaced_resolver.rb +31 -0
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +1 -1
- data/lib/lighthouse/directive_resolvers/resolution.rb +19 -4
- data/lib/lighthouse/directive_resolvers/subscription_resolver.rb +14 -5
- data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +77 -0
- data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +81 -0
- data/lib/lighthouse/directive_resolvers/with_count_resolver.rb +43 -0
- data/lib/lighthouse/directive_resolvers/with_resolver.rb +42 -0
- data/lib/lighthouse/directives/arguments/more_filters.rb +215 -0
- data/lib/lighthouse/directives/definitions.rb +52 -0
- data/lib/lighthouse/directives/model_directive.rb +131 -0
- data/lib/lighthouse/directives/node_directive.rb +134 -0
- data/lib/lighthouse/directives/pagination_manipulator.rb +1 -1
- data/lib/lighthouse/directives/registry.rb +21 -20
- data/lib/lighthouse/directives/visibility_directive.rb +71 -0
- data/lib/lighthouse/enum_directive.rb +41 -0
- data/lib/lighthouse/events/execution_trace.rb +2 -1
- data/lib/lighthouse/events/lifecycle.rb +3 -1
- data/lib/lighthouse/field_resolver_stack.rb +59 -11
- data/lib/lighthouse/generated_resolvers.rb +13 -8
- data/lib/lighthouse/global_id.rb +31 -0
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/multipart_request.rb +43 -0
- data/lib/lighthouse/node_registry.rb +68 -0
- data/lib/lighthouse/pagination.rb +3 -0
- data/lib/lighthouse/rb_lighthouse.rb +36 -0
- data/lib/lighthouse/relation_counts.rb +29 -0
- data/lib/lighthouse/resolve_info.rb +80 -0
- data/lib/lighthouse/resolver_bases.rb +68 -0
- data/lib/lighthouse/resolver_dispatch.rb +36 -11
- data/lib/lighthouse/scalar_coercion.rb +72 -0
- data/lib/lighthouse/schema_cache.rb +2 -1
- data/lib/lighthouse/schema_factory.rb +0 -1
- data/lib/lighthouse/schema_generator.rb +21 -12
- data/lib/lighthouse/schema_implementation.rb +4 -8
- data/lib/lighthouse/sdl_loader.rb +0 -2
- data/lib/lighthouse/support/environment.rb +13 -0
- data/lib/lighthouse/support/model_resolver.rb +8 -6
- data/lib/lighthouse/support/preload.rb +49 -0
- data/lib/lighthouse/type.rb +103 -0
- data/lib/lighthouse/type_resolution.rb +150 -0
- data/lib/lighthouse/upload.rb +26 -0
- data/lib/lighthouse/upload_store.rb +32 -0
- data/sig/lighthouse/directives.rbs +3 -2
- data/sig/lighthouse/graphql.rbs +29 -0
- data/sig/lighthouse.rbs +18 -0
- metadata +43 -1
|
@@ -15,8 +15,8 @@ module Lighthouse::GraphQL::SchemaGenerator
|
|
|
15
15
|
obj.respond_to?(field_name) ? obj.public_send(field_name) : nil
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def resolve_type(
|
|
19
|
-
|
|
18
|
+
def resolve_type(abstract_type, obj, ctx)
|
|
19
|
+
Lighthouse::GraphQL::TypeResolution.resolve(ctx.respond_to?(:schema) && ctx.schema ? ctx.schema : schema, abstract_type, obj, ctx)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def coerce_input(_type, value, _ctx)
|
|
@@ -45,17 +45,21 @@ module Lighthouse::GraphQL::SchemaGenerator
|
|
|
45
45
|
# and fresh (see SchemaCache), else built now.
|
|
46
46
|
document = build_document(sdl_string)
|
|
47
47
|
impl = default_implementation
|
|
48
|
+
# base_types' resolver / mutation / subscription are Lighthouse's, not graphql-ruby's.
|
|
49
|
+
bases = Lighthouse::GraphQL::ResolverBases.extract!(opts)
|
|
48
50
|
schema = from_document(document, impl, opts)
|
|
49
51
|
|
|
50
52
|
delegate_to_federation_sdl(schema)
|
|
51
53
|
Lighthouse::GraphQL::GeneratedResolvers.install_dispatcher(schema)
|
|
52
54
|
attach_schema(impl, schema)
|
|
53
|
-
post_process(schema, impl, **opts)
|
|
55
|
+
post_process(schema, impl, bases: bases, **opts)
|
|
54
56
|
schema.trace_with(Lighthouse::Events::ExecutionTrace)
|
|
55
57
|
|
|
56
58
|
# @cacheControl hints become the response's Cache-Control header,
|
|
57
59
|
# computed per query from the fields it selects.
|
|
58
60
|
schema.query_analyzer(Lighthouse::GraphQL::CacheControl::Analyzer) if uses_cache_control
|
|
61
|
+
# The score graphql-ruby checks against max_complexity, for BuildExtensionsResponse.
|
|
62
|
+
schema.query_analyzer(Lighthouse::GraphQL::Complexity::Analyzer)
|
|
59
63
|
|
|
60
64
|
schema
|
|
61
65
|
end
|
|
@@ -124,7 +128,8 @@ module Lighthouse::GraphQL::SchemaGenerator
|
|
|
124
128
|
augment_sdl_with_connections(inject_directive_definitions(sdl))
|
|
125
129
|
end
|
|
126
130
|
|
|
127
|
-
|
|
131
|
+
# `bases`: { root kind => the base class its fields' classes inherit from } (ResolverBases).
|
|
132
|
+
def post_process(schema, _impl, bases: {}, **_opts)
|
|
128
133
|
root_kinds = root_kinds(schema)
|
|
129
134
|
|
|
130
135
|
# Inspect types and fields for directives and attach generated resolvers
|
|
@@ -134,15 +139,23 @@ module Lighthouse::GraphQL::SchemaGenerator
|
|
|
134
139
|
# Apply object-level federation directives to generated types so the
|
|
135
140
|
# Apollo Federation helper can detect entities and add _entities.
|
|
136
141
|
Lighthouse::GraphQL::FederationDirectives.apply(type)
|
|
137
|
-
|
|
142
|
+
root_kind = root_kinds[type.graphql_name]
|
|
143
|
+
type_class = root_kind ? nil : Lighthouse::GraphQL::DirectiveResolvers::TypeMethodResolver.type_class(type.graphql_name, bases)
|
|
144
|
+
type.fields.each_value { |field_def| process_field(type, field_def, root_kind, bases, type_class) }
|
|
138
145
|
end
|
|
139
146
|
|
|
147
|
+
# @complexity, and paginated fields, give graphql-ruby's complexity analysis their cost.
|
|
148
|
+
Lighthouse::GraphQL::Complexity.apply(schema, root_kinds)
|
|
149
|
+
|
|
140
150
|
# @feature hides a field or type per query, through graphql-ruby's visibility.
|
|
141
151
|
Lighthouse::GraphQL::FeatureDirective.apply(schema)
|
|
142
152
|
|
|
143
153
|
# Every @broadcast must name a field of the Subscription type.
|
|
144
154
|
Lighthouse::GraphQL::Broadcasting.validate!(schema)
|
|
145
155
|
|
|
156
|
+
# Unions, interfaces and scalars get the classes that resolve and coerce them.
|
|
157
|
+
Lighthouse::GraphQL::Type.apply(schema, bases.fetch(:types, {}))
|
|
158
|
+
|
|
146
159
|
# return schema for convenience
|
|
147
160
|
schema
|
|
148
161
|
end
|
|
@@ -155,10 +168,11 @@ module Lighthouse::GraphQL::SchemaGenerator
|
|
|
155
168
|
|
|
156
169
|
# A field's generated resolvers, then the runtime hooks that make
|
|
157
170
|
# graphql-ruby reach them.
|
|
158
|
-
def process_field(type, field_def, root_kind)
|
|
171
|
+
def process_field(type, field_def, root_kind, bases = {}, type_class = nil)
|
|
159
172
|
return unless field_def.ast_node
|
|
160
173
|
|
|
161
|
-
|
|
174
|
+
# Installed in resolution order (see FieldResolverStack).
|
|
175
|
+
Lighthouse::GraphQL::FieldResolverStack.apply(type, field_def, root_kind, bases: bases, type_class: type_class)
|
|
162
176
|
Lighthouse::GraphQL::GeneratedResolvers.route_runtime_resolver(type, field_def)
|
|
163
177
|
ensure_connection_extension(field_def)
|
|
164
178
|
end
|
|
@@ -179,11 +193,6 @@ module Lighthouse::GraphQL::SchemaGenerator
|
|
|
179
193
|
# ignore failures during post-processing
|
|
180
194
|
end
|
|
181
195
|
|
|
182
|
-
# A field's resolvers, installed in resolution order (see FieldResolverStack).
|
|
183
|
-
def apply_field_resolvers(type, field_def, directives, root_kind)
|
|
184
|
-
Lighthouse::GraphQL::FieldResolverStack.apply(type, field_def, directives, root_kind)
|
|
185
|
-
end
|
|
186
|
-
|
|
187
196
|
# Scan SDL string for list-typed fields that carry relationship directives
|
|
188
197
|
# and produce Connection/Edge types where missing. This is a lightweight
|
|
189
198
|
# transformation that mirrors Lighthouse behavior: clients can declare
|
|
@@ -27,14 +27,10 @@ class SchemaImplementation
|
|
|
27
27
|
read_attribute(obj, field_name)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Object.const_get("Types::#{type_name}Type")
|
|
35
|
-
rescue StandardError
|
|
36
|
-
nil
|
|
37
|
-
end
|
|
30
|
+
# The object type of a union's or interface's value (see TypeResolution).
|
|
31
|
+
def resolve_type(abstract_type, obj, ctx)
|
|
32
|
+
schema = ctx.respond_to?(:schema) && ctx.schema ? ctx.schema : @schema
|
|
33
|
+
Lighthouse::GraphQL::TypeResolution.resolve(schema, abstract_type, obj, ctx)
|
|
38
34
|
end
|
|
39
35
|
|
|
40
36
|
def coerce_input(_type, value, _ctx)
|
|
@@ -6,8 +6,6 @@ module Lighthouse::GraphQL::SDLLoader
|
|
|
6
6
|
def load_from_folder(folder)
|
|
7
7
|
files = Dir[File.join(folder, '**', '*.graphql')]
|
|
8
8
|
if files.empty?
|
|
9
|
-
# fallback to the monolithic schema.graphql in apps/chatwoot
|
|
10
|
-
# schema.graphql is located at apps/chatwoot/schema.graphql
|
|
11
9
|
default = File.expand_path('../../..//schema.graphql', __dir__)
|
|
12
10
|
return File.read(default) if File.exist?(default)
|
|
13
11
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The application's environment, for @hide and @show: Rails.env, else
|
|
4
|
+
# RAILS_ENV or RACK_ENV, else "development".
|
|
5
|
+
module Lighthouse::Support::Environment
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def current
|
|
9
|
+
return Rails.env.to_s if defined?(Rails) && Rails.respond_to?(:env)
|
|
10
|
+
|
|
11
|
+
ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -25,15 +25,17 @@ module Lighthouse::Support::ModelResolver
|
|
|
25
25
|
|
|
26
26
|
# `namespaces:` overrides the configured resolver_namespaces, e.g. with
|
|
27
27
|
# Config.namespaces[:builders] to look a builder class up.
|
|
28
|
-
# `fallback: false` skips the bare name, so `
|
|
29
|
-
# mistaken for the `User` model.
|
|
30
|
-
|
|
28
|
+
# `fallback: false` skips the bare name, so `Resolvers::User` is never
|
|
29
|
+
# mistaken for the `User` model. `nested: true` tries the namespaces before a
|
|
30
|
+
# name that already contains `::` too, so `Feeds::Home` is found as
|
|
31
|
+
# `Resolvers::Feeds::Home`; otherwise such a name is used as-is.
|
|
32
|
+
def constantize(name, namespaces: self.namespaces, fallback: true, nested: false)
|
|
31
33
|
return nil unless name
|
|
32
34
|
|
|
33
35
|
raw = name.to_s.strip
|
|
34
36
|
return nil if raw.empty?
|
|
35
37
|
|
|
36
|
-
candidates_for(raw, namespaces, fallback: fallback).each do |candidate|
|
|
38
|
+
candidates_for(raw, namespaces, fallback: fallback, nested: nested).each do |candidate|
|
|
37
39
|
klass = try_constantize(candidate)
|
|
38
40
|
return klass if klass
|
|
39
41
|
end
|
|
@@ -41,8 +43,8 @@ module Lighthouse::Support::ModelResolver
|
|
|
41
43
|
nil
|
|
42
44
|
end
|
|
43
45
|
|
|
44
|
-
def candidates_for(raw, namespaces = self.namespaces, fallback: true)
|
|
45
|
-
if raw.include?('::')
|
|
46
|
+
def candidates_for(raw, namespaces = self.namespaces, fallback: true, nested: false)
|
|
47
|
+
if raw.include?('::') && !nested
|
|
46
48
|
[raw]
|
|
47
49
|
else
|
|
48
50
|
Array(namespaces).map { |ns| "#{ns}::#{raw}" } + (fallback ? [raw] : [])
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Eager loading for @with and @lazyLoad: ActiveRecord's preloader, over records
|
|
4
|
+
# of any classes, with relation names as PHP Lighthouse writes them
|
|
5
|
+
# ("comments.author" is comments, and each comment's author).
|
|
6
|
+
module Lighthouse::Support::Preload
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# ["comments.author", "tags"] -> [{ comments: :author }, :tags]
|
|
10
|
+
def associations(names)
|
|
11
|
+
Array(names).map do |name|
|
|
12
|
+
*path, last = name.to_s.split('.').map(&:to_sym)
|
|
13
|
+
path.reverse.reduce(last) { |nested, segment| { segment => nested } }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Load `associations` into `records`, grouped by class; a class that has no
|
|
18
|
+
# such association is left alone. `scopes` (model scope names) narrow the
|
|
19
|
+
# loaded records, as Laravel's constrained eager loads do.
|
|
20
|
+
def records(records, associations, scopes = [])
|
|
21
|
+
records.grep(::ActiveRecord::Base).group_by(&:class).each do |klass, group|
|
|
22
|
+
own = associations.select { |association| klass.reflect_on_association(root(association)) }
|
|
23
|
+
own.each { |association| preload(klass, group, association, scopes) }
|
|
24
|
+
end
|
|
25
|
+
records
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def root(association)
|
|
29
|
+
association.is_a?(Hash) ? association.keys.first : association
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def preload(klass, records, association, scopes)
|
|
33
|
+
scope = scoped(klass, association, scopes)
|
|
34
|
+
preloader = ::ActiveRecord::Associations::Preloader
|
|
35
|
+
if preloader.method_defined?(:call)
|
|
36
|
+
preloader.new(records: records, associations: association, scope: scope).call # Rails >= 7
|
|
37
|
+
else
|
|
38
|
+
preloader.new.preload(records, association, scope) # Rails 6
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The scopes applied to the association's model, or nil for none.
|
|
43
|
+
def scoped(klass, association, scopes)
|
|
44
|
+
return if scopes.blank?
|
|
45
|
+
|
|
46
|
+
target = klass.reflect_on_association(root(association)).klass
|
|
47
|
+
scopes.reduce(target.all) { |relation, scope| relation.public_send(scope) }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/concern'
|
|
4
|
+
|
|
5
|
+
# Hands a graphql-ruby type class to Lighthouse, as `include
|
|
6
|
+
# Lighthouse::GraphQL::Subscription` does a subscription. The class keeps
|
|
7
|
+
# graphql-ruby's folder structure and naming (app/graphql/types/post_type.rb,
|
|
8
|
+
# Types::PostType for the SDL's Post type) and base classes, and does for the
|
|
9
|
+
# SDL type of its name what it does in a graphql-ruby schema:
|
|
10
|
+
#
|
|
11
|
+
# class Types::PostType < Types::BaseObject # an object type
|
|
12
|
+
# include Lighthouse::GraphQL::Type
|
|
13
|
+
#
|
|
14
|
+
# def reading_time = { words: object.body.to_s.split.size }
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# class Types::SearchResultType < Types::BaseUnion # a union
|
|
18
|
+
# include Lighthouse::GraphQL::Type
|
|
19
|
+
#
|
|
20
|
+
# def self.resolve_type(object, context) = object.is_a?(Post) ? 'Post' : 'User'
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# module Types::NamedType # an interface
|
|
24
|
+
# include Types::BaseInterface
|
|
25
|
+
# include Lighthouse::GraphQL::Type
|
|
26
|
+
#
|
|
27
|
+
# definition_methods do
|
|
28
|
+
# def resolve_type(object, context) = ...
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# class Types::DateTimeType < Types::BaseScalar # a scalar
|
|
33
|
+
# include Lighthouse::GraphQL::Type
|
|
34
|
+
#
|
|
35
|
+
# def self.coerce_input(value, context) = Time.iso8601(value)
|
|
36
|
+
# def self.coerce_result(value, context) = value.iso8601
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# An object type's methods resolve its fields that have no resolving
|
|
40
|
+
# directive, with graphql-ruby's `object` (the parent), `context`, the
|
|
41
|
+
# `dataload*` helpers, and the field's arguments as snake_case keywords. A
|
|
42
|
+
# union's or interface's `resolve_type` picks the object type of a value (a
|
|
43
|
+
# type name, or a type class such as Types::PostType). A scalar's
|
|
44
|
+
# `coerce_input` / `coerce_result` convert its values. The SDL still declares
|
|
45
|
+
# the types and their fields: the class only lends its behaviour, so `field`
|
|
46
|
+
# declarations in it are ignored, and an object type's `authorized?` does not run.
|
|
47
|
+
#
|
|
48
|
+
# The model an object type resolves is the SDL's to say, with @model(class:).
|
|
49
|
+
#
|
|
50
|
+
# A Types::<Name>Type that does not include the concern is not Lighthouse's,
|
|
51
|
+
# and is left alone.
|
|
52
|
+
module Lighthouse::GraphQL::Type
|
|
53
|
+
extend ActiveSupport::Concern
|
|
54
|
+
|
|
55
|
+
# The kinds of graphql-ruby class the concern can be included in.
|
|
56
|
+
KINDS = {
|
|
57
|
+
object: ::GraphQL::Schema::Object,
|
|
58
|
+
union: ::GraphQL::Schema::Union,
|
|
59
|
+
scalar: ::GraphQL::Schema::Scalar,
|
|
60
|
+
interface: ::GraphQL::Schema::Interface
|
|
61
|
+
}.freeze
|
|
62
|
+
|
|
63
|
+
# :object, :union, :scalar or :interface for a graphql-ruby type class or
|
|
64
|
+
# interface module; nil for anything else.
|
|
65
|
+
def self.kind_of(mod)
|
|
66
|
+
return :interface if !mod.is_a?(Class) && mod.is_a?(Module) && mod.include?(KINDS[:interface])
|
|
67
|
+
return unless mod.is_a?(Class)
|
|
68
|
+
|
|
69
|
+
%i[object union scalar].find { |kind| mod < KINDS[kind] }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The class of the SDL type `type_name` (`Types::PostType` for `Post`) in
|
|
73
|
+
# Config.namespaces[:types], when it includes the concern; nil otherwise. The
|
|
74
|
+
# class must be of the same `kind` as the SDL type.
|
|
75
|
+
def self.class_for(type_name, kind)
|
|
76
|
+
klass = Lighthouse::Support::ModelResolver.constantize("#{type_name}Type", namespaces: Lighthouse::Config.namespaces[:types],
|
|
77
|
+
fallback: false)
|
|
78
|
+
return unless klass.is_a?(Module) && klass.include?(self) && kind_of(klass)
|
|
79
|
+
return klass if kind_of(klass) == kind
|
|
80
|
+
|
|
81
|
+
raise Lighthouse::DefinitionError, "#{klass} is a type class of kind #{kind_of(klass)}, but #{type_name} is of kind #{kind} in the schema."
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# What the SDL's types take from their classes and directives, once the
|
|
85
|
+
# schema is built: every @model names a model (ModelDirective), every @union /
|
|
86
|
+
# @interface class exists and every type class matches its kind and base
|
|
87
|
+
# (TypeResolution), and every scalar with a class reads and writes its values
|
|
88
|
+
# through it (ScalarCoercion). `bases`: the schema's base_types, by kind.
|
|
89
|
+
def self.apply(schema, bases = {})
|
|
90
|
+
Lighthouse::GraphQL::Directives::ModelDirective.validate!(schema)
|
|
91
|
+
Lighthouse::GraphQL::TypeResolution.validate!(schema, bases)
|
|
92
|
+
Lighthouse::GraphQL::ScalarCoercion.apply(schema, bases[:scalar])
|
|
93
|
+
Lighthouse::GraphQL::EnumDirective.apply(schema)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
included do
|
|
97
|
+
unless Lighthouse::GraphQL::Type.kind_of(self)
|
|
98
|
+
raise Lighthouse::DefinitionError,
|
|
99
|
+
"#{self} includes Lighthouse::GraphQL::Type, which is for graphql-ruby types: a subclass of GraphQL::Schema::Object, " \
|
|
100
|
+
'Union or Scalar, or a module including GraphQL::Schema::Interface (class Types::PostType < Types::BaseObject).'
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Which object type a value of a union or interface is: graphql-ruby asks the
|
|
4
|
+
# schema (its `resolve_type`) each time a field returns one. In order:
|
|
5
|
+
#
|
|
6
|
+
# 1. `@union(resolveType: "Name")` / `@interface(resolveType: "Name")` on the
|
|
7
|
+
# SDL type names a class, looked up in Config.namespaces[:types] (nested
|
|
8
|
+
# names too) and then as written, with `resolve_type(object, context)` or
|
|
9
|
+
# `call(object, context)`. PHP Lighthouse's directives.
|
|
10
|
+
# 2. Its type class, Types::<Name>Type including Lighthouse::GraphQL::Type: a
|
|
11
|
+
# union's `self.resolve_type(object, context)`, or an interface's
|
|
12
|
+
# `definition_methods { def resolve_type(object, context) }`, as in graphql-ruby.
|
|
13
|
+
# 3. The default, as PHP Lighthouse's: a Hash's `__typename`; else the type
|
|
14
|
+
# named after the object's class, or one of its superclasses (STI); else the
|
|
15
|
+
# type whose @model names the object's model (`type Article @model(class: "Post")`).
|
|
16
|
+
#
|
|
17
|
+
# A class returns a type name, the SDL type, or a type class (Types::PostType),
|
|
18
|
+
# which stands for the SDL type of its graphql_name; or, as graphql-ruby allows,
|
|
19
|
+
# [type, value] to resolve the type's fields on `value`. nil falls through to the
|
|
20
|
+
# next step. A value no step resolves, or one resolved to a type the union or
|
|
21
|
+
# interface does not include, raises Lighthouse::DefinitionError.
|
|
22
|
+
module Lighthouse::GraphQL::TypeResolution
|
|
23
|
+
DEFINITIONS = {
|
|
24
|
+
'union' => <<~GRAPHQL.chomp,
|
|
25
|
+
"""
|
|
26
|
+
The class that picks the object type of this union's values: `resolve_type(object, context)`
|
|
27
|
+
or `call(object, context)`, returning a type name or a type class.
|
|
28
|
+
"""
|
|
29
|
+
directive @union(resolveType: String!) on UNION
|
|
30
|
+
GRAPHQL
|
|
31
|
+
'interface' => <<~GRAPHQL.chomp
|
|
32
|
+
"""
|
|
33
|
+
The class that picks the object type of this interface's values: `resolve_type(object, context)`
|
|
34
|
+
or `call(object, context)`, returning a type name or a type class.
|
|
35
|
+
"""
|
|
36
|
+
directive @interface(resolveType: String!) on INTERFACE
|
|
37
|
+
GRAPHQL
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
DIRECTIVES = DEFINITIONS.keys.freeze
|
|
41
|
+
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
# The object type, or [object type, value] when a class answered with a
|
|
45
|
+
# value to resolve the fields on instead of `object` (as graphql-ruby allows).
|
|
46
|
+
def resolve(schema, abstract_type, object, context)
|
|
47
|
+
answer = custom(abstract_type, object, context)
|
|
48
|
+
answer, value = answer if answer.is_a?(Array) && answer.size == 2
|
|
49
|
+
type = type_named(schema, answer) || default(schema, abstract_type, object)
|
|
50
|
+
unless type && schema.possible_types(abstract_type).include?(type)
|
|
51
|
+
raise Lighthouse::DefinitionError, unresolved_message(schema, abstract_type, object, type)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
value.nil? ? type : [type, value]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# What the directive's class or the type class says, or nil.
|
|
58
|
+
def custom(abstract_type, object, context)
|
|
59
|
+
resolver = directive_class(abstract_type) || type_class(abstract_type)
|
|
60
|
+
return unless resolver
|
|
61
|
+
|
|
62
|
+
resolver.respond_to?(:resolve_type) ? resolver.resolve_type(object, context) : resolver.call(object, context)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def default(schema, abstract_type, object)
|
|
66
|
+
candidates = schema.possible_types(abstract_type)
|
|
67
|
+
by_name = candidates.index_by(&:graphql_name)
|
|
68
|
+
|
|
69
|
+
typename = typename_of(object)
|
|
70
|
+
return by_name[typename] if typename
|
|
71
|
+
|
|
72
|
+
class_chain(object.class).each { |name| return by_name[name] if by_name.key?(name) }
|
|
73
|
+
candidates.find { |type| model_of(type)&.then { |model| object.is_a?(model) } }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Fail at build time, not on the first query: every class a @union /
|
|
77
|
+
# @interface names exists and answers, and every type class matches its kind.
|
|
78
|
+
# `bases`: the schema's base_types, by kind, that type classes inherit from.
|
|
79
|
+
def validate!(schema, bases = {})
|
|
80
|
+
schema.types.each_value do |type|
|
|
81
|
+
next unless type.kind.union? || type.kind.interface?
|
|
82
|
+
|
|
83
|
+
directive_class(type)
|
|
84
|
+
kind = type.kind.union? ? :union : :interface
|
|
85
|
+
klass = Lighthouse::GraphQL::Type.class_for(type.graphql_name, kind)
|
|
86
|
+
Lighthouse::GraphQL::ResolverBases.check!(klass, bases[kind], "the #{type.graphql_name} #{kind}") if klass
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The SDL type a resolver's answer stands for: a name, a type, or a type class.
|
|
91
|
+
def type_named(schema, answer)
|
|
92
|
+
return if answer.nil?
|
|
93
|
+
|
|
94
|
+
schema.get_type(answer.respond_to?(:graphql_name) ? answer.graphql_name : answer.to_s)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def directive_class(abstract_type)
|
|
98
|
+
directive, name = resolve_type_directive(abstract_type)
|
|
99
|
+
return unless name
|
|
100
|
+
|
|
101
|
+
klass = Lighthouse::Support::ModelResolver.constantize(name, namespaces: Lighthouse::Config.namespaces[:types], nested: true)
|
|
102
|
+
return klass if klass.respond_to?(:resolve_type) || klass.respond_to?(:call)
|
|
103
|
+
|
|
104
|
+
raise Lighthouse::DefinitionError,
|
|
105
|
+
"@#{directive.name}(resolveType: #{name.inspect}) on #{abstract_type.graphql_name} names no class with resolve_type(object, context) " \
|
|
106
|
+
'or call(object, context).'
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The @union / @interface on the type and the class name it gives, or nil.
|
|
110
|
+
def resolve_type_directive(abstract_type)
|
|
111
|
+
directive = (abstract_type.ast_node&.directives || []).find { |d| DIRECTIVES.include?(d.name) }
|
|
112
|
+
[directive, Lighthouse::Support::DirectiveArgs.literal(directive, 'resolveType')] if directive
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def type_class(abstract_type)
|
|
116
|
+
kind = abstract_type.kind.union? ? :union : :interface
|
|
117
|
+
klass = Lighthouse::GraphQL::Type.class_for(abstract_type.graphql_name, kind)
|
|
118
|
+
klass if klass.respond_to?(:resolve_type)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def typename_of(object)
|
|
122
|
+
return unless object.is_a?(Hash)
|
|
123
|
+
|
|
124
|
+
(object['__typename'] || object[:__typename])&.to_s
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Admin, User, ApplicationRecord, ... for an Admin < User record.
|
|
128
|
+
def class_chain(klass)
|
|
129
|
+
names = []
|
|
130
|
+
while klass
|
|
131
|
+
names << klass.name.demodulize if klass.name
|
|
132
|
+
klass = klass.superclass
|
|
133
|
+
end
|
|
134
|
+
names
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def model_of(type)
|
|
138
|
+
Lighthouse::GraphQL::Directives::ModelDirective.declared_model(type)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def unresolved_message(schema, abstract_type, object, type)
|
|
142
|
+
possible = schema.possible_types(abstract_type).map(&:graphql_name).join(' | ')
|
|
143
|
+
kind = abstract_type.kind.union? ? 'union' : 'interface'
|
|
144
|
+
return "#{abstract_type.graphql_name} resolved a #{object.class} to #{type.graphql_name}, which is not one of its types (#{possible})." if type
|
|
145
|
+
|
|
146
|
+
"Lighthouse could not tell the type of a #{object.class} in the #{kind} #{abstract_type.graphql_name} (#{possible}): " \
|
|
147
|
+
"name a type after its class, map one to it with @model(class: \"#{object.class}\"), or resolve it in " \
|
|
148
|
+
"Types::#{abstract_type.graphql_name}Type.resolve_type or with @#{kind}(resolveType:)."
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The `Upload` scalar: a file a client sends with a multipart request (the
|
|
4
|
+
# GraphQL multipart request specification, MultipartRequest). `scalar Upload`
|
|
5
|
+
# in the SDL reads its values through it unless the SDL or a type class names
|
|
6
|
+
# another class. The resolver gets the uploaded file (an
|
|
7
|
+
# ActionDispatch::Http::UploadedFile in Rails): attach it to a model
|
|
8
|
+
# (`avatar: Upload` with @create on a model with has_one_attached :avatar), or
|
|
9
|
+
# store it with @upload.
|
|
10
|
+
class Lighthouse::GraphQL::Upload < GraphQL::Schema::Scalar
|
|
11
|
+
graphql_name 'Upload'
|
|
12
|
+
|
|
13
|
+
def self.coerce_input(value, _context)
|
|
14
|
+
return value if file?(value)
|
|
15
|
+
|
|
16
|
+
raise ::GraphQL::CoercionError, 'Could not get uploaded file, be sure to conform to the GraphQL multipart request specification.'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.coerce_result(_value, _context)
|
|
20
|
+
raise ::GraphQL::Error, 'The Upload scalar can only be used as an argument, not returned.'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.file?(value)
|
|
24
|
+
value.respond_to?(:read) && (value.respond_to?(:original_filename) || value.respond_to?(:path))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
# Where @upload puts a file, by default: an ActiveStorage blob, on the service
|
|
6
|
+
# `disk` names (the default service without it), its key under `path`. The
|
|
7
|
+
# argument then holds the blob's signed id, which a has_one_attached /
|
|
8
|
+
# has_many_attached attribute attaches, and which ActiveStorage::Blob.find_signed
|
|
9
|
+
# finds. Config.upload_store takes another `call(file, disk:, path:, public:)`.
|
|
10
|
+
module Lighthouse::GraphQL::UploadStore
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def call(file, disk: nil, path: nil, public: false) # rubocop:disable Lint/UnusedMethodArgument -- public is the service's setting
|
|
14
|
+
unless defined?(::ActiveStorage::Blob)
|
|
15
|
+
raise Lighthouse::DefinitionError,
|
|
16
|
+
'@upload stores files with ActiveStorage: install it (bin/rails active_storage:install), or set Config.upload_store.'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
attributes = { io: file, filename: filename(file), content_type: file.try(:content_type), key: key(path) }
|
|
20
|
+
attributes[:service_name] = disk if disk
|
|
21
|
+
::ActiveStorage::Blob.create_and_upload!(**attributes.compact).signed_id
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def filename(file)
|
|
25
|
+
file.respond_to?(:original_filename) ? file.original_filename : File.basename(file.path)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def key(path)
|
|
29
|
+
prefix = path.to_s.delete_prefix('/').delete_suffix('/')
|
|
30
|
+
prefix.empty? ? nil : "#{prefix}/#{SecureRandom.base36(28)}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -104,8 +104,9 @@ module Lighthouse
|
|
|
104
104
|
# Install the field's resolver on `target_type_class` (a graphql-ruby type class).
|
|
105
105
|
def define_resolver: (untyped target_type_class) -> untyped
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
def
|
|
107
|
+
# `ctx`: the query's context, which @builder on an argument receives.
|
|
108
|
+
def apply_where_filters: (relation relation, untyped args, ?context? ctx) -> relation
|
|
109
|
+
def apply_arg_builders: (relation relation, arguments args_h, ?context? ctx) -> relation
|
|
109
110
|
def extract_relation_name_from_directive: (untyped arg_node) -> String?
|
|
110
111
|
def constantize_model: (String? name) -> Class?
|
|
111
112
|
def apply_scopes: (relation relation) -> relation
|
data/sig/lighthouse/graphql.rbs
CHANGED
|
@@ -97,6 +97,35 @@ module Lighthouse
|
|
|
97
97
|
def self?.apply: (untyped response, untyped result) -> String?
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
# What fields cost in graphql-ruby's complexity analysis (@complexity, and
|
|
101
|
+
# paginated fields scaled by `first`).
|
|
102
|
+
module Complexity
|
|
103
|
+
DEFINITION: String
|
|
104
|
+
CONTEXT_KEY: Symbol
|
|
105
|
+
|
|
106
|
+
# Give the schema's fields their cost; `root_kinds` maps root type names to
|
|
107
|
+
# :queries / :mutations / :subscriptions.
|
|
108
|
+
def self?.apply: (untyped schema, ?Hash[String, Symbol] root_kinds) -> untyped
|
|
109
|
+
# A query's score, once scored for BuildExtensionsResponse; nil otherwise.
|
|
110
|
+
def self?.query_complexity: (untyped query) -> Integer?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# A concern for a GraphQL::Analysis::Analyzer that @complexity(resolver:)
|
|
114
|
+
# names (graphql-ruby ships no RBS, so the class it is included in is not
|
|
115
|
+
# declared): its `complexity` is the cost of the fields naming the class.
|
|
116
|
+
module ComplexityAnalyzer
|
|
117
|
+
# Only when the class has a `result` of its own.
|
|
118
|
+
def analyze?: () -> bool
|
|
119
|
+
def result: () -> nil
|
|
120
|
+
|
|
121
|
+
# The class methods the concern adds to its includer.
|
|
122
|
+
module ClassMethods
|
|
123
|
+
# A number (added to the selection's cost) or a proc of
|
|
124
|
+
# (context, args, child_complexity); nil when never set.
|
|
125
|
+
def complexity: (?(Numeric | ^(context, arguments, Integer) -> Integer)? new_complexity) -> (Numeric | ^(context, arguments, Integer) -> Integer)?
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
100
129
|
# The opt-in cache of the manipulated schema document, in tmp/cache.
|
|
101
130
|
module SchemaCache
|
|
102
131
|
PREFIX: String
|
data/sig/lighthouse.rbs
CHANGED
|
@@ -39,6 +39,18 @@ module Lighthouse
|
|
|
39
39
|
def call: (StandardError error, context? context) -> void
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Config.global_id: encodes and decodes Relay global ids (@globalId, @node).
|
|
43
|
+
interface _GlobalId
|
|
44
|
+
def encode: (String type, untyped id) -> String
|
|
45
|
+
def decode: (String global_id) -> Array[String]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Config.upload_store: stores the file an @upload argument carries, and returns
|
|
49
|
+
# what the argument holds instead (an ActiveStorage blob's signed id by default).
|
|
50
|
+
interface _UploadStore
|
|
51
|
+
def call: (untyped file, ?disk: String?, ?path: String?, ?public: bool?) -> untyped
|
|
52
|
+
end
|
|
53
|
+
|
|
42
54
|
# Config.authenticated_user and Config.policy_user: the user of a request.
|
|
43
55
|
interface _UserReader
|
|
44
56
|
def call: (context? context) -> untyped
|
|
@@ -132,6 +144,12 @@ module Lighthouse
|
|
|
132
144
|
def self.convert_empty_strings_to_null: () -> bool
|
|
133
145
|
def self.convert_empty_strings_to_null=: (bool? enabled) -> bool?
|
|
134
146
|
|
|
147
|
+
def self.global_id: () -> _GlobalId
|
|
148
|
+
def self.global_id=: (_GlobalId? codec) -> _GlobalId?
|
|
149
|
+
|
|
150
|
+
def self.upload_store: () -> _UploadStore
|
|
151
|
+
def self.upload_store=: (_UploadStore? store) -> _UploadStore?
|
|
152
|
+
|
|
135
153
|
# Whether @broadcast triggers in an ActiveJob (Lighthouse::GraphQL::BroadcastJob)
|
|
136
154
|
# unless the directive says shouldQueue; and the job's queue (nil: ActiveJob's default).
|
|
137
155
|
def self.queue_broadcasts: () -> bool
|