lighthouse-graphql 0.1.1 → 0.2.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 +154 -1
- data/README.md +42 -2
- data/docs/authorization.md +116 -57
- data/docs/best-practices.md +16 -3
- data/docs/caching.md +161 -0
- data/docs/configuration.md +83 -4
- data/docs/custom-directives.md +5 -1
- data/docs/directives.md +117 -38
- data/docs/getting-started.md +84 -0
- data/docs/mutations.md +260 -0
- data/docs/performance.md +138 -0
- data/docs/relationships.md +80 -33
- data/docs/resolvers.md +179 -0
- data/docs/validation.md +132 -0
- data/lib/generators/lighthouse/builder_generator.rb +30 -0
- data/lib/generators/lighthouse/class_generator.rb +61 -0
- data/lib/generators/lighthouse/install/install_generator.rb +127 -0
- data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +8 -0
- data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +57 -0
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +67 -0
- data/lib/generators/lighthouse/install/templates/schema.graphql.tt +19 -0
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -0
- data/lib/generators/lighthouse/mutation_generator.rb +30 -0
- data/lib/generators/lighthouse/query_generator.rb +29 -0
- data/lib/generators/lighthouse/validator_generator.rb +36 -0
- data/lib/lighthouse/cache_control.rb +126 -0
- data/lib/lighthouse/caching.rb +68 -0
- data/lib/lighthouse/config.rb +85 -19
- data/lib/lighthouse/contracts/arg_builder.rb +8 -12
- data/lib/lighthouse/contracts/arg_manipulator.rb +8 -12
- data/lib/lighthouse/contracts/directive.rb +10 -14
- data/lib/lighthouse/contracts/field_manipulator.rb +8 -12
- data/lib/lighthouse/contracts/field_middleware.rb +7 -11
- data/lib/lighthouse/contracts/field_resolver.rb +9 -13
- data/lib/lighthouse/dataloader_sources/association_loader.rb +28 -19
- data/lib/lighthouse/dataloader_sources/model_loader.rb +10 -14
- data/lib/lighthouse/dataloader_sources/relation_count_loader.rb +16 -0
- data/lib/lighthouse/dataloader_sources/relation_page_loader.rb +18 -0
- data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +57 -69
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +26 -57
- data/lib/lighthouse/directive_resolvers/association_resolvers.rb +161 -0
- data/lib/lighthouse/directive_resolvers/auth_resolver.rb +41 -38
- data/lib/lighthouse/directive_resolvers/base.rb +142 -86
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +58 -56
- data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +133 -0
- data/lib/lighthouse/directive_resolvers/can_resolvers.rb +177 -0
- data/lib/lighthouse/directive_resolvers/convention_resolver.rb +70 -0
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +42 -48
- data/lib/lighthouse/directive_resolvers/field_resolver.rb +27 -68
- data/lib/lighthouse/directive_resolvers/find_resolver.rb +42 -52
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +39 -45
- data/lib/lighthouse/directive_resolvers/guard_resolver.rb +16 -22
- data/lib/lighthouse/directive_resolvers/method_resolver.rb +19 -25
- data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +219 -0
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +70 -128
- data/lib/lighthouse/directive_resolvers/rename_resolver.rb +18 -24
- data/lib/lighthouse/directive_resolvers/resolution.rb +31 -27
- data/lib/lighthouse/directive_resolvers/validator_resolver.rb +193 -0
- data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +164 -163
- data/lib/lighthouse/directives/arguments/base.rb +23 -31
- data/lib/lighthouse/directives/arguments/filters.rb +113 -93
- data/lib/lighthouse/directives/arguments/order_by.rb +41 -41
- data/lib/lighthouse/directives/pagination_info_types.rb +69 -0
- data/lib/lighthouse/directives/pagination_manipulator.rb +176 -0
- data/lib/lighthouse/directives/registry.rb +210 -141
- data/lib/lighthouse/directives/where_conditions_directive.rb +236 -200
- data/lib/lighthouse/errors.rb +27 -0
- data/lib/lighthouse/federation_directives.rb +53 -0
- data/lib/lighthouse/field_resolver_stack.rb +56 -0
- data/lib/lighthouse/generated_resolvers.rb +82 -0
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/graphql_ruby_patches.rb +18 -0
- data/lib/lighthouse/ide_helper.rb +98 -0
- data/lib/lighthouse/namespaces.rb +21 -0
- data/lib/lighthouse/nested_mutations/belongs_to_operations.rb +50 -0
- data/lib/lighthouse/nested_mutations/executor.rb +149 -0
- data/lib/lighthouse/nested_mutations/has_operations.rb +66 -0
- data/lib/lighthouse/nested_mutations/many_to_many_operations.rb +44 -0
- data/lib/lighthouse/nested_mutations/partitioner.rb +125 -0
- data/lib/lighthouse/nested_mutations.rb +210 -0
- data/lib/lighthouse/pagination.rb +221 -0
- data/lib/lighthouse/persisted_queries.rb +167 -0
- data/lib/lighthouse/railtie.rb +26 -0
- data/lib/lighthouse/rb_lighthouse.rb +50 -29
- data/lib/lighthouse/reference_resolver.rb +13 -15
- data/lib/lighthouse/relation_batch.rb +156 -0
- data/lib/lighthouse/resolver_dispatch.rb +26 -0
- data/lib/lighthouse/schema_cache.rb +120 -0
- data/lib/lighthouse/schema_factory.rb +27 -37
- data/lib/lighthouse/schema_generator.rb +196 -239
- data/lib/lighthouse/schema_implementation.rb +51 -48
- data/lib/lighthouse/sdl_loader.rb +17 -20
- data/lib/lighthouse/support/arguments.rb +119 -0
- data/lib/lighthouse/support/authorization.rb +18 -19
- data/lib/lighthouse/support/batching.rb +16 -0
- data/lib/lighthouse/support/directive_args.rb +26 -24
- data/lib/lighthouse/support/model_resolver.rb +56 -56
- data/lib/lighthouse/support/naming.rb +55 -59
- data/lib/lighthouse/tasks/lighthouse.rake +33 -0
- data/lib/lighthouse/where_conditions/operator_map.rb +25 -29
- metadata +51 -63
- data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +0 -56
- data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +0 -47
- data/lib/lighthouse/directive_resolvers/can_resolver.rb +0 -86
- data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +0 -109
- data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +0 -43
- data/lib/lighthouse/directives/relation_directive.rb +0 -332
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Field-argument plumbing shared by resolvers: plain-Ruby conversion, @inject,
|
|
4
|
+
# @spread, and dot paths ("input.id", "*.id") into arguments and results.
|
|
5
|
+
module Lighthouse::Support::Arguments
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# The arguments a resolver class receives: @inject applied, then @spread.
|
|
9
|
+
# Values keep their GraphQL input-object form except where spread.
|
|
10
|
+
def prepare(field_def, args, context)
|
|
11
|
+
values = inject(field_def.ast_node, shallow(args), context)
|
|
12
|
+
spread(field_def.arguments, values)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Arguments / input objects as a Hash with symbol keys, one level deep.
|
|
16
|
+
def shallow(value)
|
|
17
|
+
case value
|
|
18
|
+
when ::GraphQL::Schema::InputObject then value.to_h.transform_keys(&:to_sym)
|
|
19
|
+
when ::GraphQL::Execution::Interpreter::Arguments then value.keyword_arguments.dup
|
|
20
|
+
when Hash then value.transform_keys(&:to_sym)
|
|
21
|
+
else {}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Input objects, arguments and nested collections as plain Ruby values.
|
|
26
|
+
def plain(value)
|
|
27
|
+
case value
|
|
28
|
+
when ::GraphQL::Schema::InputObject then plain(value.to_h)
|
|
29
|
+
when ::GraphQL::Execution::Interpreter::Arguments then plain(value.keyword_arguments)
|
|
30
|
+
when Hash then value.to_h { |k, v| [k.to_sym, plain(v)] }
|
|
31
|
+
when Array then value.map { |v| plain(v) }
|
|
32
|
+
else value
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @inject(context: "current_user.id", name: "user_id"): server-side values
|
|
37
|
+
# written into the arguments, so clients cannot set them.
|
|
38
|
+
def inject(field_ast_node, values, context)
|
|
39
|
+
directives = (field_ast_node&.directives || []).select { |d| d.name == 'inject' }
|
|
40
|
+
directives.reduce(values) do |acc, directive|
|
|
41
|
+
from = Lighthouse::Support::DirectiveArgs.literal(directive, 'context').to_s
|
|
42
|
+
to = Lighthouse::Support::DirectiveArgs.literal(directive, 'name').to_s
|
|
43
|
+
set_path(acc, to.split('.'), dig(context, from).first)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Merge every @spread argument's input object into its parent, recursively.
|
|
48
|
+
def spread(argument_defs, values)
|
|
49
|
+
by_keyword = keyword_index(argument_defs)
|
|
50
|
+
values.each_with_object({}) do |(key, value), out|
|
|
51
|
+
argument = by_keyword[key.to_sym]
|
|
52
|
+
if argument && spread?(argument) && !value.nil?
|
|
53
|
+
out.merge!(spread(argument.type.unwrap.arguments, shallow(value)))
|
|
54
|
+
else
|
|
55
|
+
out[key] = value
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def spread?(argument)
|
|
61
|
+
(argument.ast_node&.directives || []).any? { |d| d.name == 'spread' }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def keyword_index(argument_defs)
|
|
65
|
+
(argument_defs || {}).values.index_by(&:keyword)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Every value at a dot `path` under `root`; `*` walks each list element.
|
|
69
|
+
# GraphQL names are matched in their snake_case form. Always an Array.
|
|
70
|
+
def dig(root, path)
|
|
71
|
+
path.to_s.split('.').reduce([root]) do |nodes, segment|
|
|
72
|
+
nodes.flat_map do |node|
|
|
73
|
+
next [] if node.nil?
|
|
74
|
+
next list(node) || [node] if segment == '*'
|
|
75
|
+
|
|
76
|
+
[read(node, segment)]
|
|
77
|
+
end
|
|
78
|
+
end.compact
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The items of a collection (Array, Set, relation), or nil for a single
|
|
82
|
+
# object — records and Structs answer #to_a too, so that test won't do.
|
|
83
|
+
def list(value)
|
|
84
|
+
case value
|
|
85
|
+
when Array then value
|
|
86
|
+
when Set then value.to_a
|
|
87
|
+
else value.to_a if defined?(::ActiveRecord::Relation) && value.is_a?(::ActiveRecord::Relation)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def read(node, segment)
|
|
92
|
+
keys = [segment, segment.underscore].uniq
|
|
93
|
+
if node.respond_to?(:key?)
|
|
94
|
+
stored = stored_key(node, keys)
|
|
95
|
+
return node[stored] unless stored.nil?
|
|
96
|
+
return nil if node.is_a?(Hash)
|
|
97
|
+
end
|
|
98
|
+
method = keys.reverse.find { |key| node.respond_to?(key) }
|
|
99
|
+
method ? node.public_send(method) : nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The key a keyed node holds the first of `keys` under — Symbol before
|
|
103
|
+
# String — or nil when it holds none of them.
|
|
104
|
+
def stored_key(node, keys)
|
|
105
|
+
keys.each do |key|
|
|
106
|
+
return key.to_sym if node.key?(key.to_sym)
|
|
107
|
+
return key if node.key?(key)
|
|
108
|
+
end
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def set_path(values, segments, value)
|
|
113
|
+
key, *rest = segments
|
|
114
|
+
key = key.underscore.to_sym
|
|
115
|
+
copy = values.dup
|
|
116
|
+
copy[key] = rest.empty? ? value : set_path(shallow(copy[key]), rest, value)
|
|
117
|
+
copy
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
module Authorization
|
|
8
|
-
module_function
|
|
3
|
+
# Thin wrapper over the configurable authorization seams, used by the @guard
|
|
4
|
+
# and @can directives so they stay framework-agnostic.
|
|
5
|
+
module Lighthouse::Support::Authorization
|
|
6
|
+
module_function
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
def authenticated_user(context)
|
|
9
|
+
Lighthouse::Config.authenticated_user.call(context)
|
|
10
|
+
end
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
def authenticated?(context)
|
|
13
|
+
!authenticated_user(context).nil?
|
|
14
|
+
end
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
# Returns true/false for `ability` against `record` (an instance, or a model
|
|
17
|
+
# class) for the current policy user. Defaults to Pundit (see
|
|
18
|
+
# Config#authorizer). `args` reach the authorizer only when given, so a
|
|
19
|
+
# three-argument authorizer keeps working for checks without them.
|
|
20
|
+
def authorized?(context, record, ability, args = [])
|
|
21
|
+
user = Lighthouse::Config.policy_user.call(context)
|
|
22
|
+
authorizer = Lighthouse::Config.authorizer
|
|
23
|
+
args.empty? ? authorizer.call(user, record, ability) : authorizer.call(user, record, ability, args)
|
|
25
24
|
end
|
|
26
25
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Access to the query's GraphQL::Dataloader. graphql-ruby hands every query a
|
|
4
|
+
# dataloader — a NullDataloader, whose sources refuse to run, when the schema
|
|
5
|
+
# does not `use GraphQL::Dataloader` — so "is there one" needs this check.
|
|
6
|
+
module Lighthouse::Support::Batching
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def dataloader(context)
|
|
10
|
+
dataloader = context.respond_to?(:dataloader) ? context.dataloader : nil
|
|
11
|
+
return nil if dataloader.nil?
|
|
12
|
+
return nil if defined?(::GraphQL::Dataloader::NullDataloader) && dataloader.is_a?(::GraphQL::Dataloader::NullDataloader)
|
|
13
|
+
|
|
14
|
+
dataloader
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -1,33 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
module DirectiveArgs
|
|
10
|
-
module_function
|
|
3
|
+
# Extracts argument values from a GraphQL directive AST node, unwrapping the
|
|
4
|
+
# various value-node shapes (scalar literals, enums, lists) into plain Ruby
|
|
5
|
+
# values. Consolidates the repeated
|
|
6
|
+
# `arg.value.respond_to?(:value) ? arg.value.value : arg.value` pattern.
|
|
7
|
+
module Lighthouse::Support::DirectiveArgs
|
|
8
|
+
module_function
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
# Return the literal value of directive argument `name`, or nil.
|
|
11
|
+
def literal(directive_node, name)
|
|
12
|
+
return nil unless directive_node.respond_to?(:arguments)
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
arg = directive_node.arguments&.find { |a| a.name == name.to_s }
|
|
15
|
+
return nil unless arg
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
unwrap(arg.value)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def unwrap(value)
|
|
21
|
+
case value
|
|
22
|
+
when ::GraphQL::Language::Nodes::Enum then return value.name
|
|
23
|
+
when ::GraphQL::Language::Nodes::NullValue then return nil
|
|
24
|
+
when ::GraphQL::Language::Nodes::InputObject then return value.to_h
|
|
25
|
+
end
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
value
|
|
29
|
-
end
|
|
30
|
-
end
|
|
27
|
+
if value.respond_to?(:value)
|
|
28
|
+
value.value
|
|
29
|
+
elsif value.is_a?(Array)
|
|
30
|
+
value.map { |item| unwrap(item) }
|
|
31
|
+
else
|
|
32
|
+
value
|
|
31
33
|
end
|
|
32
34
|
end
|
|
33
35
|
end
|
|
@@ -1,61 +1,61 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
3
|
+
# Resolves a class name string (from a directive argument such as
|
|
4
|
+
# `@field(resolver: "Resolvers::ContactPerson")` or `@all(model: "Contact")`)
|
|
5
|
+
# into an actual constant.
|
|
6
|
+
#
|
|
7
|
+
# The list of namespaces to try is configurable via
|
|
8
|
+
# `Lighthouse::Config.resolver_namespaces`, so the library does not need to
|
|
9
|
+
# know any app-specific namespace. This is a key seam for extracting the
|
|
10
|
+
# library into a standalone gem.
|
|
11
|
+
module Lighthouse::Support::ModelResolver
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
DEFAULT_NAMESPACES = ['App::Graphql'].freeze
|
|
15
|
+
|
|
16
|
+
def namespaces
|
|
17
|
+
if defined?(Lighthouse::Config) &&
|
|
18
|
+
Lighthouse::Config.respond_to?(:resolver_namespaces) &&
|
|
19
|
+
Lighthouse::Config.resolver_namespaces
|
|
20
|
+
Array(Lighthouse::Config.resolver_namespaces)
|
|
21
|
+
else
|
|
22
|
+
DEFAULT_NAMESPACES
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# `namespaces:` overrides the configured resolver_namespaces, e.g. with
|
|
27
|
+
# Config.namespaces[:builders] to look a builder class up.
|
|
28
|
+
# `fallback: false` skips the bare name, so `Queries::User` is never
|
|
29
|
+
# mistaken for the `User` model.
|
|
30
|
+
def constantize(name, namespaces: self.namespaces, fallback: true)
|
|
31
|
+
return nil unless name
|
|
32
|
+
|
|
33
|
+
raw = name.to_s.strip
|
|
34
|
+
return nil if raw.empty?
|
|
35
|
+
|
|
36
|
+
candidates_for(raw, namespaces, fallback: fallback).each do |candidate|
|
|
37
|
+
klass = try_constantize(candidate)
|
|
38
|
+
return klass if klass
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def candidates_for(raw, namespaces = self.namespaces, fallback: true)
|
|
45
|
+
if raw.include?('::')
|
|
46
|
+
[raw]
|
|
47
|
+
else
|
|
48
|
+
Array(namespaces).map { |ns| "#{ns}::#{raw}" } + (fallback ? [raw] : [])
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def try_constantize(candidate)
|
|
53
|
+
ActiveSupport::Dependencies.constantize(candidate)
|
|
54
|
+
rescue NameError, LoadError
|
|
55
|
+
begin
|
|
56
|
+
Object.const_get(candidate)
|
|
57
|
+
rescue NameError
|
|
58
|
+
nil
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
end
|
|
@@ -1,71 +1,67 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
module Naming
|
|
12
|
-
module_function
|
|
3
|
+
# Central rule for translating GraphQL (camelCase) names into Rails
|
|
4
|
+
# (snake_case) attributes/columns, and for tolerant argument lookup.
|
|
5
|
+
#
|
|
6
|
+
# This is the single home for casing logic; every directive/resolver that
|
|
7
|
+
# needs to map a GraphQL name to a model attribute or DB column routes here
|
|
8
|
+
# instead of re-implementing `.underscore` fallbacks.
|
|
9
|
+
module Lighthouse::Support::Naming
|
|
10
|
+
module_function
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
# GraphQL field name -> Rails attribute/method name.
|
|
13
|
+
# e.g. "customAttributes" -> "custom_attributes"
|
|
14
|
+
#
|
|
15
|
+
# This is the default field-reader rule and the basis for relation method
|
|
16
|
+
# name derivation. The `@rename(attribute:)` directive is the explicit
|
|
17
|
+
# override that bypasses this.
|
|
18
|
+
def attribute(graphql_name)
|
|
19
|
+
graphql_name.to_s.underscore
|
|
20
|
+
end
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
22
|
+
# Resolve a GraphQL "column" token (often an ALLCAPS enum key, or a raw
|
|
23
|
+
# string) to a usable column name.
|
|
24
|
+
#
|
|
25
|
+
# NOTE: kept behavior-compatible with the historical `resolve_column_name`
|
|
26
|
+
# so existing whereConditions queries are unaffected. Word-boundary
|
|
27
|
+
# recovery for enum keys is handled separately by generating column enums
|
|
28
|
+
# with explicit `@enum(value:)` mappings.
|
|
29
|
+
def column(graphql_name, _relation = nil)
|
|
30
|
+
name = graphql_name.to_s
|
|
31
|
+
name = name.downcase if name == name.upcase
|
|
32
|
+
name = name.tr('-', '_')
|
|
33
|
+
name = name.underscore if name.include?(' ')
|
|
34
|
+
name
|
|
35
|
+
end
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
# Fetch an argument value from a hash tolerant of camelCase/snake_case and
|
|
38
|
+
# string/symbol keys. Replaces the ad-hoc `args[:x] || args['x'] || ...`
|
|
39
|
+
# chains scattered across resolvers.
|
|
40
|
+
def fetch_arg(args_h, name)
|
|
41
|
+
return nil if args_h.nil?
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
candidate_keys(name).each do |key|
|
|
44
|
+
value = args_h[key]
|
|
45
|
+
return value unless value.nil?
|
|
46
|
+
end
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
def candidate_keys(name)
|
|
51
|
+
raw = name.to_s
|
|
52
|
+
snake = raw.underscore
|
|
53
|
+
[raw.to_sym, raw, snake.to_sym, snake].uniq
|
|
54
|
+
end
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
# Derive an Eloquent-style relation name from a `has<Relation>` argument
|
|
57
|
+
# name, e.g. "hasContact" -> "contacts". Returns nil when the argument does
|
|
58
|
+
# not follow the `has` convention.
|
|
59
|
+
def derive_relation_name(arg_name)
|
|
60
|
+
return nil unless arg_name.to_s.match?(/^has/i)
|
|
63
61
|
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
trailing = arg_name.to_s.sub(/^has/i, '')
|
|
63
|
+
return nil if trailing.empty?
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
end
|
|
69
|
-
end
|
|
65
|
+
trailing.underscore.pluralize
|
|
70
66
|
end
|
|
71
67
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :lighthouse do
|
|
4
|
+
desc 'Write schema-directives.graphql and programmatic-types.graphql for IDE support (OUTPUT=dir, default Rails.root)'
|
|
5
|
+
task ide_helper: :environment do
|
|
6
|
+
output_dir = ENV.fetch('OUTPUT') { Rails.root.to_s }
|
|
7
|
+
Lighthouse::IdeHelper.write(output_dir: output_dir).each { |path| puts "Wrote #{path}" }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
desc 'Build the schema cache in tmp/cache (for config.schema_cache; e.g. during a Docker build)'
|
|
11
|
+
task cache: :environment do
|
|
12
|
+
puts "Wrote #{Lighthouse::GraphQL::SchemaCache.warm!}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc 'Delete the schema cache'
|
|
16
|
+
task clear_cache: :environment do
|
|
17
|
+
Lighthouse::GraphQL::SchemaCache.clear!
|
|
18
|
+
puts "Cleared #{Lighthouse::GraphQL::SchemaCache.directory}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
namespace :persisted_queries do
|
|
22
|
+
desc 'Validate every operation of the persisted query manifest against the schema (SCHEMA=AppSchema)'
|
|
23
|
+
task verify: :environment do
|
|
24
|
+
schema = Object.const_get(ENV.fetch('SCHEMA', 'AppSchema'))
|
|
25
|
+
failures = Lighthouse::GraphQL::PersistedQueries.manifest.filter_map do |hash, body|
|
|
26
|
+
errors = schema.validate(body)
|
|
27
|
+
"#{hash}: #{errors.map(&:message).join('; ')}" if errors.any?
|
|
28
|
+
end
|
|
29
|
+
abort("Operations the schema no longer accepts:\n#{failures.join("\n")}") if failures.any?
|
|
30
|
+
puts "#{Lighthouse::GraphQL::PersistedQueries.manifest.size} persisted operations are valid."
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -1,35 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
'IS_NOT_NULL' => :not_eq
|
|
24
|
-
}.freeze
|
|
3
|
+
# Maps the GraphQL SQLOperator enum values to Arel predicate methods.
|
|
4
|
+
# Extracted from WhereConditionsApplier so that simple argument-filter
|
|
5
|
+
# directives (@eq, @neq, @in, @notIn, @like, @where) can share the same
|
|
6
|
+
# operator semantics.
|
|
7
|
+
module Lighthouse::WhereConditions::OperatorMap
|
|
8
|
+
OPERATORS = {
|
|
9
|
+
'EQ' => :eq,
|
|
10
|
+
'NEQ' => :not_eq,
|
|
11
|
+
'GT' => :gt,
|
|
12
|
+
'GTE' => :gteq,
|
|
13
|
+
'LT' => :lt,
|
|
14
|
+
'LTE' => :lteq,
|
|
15
|
+
'IN' => :in,
|
|
16
|
+
'NIN' => :not_in,
|
|
17
|
+
'CONTAINS' => :matches,
|
|
18
|
+
'STARTS_WITH' => :matches,
|
|
19
|
+
'ENDS_WITH' => :matches,
|
|
20
|
+
'IS_NULL' => :eq,
|
|
21
|
+
'IS_NOT_NULL' => :not_eq
|
|
22
|
+
}.freeze
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
def self.[](operator)
|
|
25
|
+
OPERATORS[operator]
|
|
26
|
+
end
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
end
|
|
33
|
-
end
|
|
28
|
+
def self.supported?(operator)
|
|
29
|
+
OPERATORS.key?(operator)
|
|
34
30
|
end
|
|
35
31
|
end
|