lighthouse-graphql 0.6.0 → 0.7.1

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +107 -1
  3. data/README.md +15 -7
  4. data/docs/best-practices.md +6 -4
  5. data/docs/configuration.md +19 -5
  6. data/docs/custom-directives.md +2 -1
  7. data/docs/directives.md +65 -7
  8. data/docs/federation.md +1 -1
  9. data/docs/filtering-and-ordering.md +330 -30
  10. data/docs/generators.md +2 -2
  11. data/docs/getting-started.md +43 -10
  12. data/docs/motivation.md +3 -2
  13. data/docs/performance.md +4 -3
  14. data/docs/relationships.md +2 -1
  15. data/docs/relay.md +52 -3
  16. data/docs/resolvers.md +2 -2
  17. data/docs/schema-organisation.md +177 -0
  18. data/lib/generators/lighthouse/install/graphql_ruby_install.rb +14 -0
  19. data/lib/generators/lighthouse/install/install_generator.rb +31 -12
  20. data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +2 -1
  21. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +11 -4
  22. data/lib/generators/lighthouse/install/templates/schema.graphql.tt +30 -0
  23. data/lib/lighthouse/ast_builder.rb +124 -0
  24. data/lib/lighthouse/config.rb +23 -4
  25. data/lib/lighthouse/contracts/arg_manipulator.rb +9 -5
  26. data/lib/lighthouse/contracts/field_builder.rb +13 -0
  27. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +1 -2
  28. data/lib/lighthouse/directive_resolvers/all_resolver.rb +0 -2
  29. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +9 -6
  30. data/lib/lighthouse/directive_resolvers/base.rb +6 -37
  31. data/lib/lighthouse/directive_resolvers/builder_applier.rb +21 -6
  32. data/lib/lighthouse/directive_resolvers/count_resolver.rb +0 -1
  33. data/lib/lighthouse/directive_resolvers/first_resolver.rb +0 -2
  34. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +2 -3
  35. data/lib/lighthouse/directives/arguments/order_by.rb +190 -26
  36. data/lib/lighthouse/directives/arguments/where_conditions.rb +25 -0
  37. data/lib/lighthouse/directives/arguments/where_conditions_base.rb +84 -0
  38. data/lib/lighthouse/directives/arguments/where_has_conditions.rb +56 -0
  39. data/lib/lighthouse/directives/definitions.rb +0 -1
  40. data/lib/lighthouse/directives/generated_types.rb +103 -0
  41. data/lib/lighthouse/directives/registry.rb +39 -10
  42. data/lib/lighthouse/document_ast.rb +111 -0
  43. data/lib/lighthouse/graphql/version.rb +1 -1
  44. data/lib/lighthouse/ide_helper.rb +80 -23
  45. data/lib/lighthouse/namespaces.rb +1 -0
  46. data/lib/lighthouse/order_by/types.rb +137 -0
  47. data/lib/lighthouse/railtie.rb +9 -2
  48. data/lib/lighthouse/rb_lighthouse.rb +15 -5
  49. data/lib/lighthouse/resolve_info.rb +5 -5
  50. data/lib/lighthouse/schema_cache.rb +8 -4
  51. data/lib/lighthouse/schema_factory.rb +6 -9
  52. data/lib/lighthouse/schema_generator.rb +4 -15
  53. data/lib/lighthouse/schema_stitcher.rb +64 -0
  54. data/lib/lighthouse/support/naming.rb +0 -12
  55. data/lib/lighthouse/where_conditions/columns.rb +36 -0
  56. data/lib/lighthouse/where_conditions/handler.rb +152 -0
  57. data/lib/lighthouse/where_conditions/json_column.rb +99 -0
  58. data/lib/lighthouse/where_conditions/operator_map.rb +1 -4
  59. data/lib/lighthouse/where_conditions/types.rb +110 -0
  60. data/sig/lighthouse/directives.rbs +16 -3
  61. data/sig/lighthouse/graphql.rbs +48 -4
  62. data/sig/lighthouse.rbs +8 -2
  63. metadata +15 -4
  64. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +0 -170
  65. data/lib/lighthouse/directives/where_conditions_directive.rb +0 -244
  66. data/lib/lighthouse/sdl_loader.rb +0 -22
@@ -1,244 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'graphql'
4
-
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
67
- end
68
- end
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
244
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lighthouse::GraphQL::SDLLoader
4
- module_function
5
-
6
- def load_from_folder(folder)
7
- files = Dir[File.join(folder, '**', '*.graphql')]
8
- if files.empty?
9
- default = File.expand_path('../../..//schema.graphql', __dir__)
10
- return File.read(default) if File.exist?(default)
11
-
12
- return ''
13
- end
14
-
15
- files.map! { |f| File.read(f) }
16
- files.join("\n\n")
17
- end
18
-
19
- def load_from_file(path)
20
- File.read(path)
21
- end
22
- end