lighthouse-graphql 0.1.1 → 0.2.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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +175 -1
  3. data/README.md +47 -3
  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/motivation.md +184 -0
  12. data/docs/mutations.md +260 -0
  13. data/docs/performance.md +138 -0
  14. data/docs/relationships.md +80 -33
  15. data/docs/resolvers.md +189 -0
  16. data/docs/validation.md +132 -0
  17. data/lib/generators/lighthouse/builder_generator.rb +30 -0
  18. data/lib/generators/lighthouse/class_generator.rb +61 -0
  19. data/lib/generators/lighthouse/install/install_generator.rb +127 -0
  20. data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +8 -0
  21. data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +57 -0
  22. data/lib/generators/lighthouse/install/templates/initializer.rb.tt +67 -0
  23. data/lib/generators/lighthouse/install/templates/schema.graphql.tt +19 -0
  24. data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -0
  25. data/lib/generators/lighthouse/mutation_generator.rb +30 -0
  26. data/lib/generators/lighthouse/query_generator.rb +29 -0
  27. data/lib/generators/lighthouse/validator_generator.rb +36 -0
  28. data/lib/lighthouse/cache_control.rb +126 -0
  29. data/lib/lighthouse/caching.rb +68 -0
  30. data/lib/lighthouse/config.rb +85 -19
  31. data/lib/lighthouse/contracts/arg_builder.rb +8 -12
  32. data/lib/lighthouse/contracts/arg_manipulator.rb +8 -12
  33. data/lib/lighthouse/contracts/directive.rb +10 -14
  34. data/lib/lighthouse/contracts/field_manipulator.rb +8 -12
  35. data/lib/lighthouse/contracts/field_middleware.rb +7 -11
  36. data/lib/lighthouse/contracts/field_resolver.rb +9 -13
  37. data/lib/lighthouse/dataloader_sources/association_loader.rb +28 -19
  38. data/lib/lighthouse/dataloader_sources/model_loader.rb +10 -14
  39. data/lib/lighthouse/dataloader_sources/relation_count_loader.rb +16 -0
  40. data/lib/lighthouse/dataloader_sources/relation_page_loader.rb +18 -0
  41. data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +57 -69
  42. data/lib/lighthouse/directive_resolvers/all_resolver.rb +26 -57
  43. data/lib/lighthouse/directive_resolvers/association_resolvers.rb +161 -0
  44. data/lib/lighthouse/directive_resolvers/auth_resolver.rb +41 -38
  45. data/lib/lighthouse/directive_resolvers/base.rb +142 -86
  46. data/lib/lighthouse/directive_resolvers/builder_applier.rb +58 -56
  47. data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +133 -0
  48. data/lib/lighthouse/directive_resolvers/can_resolvers.rb +177 -0
  49. data/lib/lighthouse/directive_resolvers/convention_resolver.rb +70 -0
  50. data/lib/lighthouse/directive_resolvers/count_resolver.rb +42 -48
  51. data/lib/lighthouse/directive_resolvers/field_resolver.rb +27 -68
  52. data/lib/lighthouse/directive_resolvers/find_resolver.rb +42 -52
  53. data/lib/lighthouse/directive_resolvers/first_resolver.rb +39 -45
  54. data/lib/lighthouse/directive_resolvers/guard_resolver.rb +16 -22
  55. data/lib/lighthouse/directive_resolvers/method_resolver.rb +19 -25
  56. data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +219 -0
  57. data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +70 -128
  58. data/lib/lighthouse/directive_resolvers/rename_resolver.rb +18 -24
  59. data/lib/lighthouse/directive_resolvers/resolution.rb +31 -27
  60. data/lib/lighthouse/directive_resolvers/validator_resolver.rb +193 -0
  61. data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +164 -163
  62. data/lib/lighthouse/directives/arguments/base.rb +23 -31
  63. data/lib/lighthouse/directives/arguments/filters.rb +113 -93
  64. data/lib/lighthouse/directives/arguments/order_by.rb +41 -41
  65. data/lib/lighthouse/directives/pagination_info_types.rb +69 -0
  66. data/lib/lighthouse/directives/pagination_manipulator.rb +176 -0
  67. data/lib/lighthouse/directives/registry.rb +210 -141
  68. data/lib/lighthouse/directives/where_conditions_directive.rb +236 -200
  69. data/lib/lighthouse/errors.rb +27 -0
  70. data/lib/lighthouse/federation_directives.rb +53 -0
  71. data/lib/lighthouse/field_resolver_stack.rb +56 -0
  72. data/lib/lighthouse/generated_resolvers.rb +82 -0
  73. data/lib/lighthouse/graphql/version.rb +1 -1
  74. data/lib/lighthouse/graphql_ruby_patches.rb +18 -0
  75. data/lib/lighthouse/ide_helper.rb +98 -0
  76. data/lib/lighthouse/namespaces.rb +21 -0
  77. data/lib/lighthouse/nested_mutations/belongs_to_operations.rb +50 -0
  78. data/lib/lighthouse/nested_mutations/executor.rb +149 -0
  79. data/lib/lighthouse/nested_mutations/has_operations.rb +66 -0
  80. data/lib/lighthouse/nested_mutations/many_to_many_operations.rb +44 -0
  81. data/lib/lighthouse/nested_mutations/partitioner.rb +125 -0
  82. data/lib/lighthouse/nested_mutations.rb +210 -0
  83. data/lib/lighthouse/pagination.rb +221 -0
  84. data/lib/lighthouse/persisted_queries.rb +167 -0
  85. data/lib/lighthouse/railtie.rb +26 -0
  86. data/lib/lighthouse/rb_lighthouse.rb +50 -29
  87. data/lib/lighthouse/reference_resolver.rb +13 -15
  88. data/lib/lighthouse/relation_batch.rb +156 -0
  89. data/lib/lighthouse/resolver_dispatch.rb +26 -0
  90. data/lib/lighthouse/schema_cache.rb +120 -0
  91. data/lib/lighthouse/schema_factory.rb +27 -37
  92. data/lib/lighthouse/schema_generator.rb +196 -239
  93. data/lib/lighthouse/schema_implementation.rb +51 -48
  94. data/lib/lighthouse/sdl_loader.rb +17 -20
  95. data/lib/lighthouse/support/arguments.rb +119 -0
  96. data/lib/lighthouse/support/authorization.rb +18 -19
  97. data/lib/lighthouse/support/batching.rb +16 -0
  98. data/lib/lighthouse/support/directive_args.rb +26 -24
  99. data/lib/lighthouse/support/model_resolver.rb +56 -56
  100. data/lib/lighthouse/support/naming.rb +55 -59
  101. data/lib/lighthouse/tasks/lighthouse.rake +33 -0
  102. data/lib/lighthouse/where_conditions/operator_map.rb +25 -29
  103. data/sig/lighthouse/directives.rbs +158 -0
  104. data/sig/lighthouse/graphql.rbs +78 -0
  105. data/sig/lighthouse.rbs +142 -0
  106. metadata +56 -64
  107. data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +0 -56
  108. data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +0 -47
  109. data/lib/lighthouse/directive_resolvers/can_resolver.rb +0 -86
  110. data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +0 -109
  111. data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +0 -43
  112. data/lib/lighthouse/directives/relation_directive.rb +0 -332
@@ -1,168 +1,169 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Lighthouse
4
- module GraphQL
5
- module DirectiveResolvers
6
- class WhereConditionsApplier
7
- # Operator semantics live in Lighthouse::WhereConditions::OperatorMap so
8
- # they can be shared with the simple argument-filter directives.
9
- SUPPORTED_OPERATORS = Lighthouse::WhereConditions::OperatorMap::OPERATORS
10
-
11
- class << self
12
- def apply(relation, value)
13
- return relation unless relation.respond_to?(:where)
14
- return relation if value.nil?
15
-
16
- condition = value.is_a?(Hash) ? value.deep_symbolize_keys : symbolized(value)
17
- return relation unless condition.is_a?(Hash)
18
-
19
- result = relation
20
-
21
- if condition.key?(:AND)
22
- Array(condition[:AND]).each do |sub|
23
- result = apply(result, sub)
24
- end
25
- end
26
-
27
- if condition.key?(:OR)
28
- or_rel = nil
29
- Array(condition[:OR]).each do |sub|
30
- sub_scope = apply(relation, sub)
31
- or_rel = or_rel ? or_rel.or(sub_scope) : sub_scope
32
- end
33
- result = or_rel if or_rel
34
- end
35
-
36
- if condition.key?(:HAS)
37
- has_cond = condition[:HAS]
38
- if has_cond.is_a?(Hash) && has_cond[:relation]
39
- result = apply_has(result, has_cond)
40
- end
41
- end
42
-
43
- if condition.key?(:column)
44
- result = apply_single_condition(result, condition)
45
- end
46
-
47
- result
48
- end
49
-
50
- def apply_has(relation, has_cond)
51
- relation_name = has_cond[:relation].to_s
52
- return relation unless relation_name.present?
53
-
54
- resolved_relation_name = resolve_relation_name(relation, relation_name)
55
- return relation unless resolved_relation_name.present?
56
-
57
- source_klass = relation.respond_to?(:klass) ? relation.klass : relation
58
- reflection = source_klass.respond_to?(:reflect_on_association) ? source_klass.reflect_on_association(resolved_relation_name.to_sym) : nil
59
- return relation unless reflection&.klass
60
-
61
- if has_cond[:condition].present?
62
- child_scope = reflection.klass.all
63
- child_scope = apply(child_scope, has_cond[:condition])
64
-
65
- # Use an EXISTS-style filter to safely apply conditions from the related model.
66
- table_name = reflection.klass.table_name
67
- relation.joins(resolved_relation_name.to_sym)
68
- .where(table_name => { id: child_scope.select(:id) })
69
- .distinct
70
- else
71
- relation.joins(resolved_relation_name.to_sym).distinct
72
- end
73
- rescue StandardError
74
- relation
75
- end
76
-
77
- def resolve_relation_name(relation, relation_name)
78
- candidate_names = [relation_name.to_s, relation_name.to_s.singularize, relation_name.to_s.pluralize].uniq
79
-
80
- candidate_names.each do |candidate|
81
- next if candidate.blank?
82
- return candidate if association_exists?(relation, candidate)
83
- end
84
-
85
- nil
86
- rescue StandardError
87
- nil
88
- end
89
-
90
- def association_exists?(relation, name)
91
- klass = relation.respond_to?(:klass) ? relation.klass : relation
92
- return false unless klass.respond_to?(:reflect_on_association)
93
-
94
- klass.reflect_on_association(name.to_sym).present?
95
- rescue StandardError
96
- false
97
- end
98
-
99
- def apply_has_argument(relation, arg_name, arg_value, relation_name = nil)
100
- relation_name ||= derive_relation_name(arg_name)
101
- return relation unless relation_name.present? && arg_value.present?
102
-
103
- condition = arg_value.is_a?(Hash) ? arg_value : symbolized(arg_value)
104
- has_payload = { relation: relation_name, condition: condition }
105
- apply_has(relation, has_payload)
106
- end
107
-
108
- private
109
-
110
- def derive_relation_name(arg_name)
111
- Lighthouse::Support::Naming.derive_relation_name(arg_name)
112
- end
113
-
114
- def apply_single_condition(relation, condition)
115
- col = condition[:column]
116
- op = condition[:operator]&.to_s&.upcase || 'EQ'
117
- value = condition[:value]
118
-
119
- return relation if col.blank? || !relation.respond_to?(:where)
120
-
121
- column_name = resolve_column_name(col)
122
- return relation if column_name.blank?
123
-
124
- case op
125
- when 'IS_NULL'
126
- relation.where(column_name => nil)
127
- when 'IS_NOT_NULL'
128
- relation.where.not(column_name => nil)
129
- when 'IN'
130
- relation.where(column_name => Array(value))
131
- when 'NIN'
132
- relation.where.not(column_name => Array(value))
133
- when 'CONTAINS'
134
- relation.where(arel_table(relation)[column_name].matches("%#{value}%"))
135
- when 'STARTS_WITH'
136
- relation.where(arel_table(relation)[column_name].matches("#{value}%"))
137
- when 'ENDS_WITH'
138
- relation.where(arel_table(relation)[column_name].matches("%#{value}"))
139
- else
140
- if SUPPORTED_OPERATORS[op]
141
- relation.where(arel_table(relation)[column_name].public_send(SUPPORTED_OPERATORS[op], value))
142
- else
143
- relation.where(column_name => value)
144
- end
145
- end
146
- rescue StandardError
147
- relation
148
- end
149
-
150
- def resolve_column_name(column)
151
- Lighthouse::Support::Naming.column(column)
152
- end
153
-
154
- def arel_table(relation)
155
- relation.klass.arel_table
156
- end
157
-
158
- def symbolized(obj)
159
- if obj.respond_to?(:to_h)
160
- obj.to_h.deep_symbolize_keys
161
- else
162
- {}
163
- end
164
- end
165
- end
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
- module Lighthouse
4
- module GraphQL
5
- module Directives
6
- module Arguments
7
- # Base class for argument-builder directives (@eq, @neq, @in, @notIn,
8
- # @like, @where, @orderBy). Each receives the argument AST node and the
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
- def initialize(arg_node, directive_node)
15
- @arg_node = arg_node
16
- @directive_node = directive_node
17
- end
10
+ def initialize(arg_node, directive_node)
11
+ @arg_node = arg_node
12
+ @directive_node = directive_node
13
+ end
18
14
 
19
- # @return relation
20
- def handle_builder(_relation, _value)
21
- raise NotImplementedError
22
- end
15
+ # @return relation
16
+ def handle_builder(_relation, _value)
17
+ raise NotImplementedError
18
+ end
23
19
 
24
- private
20
+ private
25
21
 
26
- # Column to filter on: the directive's `key:` argument if present,
27
- # otherwise the snake_case form of the GraphQL argument name.
28
- def column
29
- Lighthouse::Support::DirectiveArgs.literal(@directive_node, 'key') ||
30
- Lighthouse::Support::Naming.attribute(@arg_node.name)
31
- end
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
- def arel_column(relation)
34
- relation.klass.arel_table[column]
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
@@ -2,100 +2,120 @@
2
2
 
3
3
  require_relative 'base'
4
4
 
5
- module Lighthouse
6
- module GraphQL
7
- module Directives
8
- module Arguments
9
- # @eq — equality filter (the default). `field(status: String @eq)`
10
- class Eq < Base
11
- def self.directive_name = 'eq'
12
- def self.definition = 'directive @eq(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
13
-
14
- def handle_builder(relation, value)
15
- return relation if value.nil?
16
-
17
- relation.where(column => value)
18
- end
19
- end
20
-
21
- # @neq — inequality filter.
22
- class Neq < Base
23
- def self.directive_name = 'neq'
24
- def self.definition = 'directive @neq(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
25
-
26
- def handle_builder(relation, value)
27
- return relation if value.nil?
28
-
29
- relation.where.not(column => value)
30
- end
31
- end
32
-
33
- # @in — value is one of a list.
34
- class In < Base
35
- def self.directive_name = 'in'
36
- def self.definition = 'directive @in(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
37
-
38
- def handle_builder(relation, value)
39
- return relation if value.nil?
40
-
41
- relation.where(column => Array(value))
42
- end
43
- end
44
-
45
- # @notIn — value is none of a list.
46
- class NotIn < Base
47
- def self.directive_name = 'notIn'
48
- def self.definition = 'directive @notIn(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
49
-
50
- def handle_builder(relation, value)
51
- return relation if value.nil?
52
-
53
- relation.where.not(column => Array(value))
54
- end
55
- end
56
-
57
- # @like — SQL LIKE with the value wrapped in wildcards.
58
- class Like < Base
59
- def self.directive_name = 'like'
60
- def self.definition = 'directive @like(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
61
-
62
- def handle_builder(relation, value)
63
- return relation if value.nil?
64
-
65
- relation.where(arel_column(relation).matches("%#{value}%"))
66
- end
67
- end
68
-
69
- # @where — generic operator filter, sharing the SQLOperator semantics with
70
- # the @whereConditions extension. `field(age: Int @where(operator: "GT"))`
71
- class Where < Base
72
- def self.directive_name = 'where'
73
- def self.definition = 'directive @where(key: String, operator: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
74
-
75
- def handle_builder(relation, value)
76
- return relation if value.nil?
77
-
78
- op = (Lighthouse::Support::DirectiveArgs.literal(@directive_node, 'operator') || 'EQ').to_s.upcase
79
-
80
- case op
81
- when 'IN' then relation.where(column => Array(value))
82
- when 'NIN' then relation.where.not(column => Array(value))
83
- when 'IS_NULL' then relation.where(column => nil)
84
- when 'IS_NOT_NULL' then relation.where.not(column => nil)
85
- when 'CONTAINS' then relation.where(arel_column(relation).matches("%#{value}%"))
86
- when 'STARTS_WITH' then relation.where(arel_column(relation).matches("#{value}%"))
87
- when 'ENDS_WITH' then relation.where(arel_column(relation).matches("%#{value}"))
88
- else
89
- arel_method = Lighthouse::WhereConditions::OperatorMap[op]
90
- if arel_method
91
- relation.where(arel_column(relation).public_send(arel_method, value))
92
- else
93
- relation.where(column => value)
94
- end
95
- end
96
- end
97
- end
5
+ module Lighthouse::GraphQL::Directives::Arguments
6
+ # @eq — equality filter (the default). `field(status: String @eq)`
7
+ class Eq < Base
8
+ def self.directive_name = 'eq'
9
+ def self.definition = 'directive @eq(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
10
+
11
+ def handle_builder(relation, value)
12
+ return relation if value.nil?
13
+
14
+ relation.where(column => value)
15
+ end
16
+ end
17
+
18
+ # @whereKey — filter by the model's primary key, whatever its column is
19
+ # named; a list matches any of its values. `deletePosts(ids: [ID!]! @whereKey)`
20
+ class WhereKey < Base
21
+ def self.directive_name = 'whereKey'
22
+ def self.definition = 'directive @whereKey on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
23
+
24
+ def handle_builder(relation, value)
25
+ return relation if value.nil?
26
+
27
+ relation.where(relation.klass.primary_key => value)
28
+ end
29
+ end
30
+
31
+ # @neq — inequality filter.
32
+ class Neq < Base
33
+ def self.directive_name = 'neq'
34
+ def self.definition = 'directive @neq(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
35
+
36
+ def handle_builder(relation, value)
37
+ return relation if value.nil?
38
+
39
+ relation.where.not(column => value)
40
+ end
41
+ end
42
+
43
+ # @in — value is one of a list.
44
+ class In < Base
45
+ def self.directive_name = 'in'
46
+ def self.definition = 'directive @in(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
47
+
48
+ def handle_builder(relation, value)
49
+ return relation if value.nil?
50
+
51
+ relation.where(column => Array(value))
52
+ end
53
+ end
54
+
55
+ # @notIn — value is none of a list.
56
+ class NotIn < Base
57
+ def self.directive_name = 'notIn'
58
+ def self.definition = 'directive @notIn(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
59
+
60
+ def handle_builder(relation, value)
61
+ return relation if value.nil?
62
+
63
+ relation.where.not(column => Array(value))
64
+ end
65
+ end
66
+
67
+ # @like — SQL LIKE with the value wrapped in wildcards.
68
+ class Like < Base
69
+ def self.directive_name = 'like'
70
+ def self.definition = 'directive @like(key: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
71
+
72
+ def handle_builder(relation, value)
73
+ return relation if value.nil?
74
+
75
+ relation.where(arel_column(relation).matches("%#{value}%"))
76
+ end
77
+ end
78
+
79
+ # @where — generic operator filter, sharing the SQLOperator semantics with
80
+ # the @whereConditions extension. `field(age: Int @where(operator: "GT"))`
81
+ class Where < Base
82
+ def self.directive_name = 'where'
83
+ def self.definition = 'directive @where(key: String, operator: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION'
84
+
85
+ # LIKE pattern of each wildcard operator, built from the client value.
86
+ LIKE_PATTERNS = {
87
+ 'CONTAINS' => ->(value) { "%#{value}%" },
88
+ 'STARTS_WITH' => ->(value) { "#{value}%" },
89
+ 'ENDS_WITH' => ->(value) { "%#{value}" }
90
+ }.freeze
91
+
92
+ def handle_builder(relation, value)
93
+ return relation if value.nil?
94
+
95
+ operator = sql_operator
96
+ case operator
97
+ when 'IN' then relation.where(column => Array(value))
98
+ when 'NIN' then relation.where.not(column => Array(value))
99
+ when 'IS_NULL' then relation.where(column => nil)
100
+ when 'IS_NOT_NULL' then relation.where.not(column => nil)
101
+ when *LIKE_PATTERNS.keys then relation.where(arel_column(relation).matches(LIKE_PATTERNS[operator].call(value)))
102
+ else compare(relation, operator, value)
98
103
  end
99
104
  end
105
+
106
+ private
107
+
108
+ def sql_operator
109
+ (Lighthouse::Support::DirectiveArgs.literal(@directive_node, 'operator') || 'EQ').to_s.upcase
110
+ end
111
+
112
+ # Comparison operators map onto Arel predicates; an unknown one falls
113
+ # back to equality.
114
+ def compare(relation, operator, value)
115
+ arel_method = Lighthouse::WhereConditions::OperatorMap[operator]
116
+ return relation.where(column => value) unless arel_method
117
+
118
+ relation.where(arel_column(relation).public_send(arel_method, value))
119
+ end
100
120
  end
101
121
  end