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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +154 -1
  3. data/README.md +42 -2
  4. data/docs/authorization.md +116 -57
  5. data/docs/best-practices.md +16 -3
  6. data/docs/caching.md +161 -0
  7. data/docs/configuration.md +83 -4
  8. data/docs/custom-directives.md +5 -1
  9. data/docs/directives.md +117 -38
  10. data/docs/getting-started.md +84 -0
  11. data/docs/mutations.md +260 -0
  12. data/docs/performance.md +138 -0
  13. data/docs/relationships.md +80 -33
  14. data/docs/resolvers.md +179 -0
  15. data/docs/validation.md +132 -0
  16. data/lib/generators/lighthouse/builder_generator.rb +30 -0
  17. data/lib/generators/lighthouse/class_generator.rb +61 -0
  18. data/lib/generators/lighthouse/install/install_generator.rb +127 -0
  19. data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +8 -0
  20. data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +57 -0
  21. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +67 -0
  22. data/lib/generators/lighthouse/install/templates/schema.graphql.tt +19 -0
  23. data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -0
  24. data/lib/generators/lighthouse/mutation_generator.rb +30 -0
  25. data/lib/generators/lighthouse/query_generator.rb +29 -0
  26. data/lib/generators/lighthouse/validator_generator.rb +36 -0
  27. data/lib/lighthouse/cache_control.rb +126 -0
  28. data/lib/lighthouse/caching.rb +68 -0
  29. data/lib/lighthouse/config.rb +85 -19
  30. data/lib/lighthouse/contracts/arg_builder.rb +8 -12
  31. data/lib/lighthouse/contracts/arg_manipulator.rb +8 -12
  32. data/lib/lighthouse/contracts/directive.rb +10 -14
  33. data/lib/lighthouse/contracts/field_manipulator.rb +8 -12
  34. data/lib/lighthouse/contracts/field_middleware.rb +7 -11
  35. data/lib/lighthouse/contracts/field_resolver.rb +9 -13
  36. data/lib/lighthouse/dataloader_sources/association_loader.rb +28 -19
  37. data/lib/lighthouse/dataloader_sources/model_loader.rb +10 -14
  38. data/lib/lighthouse/dataloader_sources/relation_count_loader.rb +16 -0
  39. data/lib/lighthouse/dataloader_sources/relation_page_loader.rb +18 -0
  40. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +57 -69
  41. data/lib/lighthouse/directive_resolvers/all_resolver.rb +26 -57
  42. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +161 -0
  43. data/lib/lighthouse/directive_resolvers/auth_resolver.rb +41 -38
  44. data/lib/lighthouse/directive_resolvers/base.rb +142 -86
  45. data/lib/lighthouse/directive_resolvers/builder_applier.rb +58 -56
  46. data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +133 -0
  47. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +177 -0
  48. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +70 -0
  49. data/lib/lighthouse/directive_resolvers/count_resolver.rb +42 -48
  50. data/lib/lighthouse/directive_resolvers/field_resolver.rb +27 -68
  51. data/lib/lighthouse/directive_resolvers/find_resolver.rb +42 -52
  52. data/lib/lighthouse/directive_resolvers/first_resolver.rb +39 -45
  53. data/lib/lighthouse/directive_resolvers/guard_resolver.rb +16 -22
  54. data/lib/lighthouse/directive_resolvers/method_resolver.rb +19 -25
  55. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +219 -0
  56. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +70 -128
  57. data/lib/lighthouse/directive_resolvers/rename_resolver.rb +18 -24
  58. data/lib/lighthouse/directive_resolvers/resolution.rb +31 -27
  59. data/lib/lighthouse/directive_resolvers/validator_resolver.rb +193 -0
  60. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +164 -163
  61. data/lib/lighthouse/directives/arguments/base.rb +23 -31
  62. data/lib/lighthouse/directives/arguments/filters.rb +113 -93
  63. data/lib/lighthouse/directives/arguments/order_by.rb +41 -41
  64. data/lib/lighthouse/directives/pagination_info_types.rb +69 -0
  65. data/lib/lighthouse/directives/pagination_manipulator.rb +176 -0
  66. data/lib/lighthouse/directives/registry.rb +210 -141
  67. data/lib/lighthouse/directives/where_conditions_directive.rb +236 -200
  68. data/lib/lighthouse/errors.rb +27 -0
  69. data/lib/lighthouse/federation_directives.rb +53 -0
  70. data/lib/lighthouse/field_resolver_stack.rb +56 -0
  71. data/lib/lighthouse/generated_resolvers.rb +82 -0
  72. data/lib/lighthouse/graphql/version.rb +1 -1
  73. data/lib/lighthouse/graphql_ruby_patches.rb +18 -0
  74. data/lib/lighthouse/ide_helper.rb +98 -0
  75. data/lib/lighthouse/namespaces.rb +21 -0
  76. data/lib/lighthouse/nested_mutations/belongs_to_operations.rb +50 -0
  77. data/lib/lighthouse/nested_mutations/executor.rb +149 -0
  78. data/lib/lighthouse/nested_mutations/has_operations.rb +66 -0
  79. data/lib/lighthouse/nested_mutations/many_to_many_operations.rb +44 -0
  80. data/lib/lighthouse/nested_mutations/partitioner.rb +125 -0
  81. data/lib/lighthouse/nested_mutations.rb +210 -0
  82. data/lib/lighthouse/pagination.rb +221 -0
  83. data/lib/lighthouse/persisted_queries.rb +167 -0
  84. data/lib/lighthouse/railtie.rb +26 -0
  85. data/lib/lighthouse/rb_lighthouse.rb +50 -29
  86. data/lib/lighthouse/reference_resolver.rb +13 -15
  87. data/lib/lighthouse/relation_batch.rb +156 -0
  88. data/lib/lighthouse/resolver_dispatch.rb +26 -0
  89. data/lib/lighthouse/schema_cache.rb +120 -0
  90. data/lib/lighthouse/schema_factory.rb +27 -37
  91. data/lib/lighthouse/schema_generator.rb +196 -239
  92. data/lib/lighthouse/schema_implementation.rb +51 -48
  93. data/lib/lighthouse/sdl_loader.rb +17 -20
  94. data/lib/lighthouse/support/arguments.rb +119 -0
  95. data/lib/lighthouse/support/authorization.rb +18 -19
  96. data/lib/lighthouse/support/batching.rb +16 -0
  97. data/lib/lighthouse/support/directive_args.rb +26 -24
  98. data/lib/lighthouse/support/model_resolver.rb +56 -56
  99. data/lib/lighthouse/support/naming.rb +55 -59
  100. data/lib/lighthouse/tasks/lighthouse.rake +33 -0
  101. data/lib/lighthouse/where_conditions/operator_map.rb +25 -29
  102. metadata +51 -63
  103. data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +0 -56
  104. data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +0 -47
  105. data/lib/lighthouse/directive_resolvers/can_resolver.rb +0 -86
  106. data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +0 -109
  107. data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +0 -43
  108. data/lib/lighthouse/directives/relation_directive.rb +0 -332
@@ -2,207 +2,243 @@
2
2
 
3
3
  require 'graphql'
4
4
 
5
- module Lighthouse
6
- module GraphQL
7
- module Directives
8
- class WhereConditionsDirective
9
- SQLOPERATOR_TYPE_NAME = 'SQLOperator'.freeze
10
- MIXED_SCALAR_NAME = 'Mixed'.freeze
11
- PLACEHOLDER_SCALAR_NAME = '_'.freeze
12
-
13
- # Supported operators in the generated GraphQL input type performance.
14
- SUPPORTED_OPERATORS = %w[EQ NEQ GT GTE LT LTE IN NIN CONTAINS STARTS_WITH ENDS_WITH IS_NULL IS_NOT_NULL].freeze
15
-
16
- def self.manipulate(sdl)
17
- return sdl unless sdl.is_a?(String) && !sdl.empty?
18
- begin
19
- ast = ::GraphQL::Language::Parser.parse(sdl)
20
- rescue => e
21
- warn("Lighthouse::GraphQL::Directives::WhereConditionsDirective: parse failed: #{e.class}: #{e.message}")
22
- return sdl
23
- end
24
- existing_defs = ast.definitions.map { |defn| defn.respond_to?(:name) ? defn.name : nil }.compact
25
-
26
- generated = []
27
-
28
- object_types = ast.definitions.select do |defn|
29
- defn.is_a?(::GraphQL::Language::Nodes::ObjectTypeDefinition) || defn.is_a?(::GraphQL::Language::Nodes::InterfaceTypeDefinition)
30
- end
31
-
32
- object_types.each do |type_def|
33
- (type_def.fields || []).each do |field|
34
- (field.arguments || []).each do |arg|
35
- directive = arg.directives&.find { |d| %w[whereConditions whereHasConditions].include?(d.name) }
36
- next unless directive
37
-
38
- if arg.description.nil? || arg.description.empty?
39
- arg.instance_variable_set(
40
- :@description,
41
- directive.name == 'whereHasConditions' ? 'Filter results by related records conditions.' : 'Filter results by field conditions.'
42
- )
43
- end
44
-
45
- input_type_name = "#{camelize(type_def.name)}#{camelize(field.name)}#{camelize(arg.name)}WhereConditions"
46
- column_enum_name = "#{camelize(type_def.name)}#{camelize(field.name)}#{camelize(arg.name)}Column"
47
- relation_input_name = "#{input_type_name}Relation"
48
-
49
- apply_arg_type_replacement(arg, input_type_name)
50
-
51
- # Build input type if needed.
52
- unless existing_defs.include?(input_type_name)
53
- generated << build_where_condition_input(input_type_name, column_enum_name, relation_input_name, directive)
54
- existing_defs << input_type_name
55
- end
56
-
57
- # Build relation type for HAS support in whereConditions
58
- if directive.name == 'whereConditions' && !existing_defs.include?(relation_input_name)
59
- generated << build_where_condition_relation_input(relation_input_name, input_type_name)
60
- existing_defs << relation_input_name
61
- end
62
-
63
- # Build column enum type unless columnsEnum is provided.
64
- columns_enum = directive_argument_value(directive, 'columnsEnum')
65
- columns = directive_argument_value(directive, 'columns')
66
- if columns_enum.nil? && columns && !columns.empty? && !existing_defs.include?(column_enum_name)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lighthouse
4
4
  module GraphQL
5
- VERSION = '0.1.1'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end