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,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
# @validator — validate arguments with a contract class before the field
|
|
6
|
+
# resolves, as PHP Lighthouse's validator classes do.
|
|
7
|
+
#
|
|
8
|
+
# type Mutation { updateUser(id: ID!, name: String): User @update @validator }
|
|
9
|
+
# input CreatePostInput @validator { title: String }
|
|
10
|
+
#
|
|
11
|
+
# On a field, the contract gets all the field's arguments; the class
|
|
12
|
+
# defaults to Validators::<ParentType>::<FieldName>Validator. On an input
|
|
13
|
+
# type it gets that input's values wherever the type is used — nested,
|
|
14
|
+
# and per item of a list — defaulting to Validators::<InputType>Validator.
|
|
15
|
+
# Classes are looked up in Config.namespaces[:validators].
|
|
16
|
+
#
|
|
17
|
+
# A contract is a Dry::Validation::Contract (values arrive with
|
|
18
|
+
# snake_case symbol keys; declare `option :graphql_context` to receive the
|
|
19
|
+
# GraphQL context), or any object whose `call(values)` returns such a
|
|
20
|
+
# result or a { key => [messages] } Hash. Failures raise one
|
|
21
|
+
# Lighthouse::ValidationError keyed by GraphQL argument path.
|
|
22
|
+
#
|
|
23
|
+
# Model validations need no directive: @create/@update report them the
|
|
24
|
+
# same way. Use @validator for rules that are not the model's — checks
|
|
25
|
+
# across arguments, arguments of a query, or input that never reaches a model.
|
|
26
|
+
class Lighthouse::GraphQL::DirectiveResolvers::ValidatorDirectiveResolver < Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
27
|
+
def self.directive_name = 'validator'
|
|
28
|
+
def self.definition = 'directive @validator(class: String) on FIELD_DEFINITION | INPUT_OBJECT'
|
|
29
|
+
|
|
30
|
+
# A default name, e.g. Mutation::SignUpValidator: always looked up under
|
|
31
|
+
# the validators namespaces, although it contains `::`.
|
|
32
|
+
Conventional = Struct.new(:name) do
|
|
33
|
+
def to_s = name
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# One validation of a field: the GraphQL context the contracts receive,
|
|
37
|
+
# and the messages collected so far, keyed by argument path.
|
|
38
|
+
Pass = Struct.new(:ctx, :messages)
|
|
39
|
+
|
|
40
|
+
# Does any input type the field takes (at any depth) carry @validator?
|
|
41
|
+
def self.input_validators?(field_def, seen = Set.new)
|
|
42
|
+
field_def.arguments.each_value.any? do |argument|
|
|
43
|
+
type = argument.type.unwrap
|
|
44
|
+
next false unless type.kind.input_object? && seen.add?(type.graphql_name)
|
|
45
|
+
|
|
46
|
+
validator_directive(type) || input_validators?(type, seen)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.validator_directive(type)
|
|
51
|
+
type.ast_node&.directives&.find { |d| d.name == 'validator' }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def define_resolver(target_type_class)
|
|
55
|
+
me = self
|
|
56
|
+
field_contract =
|
|
57
|
+
(directive_arg('class') || Conventional.new("#{@type_def.graphql_name}::#{@field_def.name.to_s.camelize}Validator") if @directive_node)
|
|
58
|
+
inner = capture_inner(target_type_class)
|
|
59
|
+
|
|
60
|
+
install(target_type_class, lambda do |obj, args, ctx|
|
|
61
|
+
me.send(:validate!, field_contract, args, ctx)
|
|
62
|
+
inner.call(obj, args, ctx)
|
|
63
|
+
end)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
protected
|
|
67
|
+
|
|
68
|
+
def validate!(field_contract, args, ctx)
|
|
69
|
+
values = Lighthouse::Support::Arguments.plain(args)
|
|
70
|
+
pass = Pass.new(ctx, {})
|
|
71
|
+
run(field_contract, values, [], @field_def.arguments, pass) if field_contract
|
|
72
|
+
validate_inputs(@field_def.arguments, values, [], pass)
|
|
73
|
+
raise Lighthouse::ValidationError.new(@field_def.name, pass.messages) unless pass.messages.empty?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def validate_inputs(argument_defs, values, prefix, pass)
|
|
77
|
+
by_keyword = Lighthouse::Support::Arguments.keyword_index(argument_defs)
|
|
78
|
+
values.each do |key, value|
|
|
79
|
+
argument = by_keyword[key.to_sym]
|
|
80
|
+
next if argument.nil? || value.nil?
|
|
81
|
+
|
|
82
|
+
type = argument.type.unwrap
|
|
83
|
+
next unless type.kind.input_object?
|
|
84
|
+
|
|
85
|
+
input_items(value, prefix + [argument.graphql_name]).each do |item, item_path|
|
|
86
|
+
validate_input(type, item, item_path, pass)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# One input object value: its own @validator contract, then its nested inputs.
|
|
92
|
+
def validate_input(type, values, path, pass)
|
|
93
|
+
directive = self.class.validator_directive(type)
|
|
94
|
+
if directive
|
|
95
|
+
name = Lighthouse::Support::DirectiveArgs.literal(directive, 'class') || Conventional.new("#{type.graphql_name}Validator")
|
|
96
|
+
run(name, values, path, type.arguments, pass)
|
|
97
|
+
end
|
|
98
|
+
validate_inputs(type.arguments, values, path, pass)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def run(class_name, values, prefix, argument_defs, pass)
|
|
102
|
+
contract = instantiate(validator_class(class_name), pass.ctx)
|
|
103
|
+
errors = error_hash(contract.call(values))
|
|
104
|
+
|
|
105
|
+
flatten(errors).each do |segments, list|
|
|
106
|
+
path = (prefix + graphql_path(segments, argument_defs)).join('.')
|
|
107
|
+
(pass.messages[path.empty? ? @field_def.name : path] ||= []).concat(list)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
# A list value validates per item, its path extended by the index.
|
|
114
|
+
def input_items(value, path)
|
|
115
|
+
items = Lighthouse::Support::Arguments.list(value)
|
|
116
|
+
return [[value, path]] unless items
|
|
117
|
+
|
|
118
|
+
items.each_with_index.map { |item, i| [item, path + [i.to_s]] }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# A contract answers a { key => [messages] } Hash, or a Dry::Validation
|
|
122
|
+
# result whose message set converts to one.
|
|
123
|
+
def error_hash(result)
|
|
124
|
+
return result if result.is_a?(Hash)
|
|
125
|
+
return {} if result.respond_to?(:success?) && result.success?
|
|
126
|
+
|
|
127
|
+
message_set = result.errors
|
|
128
|
+
message_set.to_h
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def validator_class(name)
|
|
132
|
+
namespaces = Lighthouse::Config.namespaces[:validators]
|
|
133
|
+
found =
|
|
134
|
+
if name.is_a?(Conventional)
|
|
135
|
+
namespaces.lazy.filter_map { |ns| Lighthouse::Support::ModelResolver.constantize("#{ns}::#{name}") }.first
|
|
136
|
+
else
|
|
137
|
+
Lighthouse::Support::ModelResolver.constantize(name, namespaces: namespaces)
|
|
138
|
+
end
|
|
139
|
+
found ||
|
|
140
|
+
raise(Lighthouse::DefinitionError,
|
|
141
|
+
"@validator on #{@type_def.graphql_name}.#{@field_def.name}: no validator class #{name} " \
|
|
142
|
+
"in #{Lighthouse::Config.namespaces[:validators].join(', ')}")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def instantiate(klass, ctx)
|
|
146
|
+
return klass unless klass.is_a?(Class)
|
|
147
|
+
return klass.new(graphql_context: ctx) if dry_option?(klass, :graphql_context)
|
|
148
|
+
|
|
149
|
+
klass.new
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def dry_option?(klass, name)
|
|
153
|
+
klass.respond_to?(:dry_initializer) && klass.dry_initializer.options.any? { |option| option.target == name }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Dry's nested errors hash as [[key, ...], [messages]] pairs; base errors
|
|
157
|
+
# use a nil key.
|
|
158
|
+
def flatten(errors, prefix = [])
|
|
159
|
+
errors.flat_map do |key, value|
|
|
160
|
+
path = key.nil? ? prefix : prefix + [key]
|
|
161
|
+
case value
|
|
162
|
+
when Hash then flatten(value, path)
|
|
163
|
+
when Array then flatten_messages(value, path)
|
|
164
|
+
else [[path, [value.to_s]]]
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# A key with both its own messages and nested ones comes as
|
|
170
|
+
# [messages, { nested }].
|
|
171
|
+
def flatten_messages(value, path)
|
|
172
|
+
if value.length == 2 && value.first.is_a?(Array) && value.last.is_a?(Hash)
|
|
173
|
+
[[path, value.first]] + flatten(value.last, path)
|
|
174
|
+
else
|
|
175
|
+
[[path, value.map(&:to_s)]]
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Map snake_case contract keys back to the GraphQL argument names.
|
|
180
|
+
def graphql_path(segments, argument_defs)
|
|
181
|
+
defs = argument_defs
|
|
182
|
+
segments.map do |segment|
|
|
183
|
+
next segment.to_s if segment.is_a?(Integer)
|
|
184
|
+
|
|
185
|
+
argument = Lighthouse::Support::Arguments.keyword_index(defs)[segment.to_sym]
|
|
186
|
+
next segment.to_s.camelize(:lower) unless argument
|
|
187
|
+
|
|
188
|
+
type = argument.type.unwrap
|
|
189
|
+
defs = type.kind.input_object? ? type.arguments : {}
|
|
190
|
+
argument.graphql_name
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -1,168 +1,169 @@
|
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
3
|
+
class Lighthouse::GraphQL::DirectiveResolvers::WhereConditionsApplier
|
|
4
|
+
# Operator semantics live in Lighthouse::WhereConditions::OperatorMap so
|
|
5
|
+
# they can be shared with the simple argument-filter directives.
|
|
6
|
+
SUPPORTED_OPERATORS = Lighthouse::WhereConditions::OperatorMap::OPERATORS
|
|
7
|
+
# graphql-ruby hands input objects over with Ruby-style keys, so the
|
|
8
|
+
# generated input's AND/OR/HAS fields arrive as :and/:or/:has.
|
|
9
|
+
LOGICAL_KEYS = { and: :AND, or: :OR, has: :HAS }.freeze
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def apply(relation, value)
|
|
13
|
+
return relation unless relation.respond_to?(:where)
|
|
14
|
+
|
|
15
|
+
condition = normalize_condition(value)
|
|
16
|
+
return relation unless condition
|
|
17
|
+
|
|
18
|
+
# AND narrows the incoming scope step by step; each OR branch starts
|
|
19
|
+
# from the incoming scope, and their union replaces it.
|
|
20
|
+
result = apply_all(relation, condition[:AND])
|
|
21
|
+
result = apply_any(relation, condition[:OR]) || result
|
|
22
|
+
result = apply_has_condition(result, condition[:HAS])
|
|
23
|
+
result = apply_single_condition(result, condition) if condition.key?(:column)
|
|
24
|
+
result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def apply_has(relation, has_cond)
|
|
28
|
+
relation_name = has_cond[:relation].to_s
|
|
29
|
+
return relation if relation_name.blank?
|
|
30
|
+
|
|
31
|
+
resolved_relation_name = resolve_relation_name(relation, relation_name)
|
|
32
|
+
return relation if resolved_relation_name.blank?
|
|
33
|
+
|
|
34
|
+
reflection = reflection_for(relation, resolved_relation_name)
|
|
35
|
+
return relation unless reflection&.klass
|
|
36
|
+
|
|
37
|
+
joined = relation.joins(resolved_relation_name.to_sym)
|
|
38
|
+
return joined.distinct if has_cond[:condition].blank?
|
|
39
|
+
|
|
40
|
+
# Use an EXISTS-style filter to safely apply conditions from the related model.
|
|
41
|
+
child_scope = apply(reflection.klass.all, has_cond[:condition])
|
|
42
|
+
joined.where(reflection.klass.table_name => { id: child_scope.select(:id) }).distinct
|
|
43
|
+
rescue StandardError
|
|
44
|
+
relation
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def resolve_relation_name(relation, relation_name)
|
|
48
|
+
candidate_names = [relation_name.to_s, relation_name.to_s.singularize, relation_name.to_s.pluralize].uniq
|
|
49
|
+
|
|
50
|
+
candidate_names.each do |candidate|
|
|
51
|
+
next if candidate.blank?
|
|
52
|
+
return candidate if association_exists?(relation, candidate)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
nil
|
|
56
|
+
rescue StandardError
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def association_exists?(relation, name)
|
|
61
|
+
reflection_for(relation, name).present?
|
|
62
|
+
rescue StandardError
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def apply_has_argument(relation, arg_name, arg_value, relation_name = nil)
|
|
67
|
+
relation_name ||= derive_relation_name(arg_name)
|
|
68
|
+
return relation unless relation_name.present? && arg_value.present?
|
|
69
|
+
|
|
70
|
+
condition = arg_value.is_a?(Hash) ? arg_value : symbolized(arg_value)
|
|
71
|
+
has_payload = { relation: relation_name, condition: condition }
|
|
72
|
+
apply_has(relation, has_payload)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# The condition as a symbol-keyed Hash, or nil when there is none.
|
|
78
|
+
def normalize_condition(value)
|
|
79
|
+
return nil if value.nil?
|
|
80
|
+
|
|
81
|
+
condition = value.is_a?(Hash) ? value.deep_symbolize_keys : symbolized(value)
|
|
82
|
+
condition.transform_keys { |key| LOGICAL_KEYS.fetch(key, key) } if condition.is_a?(Hash)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def apply_all(relation, conditions)
|
|
86
|
+
Array(conditions).reduce(relation) { |acc, sub| apply(acc, sub) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The union of the branches, or nil when there are none.
|
|
90
|
+
def apply_any(relation, conditions)
|
|
91
|
+
Array(conditions).map { |sub| apply(relation, sub) }.reduce { |acc, sub_scope| acc.or(sub_scope) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def apply_has_condition(relation, has_cond)
|
|
95
|
+
return relation unless has_cond.is_a?(Hash) && has_cond[:relation]
|
|
96
|
+
|
|
97
|
+
apply_has(relation, has_cond)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def reflection_for(relation, name)
|
|
101
|
+
klass = relation.respond_to?(:klass) ? relation.klass : relation
|
|
102
|
+
klass.reflect_on_association(name.to_sym) if klass.respond_to?(:reflect_on_association)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def derive_relation_name(arg_name)
|
|
106
|
+
Lighthouse::Support::Naming.derive_relation_name(arg_name)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def apply_single_condition(relation, condition)
|
|
110
|
+
return relation if condition[:column].blank? || !relation.respond_to?(:where)
|
|
111
|
+
|
|
112
|
+
column_name = resolve_column_name(condition[:column])
|
|
113
|
+
return relation if column_name.blank?
|
|
114
|
+
|
|
115
|
+
apply_operator(relation, column_name, operator_of(condition), condition[:value])
|
|
116
|
+
rescue StandardError
|
|
117
|
+
relation
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def operator_of(condition)
|
|
121
|
+
condition[:operator]&.to_s&.upcase || 'EQ'
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Null and set operators are plain hash conditions; the rest need Arel.
|
|
125
|
+
def apply_operator(relation, column_name, operator, value)
|
|
126
|
+
case operator
|
|
127
|
+
when 'IS_NULL' then relation.where(column_name => nil)
|
|
128
|
+
when 'IS_NOT_NULL' then relation.where.not(column_name => nil)
|
|
129
|
+
when 'IN' then relation.where(column_name => Array(value))
|
|
130
|
+
when 'NIN' then relation.where.not(column_name => Array(value))
|
|
131
|
+
else apply_arel_operator(relation, column_name, operator, value)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# An unknown operator falls back to equality rather than failing the query.
|
|
136
|
+
def apply_arel_operator(relation, column_name, operator, value)
|
|
137
|
+
pattern = like_pattern(operator, value)
|
|
138
|
+
return relation.where(arel_table(relation)[column_name].matches(pattern)) if pattern
|
|
139
|
+
|
|
140
|
+
predicate = SUPPORTED_OPERATORS[operator]
|
|
141
|
+
return relation.where(column_name => value) unless predicate
|
|
142
|
+
|
|
143
|
+
relation.where(arel_table(relation)[column_name].public_send(predicate, value))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def like_pattern(operator, value)
|
|
147
|
+
case operator
|
|
148
|
+
when 'CONTAINS' then "%#{value}%"
|
|
149
|
+
when 'STARTS_WITH' then "#{value}%"
|
|
150
|
+
when 'ENDS_WITH' then "%#{value}"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def resolve_column_name(column)
|
|
155
|
+
Lighthouse::Support::Naming.column(column)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def arel_table(relation)
|
|
159
|
+
relation.klass.arel_table
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def symbolized(obj)
|
|
163
|
+
if obj.respond_to?(:to_h)
|
|
164
|
+
obj.to_h.deep_symbolize_keys
|
|
165
|
+
else
|
|
166
|
+
{}
|
|
166
167
|
end
|
|
167
168
|
end
|
|
168
169
|
end
|
|
@@ -1,40 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
# directive AST node, and composes a constraint onto an ActiveRecord
|
|
10
|
-
# relation given the client-supplied value for that argument.
|
|
11
|
-
class Base
|
|
12
|
-
include Lighthouse::Contracts::ArgBuilder
|
|
3
|
+
# Base class for argument-builder directives (@eq, @neq, @in, @notIn,
|
|
4
|
+
# @like, @where, @orderBy). Each receives the argument AST node and the
|
|
5
|
+
# directive AST node, and composes a constraint onto an ActiveRecord
|
|
6
|
+
# relation given the client-supplied value for that argument.
|
|
7
|
+
class Lighthouse::GraphQL::Directives::Arguments::Base
|
|
8
|
+
include Lighthouse::Contracts::ArgBuilder
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
def initialize(arg_node, directive_node)
|
|
11
|
+
@arg_node = arg_node
|
|
12
|
+
@directive_node = directive_node
|
|
13
|
+
end
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
# @return relation
|
|
16
|
+
def handle_builder(_relation, _value)
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
end
|
|
23
19
|
|
|
24
|
-
|
|
20
|
+
private
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
# Column to filter on: the directive's `key:` argument if present,
|
|
23
|
+
# otherwise the snake_case form of the GraphQL argument name.
|
|
24
|
+
def column
|
|
25
|
+
Lighthouse::Support::DirectiveArgs.literal(@directive_node, 'key') ||
|
|
26
|
+
Lighthouse::Support::Naming.attribute(@arg_node.name)
|
|
27
|
+
end
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
29
|
+
def arel_column(relation)
|
|
30
|
+
relation.klass.arel_table[column]
|
|
39
31
|
end
|
|
40
32
|
end
|