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
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Field-argument plumbing shared by resolvers: plain-Ruby conversion, @inject,
4
+ # @spread, and dot paths ("input.id", "*.id") into arguments and results.
5
+ module Lighthouse::Support::Arguments
6
+ module_function
7
+
8
+ # The arguments a resolver class receives: @inject applied, then @spread.
9
+ # Values keep their GraphQL input-object form except where spread.
10
+ def prepare(field_def, args, context)
11
+ values = inject(field_def.ast_node, shallow(args), context)
12
+ spread(field_def.arguments, values)
13
+ end
14
+
15
+ # Arguments / input objects as a Hash with symbol keys, one level deep.
16
+ def shallow(value)
17
+ case value
18
+ when ::GraphQL::Schema::InputObject then value.to_h.transform_keys(&:to_sym)
19
+ when ::GraphQL::Execution::Interpreter::Arguments then value.keyword_arguments.dup
20
+ when Hash then value.transform_keys(&:to_sym)
21
+ else {}
22
+ end
23
+ end
24
+
25
+ # Input objects, arguments and nested collections as plain Ruby values.
26
+ def plain(value)
27
+ case value
28
+ when ::GraphQL::Schema::InputObject then plain(value.to_h)
29
+ when ::GraphQL::Execution::Interpreter::Arguments then plain(value.keyword_arguments)
30
+ when Hash then value.to_h { |k, v| [k.to_sym, plain(v)] }
31
+ when Array then value.map { |v| plain(v) }
32
+ else value
33
+ end
34
+ end
35
+
36
+ # @inject(context: "current_user.id", name: "user_id"): server-side values
37
+ # written into the arguments, so clients cannot set them.
38
+ def inject(field_ast_node, values, context)
39
+ directives = (field_ast_node&.directives || []).select { |d| d.name == 'inject' }
40
+ directives.reduce(values) do |acc, directive|
41
+ from = Lighthouse::Support::DirectiveArgs.literal(directive, 'context').to_s
42
+ to = Lighthouse::Support::DirectiveArgs.literal(directive, 'name').to_s
43
+ set_path(acc, to.split('.'), dig(context, from).first)
44
+ end
45
+ end
46
+
47
+ # Merge every @spread argument's input object into its parent, recursively.
48
+ def spread(argument_defs, values)
49
+ by_keyword = keyword_index(argument_defs)
50
+ values.each_with_object({}) do |(key, value), out|
51
+ argument = by_keyword[key.to_sym]
52
+ if argument && spread?(argument) && !value.nil?
53
+ out.merge!(spread(argument.type.unwrap.arguments, shallow(value)))
54
+ else
55
+ out[key] = value
56
+ end
57
+ end
58
+ end
59
+
60
+ def spread?(argument)
61
+ (argument.ast_node&.directives || []).any? { |d| d.name == 'spread' }
62
+ end
63
+
64
+ def keyword_index(argument_defs)
65
+ (argument_defs || {}).values.index_by(&:keyword)
66
+ end
67
+
68
+ # Every value at a dot `path` under `root`; `*` walks each list element.
69
+ # GraphQL names are matched in their snake_case form. Always an Array.
70
+ def dig(root, path)
71
+ path.to_s.split('.').reduce([root]) do |nodes, segment|
72
+ nodes.flat_map do |node|
73
+ next [] if node.nil?
74
+ next list(node) || [node] if segment == '*'
75
+
76
+ [read(node, segment)]
77
+ end
78
+ end.compact
79
+ end
80
+
81
+ # The items of a collection (Array, Set, relation), or nil for a single
82
+ # object — records and Structs answer #to_a too, so that test won't do.
83
+ def list(value)
84
+ case value
85
+ when Array then value
86
+ when Set then value.to_a
87
+ else value.to_a if defined?(::ActiveRecord::Relation) && value.is_a?(::ActiveRecord::Relation)
88
+ end
89
+ end
90
+
91
+ def read(node, segment)
92
+ keys = [segment, segment.underscore].uniq
93
+ if node.respond_to?(:key?)
94
+ stored = stored_key(node, keys)
95
+ return node[stored] unless stored.nil?
96
+ return nil if node.is_a?(Hash)
97
+ end
98
+ method = keys.reverse.find { |key| node.respond_to?(key) }
99
+ method ? node.public_send(method) : nil
100
+ end
101
+
102
+ # The key a keyed node holds the first of `keys` under — Symbol before
103
+ # String — or nil when it holds none of them.
104
+ def stored_key(node, keys)
105
+ keys.each do |key|
106
+ return key.to_sym if node.key?(key.to_sym)
107
+ return key if node.key?(key)
108
+ end
109
+ nil
110
+ end
111
+
112
+ def set_path(values, segments, value)
113
+ key, *rest = segments
114
+ key = key.underscore.to_sym
115
+ copy = values.dup
116
+ copy[key] = rest.empty? ? value : set_path(shallow(copy[key]), rest, value)
117
+ copy
118
+ end
119
+ end
@@ -1,26 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Lighthouse
4
- module Support
5
- # Thin wrapper over the configurable authorization seams, used by the @guard
6
- # and @can directives so they stay framework-agnostic.
7
- module Authorization
8
- module_function
3
+ # Thin wrapper over the configurable authorization seams, used by the @guard
4
+ # and @can directives so they stay framework-agnostic.
5
+ module Lighthouse::Support::Authorization
6
+ module_function
9
7
 
10
- def authenticated_user(context)
11
- Lighthouse::Config.authenticated_user.call(context)
12
- end
8
+ def authenticated_user(context)
9
+ Lighthouse::Config.authenticated_user.call(context)
10
+ end
13
11
 
14
- def authenticated?(context)
15
- !authenticated_user(context).nil?
16
- end
12
+ def authenticated?(context)
13
+ !authenticated_user(context).nil?
14
+ end
17
15
 
18
- # Returns true/false for `ability` against `record` for the current policy
19
- # user. Defaults to Pundit (see Config#authorizer).
20
- def authorized?(context, record, ability)
21
- user = Lighthouse::Config.policy_user.call(context)
22
- Lighthouse::Config.authorizer.call(user, record, ability)
23
- end
24
- end
16
+ # Returns true/false for `ability` against `record` (an instance, or a model
17
+ # class) for the current policy user. Defaults to Pundit (see
18
+ # Config#authorizer). `args` reach the authorizer only when given, so a
19
+ # three-argument authorizer keeps working for checks without them.
20
+ def authorized?(context, record, ability, args = [])
21
+ user = Lighthouse::Config.policy_user.call(context)
22
+ authorizer = Lighthouse::Config.authorizer
23
+ args.empty? ? authorizer.call(user, record, ability) : authorizer.call(user, record, ability, args)
25
24
  end
26
25
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Access to the query's GraphQL::Dataloader. graphql-ruby hands every query a
4
+ # dataloader — a NullDataloader, whose sources refuse to run, when the schema
5
+ # does not `use GraphQL::Dataloader` — so "is there one" needs this check.
6
+ module Lighthouse::Support::Batching
7
+ module_function
8
+
9
+ def dataloader(context)
10
+ dataloader = context.respond_to?(:dataloader) ? context.dataloader : nil
11
+ return nil if dataloader.nil?
12
+ return nil if defined?(::GraphQL::Dataloader::NullDataloader) && dataloader.is_a?(::GraphQL::Dataloader::NullDataloader)
13
+
14
+ dataloader
15
+ end
16
+ end
@@ -1,33 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Lighthouse
4
- module Support
5
- # Extracts argument values from a GraphQL directive AST node, unwrapping the
6
- # various value-node shapes (scalar literals, enums, lists) into plain Ruby
7
- # values. Consolidates the repeated
8
- # `arg.value.respond_to?(:value) ? arg.value.value : arg.value` pattern.
9
- module DirectiveArgs
10
- module_function
3
+ # Extracts argument values from a GraphQL directive AST node, unwrapping the
4
+ # various value-node shapes (scalar literals, enums, lists) into plain Ruby
5
+ # values. Consolidates the repeated
6
+ # `arg.value.respond_to?(:value) ? arg.value.value : arg.value` pattern.
7
+ module Lighthouse::Support::DirectiveArgs
8
+ module_function
11
9
 
12
- # Return the literal value of directive argument `name`, or nil.
13
- def literal(directive_node, name)
14
- return nil unless directive_node.respond_to?(:arguments)
10
+ # Return the literal value of directive argument `name`, or nil.
11
+ def literal(directive_node, name)
12
+ return nil unless directive_node.respond_to?(:arguments)
15
13
 
16
- arg = directive_node.arguments&.find { |a| a.name == name.to_s }
17
- return nil unless arg
14
+ arg = directive_node.arguments&.find { |a| a.name == name.to_s }
15
+ return nil unless arg
18
16
 
19
- unwrap(arg.value)
20
- end
17
+ unwrap(arg.value)
18
+ end
19
+
20
+ def unwrap(value)
21
+ case value
22
+ when ::GraphQL::Language::Nodes::Enum then return value.name
23
+ when ::GraphQL::Language::Nodes::NullValue then return nil
24
+ when ::GraphQL::Language::Nodes::InputObject then return value.to_h
25
+ end
21
26
 
22
- def unwrap(value)
23
- if value.respond_to?(:value)
24
- value.value
25
- elsif value.is_a?(Array)
26
- value.map { |item| unwrap(item) }
27
- else
28
- value
29
- end
30
- end
27
+ if value.respond_to?(:value)
28
+ value.value
29
+ elsif value.is_a?(Array)
30
+ value.map { |item| unwrap(item) }
31
+ else
32
+ value
31
33
  end
32
34
  end
33
35
  end
@@ -1,61 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Lighthouse
4
- module Support
5
- # Resolves a class name string (from a directive argument such as
6
- # `@field(resolver: "Resolvers::ContactPerson")` or `@all(model: "Contact")`)
7
- # into an actual constant.
8
- #
9
- # The list of namespaces to try is configurable via
10
- # `Lighthouse::Config.resolver_namespaces`, so the library does not need to
11
- # know any app-specific namespace. This is a key seam for extracting the
12
- # library into a standalone gem.
13
- module ModelResolver
14
- module_function
15
-
16
- DEFAULT_NAMESPACES = ['App::Graphql'].freeze
17
-
18
- def namespaces
19
- if defined?(Lighthouse::Config) &&
20
- Lighthouse::Config.respond_to?(:resolver_namespaces) &&
21
- Lighthouse::Config.resolver_namespaces
22
- Array(Lighthouse::Config.resolver_namespaces)
23
- else
24
- DEFAULT_NAMESPACES
25
- end
26
- end
27
-
28
- def constantize(name)
29
- return nil unless name
30
-
31
- raw = name.to_s.strip
32
- return nil if raw.empty?
33
-
34
- candidates_for(raw).each do |candidate|
35
- klass = try_constantize(candidate)
36
- return klass if klass
37
- end
38
-
39
- nil
40
- end
41
-
42
- def candidates_for(raw)
43
- if raw.include?('::')
44
- [raw]
45
- else
46
- namespaces.map { |ns| "#{ns}::#{raw}" } + [raw]
47
- end
48
- end
49
-
50
- def try_constantize(candidate)
51
- ActiveSupport::Dependencies.constantize(candidate)
52
- rescue NameError, LoadError
53
- begin
54
- Object.const_get(candidate)
55
- rescue NameError
56
- nil
57
- end
58
- end
3
+ # Resolves a class name string (from a directive argument such as
4
+ # `@field(resolver: "Resolvers::ContactPerson")` or `@all(model: "Contact")`)
5
+ # into an actual constant.
6
+ #
7
+ # The list of namespaces to try is configurable via
8
+ # `Lighthouse::Config.resolver_namespaces`, so the library does not need to
9
+ # know any app-specific namespace. This is a key seam for extracting the
10
+ # library into a standalone gem.
11
+ module Lighthouse::Support::ModelResolver
12
+ module_function
13
+
14
+ DEFAULT_NAMESPACES = ['App::Graphql'].freeze
15
+
16
+ def namespaces
17
+ if defined?(Lighthouse::Config) &&
18
+ Lighthouse::Config.respond_to?(:resolver_namespaces) &&
19
+ Lighthouse::Config.resolver_namespaces
20
+ Array(Lighthouse::Config.resolver_namespaces)
21
+ else
22
+ DEFAULT_NAMESPACES
23
+ end
24
+ end
25
+
26
+ # `namespaces:` overrides the configured resolver_namespaces, e.g. with
27
+ # Config.namespaces[:builders] to look a builder class up.
28
+ # `fallback: false` skips the bare name, so `Queries::User` is never
29
+ # mistaken for the `User` model.
30
+ def constantize(name, namespaces: self.namespaces, fallback: true)
31
+ return nil unless name
32
+
33
+ raw = name.to_s.strip
34
+ return nil if raw.empty?
35
+
36
+ candidates_for(raw, namespaces, fallback: fallback).each do |candidate|
37
+ klass = try_constantize(candidate)
38
+ return klass if klass
39
+ end
40
+
41
+ nil
42
+ end
43
+
44
+ def candidates_for(raw, namespaces = self.namespaces, fallback: true)
45
+ if raw.include?('::')
46
+ [raw]
47
+ else
48
+ Array(namespaces).map { |ns| "#{ns}::#{raw}" } + (fallback ? [raw] : [])
49
+ end
50
+ end
51
+
52
+ def try_constantize(candidate)
53
+ ActiveSupport::Dependencies.constantize(candidate)
54
+ rescue NameError, LoadError
55
+ begin
56
+ Object.const_get(candidate)
57
+ rescue NameError
58
+ nil
59
59
  end
60
60
  end
61
61
  end
@@ -1,71 +1,67 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Lighthouse
4
- module Support
5
- # Central rule for translating GraphQL (camelCase) names into Rails
6
- # (snake_case) attributes/columns, and for tolerant argument lookup.
7
- #
8
- # This is the single home for casing logic; every directive/resolver that
9
- # needs to map a GraphQL name to a model attribute or DB column routes here
10
- # instead of re-implementing `.underscore` fallbacks.
11
- module Naming
12
- module_function
3
+ # Central rule for translating GraphQL (camelCase) names into Rails
4
+ # (snake_case) attributes/columns, and for tolerant argument lookup.
5
+ #
6
+ # This is the single home for casing logic; every directive/resolver that
7
+ # needs to map a GraphQL name to a model attribute or DB column routes here
8
+ # instead of re-implementing `.underscore` fallbacks.
9
+ module Lighthouse::Support::Naming
10
+ module_function
13
11
 
14
- # GraphQL field name -> Rails attribute/method name.
15
- # e.g. "customAttributes" -> "custom_attributes"
16
- #
17
- # This is the default field-reader rule and the basis for relation method
18
- # name derivation. The `@rename(attribute:)` directive is the explicit
19
- # override that bypasses this.
20
- def attribute(graphql_name)
21
- graphql_name.to_s.underscore
22
- end
12
+ # GraphQL field name -> Rails attribute/method name.
13
+ # e.g. "customAttributes" -> "custom_attributes"
14
+ #
15
+ # This is the default field-reader rule and the basis for relation method
16
+ # name derivation. The `@rename(attribute:)` directive is the explicit
17
+ # override that bypasses this.
18
+ def attribute(graphql_name)
19
+ graphql_name.to_s.underscore
20
+ end
23
21
 
24
- # Resolve a GraphQL "column" token (often an ALLCAPS enum key, or a raw
25
- # string) to a usable column name.
26
- #
27
- # NOTE: kept behavior-compatible with the historical `resolve_column_name`
28
- # so existing whereConditions queries are unaffected. Word-boundary
29
- # recovery for enum keys is handled separately by generating column enums
30
- # with explicit `@enum(value:)` mappings.
31
- def column(graphql_name, _relation = nil)
32
- name = graphql_name.to_s
33
- name = name.downcase if name == name.upcase
34
- name = name.tr('-', '_')
35
- name = name.underscore if name.include?(' ')
36
- name
37
- end
22
+ # Resolve a GraphQL "column" token (often an ALLCAPS enum key, or a raw
23
+ # string) to a usable column name.
24
+ #
25
+ # NOTE: kept behavior-compatible with the historical `resolve_column_name`
26
+ # so existing whereConditions queries are unaffected. Word-boundary
27
+ # recovery for enum keys is handled separately by generating column enums
28
+ # with explicit `@enum(value:)` mappings.
29
+ def column(graphql_name, _relation = nil)
30
+ name = graphql_name.to_s
31
+ name = name.downcase if name == name.upcase
32
+ name = name.tr('-', '_')
33
+ name = name.underscore if name.include?(' ')
34
+ name
35
+ end
38
36
 
39
- # Fetch an argument value from a hash tolerant of camelCase/snake_case and
40
- # string/symbol keys. Replaces the ad-hoc `args[:x] || args['x'] || ...`
41
- # chains scattered across resolvers.
42
- def fetch_arg(args_h, name)
43
- return nil if args_h.nil?
37
+ # Fetch an argument value from a hash tolerant of camelCase/snake_case and
38
+ # string/symbol keys. Replaces the ad-hoc `args[:x] || args['x'] || ...`
39
+ # chains scattered across resolvers.
40
+ def fetch_arg(args_h, name)
41
+ return nil if args_h.nil?
44
42
 
45
- candidate_keys(name).each do |key|
46
- value = args_h[key]
47
- return value unless value.nil?
48
- end
49
- nil
50
- end
43
+ candidate_keys(name).each do |key|
44
+ value = args_h[key]
45
+ return value unless value.nil?
46
+ end
47
+ nil
48
+ end
51
49
 
52
- def candidate_keys(name)
53
- raw = name.to_s
54
- snake = raw.underscore
55
- [raw.to_sym, raw, snake.to_sym, snake].uniq
56
- end
50
+ def candidate_keys(name)
51
+ raw = name.to_s
52
+ snake = raw.underscore
53
+ [raw.to_sym, raw, snake.to_sym, snake].uniq
54
+ end
57
55
 
58
- # Derive an Eloquent-style relation name from a `has<Relation>` argument
59
- # name, e.g. "hasContact" -> "contacts". Returns nil when the argument does
60
- # not follow the `has` convention.
61
- def derive_relation_name(arg_name)
62
- return nil unless arg_name.to_s.match?(/^has/i)
56
+ # Derive an Eloquent-style relation name from a `has<Relation>` argument
57
+ # name, e.g. "hasContact" -> "contacts". Returns nil when the argument does
58
+ # not follow the `has` convention.
59
+ def derive_relation_name(arg_name)
60
+ return nil unless arg_name.to_s.match?(/^has/i)
63
61
 
64
- trailing = arg_name.to_s.sub(/^has/i, '')
65
- return nil if trailing.empty?
62
+ trailing = arg_name.to_s.sub(/^has/i, '')
63
+ return nil if trailing.empty?
66
64
 
67
- trailing.underscore.pluralize
68
- end
69
- end
65
+ trailing.underscore.pluralize
70
66
  end
71
67
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :lighthouse do
4
+ desc 'Write schema-directives.graphql and programmatic-types.graphql for IDE support (OUTPUT=dir, default Rails.root)'
5
+ task ide_helper: :environment do
6
+ output_dir = ENV.fetch('OUTPUT') { Rails.root.to_s }
7
+ Lighthouse::IdeHelper.write(output_dir: output_dir).each { |path| puts "Wrote #{path}" }
8
+ end
9
+
10
+ desc 'Build the schema cache in tmp/cache (for config.schema_cache; e.g. during a Docker build)'
11
+ task cache: :environment do
12
+ puts "Wrote #{Lighthouse::GraphQL::SchemaCache.warm!}"
13
+ end
14
+
15
+ desc 'Delete the schema cache'
16
+ task clear_cache: :environment do
17
+ Lighthouse::GraphQL::SchemaCache.clear!
18
+ puts "Cleared #{Lighthouse::GraphQL::SchemaCache.directory}"
19
+ end
20
+
21
+ namespace :persisted_queries do
22
+ desc 'Validate every operation of the persisted query manifest against the schema (SCHEMA=AppSchema)'
23
+ task verify: :environment do
24
+ schema = Object.const_get(ENV.fetch('SCHEMA', 'AppSchema'))
25
+ failures = Lighthouse::GraphQL::PersistedQueries.manifest.filter_map do |hash, body|
26
+ errors = schema.validate(body)
27
+ "#{hash}: #{errors.map(&:message).join('; ')}" if errors.any?
28
+ end
29
+ abort("Operations the schema no longer accepts:\n#{failures.join("\n")}") if failures.any?
30
+ puts "#{Lighthouse::GraphQL::PersistedQueries.manifest.size} persisted operations are valid."
31
+ end
32
+ end
33
+ end
@@ -1,35 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Lighthouse
4
- module WhereConditions
5
- # Maps the GraphQL SQLOperator enum values to Arel predicate methods.
6
- # Extracted from WhereConditionsApplier so that simple argument-filter
7
- # directives (@eq, @neq, @in, @notIn, @like, @where) can share the same
8
- # operator semantics.
9
- module OperatorMap
10
- OPERATORS = {
11
- 'EQ' => :eq,
12
- 'NEQ' => :not_eq,
13
- 'GT' => :gt,
14
- 'GTE' => :gteq,
15
- 'LT' => :lt,
16
- 'LTE' => :lteq,
17
- 'IN' => :in,
18
- 'NIN' => :not_in,
19
- 'CONTAINS' => :matches,
20
- 'STARTS_WITH' => :matches,
21
- 'ENDS_WITH' => :matches,
22
- 'IS_NULL' => :eq,
23
- 'IS_NOT_NULL' => :not_eq
24
- }.freeze
3
+ # Maps the GraphQL SQLOperator enum values to Arel predicate methods.
4
+ # Extracted from WhereConditionsApplier so that simple argument-filter
5
+ # directives (@eq, @neq, @in, @notIn, @like, @where) can share the same
6
+ # operator semantics.
7
+ module Lighthouse::WhereConditions::OperatorMap
8
+ OPERATORS = {
9
+ 'EQ' => :eq,
10
+ 'NEQ' => :not_eq,
11
+ 'GT' => :gt,
12
+ 'GTE' => :gteq,
13
+ 'LT' => :lt,
14
+ 'LTE' => :lteq,
15
+ 'IN' => :in,
16
+ 'NIN' => :not_in,
17
+ 'CONTAINS' => :matches,
18
+ 'STARTS_WITH' => :matches,
19
+ 'ENDS_WITH' => :matches,
20
+ 'IS_NULL' => :eq,
21
+ 'IS_NOT_NULL' => :not_eq
22
+ }.freeze
25
23
 
26
- def self.[](operator)
27
- OPERATORS[operator]
28
- end
24
+ def self.[](operator)
25
+ OPERATORS[operator]
26
+ end
29
27
 
30
- def self.supported?(operator)
31
- OPERATORS.key?(operator)
32
- end
33
- end
28
+ def self.supported?(operator)
29
+ OPERATORS.key?(operator)
34
30
  end
35
31
  end