lighthouse-graphql 0.5.0 → 0.7.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 +212 -1
- data/README.md +38 -22
- data/docs/best-practices.md +6 -4
- data/docs/caching.md +4 -3
- data/docs/configuration.md +20 -26
- data/docs/custom-directives.md +2 -1
- data/docs/directives.md +79 -14
- data/docs/federation.md +1 -1
- data/docs/filtering-and-ordering.md +330 -30
- data/docs/generators.md +42 -40
- data/docs/getting-started.md +128 -58
- data/docs/motivation.md +3 -2
- data/docs/performance.md +5 -4
- data/docs/relationships.md +7 -5
- data/docs/relay.md +63 -0
- data/docs/resolvers.md +92 -39
- data/docs/schema-organisation.md +177 -0
- data/docs/security.md +64 -16
- data/docs/subscriptions.md +16 -9
- data/docs/types.md +16 -17
- data/docs/uploads.md +41 -14
- data/lib/generators/lighthouse/base_classes.rb +9 -14
- data/lib/generators/lighthouse/install/graphql_ruby_install.rb +113 -0
- data/lib/generators/lighthouse/install/install_generator.rb +125 -99
- data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +2 -1
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +14 -13
- data/lib/lighthouse/ast_builder.rb +124 -0
- data/lib/lighthouse/caching.rb +9 -3
- data/lib/lighthouse/config.rb +27 -25
- data/lib/lighthouse/contracts/arg_manipulator.rb +9 -5
- data/lib/lighthouse/contracts/field_builder.rb +13 -0
- data/lib/lighthouse/controller.rb +28 -5
- data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +1 -2
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +0 -2
- data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +18 -18
- data/lib/lighthouse/directive_resolvers/association_resolvers.rb +9 -6
- data/lib/lighthouse/directive_resolvers/base.rb +6 -37
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +21 -6
- data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +1 -1
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +0 -1
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +0 -2
- data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +3 -1
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +2 -3
- data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +85 -26
- data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +12 -6
- data/lib/lighthouse/directives/arguments/more_filters.rb +1 -1
- data/lib/lighthouse/directives/arguments/order_by.rb +190 -26
- data/lib/lighthouse/directives/arguments/where_conditions.rb +25 -0
- data/lib/lighthouse/directives/arguments/where_conditions_base.rb +84 -0
- data/lib/lighthouse/directives/arguments/where_has_conditions.rb +56 -0
- data/lib/lighthouse/directives/definitions.rb +0 -1
- data/lib/lighthouse/directives/generated_types.rb +103 -0
- data/lib/lighthouse/directives/registry.rb +39 -10
- data/lib/lighthouse/document_ast.rb +111 -0
- data/lib/lighthouse/field_resolver_stack.rb +9 -6
- data/lib/lighthouse/global_id.rb +29 -3
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/graphql_ruby_patches.rb +45 -0
- data/lib/lighthouse/ide_helper.rb +80 -23
- data/lib/lighthouse/namespaces.rb +1 -0
- data/lib/lighthouse/order_by/types.rb +137 -0
- data/lib/lighthouse/railtie.rb +9 -2
- data/lib/lighthouse/rb_lighthouse.rb +16 -5
- data/lib/lighthouse/resolve_info.rb +5 -5
- data/lib/lighthouse/resolver_bases.rb +18 -4
- data/lib/lighthouse/resolver_dispatch.rb +16 -1
- data/lib/lighthouse/root_types.rb +117 -0
- data/lib/lighthouse/schema_cache.rb +8 -4
- data/lib/lighthouse/schema_factory.rb +6 -9
- data/lib/lighthouse/schema_generator.rb +8 -18
- data/lib/lighthouse/schema_stitcher.rb +64 -0
- data/lib/lighthouse/support/naming.rb +0 -12
- data/lib/lighthouse/upload.rb +4 -2
- data/lib/lighthouse/upload_store.rb +39 -14
- data/lib/lighthouse/where_conditions/columns.rb +36 -0
- data/lib/lighthouse/where_conditions/handler.rb +152 -0
- data/lib/lighthouse/where_conditions/json_column.rb +99 -0
- data/lib/lighthouse/where_conditions/operator_map.rb +1 -4
- data/lib/lighthouse/where_conditions/types.rb +110 -0
- data/sig/lighthouse/directives.rbs +16 -3
- data/sig/lighthouse/graphql.rbs +48 -4
- data/sig/lighthouse.rbs +8 -12
- metadata +17 -7
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +0 -29
- data/lib/generators/lighthouse/mutation_generator.rb +0 -31
- data/lib/generators/lighthouse/query_generator.rb +0 -30
- data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +0 -170
- data/lib/lighthouse/directives/where_conditions_directive.rb +0 -244
- data/lib/lighthouse/sdl_loader.rb +0 -22
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
# What @whereConditions and @whereHasConditions share, as PHP Lighthouse's
|
|
6
|
+
# WhereConditionsBaseDirective: an argument written with the `_` placeholder
|
|
7
|
+
# gets the input types the directive generates (Lighthouse::WhereConditions::Types),
|
|
8
|
+
# and the client's value becomes WHERE conditions on the field's query
|
|
9
|
+
# (Lighthouse::WhereConditions::Handler).
|
|
10
|
+
#
|
|
11
|
+
# type Query {
|
|
12
|
+
# people(where: _ @whereConditions(columns: ["age", "type"])): [Person!]! @all
|
|
13
|
+
# users(hasPosts: _ @whereHasConditions(columns: ["title"])): [User!]! @all
|
|
14
|
+
# }
|
|
15
|
+
#
|
|
16
|
+
# `columns:` restricts the columns to an enum generated from them,
|
|
17
|
+
# `columnsEnum:` to an enum of the schema's; without either the column is any
|
|
18
|
+
# identifier the client sends.
|
|
19
|
+
class Lighthouse::GraphQL::Directives::Arguments::WhereConditionsBase < Lighthouse::GraphQL::Directives::Arguments::Base
|
|
20
|
+
include Lighthouse::Contracts::ArgManipulator
|
|
21
|
+
|
|
22
|
+
GeneratedTypes = Lighthouse::GraphQL::Directives::GeneratedTypes
|
|
23
|
+
|
|
24
|
+
COLUMN_ARGUMENTS = <<~GRAPHQL.chomp
|
|
25
|
+
"""
|
|
26
|
+
Restrict the allowed column names to a well-defined list.
|
|
27
|
+
This improves introspection capabilities and security.
|
|
28
|
+
Mutually exclusive with `columnsEnum`.
|
|
29
|
+
"""
|
|
30
|
+
columns: [String!]
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
Use an existing enumeration type to restrict the allowed columns to a predefined list.
|
|
34
|
+
This allows you to re-use the same enum for multiple fields.
|
|
35
|
+
Mutually exclusive with `columns`.
|
|
36
|
+
"""
|
|
37
|
+
columnsEnum: String
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
A class that applies the client given conditions to the query: `call(relation, conditions)`
|
|
41
|
+
answers the relation. "Class@method" names another method. Looked up in the builders namespaces.
|
|
42
|
+
"""
|
|
43
|
+
handler: String
|
|
44
|
+
GRAPHQL
|
|
45
|
+
|
|
46
|
+
# The argument, typed with the conditions input generated for it.
|
|
47
|
+
def manipulate_arg_definition(document_ast, field, type)
|
|
48
|
+
path = "the `#{@arg_node.name}` argument of #{type.name}.#{field.name}"
|
|
49
|
+
arg = GeneratedTypes.describe(@arg_node, self.class::DESCRIPTION)
|
|
50
|
+
types = Lighthouse::WhereConditions::Types
|
|
51
|
+
unless GeneratedTypes.allowed_columns?(@directive_node)
|
|
52
|
+
types.define_default(document_ast)
|
|
53
|
+
return GeneratedTypes.replace_placeholder(arg, types::DEFAULT)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
name = GeneratedTypes.qualified_arg_type(@arg_node, field, type) + self.class::SUFFIX
|
|
57
|
+
types.define(document_ast, name, GeneratedTypes.columns_enum(document_ast, @directive_node, @arg_node, field, type), path)
|
|
58
|
+
GeneratedTypes.replace_placeholder(arg, name)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def handle_builder(relation, value)
|
|
62
|
+
return relation if value.nil?
|
|
63
|
+
|
|
64
|
+
handler.call(relation, conditions(value)) || relation
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def conditions(value) = value
|
|
70
|
+
|
|
71
|
+
# Lighthouse::WhereConditions::Handler, or the class (or instance) `handler:` names.
|
|
72
|
+
def handler
|
|
73
|
+
reference = Lighthouse::Support::DirectiveArgs.literal(@directive_node, 'handler')
|
|
74
|
+
return Lighthouse::WhereConditions::Handler unless reference
|
|
75
|
+
|
|
76
|
+
class_name, method_name = reference.to_s.split('@', 2)
|
|
77
|
+
klass = Lighthouse::Support::ModelResolver.constantize(class_name, namespaces: Lighthouse::Config.namespaces[:builders])
|
|
78
|
+
raise Lighthouse::DefinitionError, "@#{@directive_node.name}(handler: \"#{reference}\"): no class #{class_name}" unless klass
|
|
79
|
+
|
|
80
|
+
method_name = method_name.presence || 'call'
|
|
81
|
+
receiver = klass.respond_to?(method_name) ? klass : klass.new
|
|
82
|
+
receiver.method(method_name)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'where_conditions_base'
|
|
4
|
+
|
|
5
|
+
# @whereHasConditions: the records with a related record matching the
|
|
6
|
+
# client's conditions.
|
|
7
|
+
#
|
|
8
|
+
# users(hasPosts: _ @whereHasConditions(columns: ["title"])): [User!]! @all
|
|
9
|
+
# { users(hasPosts: { column: TITLE, value: "Notes" }) { name } }
|
|
10
|
+
class Lighthouse::GraphQL::Directives::Arguments::WhereHasConditions < Lighthouse::GraphQL::Directives::Arguments::WhereConditionsBase
|
|
11
|
+
SUFFIX = 'WhereHasConditions'
|
|
12
|
+
DESCRIPTION = 'Filter results by related records conditions.'
|
|
13
|
+
|
|
14
|
+
DEFINITION = <<~GRAPHQL.chomp
|
|
15
|
+
"""
|
|
16
|
+
Allows clients to filter a query based on the existence of a related model, using
|
|
17
|
+
a dynamically controlled `WHERE` condition that applies to the relationship.
|
|
18
|
+
"""
|
|
19
|
+
directive @whereHasConditions(
|
|
20
|
+
"""
|
|
21
|
+
The relationship that the conditions will be applied to.
|
|
22
|
+
This argument can be omitted if the argument name follows the naming
|
|
23
|
+
convention `has{$RELATION}`. For example, if the relationship
|
|
24
|
+
is named `posts`, the argument name must be `hasPosts`.
|
|
25
|
+
"""
|
|
26
|
+
relation: String
|
|
27
|
+
|
|
28
|
+
#{COLUMN_ARGUMENTS.gsub(/^/, ' ')}
|
|
29
|
+
) on ARGUMENT_DEFINITION
|
|
30
|
+
GRAPHQL
|
|
31
|
+
|
|
32
|
+
def self.directive_name = 'whereHasConditions'
|
|
33
|
+
|
|
34
|
+
def self.definition = DEFINITION
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# The records with at least one related record matching the value (`{}`: any related record).
|
|
39
|
+
def conditions(value)
|
|
40
|
+
condition = value.respond_to?(:to_h) ? value.to_h : value
|
|
41
|
+
{
|
|
42
|
+
has: {
|
|
43
|
+
relation: relation_name,
|
|
44
|
+
operator: Lighthouse::WhereConditions::Types::DEFAULT_HAS_OPERATOR,
|
|
45
|
+
amount: Lighthouse::WhereConditions::Types::DEFAULT_HAS_AMOUNT,
|
|
46
|
+
condition: condition.presence
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# `relation:`, else the argument name without its `has` prefix: hasPosts -> posts.
|
|
52
|
+
def relation_name
|
|
53
|
+
Lighthouse::Support::DirectiveArgs.literal(@directive_node, 'relation') ||
|
|
54
|
+
@arg_node.name.sub(/\Ahas(?=[A-Z_])/, '').then { |name| name[0].downcase + name[1..] }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -31,7 +31,6 @@ module Lighthouse::GraphQL::Directives::Definitions
|
|
|
31
31
|
graphql::Directives::NodeDirective::DEFINITIONS,
|
|
32
32
|
graphql::Directives::ModelDirective::DEFINITIONS,
|
|
33
33
|
graphql::Directives::VisibilityDirective::DEFINITIONS,
|
|
34
|
-
graphql::Directives::WhereConditionsDirective::DEFINITIONS,
|
|
35
34
|
graphql::Directives::SoftDeletesDirective::DEFINITIONS
|
|
36
35
|
].reduce(:merge)
|
|
37
36
|
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# What the argument manipulators that generate types share, as PHP
|
|
4
|
+
# Lighthouse's ASTHelper and GeneratesColumnsEnum: the qualified name of an
|
|
5
|
+
# argument's types, the enum of its allowed columns, and the `_` placeholder
|
|
6
|
+
# the argument is written with.
|
|
7
|
+
#
|
|
8
|
+
# type Query { posts(orderBy: _ @orderBy(columns: ["posted_at"])): [Post!]! @all }
|
|
9
|
+
#
|
|
10
|
+
# names Query.posts(orderBy:)'s types QueryPostsOrderBy..., its columns enum
|
|
11
|
+
# QueryPostsOrderByColumn { POSTED_AT @enum(value: "posted_at") }.
|
|
12
|
+
module Lighthouse::GraphQL::Directives::GeneratedTypes
|
|
13
|
+
PLACEHOLDER = '_'
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# Studly(Type) + Studly(field) + Studly(argument): QueryPostsOrderBy.
|
|
18
|
+
def qualified_arg_type(arg, field, type)
|
|
19
|
+
[type.name, field.name, arg.name].map { |name| studly(name) }.join
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def studly(name)
|
|
23
|
+
name.to_s.split(/[_\s-]+/).map { |part| part[0].to_s.upcase + part[1..].to_s }.join
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# `argument` of the directive as a Ruby value (`columns: ["a"]` -> ["a"]), or nil.
|
|
27
|
+
def directive_value(directive, name)
|
|
28
|
+
Lighthouse::Support::DirectiveArgs.literal(directive, name)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def directive_argument?(directive, name)
|
|
32
|
+
directive.arguments.any? { |argument| argument.name == name }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Raises unless at most one of `names` is given to the directive.
|
|
36
|
+
def mutually_exclusive!(directive, names)
|
|
37
|
+
given = names.select { |name| directive_argument?(directive, name) }
|
|
38
|
+
return if given.size <= 1
|
|
39
|
+
|
|
40
|
+
raise Lighthouse::DefinitionError,
|
|
41
|
+
"The @#{directive.name} directive can only have one of the following arguments: #{names.map { |n| "`#{n}`" }.join(', ')}."
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Does the directive restrict the columns (`columns:` or `columnsEnum:`)?
|
|
45
|
+
def allowed_columns?(directive)
|
|
46
|
+
mutually_exclusive!(directive, %w[columns columnsEnum])
|
|
47
|
+
directive_argument?(directive, 'columns') || directive_argument?(directive, 'columnsEnum')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The enum of the allowed columns: `columnsEnum:`, or one generated from
|
|
51
|
+
# `columns:` as <QualifiedArgType>Column.
|
|
52
|
+
def columns_enum(document_ast, directive, arg, field, type)
|
|
53
|
+
enum = directive_value(directive, 'columnsEnum')
|
|
54
|
+
return enum if enum
|
|
55
|
+
|
|
56
|
+
name = "#{qualified_arg_type(arg, field, type)}Column"
|
|
57
|
+
define_columns_enum(document_ast, name, directive_value(directive, 'columns'), "#{type.name}.#{field.name}.#{arg.name}")
|
|
58
|
+
name
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def define_columns_enum(document_ast, name, columns, path)
|
|
62
|
+
values = Array(columns).map { |column| %( #{enum_value_name(column)} @enum(value: "#{column}")) }.uniq
|
|
63
|
+
document_ast.put_type(parse(<<~GRAPHQL))
|
|
64
|
+
"Allowed column names for #{path}."
|
|
65
|
+
enum #{name} {
|
|
66
|
+
#{values.join("\n")}
|
|
67
|
+
}
|
|
68
|
+
GRAPHQL
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# `posted_at` -> POSTED_AT, `roles.id` -> ROLES_ID.
|
|
72
|
+
def enum_value_name(column)
|
|
73
|
+
name = column.to_s.gsub(/[^0-9A-Za-z_]/, '_').upcase
|
|
74
|
+
name.match?(/\A[0-9]/) ? "_#{name}" : name
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The argument with its type replaced by `type_string` ("[QueryPostsOrderByClause!]"),
|
|
78
|
+
# when it is written with the `_` placeholder. An argument given a type of
|
|
79
|
+
# its own keeps it.
|
|
80
|
+
def replace_placeholder(arg, type_string)
|
|
81
|
+
return arg unless placeholder?(arg.type)
|
|
82
|
+
|
|
83
|
+
arg.merge(type: parse_type(type_string))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def placeholder?(type)
|
|
87
|
+
type = type.of_type while type.respond_to?(:of_type)
|
|
88
|
+
type.name == PLACEHOLDER
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The argument with `description`, unless it has one.
|
|
92
|
+
def describe(arg, description)
|
|
93
|
+
arg.description.to_s.empty? ? arg.merge(description: description) : arg
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def parse(sdl)
|
|
97
|
+
::GraphQL::Language::Parser.parse(sdl).definitions.first
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def parse_type(type_string)
|
|
101
|
+
parse("input LighthouseTypeReference { field: #{type_string} }").fields.first.type
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
#
|
|
14
14
|
# A registered class is classified as a field resolver when it includes
|
|
15
15
|
# Contracts::FieldResolver OR responds to the legacy `#define_resolver`
|
|
16
|
-
# instance method.
|
|
16
|
+
# instance method. A class including Contracts::ArgManipulator also rewrites
|
|
17
|
+
# its arguments while the schema is built (ASTBuilder), and one including
|
|
18
|
+
# Contracts::FieldBuilder constrains its fields' queries.
|
|
17
19
|
class Lighthouse::DirectiveRegistry
|
|
18
20
|
class << self
|
|
19
21
|
def default
|
|
@@ -29,6 +31,8 @@ class Lighthouse::DirectiveRegistry
|
|
|
29
31
|
def initialize
|
|
30
32
|
@resolution = {}
|
|
31
33
|
@arg_builders = {}
|
|
34
|
+
@arg_manipulators = {}
|
|
35
|
+
@field_builders = {}
|
|
32
36
|
@sdl_manipulators = []
|
|
33
37
|
@definitions = {}
|
|
34
38
|
@type_definitions = {}
|
|
@@ -42,11 +46,7 @@ class Lighthouse::DirectiveRegistry
|
|
|
42
46
|
name = name_or_klass
|
|
43
47
|
end
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
@arg_builders[name.to_s] = klass
|
|
47
|
-
elsif resolver?(klass)
|
|
48
|
-
@resolution[name.to_s] = klass
|
|
49
|
-
end
|
|
49
|
+
classify(name.to_s, klass)
|
|
50
50
|
self
|
|
51
51
|
end
|
|
52
52
|
|
|
@@ -97,6 +97,21 @@ class Lighthouse::DirectiveRegistry
|
|
|
97
97
|
@arg_builders.keys
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
# Argument directives that rewrite their argument while the schema is built
|
|
101
|
+
# (@whereConditions, @whereHasConditions, @orderBy), as ASTBuilder applies them.
|
|
102
|
+
def arg_manipulator_class(name)
|
|
103
|
+
@arg_manipulators[name.to_s]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def arg_manipulator_classes
|
|
107
|
+
@arg_manipulators.values
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Field directives that constrain the field's query (@orderBy on a field).
|
|
111
|
+
def field_builder_class(name)
|
|
112
|
+
@field_builders[name.to_s]
|
|
113
|
+
end
|
|
114
|
+
|
|
100
115
|
# SDL `directive @name(...) on ...` definitions contributed by registered,
|
|
101
116
|
# self-describing directive classes. The generator injects these so the app
|
|
102
117
|
# does not have to hand-declare every directive it uses.
|
|
@@ -193,7 +208,7 @@ class Lighthouse::DirectiveRegistry
|
|
|
193
208
|
|
|
194
209
|
# Argument-builder filter/sort directives.
|
|
195
210
|
ARG_BUILDERS = %i[
|
|
196
|
-
Eq Neq In NotIn Like Where WhereKey OrderBy Trashed WithoutGlobalScopes
|
|
211
|
+
Eq Neq In NotIn Like Where WhereKey OrderBy WhereConditions WhereHasConditions Trashed WithoutGlobalScopes
|
|
197
212
|
WhereNull WhereNotNull WhereBetween WhereNotBetween WhereJsonContains Scope Limit
|
|
198
213
|
].freeze
|
|
199
214
|
|
|
@@ -205,11 +220,11 @@ class Lighthouse::DirectiveRegistry
|
|
|
205
220
|
|
|
206
221
|
# SDL manipulators run in registration order during the manipulation phase.
|
|
207
222
|
# @hide / @show first, so a removed field generates nothing; then @model,
|
|
208
|
-
# which names the model of fields @paginate then rewrites.
|
|
223
|
+
# which names the model of fields @paginate then rewrites. The argument
|
|
224
|
+
# manipulators (@whereConditions, @orderBy, ...) run after all of them.
|
|
209
225
|
register_manipulator(Lighthouse::GraphQL::Directives::VisibilityDirective)
|
|
210
226
|
register_manipulator(Lighthouse::GraphQL::Directives::ModelDirective)
|
|
211
227
|
register_manipulator(Lighthouse::GraphQL::Directives::PaginationManipulator)
|
|
212
|
-
register_manipulator(Lighthouse::GraphQL::Directives::WhereConditionsDirective)
|
|
213
228
|
register_manipulator(Lighthouse::GraphQL::Directives::SoftDeletesDirective)
|
|
214
229
|
register_manipulator(Lighthouse::GraphQL::Directives::NodeDirective)
|
|
215
230
|
|
|
@@ -232,8 +247,22 @@ class Lighthouse::DirectiveRegistry
|
|
|
232
247
|
nil
|
|
233
248
|
end
|
|
234
249
|
|
|
250
|
+
def classify(name, klass)
|
|
251
|
+
if arg_builder?(klass)
|
|
252
|
+
@arg_builders[name] = klass
|
|
253
|
+
elsif resolver?(klass)
|
|
254
|
+
@resolution[name] = klass
|
|
255
|
+
end
|
|
256
|
+
@arg_manipulators[name] = klass if includes?(klass, Lighthouse::Contracts::ArgManipulator)
|
|
257
|
+
@field_builders[name] = klass if includes?(klass, Lighthouse::Contracts::FieldBuilder)
|
|
258
|
+
end
|
|
259
|
+
|
|
235
260
|
def arg_builder?(klass)
|
|
236
|
-
|
|
261
|
+
includes?(klass, Lighthouse::Contracts::ArgBuilder)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def includes?(klass, contract)
|
|
265
|
+
klass.respond_to?(:include?) && klass.include?(contract)
|
|
237
266
|
end
|
|
238
267
|
|
|
239
268
|
def resolver?(klass)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The schema document as the manipulation phase works on it, as PHP
|
|
4
|
+
# Lighthouse's DocumentAST: the type definitions by name, the type extensions
|
|
5
|
+
# by the name they extend, the directive definitions and the schema
|
|
6
|
+
# definition/extensions.
|
|
7
|
+
#
|
|
8
|
+
# A type is defined once. A second definition of the same name is an error,
|
|
9
|
+
# not a merge: add to a type from another file with `extend type` (or
|
|
10
|
+
# `extend input`, `extend enum`, ...), which ASTBuilder merges into it.
|
|
11
|
+
#
|
|
12
|
+
# graphql-ruby's nodes are immutable, so a manipulation replaces a type with
|
|
13
|
+
# `put_type(type.merge(...))` rather than editing it.
|
|
14
|
+
class Lighthouse::GraphQL::DocumentAST
|
|
15
|
+
Nodes = ::GraphQL::Language::Nodes
|
|
16
|
+
|
|
17
|
+
# The SDL keyword each kind of definition is extended with.
|
|
18
|
+
KEYWORDS = {
|
|
19
|
+
Nodes::ObjectTypeDefinition => 'type',
|
|
20
|
+
Nodes::InterfaceTypeDefinition => 'interface',
|
|
21
|
+
Nodes::InputObjectTypeDefinition => 'input',
|
|
22
|
+
Nodes::EnumTypeDefinition => 'enum',
|
|
23
|
+
Nodes::UnionTypeDefinition => 'union',
|
|
24
|
+
Nodes::ScalarTypeDefinition => 'scalar'
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
EXTENSIONS = [
|
|
28
|
+
Nodes::ObjectTypeExtension, Nodes::InterfaceTypeExtension, Nodes::InputObjectTypeExtension,
|
|
29
|
+
Nodes::EnumTypeExtension, Nodes::UnionTypeExtension, Nodes::ScalarTypeExtension
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
attr_reader :types, :type_extensions, :directives, :schema_definitions
|
|
33
|
+
|
|
34
|
+
def self.from_source(sdl)
|
|
35
|
+
document = begin
|
|
36
|
+
::GraphQL::Language::Parser.parse(sdl.to_s)
|
|
37
|
+
rescue ::GraphQL::ParseError => e
|
|
38
|
+
raise Lighthouse::DefinitionError, "The schema is not valid GraphQL: #{e.message}"
|
|
39
|
+
end
|
|
40
|
+
from_document(document)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.from_document(document)
|
|
44
|
+
new.tap { |ast| document.definitions.each { |definition| ast.add(definition) } }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def initialize
|
|
48
|
+
@types = {}
|
|
49
|
+
@type_extensions = Hash.new { |hash, name| hash[name] = [] }
|
|
50
|
+
@directives = {}
|
|
51
|
+
@schema_definitions = []
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def add(definition)
|
|
55
|
+
case definition
|
|
56
|
+
when *KEYWORDS.keys then add_type(definition)
|
|
57
|
+
when *EXTENSIONS then @type_extensions[definition.name] << definition
|
|
58
|
+
when Nodes::DirectiveDefinition then add_directive(definition)
|
|
59
|
+
when Nodes::SchemaDefinition, Nodes::SchemaExtension then @schema_definitions << definition
|
|
60
|
+
else raise Lighthouse::DefinitionError, "Unknown definition type: #{definition.class}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Adds a type, or replaces the definition of the same name.
|
|
65
|
+
def put_type(definition)
|
|
66
|
+
@types[definition.name] = definition
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Adds the type parsed from `sdl`, unless the document has a type of that name.
|
|
71
|
+
def define_type(name, sdl = nil)
|
|
72
|
+
return @types[name] if @types.key?(name)
|
|
73
|
+
|
|
74
|
+
definition = ::GraphQL::Language::Parser.parse(sdl || yield).definitions.first
|
|
75
|
+
put_type(definition)
|
|
76
|
+
definition
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def type?(name) = @types.key?(name)
|
|
80
|
+
|
|
81
|
+
# The names of the types, as they are now: manipulators add types while
|
|
82
|
+
# ASTBuilder goes over these.
|
|
83
|
+
def type_names = @types.keys
|
|
84
|
+
|
|
85
|
+
def to_document
|
|
86
|
+
Nodes::Document.new(definitions: @schema_definitions + @directives.values + @types.values + @type_extensions.values.flatten)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def to_sdl
|
|
90
|
+
::GraphQL::Language::Printer.new.print(to_document)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def add_type(definition)
|
|
96
|
+
if @types.key?(definition.name)
|
|
97
|
+
keyword = KEYWORDS.fetch(definition.class)
|
|
98
|
+
raise Lighthouse::DefinitionError,
|
|
99
|
+
"Duplicate definition #{definition.name} found when parsing the schema. Define a type once and add to it " \
|
|
100
|
+
"from other files with `extend #{keyword} #{definition.name}`."
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
@types[definition.name] = definition
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def add_directive(definition)
|
|
107
|
+
raise Lighthouse::DefinitionError, "Duplicate definition @#{definition.name} found when parsing the schema." if @directives.key?(definition.name)
|
|
108
|
+
|
|
109
|
+
@directives[definition.name] = definition
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -55,20 +55,23 @@ module Lighthouse::GraphQL::FieldResolverStack
|
|
|
55
55
|
[0, -> { resolver.define_resolver(type) }]
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
# What resolves a field with no resolving directive: the
|
|
59
|
-
#
|
|
58
|
+
# What resolves a field with no resolving directive: the method of the
|
|
59
|
+
# type's class (Types::QueryType#latest_posts too, as in graphql-ruby), else
|
|
60
|
+
# for a Query or Mutation field the conventional class. nil: the default reader.
|
|
60
61
|
def own_step(type, field_def, root_kind, bases, type_class)
|
|
61
|
-
return convention_step(type, field_def, root_kind, bases) if root_kind
|
|
62
|
+
return convention_step(type, field_def, root_kind, bases) if root_kind == :subscriptions
|
|
62
63
|
|
|
63
|
-
type_method_step(type, field_def, type_class) if type_class
|
|
64
|
+
step = type_method_step(type, field_def, type_class, root: !root_kind.nil?) if type_class
|
|
65
|
+
step || (convention_step(type, field_def, root_kind, bases) if root_kind)
|
|
64
66
|
end
|
|
65
67
|
|
|
66
68
|
# Nothing to install when the class does not define the field's method: the
|
|
67
69
|
# field is read off the object.
|
|
68
|
-
|
|
70
|
+
# `root`: the class is the root type's, which the type is built on, not one found by name.
|
|
71
|
+
def type_method_step(type, field_def, type_class, root: false)
|
|
69
72
|
resolver = Lighthouse::GraphQL::DirectiveResolvers::TypeMethodResolver
|
|
70
73
|
method_name = resolver.method_for(type_class, field_def)
|
|
71
|
-
[0, -> { resolver.new(field_def, type, method_name).define_resolver(type) }] if method_name
|
|
74
|
+
[0, -> { resolver.new(field_def, type, method_name, root ? type_class : nil).define_resolver(type) }] if method_name
|
|
72
75
|
end
|
|
73
76
|
|
|
74
77
|
# An explicit @validator on the field already validates its arguments.
|
data/lib/lighthouse/global_id.rb
CHANGED
|
@@ -7,6 +7,12 @@ require 'base64'
|
|
|
7
7
|
# Config.global_id takes another object with `encode(type, id)` and
|
|
8
8
|
# `decode(global_id)` -> [type, id], as PHP Lighthouse lets the GlobalId
|
|
9
9
|
# contract be rebound.
|
|
10
|
+
#
|
|
11
|
+
# A schema that implements graphql-ruby's Relay ids, `id_from_object` and
|
|
12
|
+
# `object_from_id` (graphql-ruby's install writes them, on Rails' GlobalID),
|
|
13
|
+
# has one kind of global id: @globalId encodes an object's own id with
|
|
14
|
+
# `id_from_object`, and decodes with `object_from_id`, as graphql-ruby's
|
|
15
|
+
# Query.node and Node's `id` do.
|
|
10
16
|
module Lighthouse::GraphQL::GlobalId
|
|
11
17
|
module_function
|
|
12
18
|
|
|
@@ -20,12 +26,32 @@ module Lighthouse::GraphQL::GlobalId
|
|
|
20
26
|
[]
|
|
21
27
|
end
|
|
22
28
|
|
|
23
|
-
# [type, id] for a global id a client sent, through
|
|
29
|
+
# [type, id] for a global id a client sent, through the schema's
|
|
30
|
+
# object_from_id (schema_ids?) or Config.global_id; a
|
|
24
31
|
# GraphQL::ExecutionError when it is not one.
|
|
25
|
-
def decode!(global_id)
|
|
26
|
-
type, id = Lighthouse::Config.global_id.decode(global_id)
|
|
32
|
+
def decode!(global_id, context = nil)
|
|
33
|
+
type, id = schema_ids?(context&.schema) ? decode_object(global_id, context) : Lighthouse::Config.global_id.decode(global_id)
|
|
27
34
|
return [type, id] if type.present? && !id.nil?
|
|
28
35
|
|
|
29
36
|
raise ::GraphQL::ExecutionError, "#{global_id.inspect} is not a valid global id."
|
|
30
37
|
end
|
|
38
|
+
|
|
39
|
+
# The global id of `object`'s own id (`id`, what a field of `type` resolved)
|
|
40
|
+
# through the schema's id_from_object (schema_ids?); nil for anything else.
|
|
41
|
+
def encode_field(type, object, id, context)
|
|
42
|
+
return unless schema_ids?(context&.schema) && object.respond_to?(:id) && object.id.to_s == id.to_s
|
|
43
|
+
|
|
44
|
+
context.schema.id_from_object(object, type, context)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Does `schema` define graphql-ruby's object_from_id (GraphQL::Schema's own raises)?
|
|
48
|
+
def schema_ids?(schema)
|
|
49
|
+
schema.respond_to?(:object_from_id) && schema.method(:object_from_id).owner != ::GraphQL::Schema.singleton_class
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# [type, id] of the object the schema's object_from_id finds.
|
|
53
|
+
def decode_object(global_id, context)
|
|
54
|
+
object = context.schema.object_from_id(global_id, context)
|
|
55
|
+
object.nil? ? [] : [object.class.name.demodulize, object.id]
|
|
56
|
+
end
|
|
31
57
|
end
|
|
@@ -15,4 +15,49 @@ module Lighthouse::GraphQL
|
|
|
15
15
|
# skip the fix rather than fail to load (explicit nulls then fail as before).
|
|
16
16
|
builder = ::GraphQL::Schema::BuildFromDefinition.const_get(:Builder) if ::GraphQL::Schema::BuildFromDefinition.const_defined?(:Builder)
|
|
17
17
|
builder.prepend(NullDirectiveArguments) if builder && (builder.method_defined?(:args_to_kwargs) || builder.private_method_defined?(:args_to_kwargs))
|
|
18
|
+
|
|
19
|
+
# graphql-ruby builds every SDL object type on base_types[:object], and every
|
|
20
|
+
# interface from base_types[:interface]. RootTypes builds a root type on the
|
|
21
|
+
# app's code-first one (Types::QueryType) instead, and takes the app's Node
|
|
22
|
+
# interface (Types::NodeType) as the SDL's Node.
|
|
23
|
+
module RootTypeBuilder
|
|
24
|
+
def build_object_type(definition, type_resolver, base_type)
|
|
25
|
+
root = Lighthouse::GraphQL::RootTypes.base_for(definition.name)
|
|
26
|
+
return super unless root
|
|
27
|
+
|
|
28
|
+
super(Lighthouse::GraphQL::RootTypes.definition_on(definition, root), type_resolver, root)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def build_interface_type(definition, type_resolver, base_type)
|
|
32
|
+
Lighthouse::GraphQL::RootTypes.base_for(definition.name) || super
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# As above, a graphql-ruby release that reshapes the builder skips the hook:
|
|
37
|
+
# root types are then built on base_types[:object], as before.
|
|
38
|
+
if builder && %i[build_object_type build_interface_type].all? { |name| builder.method_defined?(name) || builder.private_method_defined?(name) }
|
|
39
|
+
builder.prepend(RootTypeBuilder)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The app's schema class inherits Lighthouse's (`class AppSchema <
|
|
43
|
+
# get_schema(...)`), so its Subscription root type is the parent's. A
|
|
44
|
+
# subscriptions backend the subclass `use`s (ActionCableSubscriptions) then
|
|
45
|
+
# finds no root type of the subclass's own, and graphql-ruby adds its
|
|
46
|
+
# subscription extension to no field. Look the root type up through the
|
|
47
|
+
# parent too. Extended onto the schemas Lighthouse builds (SchemaGenerator).
|
|
48
|
+
module InheritedSubscriptionRoot
|
|
49
|
+
def add_subscription_extension_if_necessary
|
|
50
|
+
return super if @subscription_object.is_a?(Class) || !defined?(::GraphQL::Subscriptions::DefaultSubscriptionResolveExtension)
|
|
51
|
+
return unless inherited_subscription_root_pending?
|
|
52
|
+
|
|
53
|
+
@subscription_extension_added = true
|
|
54
|
+
extension = ::GraphQL::Subscriptions::DefaultSubscriptionResolveExtension
|
|
55
|
+
subscription.all_field_definitions.each { |field| field.extension(extension) unless field.extensions.any?(extension) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# A backend, a root type from the parent, and no extension added yet.
|
|
59
|
+
def inherited_subscription_root_pending?
|
|
60
|
+
!defined?(@subscription_extension_added) && subscription.is_a?(Class) && !subscriptions.nil?
|
|
61
|
+
end
|
|
62
|
+
end
|
|
18
63
|
end
|