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
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The types @whereConditions and @whereHasConditions generate, as PHP
4
+ # Lighthouse's WhereConditionsServiceProvider defines them. For an argument
5
+ # whose input is named N:
6
+ #
7
+ # N column, operator, value, AND, OR, HAS: the conditions
8
+ # NRelation HAS: a relation, how many of its records (operator, amount), and a condition on them
9
+ # NHasCondition the condition on the related records: as N, but on any column
10
+ #
11
+ # Without `columns:` / `columnsEnum:` the argument shares the unrestricted
12
+ # WhereConditions (and its WhereConditionsRelation / WhereConditionsHasCondition).
13
+ module Lighthouse::WhereConditions::Types
14
+ DEFAULT = 'WhereConditions'
15
+ SQL_OPERATOR = 'SQLOperator'
16
+ MIXED = 'Mixed'
17
+
18
+ # The SQLOperator enum: what a condition compares the column with.
19
+ OPERATORS = {
20
+ 'EQ' => 'Equal operator (`=`)',
21
+ 'NEQ' => 'Not equal operator (`!=`)',
22
+ 'GT' => 'Greater than operator (`>`)',
23
+ 'GTE' => 'Greater than or equal operator (`>=`)',
24
+ 'LT' => 'Less than operator (`<`)',
25
+ 'LTE' => 'Less than or equal operator (`<=`)',
26
+ 'LIKE' => 'Simple pattern matching (`LIKE`)',
27
+ 'NOT_LIKE' => 'Negation of simple pattern matching (`NOT LIKE`)',
28
+ 'IN' => 'Whether a value is within a set of values (`IN`)',
29
+ 'NOT_IN' => 'Whether a value is not within a set of values (`NOT IN`)',
30
+ 'BETWEEN' => 'Whether a value is within a range of values (`BETWEEN`)',
31
+ 'NOT_BETWEEN' => 'Whether a value is not within a range of values (`NOT BETWEEN`)',
32
+ 'IS_NULL' => 'Whether a value is null (`IS NULL`)',
33
+ 'IS_NOT_NULL' => 'Whether a value is not null (`IS NOT NULL`)'
34
+ }.freeze
35
+
36
+ DEFAULT_OPERATOR = 'EQ'
37
+ DEFAULT_HAS_OPERATOR = 'GTE'
38
+ DEFAULT_HAS_AMOUNT = 1
39
+
40
+ module_function
41
+
42
+ # Defines N, NHasCondition and NRelation, and the SQLOperator and Mixed they use.
43
+ def define(document_ast, name, column_type, path)
44
+ document_ast.define_type(SQL_OPERATOR) { sql_operator }
45
+ document_ast.define_type(MIXED, 'scalar Mixed')
46
+ document_ast.define_type(name) { conditions(name, "Dynamic WHERE conditions for #{path}.", column_type) }
47
+ document_ast.define_type("#{name}HasCondition") do
48
+ conditions("#{name}HasCondition", "Dynamic WHERE HAS conditions for #{path}.", 'String')
49
+ end
50
+ document_ast.define_type("#{name}Relation") { relation(name, "Dynamic HAS conditions for WHERE conditions for #{path}.") }
51
+ end
52
+
53
+ def define_default(document_ast)
54
+ define(document_ast, DEFAULT, 'String', 'any column')
55
+ end
56
+
57
+ def conditions(name, description, column_type)
58
+ <<~GRAPHQL
59
+ "#{description}"
60
+ input #{name} {
61
+ "The column that is used for the condition."
62
+ column: #{column_type}
63
+
64
+ "The operator that is used for the condition."
65
+ operator: #{SQL_OPERATOR} = #{DEFAULT_OPERATOR}
66
+
67
+ "The value that is used for the condition."
68
+ value: #{MIXED}
69
+
70
+ "A set of conditions that requires all conditions to match."
71
+ AND: [#{name}!]
72
+
73
+ "A set of conditions that requires at least one condition to match."
74
+ OR: [#{name}!]
75
+
76
+ "Check whether a relation exists. Extra conditions or a minimum amount can be applied."
77
+ HAS: #{name.delete_suffix('HasCondition')}Relation
78
+ }
79
+ GRAPHQL
80
+ end
81
+
82
+ def relation(name, description)
83
+ <<~GRAPHQL
84
+ "#{description}"
85
+ input #{name}Relation {
86
+ "The relation that is checked."
87
+ relation: String!
88
+
89
+ "The comparison operator to test against the amount."
90
+ operator: #{SQL_OPERATOR} = #{DEFAULT_HAS_OPERATOR}
91
+
92
+ "The amount to test."
93
+ amount: Int = #{DEFAULT_HAS_AMOUNT}
94
+
95
+ "Additional condition logic."
96
+ condition: #{name}HasCondition
97
+ }
98
+ GRAPHQL
99
+ end
100
+
101
+ def sql_operator
102
+ values = OPERATORS.map { |value, description| %( "#{description}"\n #{value}) }
103
+ <<~GRAPHQL
104
+ "The available SQL operators that are used to filter query results."
105
+ enum #{SQL_OPERATOR} {
106
+ #{values.join("\n\n")}
107
+ }
108
+ GRAPHQL
109
+ end
110
+ end
@@ -36,9 +36,17 @@ module Lighthouse
36
36
  include Directive
37
37
  end
38
38
 
39
+ # Its instances, built with (arg_node, directive_node), answer
40
+ # `manipulate_arg_definition(document_ast, field_node, type_node)` with the argument node.
39
41
  module ArgManipulator
40
42
  include Directive
41
43
  end
44
+
45
+ # Its instances, built with (field_node, directive_node), answer
46
+ # `handle_field_builder(relation)` with the constrained relation.
47
+ module FieldBuilder
48
+ include Directive
49
+ end
42
50
  end
43
51
 
44
52
  class DirectiveRegistry
@@ -67,6 +75,9 @@ module Lighthouse
67
75
  def resolution_names: () -> Array[String]
68
76
  def arg_builder_class: (String | Symbol name) -> Class?
69
77
  def arg_builder_names: () -> Array[String]
78
+ def arg_manipulator_class: (String | Symbol name) -> Class?
79
+ def arg_manipulator_classes: () -> Array[Class]
80
+ def field_builder_class: (String | Symbol name) -> Class?
70
81
 
71
82
  # { "name" => "directive @name(...) on ..." }
72
83
  def directive_definitions: () -> Hash[String, String]
@@ -83,9 +94,11 @@ module Lighthouse
83
94
  PROGRAMMATIC_TYPES_FILE: String
84
95
 
85
96
  # Writes both files into `output_dir` and returns their paths.
86
- def self?.write: (output_dir: path, ?sdl_folder: path?, ?registry: DirectiveRegistry) -> Array[String]
97
+ def self?.write: (output_dir: path, ?schema_path: path?, ?registry: DirectiveRegistry) -> Array[String]
98
+ # Config.ide_helper's rewrite, on boot and every reload.
99
+ def self?.refresh: (?output_dir: path) -> Array[String]
87
100
  def self?.directives_sdl: (?String sdl, ?registry: DirectiveRegistry) -> String
88
- def self?.programmatic_types_sdl: (String sdl, ?registry: DirectiveRegistry) -> String
101
+ def self?.programmatic_types_sdl: (String sdl, ?registry: DirectiveRegistry, ?directives: String) -> String
89
102
  end
90
103
 
91
104
  module GraphQL
@@ -107,7 +120,7 @@ module Lighthouse
107
120
  # `ctx`: the query's context, which @builder on an argument receives.
108
121
  def apply_where_filters: (relation relation, untyped args, ?context? ctx) -> relation
109
122
  def apply_arg_builders: (relation relation, arguments args_h, ?context? ctx) -> relation
110
- def extract_relation_name_from_directive: (untyped arg_node) -> String?
123
+ def apply_field_builders: (relation relation, ?arguments args_h, ?context? ctx) -> relation
111
124
  def constantize_model: (String? name) -> Class?
112
125
  def apply_scopes: (relation relation) -> relation
113
126
 
@@ -9,16 +9,60 @@ module Lighthouse
9
9
  type schema = untyped
10
10
 
11
11
  module RbLightHouse
12
- # The schema built from the SDL in `sdl_folder` (default: Config.sdl_folder).
12
+ # The schema built from the SDL `schema_path` (default: Config.schema_path) and its imports.
13
13
  # `options` takes :base_types, :base_schema and the schema's own options.
14
- def self.get_schema: (?sdl_folder: path?, ?options: Hash[Symbol, untyped]) -> schema
14
+ def self.get_schema: (?schema_path: path?, ?options: Hash[Symbol, untyped]) -> schema
15
15
  def self.configure: () ?{ (singleton(Lighthouse::Config) config) -> void } -> void
16
16
  end
17
17
 
18
18
  module SchemaFactory
19
- def self?.build_base_schema: (?sdl_folder: path?, ?sdl: String?, ?base_types: Hash[untyped, untyped],
19
+ def self?.build_base_schema: (?schema_path: path?, ?sdl: String?, ?base_types: Hash[untyped, untyped],
20
20
  ?schema_options: Hash[Symbol, untyped], **untyped extra_options) -> schema
21
- def self?.load_sdl: (path? sdl_folder) -> String
21
+ def self?.load_sdl: (?path? schema_path) -> String
22
+ end
23
+
24
+ # Reads the schema from its entrypoint, pasting in what it #imports.
25
+ class SchemaStitcher
26
+ IMPORT: String
27
+
28
+ attr_reader root_schema_path: String
29
+
30
+ def initialize: (path root_schema_path) -> void
31
+ def schema_string: () -> String
32
+ # Every file the schema is read from, the entrypoint first.
33
+ def files: () -> Array[String]
34
+ end
35
+
36
+ # The schema document the manipulation phase works on: each type defined once.
37
+ class DocumentAST
38
+ KEYWORDS: Hash[Class, String]
39
+ EXTENSIONS: Array[Class]
40
+
41
+ attr_reader types: Hash[String, untyped]
42
+ attr_reader type_extensions: Hash[String, Array[untyped]]
43
+ attr_reader directives: Hash[String, untyped]
44
+ attr_reader schema_definitions: Array[untyped]
45
+
46
+ def self.from_source: (String sdl) -> DocumentAST
47
+ def self.from_document: (untyped document) -> DocumentAST
48
+
49
+ def initialize: () -> void
50
+ def add: (untyped definition) -> untyped
51
+ # Adds a type, or replaces the one of the same name.
52
+ def put_type: (untyped definition) -> self
53
+ # Adds the type parsed from the SDL (or the block's), unless one of the name exists.
54
+ def define_type: (String name, ?String? sdl) ?{ () -> String } -> untyped
55
+ def type?: (String name) -> bool
56
+ def type_names: () -> Array[String]
57
+ def to_document: () -> untyped
58
+ def to_sdl: () -> String
59
+ end
60
+
61
+ # The manipulation phase: extensions merged, then the type, field and argument manipulators.
62
+ module ASTBuilder
63
+ ROOT_TYPES: Array[String]
64
+
65
+ def self?.build: (String sdl, ?registry: DirectiveRegistry) -> String
22
66
  end
23
67
 
24
68
  # Apollo's Automatic Persisted Queries, and a manifest of trusted operations.
data/sig/lighthouse.rbs CHANGED
@@ -92,8 +92,14 @@ module Lighthouse
92
92
  def self.[]: (Symbol | String key) -> untyped
93
93
  def self.reset!: () -> void
94
94
 
95
- def self.sdl_folder: () -> path?
96
- def self.sdl_folder=: (path? folder) -> path?
95
+ # The schema's entrypoint, which imports the other files (SchemaStitcher).
96
+ def self.schema_path: () -> path?
97
+ def self.schema_path=: (path? path) -> path?
98
+ def self.schema_directory: () -> String?
99
+
100
+ # Rewrite the IDE helper files on boot and every reload; nil: in development with a graphql.config.yml.
101
+ def self.ide_helper: () -> bool?
102
+ def self.ide_helper=: (bool? enabled) -> bool?
97
103
 
98
104
  # { queries:, mutations:, subscriptions:, builders:, validators:, events: } => [namespace, ...].
99
105
  # Assigning a partial hash overrides only the kinds it names.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lighthouse-graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vaz Innovation
@@ -70,6 +70,7 @@ files:
70
70
  - docs/relationships.md
71
71
  - docs/relay.md
72
72
  - docs/resolvers.md
73
+ - docs/schema-organisation.md
73
74
  - docs/security.md
74
75
  - docs/subscriptions.md
75
76
  - docs/types.md
@@ -87,6 +88,7 @@ files:
87
88
  - lib/generators/lighthouse/subscription_generator.rb
88
89
  - lib/generators/lighthouse/validator_generator.rb
89
90
  - lib/lighthouse-graphql.rb
91
+ - lib/lighthouse/ast_builder.rb
90
92
  - lib/lighthouse/async_mutation_job.rb
91
93
  - lib/lighthouse/broadcast_job.rb
92
94
  - lib/lighthouse/broadcasting.rb
@@ -98,6 +100,7 @@ files:
98
100
  - lib/lighthouse/contracts/arg_builder.rb
99
101
  - lib/lighthouse/contracts/arg_manipulator.rb
100
102
  - lib/lighthouse/contracts/directive.rb
103
+ - lib/lighthouse/contracts/field_builder.rb
101
104
  - lib/lighthouse/contracts/field_manipulator.rb
102
105
  - lib/lighthouse/contracts/field_middleware.rb
103
106
  - lib/lighthouse/contracts/field_resolver.rb
@@ -143,7 +146,6 @@ files:
143
146
  - lib/lighthouse/directive_resolvers/throttle_resolver.rb
144
147
  - lib/lighthouse/directive_resolvers/type_method_resolver.rb
145
148
  - lib/lighthouse/directive_resolvers/validator_resolver.rb
146
- - lib/lighthouse/directive_resolvers/where_conditions_applier.rb
147
149
  - lib/lighthouse/directive_resolvers/with_count_resolver.rb
148
150
  - lib/lighthouse/directive_resolvers/with_resolver.rb
149
151
  - lib/lighthouse/directives/arguments/base.rb
@@ -151,7 +153,11 @@ files:
151
153
  - lib/lighthouse/directives/arguments/more_filters.rb
152
154
  - lib/lighthouse/directives/arguments/order_by.rb
153
155
  - lib/lighthouse/directives/arguments/soft_deletes.rb
156
+ - lib/lighthouse/directives/arguments/where_conditions.rb
157
+ - lib/lighthouse/directives/arguments/where_conditions_base.rb
158
+ - lib/lighthouse/directives/arguments/where_has_conditions.rb
154
159
  - lib/lighthouse/directives/definitions.rb
160
+ - lib/lighthouse/directives/generated_types.rb
155
161
  - lib/lighthouse/directives/model_directive.rb
156
162
  - lib/lighthouse/directives/node_directive.rb
157
163
  - lib/lighthouse/directives/pagination_info_types.rb
@@ -159,7 +165,7 @@ files:
159
165
  - lib/lighthouse/directives/registry.rb
160
166
  - lib/lighthouse/directives/soft_deletes_directive.rb
161
167
  - lib/lighthouse/directives/visibility_directive.rb
162
- - lib/lighthouse/directives/where_conditions_directive.rb
168
+ - lib/lighthouse/document_ast.rb
163
169
  - lib/lighthouse/enum_directive.rb
164
170
  - lib/lighthouse/errors.rb
165
171
  - lib/lighthouse/events.rb
@@ -184,6 +190,7 @@ files:
184
190
  - lib/lighthouse/nested_mutations/many_to_many_operations.rb
185
191
  - lib/lighthouse/nested_mutations/partitioner.rb
186
192
  - lib/lighthouse/node_registry.rb
193
+ - lib/lighthouse/order_by/types.rb
187
194
  - lib/lighthouse/pagination.rb
188
195
  - lib/lighthouse/persisted_queries.rb
189
196
  - lib/lighthouse/persisted_queries/batch.rb
@@ -201,7 +208,7 @@ files:
201
208
  - lib/lighthouse/schema_factory.rb
202
209
  - lib/lighthouse/schema_generator.rb
203
210
  - lib/lighthouse/schema_implementation.rb
204
- - lib/lighthouse/sdl_loader.rb
211
+ - lib/lighthouse/schema_stitcher.rb
205
212
  - lib/lighthouse/subscription.rb
206
213
  - lib/lighthouse/support/arguments.rb
207
214
  - lib/lighthouse/support/authorization.rb
@@ -217,7 +224,11 @@ files:
217
224
  - lib/lighthouse/type_resolution.rb
218
225
  - lib/lighthouse/upload.rb
219
226
  - lib/lighthouse/upload_store.rb
227
+ - lib/lighthouse/where_conditions/columns.rb
228
+ - lib/lighthouse/where_conditions/handler.rb
229
+ - lib/lighthouse/where_conditions/json_column.rb
220
230
  - lib/lighthouse/where_conditions/operator_map.rb
231
+ - lib/lighthouse/where_conditions/types.rb
221
232
  - sig/lighthouse.rbs
222
233
  - sig/lighthouse/controller.rbs
223
234
  - sig/lighthouse/directives.rbs
@@ -1,170 +0,0 @@
1
- # frozen_string_literal: true
2
-
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
- {}
167
- end
168
- end
169
- end
170
- end