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
|
@@ -2,207 +2,243 @@
|
|
|
2
2
|
|
|
3
3
|
require 'graphql'
|
|
4
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
|
-
generated << build_column_enum(column_enum_name, columns)
|
|
68
|
-
existing_defs << column_enum_name
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Append base utility types only once
|
|
75
|
-
unless existing_defs.include?(SQLOPERATOR_TYPE_NAME)
|
|
76
|
-
generated << build_sql_operator_enum
|
|
77
|
-
existing_defs << SQLOPERATOR_TYPE_NAME
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
unless existing_defs.include?(MIXED_SCALAR_NAME)
|
|
81
|
-
generated << "scalar #{MIXED_SCALAR_NAME}"
|
|
82
|
-
existing_defs << MIXED_SCALAR_NAME
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Some SDL files use `_` as a placeholder input type, even when no
|
|
86
|
-
# whereConditions directive is attached (e.g. WIP schema pieces). If we
|
|
87
|
-
# don't define it, GraphQL-Ruby raises UnresolvedLateBoundTypeError.
|
|
88
|
-
if contains_placeholder_type?(ast) && !existing_defs.include?(PLACEHOLDER_SCALAR_NAME)
|
|
89
|
-
generated << "scalar #{PLACEHOLDER_SCALAR_NAME}"
|
|
90
|
-
existing_defs << PLACEHOLDER_SCALAR_NAME
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
return sdl if generated.empty?
|
|
94
|
-
|
|
95
|
-
modified_sdl = ::GraphQL::Language::Printer.new.print(ast)
|
|
96
|
-
modified_sdl + "\n\n" + generated.join("\n\n")
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def self.apply_arg_type_replacement(arg, input_type_name)
|
|
100
|
-
case arg.type
|
|
101
|
-
when ::GraphQL::Language::Nodes::TypeName
|
|
102
|
-
if arg.type.name == '_'
|
|
103
|
-
new_type = ::GraphQL::Language::Nodes::TypeName.new(name: input_type_name)
|
|
104
|
-
arg.instance_variable_set(:@type, new_type)
|
|
105
|
-
end
|
|
106
|
-
when ::GraphQL::Language::Nodes::NonNullType
|
|
107
|
-
inner = arg.type.of_type
|
|
108
|
-
if inner.is_a?(::GraphQL::Language::Nodes::TypeName) && inner.name == '_'
|
|
109
|
-
arg.instance_variable_set(:@type, ::GraphQL::Language::Nodes::NonNullType.new(of_type: ::GraphQL::Language::Nodes::TypeName.new(name: input_type_name)))
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def self.build_where_condition_input(type_name, column_enum_name, relation_type_name, directive)
|
|
115
|
-
column_type = directive_argument_value(directive, 'columnsEnum') || column_enum_name
|
|
116
|
-
|
|
117
|
-
fields = []
|
|
118
|
-
fields << " column: #{column_type}"
|
|
119
|
-
fields << " operator: #{SQLOPERATOR_TYPE_NAME} = EQ"
|
|
120
|
-
fields << " value: #{MIXED_SCALAR_NAME}"
|
|
121
|
-
fields << " AND: [#{type_name}!]"
|
|
122
|
-
fields << " OR: [#{type_name}!]"
|
|
123
|
-
fields << " HAS: #{relation_type_name}" if directive.name == 'whereConditions'
|
|
124
|
-
|
|
125
|
-
<<~TEXT.strip
|
|
126
|
-
input #{type_name} {
|
|
127
|
-
#{fields.join("\n")}
|
|
128
|
-
}
|
|
129
|
-
TEXT
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def self.build_where_condition_relation_input(type_name, condition_type_name)
|
|
133
|
-
<<~TEXT.strip
|
|
134
|
-
input #{type_name} {
|
|
135
|
-
relation: String!
|
|
136
|
-
operator: #{SQLOPERATOR_TYPE_NAME} = GTE
|
|
137
|
-
amount: Int = 1
|
|
138
|
-
condition: #{condition_type_name}
|
|
139
|
-
}
|
|
140
|
-
TEXT
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def self.build_column_enum(name, columns)
|
|
144
|
-
values = Array(columns).map do |column|
|
|
145
|
-
value = column.to_s
|
|
146
|
-
key = value.gsub(/[^0-9A-Za-z]/, '_').upcase
|
|
147
|
-
" #{key}"
|
|
148
|
-
end.compact.uniq
|
|
149
|
-
|
|
150
|
-
<<~TEXT.strip
|
|
151
|
-
enum #{name} {
|
|
152
|
-
#{values.join("\n")}
|
|
153
|
-
}
|
|
154
|
-
TEXT
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def self.build_sql_operator_enum
|
|
158
|
-
values = SUPPORTED_OPERATORS.map { |op| " #{op}" }
|
|
159
|
-
<<~TEXT.strip
|
|
160
|
-
enum #{SQLOPERATOR_TYPE_NAME} {
|
|
161
|
-
#{values.join("\n")}
|
|
162
|
-
}
|
|
163
|
-
TEXT
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def self.directive_argument_value(directive, name)
|
|
167
|
-
arg = directive.arguments&.find { |a| a.name == name }
|
|
168
|
-
return nil unless arg
|
|
169
|
-
|
|
170
|
-
# Supports both Babel-style AST and raw Ruby value.
|
|
171
|
-
value = arg.value.respond_to?(:value) ? arg.value.value : arg.value
|
|
172
|
-
|
|
173
|
-
if name == 'columns' && value.is_a?(Array)
|
|
174
|
-
value.map { |it| it.respond_to?(:value) ? it.value : it }
|
|
175
|
-
else
|
|
176
|
-
value
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def self.contains_placeholder_type?(document)
|
|
181
|
-
document.definitions.any? do |defn|
|
|
182
|
-
next false unless defn.respond_to?(:fields) && defn.fields.respond_to?(:any?)
|
|
183
|
-
(defn.fields || []).any? do |field|
|
|
184
|
-
(field.respond_to?(:arguments) ? field.arguments || [] : []).any? { |arg| type_references_placeholder?(arg.type) }
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
rescue StandardError
|
|
188
|
-
false
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
def self.type_references_placeholder?(type_node)
|
|
192
|
-
case type_node
|
|
193
|
-
when ::GraphQL::Language::Nodes::TypeName
|
|
194
|
-
type_node.name == PLACEHOLDER_SCALAR_NAME
|
|
195
|
-
when ::GraphQL::Language::Nodes::NonNullType, ::GraphQL::Language::Nodes::ListType
|
|
196
|
-
type_references_placeholder?(type_node.of_type)
|
|
197
|
-
else
|
|
198
|
-
false
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
def self.camelize(value)
|
|
203
|
-
value.to_s.split(/[_\s]+/).map { |part| part.slice(0,1).upcase + part.slice(1..).to_s }.join
|
|
204
|
-
end
|
|
5
|
+
class Lighthouse::GraphQL::Directives::WhereConditionsDirective
|
|
6
|
+
SQLOPERATOR_TYPE_NAME = 'SQLOperator'
|
|
7
|
+
MIXED_SCALAR_NAME = 'Mixed'
|
|
8
|
+
PLACEHOLDER_SCALAR_NAME = '_'
|
|
9
|
+
DEFINITIONS = {
|
|
10
|
+
'whereConditions' =>
|
|
11
|
+
'directive @whereConditions(columns: [String!], columnsEnum: String) on ARGUMENT_DEFINITION',
|
|
12
|
+
'whereHasConditions' =>
|
|
13
|
+
'directive @whereHasConditions(relation: String, columns: [String!], columnsEnum: String) on ARGUMENT_DEFINITION'
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
# Default description of an undocumented argument, per directive.
|
|
17
|
+
ARGUMENT_DESCRIPTIONS = {
|
|
18
|
+
'whereConditions' => 'Filter results by field conditions.',
|
|
19
|
+
'whereHasConditions' => 'Filter results by related records conditions.'
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# Types whose field arguments may carry the directives.
|
|
23
|
+
OBJECT_DEFINITIONS = [
|
|
24
|
+
::GraphQL::Language::Nodes::ObjectTypeDefinition,
|
|
25
|
+
::GraphQL::Language::Nodes::InterfaceTypeDefinition
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
# Supported operators in the generated GraphQL input type performance.
|
|
29
|
+
SUPPORTED_OPERATORS = %w[EQ NEQ GT GTE LT LTE IN NIN CONTAINS STARTS_WITH ENDS_WITH IS_NULL IS_NOT_NULL].freeze
|
|
30
|
+
|
|
31
|
+
# `definitions` maps every type name to its generated SDL. Names the schema
|
|
32
|
+
# already declares are seeded with nil, so they are never generated again
|
|
33
|
+
# and drop out of the output.
|
|
34
|
+
def self.manipulate(sdl)
|
|
35
|
+
return sdl unless sdl.is_a?(String) && !sdl.empty?
|
|
36
|
+
|
|
37
|
+
ast = parse(sdl)
|
|
38
|
+
return sdl unless ast
|
|
39
|
+
|
|
40
|
+
definitions = ast.definitions.filter_map { |defn| defn.respond_to?(:name) ? defn.name : nil }.index_with(nil)
|
|
41
|
+
each_where_argument(ast) do |type_def, field, arg, directive|
|
|
42
|
+
expand_argument(type_def, field, arg, directive, definitions)
|
|
43
|
+
end
|
|
44
|
+
define_utility_types(ast, definitions)
|
|
45
|
+
|
|
46
|
+
generated = definitions.values.compact
|
|
47
|
+
return sdl if generated.empty?
|
|
48
|
+
|
|
49
|
+
modified_sdl = ::GraphQL::Language::Printer.new.print(ast)
|
|
50
|
+
"#{modified_sdl}\n\n#{generated.join("\n\n")}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.parse(sdl)
|
|
54
|
+
::GraphQL::Language::Parser.parse(sdl)
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
warn("Lighthouse::GraphQL::Directives::WhereConditionsDirective: parse failed: #{e.class}: #{e.message}")
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Yields every object/interface field argument carrying @whereConditions or
|
|
61
|
+
# @whereHasConditions, along with that directive.
|
|
62
|
+
def self.each_where_argument(ast)
|
|
63
|
+
each_field(ast) do |type_def, field|
|
|
64
|
+
(field.arguments || []).each do |arg|
|
|
65
|
+
directive = arg.directives&.find { |d| DEFINITIONS.key?(d.name) }
|
|
66
|
+
yield type_def, field, arg, directive if directive
|
|
205
67
|
end
|
|
206
68
|
end
|
|
207
69
|
end
|
|
70
|
+
|
|
71
|
+
def self.each_field(ast)
|
|
72
|
+
ast.definitions.each do |type_def|
|
|
73
|
+
next unless OBJECT_DEFINITIONS.any? { |kind| type_def.is_a?(kind) }
|
|
74
|
+
|
|
75
|
+
(type_def.fields || []).each { |field| yield type_def, field }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Points the argument at its generated input type and defines the types it
|
|
80
|
+
# needs: the conditions input, its HAS relation input and the column enum.
|
|
81
|
+
def self.expand_argument(type_def, field, arg, directive, definitions)
|
|
82
|
+
describe_argument(arg, directive)
|
|
83
|
+
|
|
84
|
+
prefix = "#{camelize(type_def.name)}#{camelize(field.name)}#{camelize(arg.name)}"
|
|
85
|
+
input_type_name = "#{prefix}WhereConditions"
|
|
86
|
+
column_enum_name = "#{prefix}Column"
|
|
87
|
+
relation_input_name = "#{input_type_name}Relation"
|
|
88
|
+
|
|
89
|
+
apply_arg_type_replacement(arg, input_type_name)
|
|
90
|
+
|
|
91
|
+
# Build input type if needed.
|
|
92
|
+
define_once(definitions, input_type_name) do
|
|
93
|
+
build_where_condition_input(input_type_name, column_enum_name, relation_input_name, directive)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Build relation type for HAS support in whereConditions
|
|
97
|
+
if directive.name == 'whereConditions'
|
|
98
|
+
define_once(definitions, relation_input_name) { build_where_condition_relation_input(relation_input_name, input_type_name) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
define_column_enum(definitions, column_enum_name, directive)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.describe_argument(arg, directive)
|
|
105
|
+
return if arg.description.present?
|
|
106
|
+
|
|
107
|
+
arg.instance_variable_set(:@description, ARGUMENT_DESCRIPTIONS.fetch(directive.name))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Build column enum type unless columnsEnum is provided.
|
|
111
|
+
def self.define_column_enum(definitions, column_enum_name, directive)
|
|
112
|
+
return unless directive_argument_value(directive, 'columnsEnum').nil?
|
|
113
|
+
|
|
114
|
+
columns = directive_argument_value(directive, 'columns')
|
|
115
|
+
return if Array(columns).empty?
|
|
116
|
+
|
|
117
|
+
define_once(definitions, column_enum_name) { build_column_enum(column_enum_name, columns) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def self.define_utility_types(ast, definitions)
|
|
121
|
+
# Append base utility types only once
|
|
122
|
+
define_once(definitions, SQLOPERATOR_TYPE_NAME) { build_sql_operator_enum }
|
|
123
|
+
define_once(definitions, MIXED_SCALAR_NAME) { "scalar #{MIXED_SCALAR_NAME}" }
|
|
124
|
+
|
|
125
|
+
# Some SDL files use `_` as a placeholder input type, even when no
|
|
126
|
+
# whereConditions directive is attached (e.g. WIP schema pieces). If we
|
|
127
|
+
# don't define it, GraphQL-Ruby raises UnresolvedLateBoundTypeError.
|
|
128
|
+
return unless contains_placeholder_type?(ast)
|
|
129
|
+
|
|
130
|
+
define_once(definitions, PLACEHOLDER_SCALAR_NAME) { "scalar #{PLACEHOLDER_SCALAR_NAME}" }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.define_once(definitions, name)
|
|
134
|
+
definitions[name] = yield unless definitions.key?(name)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def self.apply_arg_type_replacement(arg, input_type_name)
|
|
138
|
+
case arg.type
|
|
139
|
+
when ::GraphQL::Language::Nodes::TypeName
|
|
140
|
+
arg.instance_variable_set(:@type, ::GraphQL::Language::Nodes::TypeName.new(name: input_type_name)) if arg.type.name == '_'
|
|
141
|
+
when ::GraphQL::Language::Nodes::NonNullType
|
|
142
|
+
inner = arg.type.of_type
|
|
143
|
+
return unless inner.is_a?(::GraphQL::Language::Nodes::TypeName) && inner.name == '_'
|
|
144
|
+
|
|
145
|
+
new_type = ::GraphQL::Language::Nodes::NonNullType.new(of_type: ::GraphQL::Language::Nodes::TypeName.new(name: input_type_name))
|
|
146
|
+
arg.instance_variable_set(:@type, new_type)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.build_where_condition_input(type_name, column_enum_name, relation_type_name, directive)
|
|
151
|
+
column_type = directive_argument_value(directive, 'columnsEnum') || column_enum_name
|
|
152
|
+
|
|
153
|
+
fields = []
|
|
154
|
+
fields << " column: #{column_type}"
|
|
155
|
+
fields << " operator: #{SQLOPERATOR_TYPE_NAME} = EQ"
|
|
156
|
+
fields << " value: #{MIXED_SCALAR_NAME}"
|
|
157
|
+
fields << " AND: [#{type_name}!]"
|
|
158
|
+
fields << " OR: [#{type_name}!]"
|
|
159
|
+
fields << " HAS: #{relation_type_name}" if directive.name == 'whereConditions'
|
|
160
|
+
|
|
161
|
+
<<~TEXT.strip
|
|
162
|
+
input #{type_name} {
|
|
163
|
+
#{fields.join("\n")}
|
|
164
|
+
}
|
|
165
|
+
TEXT
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def self.build_where_condition_relation_input(type_name, condition_type_name)
|
|
169
|
+
<<~TEXT.strip
|
|
170
|
+
input #{type_name} {
|
|
171
|
+
relation: String!
|
|
172
|
+
operator: #{SQLOPERATOR_TYPE_NAME} = GTE
|
|
173
|
+
amount: Int = 1
|
|
174
|
+
condition: #{condition_type_name}
|
|
175
|
+
}
|
|
176
|
+
TEXT
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def self.build_column_enum(name, columns)
|
|
180
|
+
values = Array(columns).filter_map do |column|
|
|
181
|
+
value = column.to_s
|
|
182
|
+
key = value.gsub(/[^0-9A-Za-z]/, '_').upcase
|
|
183
|
+
" #{key}"
|
|
184
|
+
end.uniq
|
|
185
|
+
|
|
186
|
+
<<~TEXT.strip
|
|
187
|
+
enum #{name} {
|
|
188
|
+
#{values.join("\n")}
|
|
189
|
+
}
|
|
190
|
+
TEXT
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def self.build_sql_operator_enum
|
|
194
|
+
values = SUPPORTED_OPERATORS.map { |op| " #{op}" }
|
|
195
|
+
<<~TEXT.strip
|
|
196
|
+
enum #{SQLOPERATOR_TYPE_NAME} {
|
|
197
|
+
#{values.join("\n")}
|
|
198
|
+
}
|
|
199
|
+
TEXT
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def self.directive_argument_value(directive, name)
|
|
203
|
+
arg = directive.arguments&.find { |a| a.name == name }
|
|
204
|
+
return nil unless arg
|
|
205
|
+
|
|
206
|
+
value = unwrap_value(arg.value)
|
|
207
|
+
name == 'columns' && value.is_a?(Array) ? value.map { |item| unwrap_value(item) } : value
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Supports both Babel-style AST and raw Ruby value.
|
|
211
|
+
def self.unwrap_value(node)
|
|
212
|
+
node.respond_to?(:value) ? node.value : node
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def self.contains_placeholder_type?(document)
|
|
216
|
+
document.definitions.any? do |defn|
|
|
217
|
+
next false unless defn.respond_to?(:fields) && defn.fields.respond_to?(:any?)
|
|
218
|
+
|
|
219
|
+
(defn.fields || []).any? { |field| placeholder_argument?(field) }
|
|
220
|
+
end
|
|
221
|
+
rescue StandardError
|
|
222
|
+
false
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def self.placeholder_argument?(field)
|
|
226
|
+
arguments = (field.respond_to?(:arguments) && field.arguments) || []
|
|
227
|
+
arguments.any? { |arg| type_references_placeholder?(arg.type) }
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def self.type_references_placeholder?(type_node)
|
|
231
|
+
case type_node
|
|
232
|
+
when ::GraphQL::Language::Nodes::TypeName
|
|
233
|
+
type_node.name == PLACEHOLDER_SCALAR_NAME
|
|
234
|
+
when ::GraphQL::Language::Nodes::NonNullType, ::GraphQL::Language::Nodes::ListType
|
|
235
|
+
type_references_placeholder?(type_node.of_type)
|
|
236
|
+
else
|
|
237
|
+
false
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def self.camelize(value)
|
|
242
|
+
value.to_s.split(/[_\s]+/).map { |part| part.slice(0, 1).upcase + part.slice(1..).to_s }.join
|
|
243
|
+
end
|
|
208
244
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lighthouse
|
|
4
|
+
# The schema refers to something that does not exist or breaks a contract —
|
|
5
|
+
# e.g. a Mutations:: class that is not a GraphQL::Schema::Mutation, or a
|
|
6
|
+
# missing @validator class. A server bug, so not a client-facing error.
|
|
7
|
+
class DefinitionError < StandardError; end
|
|
8
|
+
|
|
9
|
+
# Raised by the @can* family when the policy denies the ability.
|
|
10
|
+
class AuthorizationError < ::GraphQL::ExecutionError
|
|
11
|
+
def initialize(message = 'This action is unauthorized.', **options)
|
|
12
|
+
super
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Invalid arguments — from a @validator contract or a model's validations.
|
|
17
|
+
# Mirrors PHP Lighthouse's response: the messages are keyed by argument path
|
|
18
|
+
# under `extensions.validation`, e.g. { "input.title" => ["Title can't be blank"] }.
|
|
19
|
+
class ValidationError < ::GraphQL::ExecutionError
|
|
20
|
+
attr_reader :messages
|
|
21
|
+
|
|
22
|
+
def initialize(field_name, messages)
|
|
23
|
+
@messages = messages
|
|
24
|
+
super("Validation failed for the field [#{field_name}].", extensions: { 'validation' => messages })
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Object-level Apollo Federation directives (@key, @extends, @shareable,
|
|
4
|
+
# @inaccessible, @interfaceObject) applied to the types built from SDL, so the
|
|
5
|
+
# Apollo Federation helper can detect entities and add _entities. Only types
|
|
6
|
+
# that know federation (they answer `key`) are touched.
|
|
7
|
+
module Lighthouse::GraphQL::FederationDirectives
|
|
8
|
+
# Directive => the type's federation method it calls. Each is called only
|
|
9
|
+
# when the type has it, so a partial federation setup still builds.
|
|
10
|
+
FLAGS = {
|
|
11
|
+
'extends' => :extend_type,
|
|
12
|
+
'shareable' => :shareable,
|
|
13
|
+
'inaccessible' => :inaccessible,
|
|
14
|
+
'interfaceObject' => :interface_object
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def apply(type)
|
|
20
|
+
return unless type.ast_node && type.respond_to?(:key)
|
|
21
|
+
|
|
22
|
+
type.ast_node.directives.each do |dnode|
|
|
23
|
+
if dnode.name == 'key'
|
|
24
|
+
apply_key(type, dnode)
|
|
25
|
+
elsif (flag = FLAGS[dnode.name]) && type.respond_to?(flag)
|
|
26
|
+
type.public_send(flag)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def apply_key(type, dnode)
|
|
32
|
+
arg = dnode.arguments.find { |a| a.name == 'fields' }
|
|
33
|
+
type.key(fields: arg.value) if arg.respond_to?(:value)
|
|
34
|
+
|
|
35
|
+
resolve_references(type)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Setup federation resolve_reference: prefer a custom resolver
|
|
39
|
+
# registered with ReferenceResolver, but fall back to the base
|
|
40
|
+
# type's own resolve_reference (e.g. BaseObject's default model
|
|
41
|
+
# lookup) so direct-model entities resolve without a custom
|
|
42
|
+
# handler. Capture the inherited implementation before shadowing.
|
|
43
|
+
def resolve_references(type)
|
|
44
|
+
default_resolve_reference = type.respond_to?(:resolve_reference) ? type.method(:resolve_reference) : nil
|
|
45
|
+
|
|
46
|
+
type.define_singleton_method(:resolve_reference) do |reference, context|
|
|
47
|
+
resolved = ::Lighthouse::ReferenceResolver.resolve(type.graphql_name, reference, context)
|
|
48
|
+
next resolved unless resolved.nil?
|
|
49
|
+
|
|
50
|
+
default_resolve_reference&.call(reference, context)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Install a field's resolvers innermost first (see
|
|
4
|
+
# DirectiveRegistry#resolution_order), so each wrapper decorates the ones
|
|
5
|
+
# before it. Besides its directives, a field gets the conventional
|
|
6
|
+
# Queries::/Mutations:: resolver when it is a root field with no resolving
|
|
7
|
+
# directive, and argument validation when an input type it takes carries
|
|
8
|
+
# @validator. The registry replaces a hardcoded `case dnode.name`: adding a
|
|
9
|
+
# directive is "register a class", not "edit the generator".
|
|
10
|
+
module Lighthouse::GraphQL::FieldResolverStack
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def apply(type, field_def, directives, root_kind)
|
|
14
|
+
opts = { root_kind: root_kind }
|
|
15
|
+
steps = directive_steps(type, field_def, directives, opts)
|
|
16
|
+
steps << convention_step(type, field_def, root_kind) if root_kind && !resolving_directive?(directives)
|
|
17
|
+
steps << validator_step(type, field_def, opts) if input_validation?(field_def, directives)
|
|
18
|
+
|
|
19
|
+
steps.each_with_index.sort_by { |(order, _), index| [order, index] }.each { |(_, step), _| step.call }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# [order, step] for every directive the registry has a resolver for.
|
|
23
|
+
def directive_steps(type, field_def, directives, opts)
|
|
24
|
+
registry = Lighthouse::DirectiveRegistry.default
|
|
25
|
+
directives.filter_map do |dnode|
|
|
26
|
+
resolver_class = registry.resolution_class(dnode.name)
|
|
27
|
+
next unless resolver_class
|
|
28
|
+
|
|
29
|
+
[registry.resolution_order(dnode.name), -> { resolver_class.new(field_def, type, dnode, opts).define_resolver(type) }]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Whether a directive resolves the field itself (a wrapper only decorates
|
|
34
|
+
# another resolver), so the field needs no conventional one.
|
|
35
|
+
def resolving_directive?(directives)
|
|
36
|
+
registry = Lighthouse::DirectiveRegistry.default
|
|
37
|
+
directives.any? { |d| registry.resolution_class(d.name) && !registry.wrapper?(d.name) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def convention_step(type, field_def, root_kind)
|
|
41
|
+
resolver = Lighthouse::GraphQL::DirectiveResolvers::ConventionResolver
|
|
42
|
+
[0, -> { resolver.new(field_def, type, root_kind).define_resolver(type) }]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# An explicit @validator on the field already validates its arguments.
|
|
46
|
+
def input_validation?(field_def, directives)
|
|
47
|
+
directives.none? { |d| d.name == 'validator' } &&
|
|
48
|
+
Lighthouse::GraphQL::DirectiveResolvers::ValidatorDirectiveResolver.input_validators?(field_def)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validator_step(type, field_def, opts)
|
|
52
|
+
resolver = Lighthouse::GraphQL::DirectiveResolvers::ValidatorDirectiveResolver
|
|
53
|
+
[Lighthouse::DirectiveRegistry.default.resolution_order('validator'),
|
|
54
|
+
-> { resolver.new(field_def, type, nil, opts).define_resolver(type) }]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# How the `resolve_field_<name>` singletons that directive resolvers define on
|
|
4
|
+
# a schema's types are found and called at runtime. graphql-ruby resolves an
|
|
5
|
+
# SDL-built schema through `definition_default_resolve` and `resolve_<field>`
|
|
6
|
+
# methods of its own; the hooks here route both to ours.
|
|
7
|
+
module Lighthouse::GraphQL::GeneratedResolvers
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def method_name(field_name)
|
|
11
|
+
"resolve_field_#{field_name}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def field_name(field)
|
|
15
|
+
field.respond_to?(:graphql_name) ? field.graphql_name.to_s : field.name.to_s
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# GraphQL-Ruby's SDL builder also defines `resolve_field_*` methods
|
|
19
|
+
# internally. Only ours are defined under `lib/lighthouse/`; calling one of
|
|
20
|
+
# graphql-ruby's would use the wrong arity.
|
|
21
|
+
def generated?(resolver)
|
|
22
|
+
source_file, = resolver.source_location
|
|
23
|
+
source_file&.include?('/lib/lighthouse/')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Generated resolvers take either (obj, args) or (obj, args, ctx).
|
|
27
|
+
def invoke(resolver, obj, args, ctx)
|
|
28
|
+
return resolver.call(obj, args) if resolver.parameters.length == 2
|
|
29
|
+
|
|
30
|
+
resolver.call(obj, args, ctx)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# GraphQL-Ruby caches field resolution procs during schema build; rather
|
|
34
|
+
# than trying to override per-field `resolve_<field>` methods post-hoc,
|
|
35
|
+
# we install a single dispatcher here that prefers our generated
|
|
36
|
+
# `resolve_field_<name>` singletons.
|
|
37
|
+
def install_dispatcher(schema)
|
|
38
|
+
return unless schema.respond_to?(:definition_default_resolve=) && schema.respond_to?(:definition_default_resolve)
|
|
39
|
+
|
|
40
|
+
previous_default = schema.definition_default_resolve
|
|
41
|
+
schema.definition_default_resolve = proc do |type, field, obj, arguments, ctx|
|
|
42
|
+
resolver = generated_resolver(schema, type, field)
|
|
43
|
+
next invoke(resolver, obj, arguments, ctx) if resolver
|
|
44
|
+
|
|
45
|
+
previous_default.call(type, field, obj, arguments, ctx)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Our generated resolver for `field`, looked up on the schema's own type
|
|
50
|
+
# (nil when there is none).
|
|
51
|
+
def generated_resolver(schema, type, field)
|
|
52
|
+
owner = schema_type(schema, type)
|
|
53
|
+
name = method_name(field_name(field))
|
|
54
|
+
return unless owner.respond_to?(name)
|
|
55
|
+
|
|
56
|
+
resolver = owner.method(name)
|
|
57
|
+
resolver if generated?(resolver)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def schema_type(schema, type)
|
|
61
|
+
if type.respond_to?(:graphql_name)
|
|
62
|
+
schema.types[type.graphql_name]
|
|
63
|
+
elsif type.respond_to?(:name)
|
|
64
|
+
schema.types[type.name.to_s]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# GraphQL-Ruby's SDL builder defines `resolve_<field>` methods that call
|
|
69
|
+
# the schema's `definition_default_resolve`. Override `resolve_<field>`
|
|
70
|
+
# to delegate to our generated `resolve_field_<field>` when present, so
|
|
71
|
+
# runtime resolution consistently reaches our directive resolvers.
|
|
72
|
+
def route_runtime_resolver(type, field_def)
|
|
73
|
+
generated = method_name(field_def.name)
|
|
74
|
+
return unless type.respond_to?(generated)
|
|
75
|
+
|
|
76
|
+
type.define_singleton_method("resolve_#{field_def.name}") do |obj, arguments, ctx|
|
|
77
|
+
public_send(generated, obj, arguments, ctx)
|
|
78
|
+
end
|
|
79
|
+
rescue StandardError
|
|
80
|
+
# ignore override failures
|
|
81
|
+
end
|
|
82
|
+
end
|