lighthouse-graphql 0.5.0 → 0.7.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +212 -1
- data/README.md +38 -22
- data/docs/best-practices.md +6 -4
- data/docs/caching.md +4 -3
- data/docs/configuration.md +20 -26
- data/docs/custom-directives.md +2 -1
- data/docs/directives.md +79 -14
- data/docs/federation.md +1 -1
- data/docs/filtering-and-ordering.md +330 -30
- data/docs/generators.md +42 -40
- data/docs/getting-started.md +128 -58
- data/docs/motivation.md +3 -2
- data/docs/performance.md +5 -4
- data/docs/relationships.md +7 -5
- data/docs/relay.md +63 -0
- data/docs/resolvers.md +92 -39
- data/docs/schema-organisation.md +177 -0
- data/docs/security.md +64 -16
- data/docs/subscriptions.md +16 -9
- data/docs/types.md +16 -17
- data/docs/uploads.md +41 -14
- data/lib/generators/lighthouse/base_classes.rb +9 -14
- data/lib/generators/lighthouse/install/graphql_ruby_install.rb +113 -0
- data/lib/generators/lighthouse/install/install_generator.rb +125 -99
- data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +2 -1
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +14 -13
- data/lib/lighthouse/ast_builder.rb +124 -0
- data/lib/lighthouse/caching.rb +9 -3
- data/lib/lighthouse/config.rb +27 -25
- data/lib/lighthouse/contracts/arg_manipulator.rb +9 -5
- data/lib/lighthouse/contracts/field_builder.rb +13 -0
- data/lib/lighthouse/controller.rb +28 -5
- data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +1 -2
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +0 -2
- data/lib/lighthouse/directive_resolvers/argument_transforms_resolver.rb +18 -18
- data/lib/lighthouse/directive_resolvers/association_resolvers.rb +9 -6
- data/lib/lighthouse/directive_resolvers/base.rb +6 -37
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +21 -6
- data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +1 -1
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +0 -1
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +0 -2
- data/lib/lighthouse/directive_resolvers/global_id_resolver.rb +3 -1
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +2 -3
- data/lib/lighthouse/directive_resolvers/throttle_resolver.rb +85 -26
- data/lib/lighthouse/directive_resolvers/type_method_resolver.rb +12 -6
- data/lib/lighthouse/directives/arguments/more_filters.rb +1 -1
- data/lib/lighthouse/directives/arguments/order_by.rb +190 -26
- data/lib/lighthouse/directives/arguments/where_conditions.rb +25 -0
- data/lib/lighthouse/directives/arguments/where_conditions_base.rb +84 -0
- data/lib/lighthouse/directives/arguments/where_has_conditions.rb +56 -0
- data/lib/lighthouse/directives/definitions.rb +0 -1
- data/lib/lighthouse/directives/generated_types.rb +103 -0
- data/lib/lighthouse/directives/registry.rb +39 -10
- data/lib/lighthouse/document_ast.rb +111 -0
- data/lib/lighthouse/field_resolver_stack.rb +9 -6
- data/lib/lighthouse/global_id.rb +29 -3
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/graphql_ruby_patches.rb +45 -0
- data/lib/lighthouse/ide_helper.rb +80 -23
- data/lib/lighthouse/namespaces.rb +1 -0
- data/lib/lighthouse/order_by/types.rb +137 -0
- data/lib/lighthouse/railtie.rb +9 -2
- data/lib/lighthouse/rb_lighthouse.rb +16 -5
- data/lib/lighthouse/resolve_info.rb +5 -5
- data/lib/lighthouse/resolver_bases.rb +18 -4
- data/lib/lighthouse/resolver_dispatch.rb +16 -1
- data/lib/lighthouse/root_types.rb +117 -0
- data/lib/lighthouse/schema_cache.rb +8 -4
- data/lib/lighthouse/schema_factory.rb +6 -9
- data/lib/lighthouse/schema_generator.rb +8 -18
- data/lib/lighthouse/schema_stitcher.rb +64 -0
- data/lib/lighthouse/support/naming.rb +0 -12
- data/lib/lighthouse/upload.rb +4 -2
- data/lib/lighthouse/upload_store.rb +39 -14
- data/lib/lighthouse/where_conditions/columns.rb +36 -0
- data/lib/lighthouse/where_conditions/handler.rb +152 -0
- data/lib/lighthouse/where_conditions/json_column.rb +99 -0
- data/lib/lighthouse/where_conditions/operator_map.rb +1 -4
- data/lib/lighthouse/where_conditions/types.rb +110 -0
- data/sig/lighthouse/directives.rbs +16 -3
- data/sig/lighthouse/graphql.rbs +48 -4
- data/sig/lighthouse.rbs +8 -12
- metadata +17 -7
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +0 -29
- data/lib/generators/lighthouse/mutation_generator.rb +0 -31
- data/lib/generators/lighthouse/query_generator.rb +0 -30
- data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +0 -170
- data/lib/lighthouse/directives/where_conditions_directive.rb +0 -244
- data/lib/lighthouse/sdl_loader.rb +0 -22
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
# Lighthouse configuration. Every option is optional; the defaults are shown.
|
|
4
4
|
# Reference: https://github.com/Vaz-Innovation/lighthouse-graphql/blob/main/docs/configuration.md
|
|
5
5
|
Lighthouse.configure do |config|
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
#
|
|
6
|
+
# The schema's entrypoint. It imports the other .graphql files
|
|
7
|
+
# (#import user.graphql, #import blog/*.graphql), which add to a type
|
|
8
|
+
# defined elsewhere with `extend type`.
|
|
9
|
+
<% if default_sdl_schema_path? -%>
|
|
10
|
+
# config.schema_path = Rails.root.join('graphql/schema.graphql').to_s
|
|
9
11
|
<% else -%>
|
|
10
|
-
config.
|
|
12
|
+
config.schema_path = Rails.root.join('<%= sdl_schema_path %>').to_s
|
|
11
13
|
<% end -%>
|
|
12
14
|
|
|
13
15
|
# Where classes named by the schema are looked up, by kind. A Query or Mutation
|
|
@@ -19,8 +21,9 @@ Lighthouse.configure do |config|
|
|
|
19
21
|
# @event(dispatch:) classes come from :events, and the graphql-ruby analyzers
|
|
20
22
|
# @complexity(resolver:) names from :analyzers. Post.readingTime, with no
|
|
21
23
|
# resolving directive, resolves with Types::PostType#reading_time (a
|
|
22
|
-
# Types::BaseObject including Lighthouse::GraphQL::Type), from :types. Generate
|
|
23
|
-
# `rails g lighthouse:
|
|
24
|
+
# Types::BaseObject including Lighthouse::GraphQL::Type), from :types. Generate a
|
|
25
|
+
# subscription, builder or validator with `rails g lighthouse:subscription|builder|validator Name`,
|
|
26
|
+
# and a type class with graphql-ruby's `rails g graphql:object|union|interface|scalar Name`.
|
|
24
27
|
# config.namespaces = {
|
|
25
28
|
# queries: ['Resolvers'], mutations: ['Mutations'], subscriptions: ['Subscriptions'],
|
|
26
29
|
# builders: ['Builders'], validators: ['Validators'], events: ['Events'], analyzers: ['Analyzers'],
|
|
@@ -33,13 +36,6 @@ Lighthouse.configure do |config|
|
|
|
33
36
|
# Fully-qualified names (containing `::`) are used as-is.
|
|
34
37
|
# config.resolver_namespaces = ['App::Graphql']
|
|
35
38
|
|
|
36
|
-
# Store for @cache, @clearCache and @throttle's counts.
|
|
37
|
-
# config.cache_store = Rails.cache
|
|
38
|
-
|
|
39
|
-
# Where @upload stores a file: call(file, disk:, path:, public:) returns what the
|
|
40
|
-
# argument holds. By default an ActiveStorage blob, and its signed id.
|
|
41
|
-
# config.upload_store = Lighthouse::GraphQL::UploadStore
|
|
42
|
-
|
|
43
39
|
# Run @create/@update/@createMany/@updateMany in a database transaction.
|
|
44
40
|
# config.transactional_mutations = true
|
|
45
41
|
|
|
@@ -64,6 +60,11 @@ Lighthouse.configure do |config|
|
|
|
64
60
|
# Reload the app (and so the schema) when a .graphql file changes.
|
|
65
61
|
# config.watch_sdl = Rails.env.development?
|
|
66
62
|
|
|
63
|
+
# Rewrite schema-directives.graphql and programmatic-types.graphql, which your
|
|
64
|
+
# IDE reads through graphql.config.yml, on boot and on every reload. nil: in
|
|
65
|
+
# development, when the app has a graphql.config.yml.
|
|
66
|
+
# config.ide_helper = nil
|
|
67
|
+
|
|
67
68
|
# Automatic Persisted Queries (Apollo's protocol). Add a GET route too, so
|
|
68
69
|
# hashed queries can be cached by CDNs: get "/graphql", to: "graphql#execute"
|
|
69
70
|
# config.persisted_queries = true
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The manipulation phase, as PHP Lighthouse's ASTBuilder runs it on the
|
|
4
|
+
# stitched schema:
|
|
5
|
+
#
|
|
6
|
+
# 1. DocumentAST.from_source: every type defined once.
|
|
7
|
+
# 2. Type extensions merged into the types they extend. An extension of a
|
|
8
|
+
# root type that has no definition (`extend type Query` alone) defines it.
|
|
9
|
+
# 3. The type and field manipulators: @hide/@show, @model, @paginate (and the
|
|
10
|
+
# relation directives' `type:`), @softDeletes, @node.
|
|
11
|
+
# 4. The argument manipulators: each argument directive whose class includes
|
|
12
|
+
# Contracts::ArgManipulator rewrites its argument (@whereConditions,
|
|
13
|
+
# @whereHasConditions, @orderBy turn `_` into the input types they generate).
|
|
14
|
+
# 5. The definitions of directives the generated types use (`@enum`).
|
|
15
|
+
#
|
|
16
|
+
# Returns the manipulated SDL; ManipulateAST listeners then see its document.
|
|
17
|
+
module Lighthouse::GraphQL::ASTBuilder
|
|
18
|
+
Nodes = ::GraphQL::Language::Nodes
|
|
19
|
+
|
|
20
|
+
EXTENSION_TO_DEFINITION = {
|
|
21
|
+
Nodes::ObjectTypeExtension => Nodes::ObjectTypeDefinition,
|
|
22
|
+
Nodes::InterfaceTypeExtension => Nodes::InterfaceTypeDefinition,
|
|
23
|
+
Nodes::InputObjectTypeExtension => Nodes::InputObjectTypeDefinition,
|
|
24
|
+
Nodes::EnumTypeExtension => Nodes::EnumTypeDefinition,
|
|
25
|
+
Nodes::UnionTypeExtension => Nodes::UnionTypeDefinition,
|
|
26
|
+
Nodes::ScalarTypeExtension => Nodes::ScalarTypeDefinition
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
# What each extension adds to its definition, besides directives.
|
|
30
|
+
MERGED_LISTS = {
|
|
31
|
+
Nodes::ObjectTypeExtension => %i[fields interfaces],
|
|
32
|
+
Nodes::InterfaceTypeExtension => %i[fields interfaces],
|
|
33
|
+
Nodes::InputObjectTypeExtension => %i[fields],
|
|
34
|
+
Nodes::EnumTypeExtension => %i[values],
|
|
35
|
+
Nodes::UnionTypeExtension => %i[types],
|
|
36
|
+
Nodes::ScalarTypeExtension => []
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
ROOT_TYPES = %w[Query Mutation Subscription].freeze
|
|
40
|
+
|
|
41
|
+
OBJECT_LIKE = [Nodes::ObjectTypeDefinition, Nodes::InterfaceTypeDefinition].freeze
|
|
42
|
+
|
|
43
|
+
module_function
|
|
44
|
+
|
|
45
|
+
def build(sdl, registry: Lighthouse::DirectiveRegistry.default)
|
|
46
|
+
document_ast = Lighthouse::GraphQL::DocumentAST.from_source(sdl)
|
|
47
|
+
extend_types(document_ast)
|
|
48
|
+
|
|
49
|
+
document_ast = Lighthouse::GraphQL::DocumentAST.from_source(registry.manipulate_sdl(document_ast.to_sdl))
|
|
50
|
+
apply_arg_manipulators(document_ast, registry)
|
|
51
|
+
|
|
52
|
+
Lighthouse::GraphQL::SchemaGenerator.inject_directive_definitions(document_ast.to_sdl)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def extend_types(document_ast)
|
|
56
|
+
document_ast.type_extensions.each do |name, extensions|
|
|
57
|
+
extensions.each { |extension| extend_type(document_ast, name, extension) }
|
|
58
|
+
end
|
|
59
|
+
document_ast.type_extensions.clear
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def extend_type(document_ast, name, extension)
|
|
63
|
+
definition = document_ast.types[name] || root_type(document_ast, name, extension)
|
|
64
|
+
unless definition.is_a?(EXTENSION_TO_DEFINITION.fetch(extension.class))
|
|
65
|
+
raise Lighthouse::DefinitionError,
|
|
66
|
+
"The type extension #{name} of kind #{kind(extension)} can not extend a definition of kind #{kind(definition)}."
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
merged = MERGED_LISTS.fetch(extension.class).index_with { |list| merge_unique(definition.public_send(list), extension.public_send(list)) }
|
|
70
|
+
document_ast.put_type(definition.merge(directives: definition.directives + extension.directives, **merged))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# `extend type Query` with no `type Query` anywhere defines it, as in PHP Lighthouse.
|
|
74
|
+
def root_type(document_ast, name, extension)
|
|
75
|
+
unless ROOT_TYPES.include?(name) && extension.is_a?(Nodes::ObjectTypeExtension)
|
|
76
|
+
raise Lighthouse::DefinitionError, "Could not find a base definition #{name} of kind #{kind(extension)} to extend."
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
Nodes::ObjectTypeDefinition.new(name: name, fields: [], interfaces: [], directives: []).tap { |root| document_ast.put_type(root) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The definition's nodes followed by the extension's; a name in both is an error.
|
|
83
|
+
def merge_unique(original, addition)
|
|
84
|
+
names = addition.to_set(&:name)
|
|
85
|
+
duplicate = original.find { |node| names.include?(node.name) }
|
|
86
|
+
raise Lighthouse::DefinitionError, "Duplicate definition #{duplicate.name} found when merging." if duplicate
|
|
87
|
+
|
|
88
|
+
original + addition
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def kind(node)
|
|
92
|
+
node.class.name.demodulize
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Every argument of every object and interface field, through the
|
|
96
|
+
# manipulators of the directives it carries, in their SDL order.
|
|
97
|
+
def apply_arg_manipulators(document_ast, registry)
|
|
98
|
+
document_ast.type_names.each do |name|
|
|
99
|
+
type = document_ast.types[name]
|
|
100
|
+
next unless OBJECT_LIKE.any? { |kind| type.is_a?(kind) }
|
|
101
|
+
|
|
102
|
+
fields = type.fields.map do |field|
|
|
103
|
+
field.merge(arguments: field.arguments.map { |arg| manipulate_argument(document_ast, registry, arg, field, type) })
|
|
104
|
+
end
|
|
105
|
+
document_ast.put_type(type.merge(fields: fields))
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def manipulate_argument(document_ast, registry, arg, field, type)
|
|
110
|
+
manipulated = arg.directives.reduce(arg) do |current, directive|
|
|
111
|
+
manipulator = registry.arg_manipulator_class(directive.name)
|
|
112
|
+
manipulator ? manipulator.new(current, directive).manipulate_arg_definition(document_ast, field, type) : current
|
|
113
|
+
end
|
|
114
|
+
return manipulated unless placeholder?(manipulated.type)
|
|
115
|
+
|
|
116
|
+
raise Lighthouse::DefinitionError,
|
|
117
|
+
"#{type.name}.#{field.name}(#{arg.name}:) has the placeholder type `_`, which only a directive that generates " \
|
|
118
|
+
'its type replaces: @whereConditions, @whereHasConditions or @orderBy.'
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def placeholder?(type)
|
|
122
|
+
Lighthouse::GraphQL::Directives::GeneratedTypes.placeholder?(type)
|
|
123
|
+
end
|
|
124
|
+
end
|
data/lib/lighthouse/caching.rb
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'active_support/cache'
|
|
3
4
|
require 'digest'
|
|
4
5
|
require 'json'
|
|
5
6
|
require 'securerandom'
|
|
6
7
|
|
|
7
|
-
#
|
|
8
|
-
#
|
|
8
|
+
# The app's cache stores, and keys and invalidation for @cache / @clearCache.
|
|
9
|
+
#
|
|
10
|
+
# Lighthouse keeps no store of its own, as PHP Lighthouse uses Laravel's: @cache,
|
|
11
|
+
# @clearCache and APQ use Rails.cache (config.cache_store), and @throttle counts
|
|
12
|
+
# where Rails' own `rate_limit` does, in the controller's cache_store.
|
|
9
13
|
#
|
|
10
14
|
# PHP Lighthouse invalidates with cache tags, which Rails cache stores do not
|
|
11
15
|
# have (and `delete_matched` is not universal). Instead every entry key embeds
|
|
@@ -19,8 +23,10 @@ module Lighthouse::GraphQL::Caching
|
|
|
19
23
|
|
|
20
24
|
module_function
|
|
21
25
|
|
|
26
|
+
# Rails.cache; outside Rails, one in-memory store for the process.
|
|
22
27
|
def store
|
|
23
|
-
|
|
28
|
+
rails_cache = ::Rails.cache if defined?(::Rails) && ::Rails.respond_to?(:cache)
|
|
29
|
+
rails_cache || (@memory_store ||= ActiveSupport::Cache::MemoryStore.new)
|
|
24
30
|
end
|
|
25
31
|
|
|
26
32
|
def entry_key(type, id, field, args, user_key)
|
data/lib/lighthouse/config.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'active_support/cache'
|
|
4
|
-
|
|
5
3
|
module Lighthouse
|
|
6
4
|
# Convenience entrypoint: `Lighthouse.configure { |c| c.resolver_namespaces = [...] }`.
|
|
7
5
|
def self.configure(&block)
|
|
@@ -26,24 +24,43 @@ module Lighthouse
|
|
|
26
24
|
|
|
27
25
|
# Every setting reset! forgets, so the next read falls back to its default.
|
|
28
26
|
SETTINGS = %i[
|
|
29
|
-
|
|
27
|
+
schema_path ide_helper resolver_namespaces namespaces transactional_mutations pagination_default_count pagination_max_count
|
|
30
28
|
schema_cache schema_cache_path watch_sdl persisted_queries persisted_queries_store persisted_queries_ttl
|
|
31
29
|
persisted_queries_manifest persisted_queries_only on_error directive_map authenticated_user policy_user authorizer
|
|
32
30
|
feature_checker feature_actor
|
|
33
31
|
convert_empty_strings_to_null
|
|
34
32
|
soft_deletes
|
|
35
33
|
queue_broadcasts broadcast_queue
|
|
36
|
-
global_id
|
|
34
|
+
global_id
|
|
37
35
|
].freeze
|
|
38
36
|
|
|
39
37
|
class << self
|
|
40
|
-
attr_writer :
|
|
38
|
+
attr_writer :schema_path, :ide_helper, :resolver_namespaces, :on_error, :authenticated_user, :policy_user, :authorizer,
|
|
41
39
|
:transactional_mutations, :schema_cache, :watch_sdl, :persisted_queries, :persisted_queries_store, :persisted_queries_only,
|
|
42
40
|
:queue_broadcasts,
|
|
43
|
-
:convert_empty_strings_to_null, :feature_checker, :feature_actor, :global_id
|
|
41
|
+
:convert_empty_strings_to_null, :feature_checker, :feature_actor, :global_id
|
|
42
|
+
|
|
43
|
+
# The schema's entrypoint, as PHP Lighthouse's `schema_path`: the one file
|
|
44
|
+
# the schema is read from, importing the others (`#import user.graphql`,
|
|
45
|
+
# `#import blog/*.graphql`; see SchemaStitcher). RAILS_ROOT/graphql/schema.graphql
|
|
46
|
+
# by default.
|
|
47
|
+
def schema_path
|
|
48
|
+
@schema_path ||= (::Rails.root.join('graphql/schema.graphql').to_s if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The folder of the schema's entrypoint, which watch_sdl watches.
|
|
52
|
+
def schema_directory
|
|
53
|
+
schema_path && File.dirname(schema_path.to_s)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Rewrite the IDE helper files (schema-directives.graphql,
|
|
57
|
+
# programmatic-types.graphql) on boot and on every reload. nil (the
|
|
58
|
+
# default): in development, when the app has a graphql.config.yml.
|
|
59
|
+
def ide_helper
|
|
60
|
+
return @ide_helper unless @ide_helper.nil?
|
|
44
61
|
|
|
45
|
-
|
|
46
|
-
|
|
62
|
+
defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env.development? && ::Rails.root &&
|
|
63
|
+
File.exist?(::Rails.root.join('graphql.config.yml'))
|
|
47
64
|
end
|
|
48
65
|
|
|
49
66
|
def directive_map = @directive_map ||= {}
|
|
@@ -66,16 +83,6 @@ module Lighthouse
|
|
|
66
83
|
@namespaces = namespaces.merge(overrides.to_h.transform_keys(&:to_sym).transform_values { |v| Array(v) })
|
|
67
84
|
end
|
|
68
85
|
|
|
69
|
-
# Store backing @cache and @clearCache. Defaults to Rails.cache.
|
|
70
|
-
def cache_store
|
|
71
|
-
@cache_store ||=
|
|
72
|
-
if defined?(Rails) && Rails.respond_to?(:cache) && Rails.cache
|
|
73
|
-
Rails.cache
|
|
74
|
-
else
|
|
75
|
-
ActiveSupport::Cache::MemoryStore.new
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
86
|
# --- Subscriptions (used by @broadcast) ----------------------------------
|
|
80
87
|
#
|
|
81
88
|
# queue_broadcasts perform @broadcast triggers in a Lighthouse::GraphQL::BroadcastJob
|
|
@@ -91,7 +98,7 @@ module Lighthouse
|
|
|
91
98
|
# schema_cache_path where; default Rails.root/tmp/cache/lighthouse
|
|
92
99
|
# watch_sdl reload the app (and so the schema) when a .graphql file changes
|
|
93
100
|
# persisted_queries accept Apollo's Automatic Persisted Queries (PersistedQueries)
|
|
94
|
-
# persisted_queries_store where APQ keeps queries; default
|
|
101
|
+
# persisted_queries_store where APQ keeps queries; default Rails.cache
|
|
95
102
|
# persisted_queries_ttl how long they live there (seconds; nil: until evicted)
|
|
96
103
|
# persisted_queries_manifest a manifest of trusted operations, served by hash
|
|
97
104
|
# persisted_queries_only reject any operation not in the manifest
|
|
@@ -110,13 +117,8 @@ module Lighthouse
|
|
|
110
117
|
# and `decode(global_id)` -> [type, id]. Base64 of "Type:id" by default.
|
|
111
118
|
def global_id = @global_id || Lighthouse::GraphQL::GlobalId
|
|
112
119
|
|
|
113
|
-
# Stores the file an @upload argument carries: `call(file, disk:, path:,
|
|
114
|
-
# public:)` returns what the argument holds instead. An ActiveStorage blob's
|
|
115
|
-
# signed id by default.
|
|
116
|
-
def upload_store = @upload_store || Lighthouse::GraphQL::UploadStore
|
|
117
|
-
|
|
118
120
|
def persisted_queries_store
|
|
119
|
-
@persisted_queries_store ||
|
|
121
|
+
@persisted_queries_store || Lighthouse::GraphQL::Caching.store
|
|
120
122
|
end
|
|
121
123
|
|
|
122
124
|
# Pagination defaults, overridden by a directive's defaultCount / maxCount.
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'directive'
|
|
4
4
|
|
|
5
|
-
# A directive on a field ARGUMENT that
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# A directive on a field ARGUMENT that rewrites the schema before it is built,
|
|
6
|
+
# as PHP Lighthouse's ArgManipulator: @whereConditions, @whereHasConditions and
|
|
7
|
+
# @orderBy generate the input types of an argument written with the `_`
|
|
8
|
+
# placeholder, and type it with them.
|
|
8
9
|
#
|
|
9
|
-
# Implementors
|
|
10
|
-
#
|
|
10
|
+
# Implementors, built with (arg_node, directive_node), provide
|
|
11
|
+
# `#manipulate_arg_definition(document_ast, field_node, type_node)` returning
|
|
12
|
+
# the argument node (graphql-ruby's nodes are immutable: `arg.merge(type: ...)`).
|
|
13
|
+
# New types go in the Lighthouse::GraphQL::DocumentAST (`define_type`).
|
|
14
|
+
# ASTBuilder calls it for every argument of an object or interface field.
|
|
11
15
|
module Lighthouse::Contracts::ArgManipulator
|
|
12
16
|
include Lighthouse::Contracts::Directive
|
|
13
17
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'directive'
|
|
4
|
+
|
|
5
|
+
# A directive on a FIELD that constrains the query of the field's resolver
|
|
6
|
+
# whatever the client sends, e.g. @orderBy(column:, direction:) on a field.
|
|
7
|
+
# Applied after the argument builders, as PHP Lighthouse's FieldBuilderDirective.
|
|
8
|
+
#
|
|
9
|
+
# Implementors, built with (field_node, directive_node), provide
|
|
10
|
+
# `#handle_field_builder(relation)` returning a relation.
|
|
11
|
+
module Lighthouse::Contracts::FieldBuilder
|
|
12
|
+
include Lighthouse::Contracts::Directive
|
|
13
|
+
end
|
|
@@ -24,6 +24,11 @@ require 'json'
|
|
|
24
24
|
# Cache-Control header the selected fields' @cacheControl hints allow, renders
|
|
25
25
|
# the JSON and dispatches EndRequest. In development an error renders as JSON.
|
|
26
26
|
#
|
|
27
|
+
# A field's @throttle that renders past its limit (its `with:` method, or
|
|
28
|
+
# `head :too_many_requests`, rate_limit's default on Rails 7.2) ends the request
|
|
29
|
+
# with that response, as a before_action that renders does: the operation stops
|
|
30
|
+
# there (Performed), and `execute` renders nothing more.
|
|
31
|
+
#
|
|
27
32
|
# Hooks, private so they are not actions: `graphql_context` (default:
|
|
28
33
|
# `{ current_user: current_user }` when the controller has a current_user, else
|
|
29
34
|
# `{}`), `graphql_schema` and `graphql_variables(param)`. A controller with an
|
|
@@ -32,6 +37,11 @@ require 'json'
|
|
|
32
37
|
module Lighthouse::GraphQL::Controller
|
|
33
38
|
extend ActiveSupport::Concern
|
|
34
39
|
|
|
40
|
+
# Raised once a @throttle `with:` method has performed the response: control
|
|
41
|
+
# flow, as `throw :abort` in a before_action, so no `rescue StandardError` (a
|
|
42
|
+
# schema's rescue_from) catches it on its way out of the operation.
|
|
43
|
+
class Performed < Exception; end # rubocop:disable Lint/InheritException
|
|
44
|
+
|
|
35
45
|
included do
|
|
36
46
|
class_attribute :lighthouse_schema_source, instance_writer: false
|
|
37
47
|
end
|
|
@@ -57,7 +67,7 @@ module Lighthouse::GraphQL::Controller
|
|
|
57
67
|
end
|
|
58
68
|
end
|
|
59
69
|
rescue StandardError => e
|
|
60
|
-
raise e unless defined?(::Rails) && ::Rails.env.development?
|
|
70
|
+
raise e unless defined?(::Rails) && ::Rails.env.development? && !rails_response?(e)
|
|
61
71
|
|
|
62
72
|
lighthouse_development_error(e)
|
|
63
73
|
end
|
|
@@ -65,13 +75,18 @@ module Lighthouse::GraphQL::Controller
|
|
|
65
75
|
private
|
|
66
76
|
|
|
67
77
|
# Dispatch StartRequest, run the block, dispatch EndRequest with the
|
|
68
|
-
# response. Only the outermost call dispatches
|
|
78
|
+
# response. Only the outermost call dispatches, and stops the block where a
|
|
79
|
+
# @throttle `with:` method performed the response.
|
|
69
80
|
def lighthouse_request
|
|
70
81
|
return yield if @lighthouse_request_started
|
|
71
82
|
|
|
72
83
|
@lighthouse_request_started = true
|
|
73
84
|
Lighthouse::Events.dispatch(Lighthouse::Events::StartRequest.new(request))
|
|
74
|
-
result =
|
|
85
|
+
result = begin
|
|
86
|
+
yield
|
|
87
|
+
rescue Performed
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
75
90
|
Lighthouse::Events.dispatch(Lighthouse::Events::EndRequest.new(response))
|
|
76
91
|
result
|
|
77
92
|
end
|
|
@@ -81,9 +96,11 @@ module Lighthouse::GraphQL::Controller
|
|
|
81
96
|
respond_to?(:current_user, true) ? { current_user: current_user } : {}
|
|
82
97
|
end
|
|
83
98
|
|
|
84
|
-
# graphql_context, with
|
|
99
|
+
# graphql_context, with what @throttle reads unless it sets its own: the
|
|
100
|
+
# client's IP (rate_limit's default by:) and the controller, whose cache_store,
|
|
101
|
+
# controller_path and methods rate_limit uses.
|
|
85
102
|
def lighthouse_context
|
|
86
|
-
{ remote_ip: request.remote_ip }.merge(graphql_context)
|
|
103
|
+
{ remote_ip: request.remote_ip, lighthouse_controller: self }.merge(graphql_context)
|
|
87
104
|
end
|
|
88
105
|
|
|
89
106
|
def graphql_schema
|
|
@@ -139,6 +156,12 @@ module Lighthouse::GraphQL::Controller
|
|
|
139
156
|
Lighthouse::GraphQL::PersistedQueries.multiplex(graphql_schema, operations)
|
|
140
157
|
end
|
|
141
158
|
|
|
159
|
+
# An error Rails answers with a status of its own (config.action_dispatch.rescue_responses):
|
|
160
|
+
# ActionController::TooManyRequests from @throttle is a 429 in development too.
|
|
161
|
+
def rails_response?(error)
|
|
162
|
+
::ActionDispatch::ExceptionWrapper.status_code_for_exception(error.class.name) != 500
|
|
163
|
+
end
|
|
164
|
+
|
|
142
165
|
def lighthouse_development_error(error)
|
|
143
166
|
logger.error(error.message)
|
|
144
167
|
logger.error(error.backtrace.join("\n"))
|
|
@@ -25,13 +25,12 @@ class Lighthouse::GraphQL::DirectiveResolvers::AggregateDirectiveResolver < Ligh
|
|
|
25
25
|
column_name = extract_directive_arg('column')
|
|
26
26
|
function_name = extract_directive_arg('function')
|
|
27
27
|
resolver_instance = self
|
|
28
|
-
field_ast_node = @field_def.ast_node
|
|
29
28
|
|
|
30
29
|
install(target_type_class, lambda do |obj, args, ctx|
|
|
31
30
|
relation = resolver_instance.send(:aggregate_source, obj, model_name, relation_name)
|
|
32
31
|
return nil unless relation
|
|
33
32
|
|
|
34
|
-
relation =
|
|
33
|
+
relation = resolver_instance.apply_where_filters(relation, args, ctx)
|
|
35
34
|
resolver_instance.send(:compute_aggregation, relation, column_name, function_name)
|
|
36
35
|
end)
|
|
37
36
|
end
|
|
@@ -25,8 +25,6 @@ class Lighthouse::GraphQL::DirectiveResolvers::AllDirectiveResolver < Lighthouse
|
|
|
25
25
|
args = arguments.respond_to?(:to_h) ? arguments.to_h : {}
|
|
26
26
|
relation = apply_scopes(model_class.all)
|
|
27
27
|
|
|
28
|
-
relation = Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply(relation, @field_def.ast_node, args, ctx, self)
|
|
29
|
-
|
|
30
28
|
relation = apply_where_filters(relation, args, ctx)
|
|
31
29
|
relation = relation.limit(args[:first]) if args[:first]
|
|
32
30
|
relation = relation.offset(args[:offset]) if args[:offset]
|
|
@@ -11,7 +11,7 @@ require_relative 'base'
|
|
|
11
11
|
# @hash the value hashed with BCrypt, for a password column
|
|
12
12
|
# @drop the value removed: the resolver never sees it
|
|
13
13
|
# @bind(class:) the value replaced by the record(s) it identifies
|
|
14
|
-
# @upload the uploaded file stored (
|
|
14
|
+
# @upload the uploaded file stored with ActiveStorage (UploadStore), its blob's signed id passed
|
|
15
15
|
#
|
|
16
16
|
# type Mutation {
|
|
17
17
|
# signUp(email: String! @trim, password: String! @trim @hash, captcha: String @drop): User! @create
|
|
@@ -60,26 +60,26 @@ class Lighthouse::GraphQL::DirectiveResolvers::ArgumentTransformsResolver < Ligh
|
|
|
60
60
|
"A model class, or a class whose `call(value, context)` returns what the value stands for."
|
|
61
61
|
class: String!
|
|
62
62
|
"The column the value is matched against, for a model."
|
|
63
|
-
column: String
|
|
63
|
+
column: String = "id"
|
|
64
64
|
"Relations to eager-load on the bound records."
|
|
65
|
-
with: [String!]
|
|
65
|
+
with: [String!] = []
|
|
66
66
|
"Pass null instead of failing when no record matches."
|
|
67
|
-
optional: Boolean
|
|
67
|
+
optional: Boolean = false
|
|
68
68
|
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
|
|
69
69
|
GRAPHQL
|
|
70
70
|
'upload' => <<~GRAPHQL.chomp
|
|
71
71
|
"""
|
|
72
|
-
Store the file an Upload argument carries, and pass along
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
Store the file an Upload argument carries with ActiveStorage, and pass along the
|
|
73
|
+
signed id of its blob instead, which an attachment attribute (has_one_attached)
|
|
74
|
+
attaches. This does not change the schema from a client perspective.
|
|
75
75
|
"""
|
|
76
76
|
directive @upload(
|
|
77
|
-
"The
|
|
77
|
+
"The ActiveStorage service to store the file on (config/storage.yml). Defaults to config.active_storage.service."
|
|
78
78
|
disk: String
|
|
79
|
-
"
|
|
80
|
-
path: String
|
|
81
|
-
"
|
|
82
|
-
public: Boolean
|
|
79
|
+
"The directory the stored file's key starts with."
|
|
80
|
+
path: String = "/"
|
|
81
|
+
"Whether the file is public. ActiveStorage sets that per service, so it must agree with the service's."
|
|
82
|
+
public: Boolean = false
|
|
83
83
|
) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION
|
|
84
84
|
GRAPHQL
|
|
85
85
|
}.freeze
|
|
@@ -154,8 +154,8 @@ class Lighthouse::GraphQL::DirectiveResolvers::ArgumentTransformsResolver < Ligh
|
|
|
154
154
|
send(TRANSFORMS.fetch(directive.name), directive, argument, value, ctx)
|
|
155
155
|
end
|
|
156
156
|
|
|
157
|
-
def decode_global_id(directive, _argument, value,
|
|
158
|
-
decode(value, Lighthouse::Support::DirectiveArgs.literal(directive, 'decode') || 'ARRAY')
|
|
157
|
+
def decode_global_id(directive, _argument, value, ctx)
|
|
158
|
+
decode(value, Lighthouse::Support::DirectiveArgs.literal(directive, 'decode') || 'ARRAY', ctx)
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
def trim_value(_directive, _argument, value, _ctx) = trim(value)
|
|
@@ -166,13 +166,13 @@ class Lighthouse::GraphQL::DirectiveResolvers::ArgumentTransformsResolver < Ligh
|
|
|
166
166
|
return value.map { |item| upload(directive, argument, item, ctx) } if value.is_a?(Array)
|
|
167
167
|
|
|
168
168
|
options = %w[disk path public].to_h { |name| [name.to_sym, Lighthouse::Support::DirectiveArgs.literal(directive, name)] }
|
|
169
|
-
Lighthouse::
|
|
169
|
+
Lighthouse::GraphQL::UploadStore.call(value, argument: argument.graphql_name, **options)
|
|
170
170
|
end
|
|
171
171
|
|
|
172
|
-
def decode(value, mode)
|
|
173
|
-
return value.map { |item| decode(item, mode) } if value.is_a?(Array)
|
|
172
|
+
def decode(value, mode, ctx = nil)
|
|
173
|
+
return value.map { |item| decode(item, mode, ctx) } if value.is_a?(Array)
|
|
174
174
|
|
|
175
|
-
type, id = Lighthouse::GraphQL::GlobalId.decode!(value)
|
|
175
|
+
type, id = Lighthouse::GraphQL::GlobalId.decode!(value, ctx)
|
|
176
176
|
{ 'TYPE' => type, 'ID' => id }.fetch(mode.to_s, [type, id])
|
|
177
177
|
end
|
|
178
178
|
|
|
@@ -36,7 +36,7 @@ module Lighthouse::GraphQL::DirectiveResolvers
|
|
|
36
36
|
return nil unless obj.respond_to?(name)
|
|
37
37
|
|
|
38
38
|
return paginate(obj, name, args, ctx) if pagination_type
|
|
39
|
-
return constrained(association_scope(obj, name), args) if constrained?(args)
|
|
39
|
+
return constrained(association_scope(obj, name), args, ctx) if constrained?(args)
|
|
40
40
|
|
|
41
41
|
dataloader = Lighthouse::Support::Batching.dataloader(ctx)
|
|
42
42
|
return obj.public_send(name) unless dataloader && obj.class.respond_to?(:reflect_on_association)
|
|
@@ -52,11 +52,14 @@ module Lighthouse::GraphQL::DirectiveResolvers
|
|
|
52
52
|
self.class.collection? && directive_arg('type')&.to_s&.upcase
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
# Scopes
|
|
56
|
-
#
|
|
55
|
+
# Scopes, a query directive on the field (@builder, @orderBy(column:)) or a
|
|
56
|
+
# supplied argument with a filter directive need a query of their own
|
|
57
|
+
# instead of the batched load.
|
|
57
58
|
def constrained?(args)
|
|
58
|
-
|
|
59
|
+
directive_arg('scopes') || Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.any?(@field_def.ast_node) || filtered?(args)
|
|
60
|
+
end
|
|
59
61
|
|
|
62
|
+
def filtered?(args)
|
|
60
63
|
registry = Lighthouse::DirectiveRegistry.default
|
|
61
64
|
(@field_def.ast_node&.arguments || []).any? do |argument|
|
|
62
65
|
args.key?(argument.name.underscore.to_sym) && argument.directives.any? { |d| filter_directive?(registry, d) }
|
|
@@ -68,8 +71,8 @@ module Lighthouse::GraphQL::DirectiveResolvers
|
|
|
68
71
|
registry.arg_builder_class(directive.name) || CONDITION_DIRECTIVES.include?(directive.name)
|
|
69
72
|
end
|
|
70
73
|
|
|
71
|
-
def constrained(relation, args)
|
|
72
|
-
relation = apply_where_filters(scoped(relation), args)
|
|
74
|
+
def constrained(relation, args, ctx)
|
|
75
|
+
relation = apply_where_filters(scoped(relation), args, ctx)
|
|
73
76
|
self.class.collection? ? relation : relation.first
|
|
74
77
|
end
|
|
75
78
|
|
|
@@ -18,12 +18,7 @@ class Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
|
18
18
|
|
|
19
19
|
args_h = args.respond_to?(:to_h) ? args.to_h : (args || {})
|
|
20
20
|
|
|
21
|
-
relation
|
|
22
|
-
@field_def.ast_node.arguments.each do |arg_node|
|
|
23
|
-
relation = apply_where_has_conditions(relation, arg_node, args_h)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
apply_arg_builders(relation, args_h, ctx)
|
|
21
|
+
apply_field_builders(apply_arg_builders(relation, args_h, ctx), args_h, ctx)
|
|
27
22
|
rescue ::GraphQL::ExecutionError
|
|
28
23
|
raise
|
|
29
24
|
rescue StandardError => e
|
|
@@ -32,7 +27,7 @@ class Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
|
32
27
|
end
|
|
33
28
|
|
|
34
29
|
# Compose argument-builder directives (@eq, @neq, @in, @notIn, @like,
|
|
35
|
-
# @where, @orderBy) onto the relation, in SDL argument order, for each
|
|
30
|
+
# @where, @orderBy, @whereConditions, @whereHasConditions) onto the relation, in SDL argument order, for each
|
|
36
31
|
# field argument that carries one and that the client supplied a value
|
|
37
32
|
# for — and for the input fields of an argument carrying @nest.
|
|
38
33
|
def apply_arg_builders(relation, args_h, ctx = nil)
|
|
@@ -51,18 +46,6 @@ class Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
|
51
46
|
relation
|
|
52
47
|
end
|
|
53
48
|
|
|
54
|
-
def extract_relation_name_from_directive(arg_node)
|
|
55
|
-
directive = arg_node.directives.find { |dir| dir.name == 'whereHasConditions' }
|
|
56
|
-
return nil unless directive
|
|
57
|
-
|
|
58
|
-
relation_arg = directive.arguments.find { |arg| arg.name == 'relation' }
|
|
59
|
-
return nil unless relation_arg
|
|
60
|
-
|
|
61
|
-
relation_arg.value.respond_to?(:value) ? relation_arg.value.value : relation_arg.value
|
|
62
|
-
rescue StandardError
|
|
63
|
-
nil
|
|
64
|
-
end
|
|
65
|
-
|
|
66
49
|
def constantize_model(name)
|
|
67
50
|
Lighthouse::Support::ModelResolver.constantize(name)
|
|
68
51
|
end
|
|
@@ -113,24 +96,10 @@ class Lighthouse::GraphQL::DirectiveResolvers::BaseDirectiveResolver
|
|
|
113
96
|
@field_def.type.unwrap.graphql_name.sub(/(SimplePaginator|Paginator|Connection)\z/, '')
|
|
114
97
|
end
|
|
115
98
|
|
|
116
|
-
# The
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
Lighthouse::GraphQL::DirectiveResolvers::WhereConditionsApplier.apply(relation, where_condition)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# A `whereHasConditions` argument the client supplied filters the
|
|
125
|
-
# relation by the related records matching its condition.
|
|
126
|
-
def apply_where_has_conditions(relation, arg_node, args_h)
|
|
127
|
-
return relation unless arg_node.directives.any? { |dir| dir.name == 'whereHasConditions' }
|
|
128
|
-
|
|
129
|
-
arg_value = Lighthouse::Support::Naming.fetch_arg(args_h, arg_node.name)
|
|
130
|
-
return relation unless arg_value
|
|
131
|
-
|
|
132
|
-
relation_name = extract_relation_name_from_directive(arg_node) || Lighthouse::Support::Naming.derive_relation_name(arg_node.name)
|
|
133
|
-
Lighthouse::GraphQL::DirectiveResolvers::WhereConditionsApplier.apply_has_argument(relation, arg_node.name, arg_value, relation_name)
|
|
99
|
+
# The field's own query directives (@builder, @whereAuth, @orderBy(column:)),
|
|
100
|
+
# in SDL order, after the arguments', as PHP Lighthouse's enhanceBuilder applies them.
|
|
101
|
+
def apply_field_builders(relation, args_h = {}, ctx = nil)
|
|
102
|
+
Lighthouse::GraphQL::DirectiveResolvers::BuilderApplier.apply(relation, @field_def.ast_node, args_h, ctx, self)
|
|
134
103
|
end
|
|
135
104
|
|
|
136
105
|
# Every (argument, directive, builder class) on the field, in SDL order.
|