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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +175 -1
- data/README.md +47 -3
- data/docs/authorization.md +116 -57
- data/docs/best-practices.md +16 -3
- data/docs/caching.md +161 -0
- data/docs/configuration.md +83 -4
- data/docs/custom-directives.md +5 -1
- data/docs/directives.md +117 -38
- data/docs/getting-started.md +84 -0
- data/docs/motivation.md +184 -0
- data/docs/mutations.md +260 -0
- data/docs/performance.md +138 -0
- data/docs/relationships.md +80 -33
- data/docs/resolvers.md +189 -0
- data/docs/validation.md +132 -0
- data/lib/generators/lighthouse/builder_generator.rb +30 -0
- data/lib/generators/lighthouse/class_generator.rb +61 -0
- data/lib/generators/lighthouse/install/install_generator.rb +127 -0
- data/lib/generators/lighthouse/install/templates/graphql.config.yml.tt +8 -0
- data/lib/generators/lighthouse/install/templates/graphql_controller.rb.tt +57 -0
- data/lib/generators/lighthouse/install/templates/initializer.rb.tt +67 -0
- data/lib/generators/lighthouse/install/templates/schema.graphql.tt +19 -0
- data/lib/generators/lighthouse/install/templates/schema.rb.tt +10 -0
- data/lib/generators/lighthouse/mutation_generator.rb +30 -0
- data/lib/generators/lighthouse/query_generator.rb +29 -0
- data/lib/generators/lighthouse/validator_generator.rb +36 -0
- data/lib/lighthouse/cache_control.rb +126 -0
- data/lib/lighthouse/caching.rb +68 -0
- data/lib/lighthouse/config.rb +85 -19
- data/lib/lighthouse/contracts/arg_builder.rb +8 -12
- data/lib/lighthouse/contracts/arg_manipulator.rb +8 -12
- data/lib/lighthouse/contracts/directive.rb +10 -14
- data/lib/lighthouse/contracts/field_manipulator.rb +8 -12
- data/lib/lighthouse/contracts/field_middleware.rb +7 -11
- data/lib/lighthouse/contracts/field_resolver.rb +9 -13
- data/lib/lighthouse/dataloader_sources/association_loader.rb +28 -19
- data/lib/lighthouse/dataloader_sources/model_loader.rb +10 -14
- data/lib/lighthouse/dataloader_sources/relation_count_loader.rb +16 -0
- data/lib/lighthouse/dataloader_sources/relation_page_loader.rb +18 -0
- data/lib/lighthouse/directive_resolvers/aggregate_resolver.rb +57 -69
- data/lib/lighthouse/directive_resolvers/all_resolver.rb +26 -57
- data/lib/lighthouse/directive_resolvers/association_resolvers.rb +161 -0
- data/lib/lighthouse/directive_resolvers/auth_resolver.rb +41 -38
- data/lib/lighthouse/directive_resolvers/base.rb +142 -86
- data/lib/lighthouse/directive_resolvers/builder_applier.rb +58 -56
- data/lib/lighthouse/directive_resolvers/cache_resolvers.rb +133 -0
- data/lib/lighthouse/directive_resolvers/can_resolvers.rb +177 -0
- data/lib/lighthouse/directive_resolvers/convention_resolver.rb +70 -0
- data/lib/lighthouse/directive_resolvers/count_resolver.rb +42 -48
- data/lib/lighthouse/directive_resolvers/field_resolver.rb +27 -68
- data/lib/lighthouse/directive_resolvers/find_resolver.rb +42 -52
- data/lib/lighthouse/directive_resolvers/first_resolver.rb +39 -45
- data/lib/lighthouse/directive_resolvers/guard_resolver.rb +16 -22
- data/lib/lighthouse/directive_resolvers/method_resolver.rb +19 -25
- data/lib/lighthouse/directive_resolvers/model_mutation_resolver.rb +219 -0
- data/lib/lighthouse/directive_resolvers/paginate_resolver.rb +70 -128
- data/lib/lighthouse/directive_resolvers/rename_resolver.rb +18 -24
- data/lib/lighthouse/directive_resolvers/resolution.rb +31 -27
- data/lib/lighthouse/directive_resolvers/validator_resolver.rb +193 -0
- data/lib/lighthouse/directive_resolvers/where_conditions_applier.rb +164 -163
- data/lib/lighthouse/directives/arguments/base.rb +23 -31
- data/lib/lighthouse/directives/arguments/filters.rb +113 -93
- data/lib/lighthouse/directives/arguments/order_by.rb +41 -41
- data/lib/lighthouse/directives/pagination_info_types.rb +69 -0
- data/lib/lighthouse/directives/pagination_manipulator.rb +176 -0
- data/lib/lighthouse/directives/registry.rb +210 -141
- data/lib/lighthouse/directives/where_conditions_directive.rb +236 -200
- data/lib/lighthouse/errors.rb +27 -0
- data/lib/lighthouse/federation_directives.rb +53 -0
- data/lib/lighthouse/field_resolver_stack.rb +56 -0
- data/lib/lighthouse/generated_resolvers.rb +82 -0
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/lib/lighthouse/graphql_ruby_patches.rb +18 -0
- data/lib/lighthouse/ide_helper.rb +98 -0
- data/lib/lighthouse/namespaces.rb +21 -0
- data/lib/lighthouse/nested_mutations/belongs_to_operations.rb +50 -0
- data/lib/lighthouse/nested_mutations/executor.rb +149 -0
- data/lib/lighthouse/nested_mutations/has_operations.rb +66 -0
- data/lib/lighthouse/nested_mutations/many_to_many_operations.rb +44 -0
- data/lib/lighthouse/nested_mutations/partitioner.rb +125 -0
- data/lib/lighthouse/nested_mutations.rb +210 -0
- data/lib/lighthouse/pagination.rb +221 -0
- data/lib/lighthouse/persisted_queries.rb +167 -0
- data/lib/lighthouse/railtie.rb +26 -0
- data/lib/lighthouse/rb_lighthouse.rb +50 -29
- data/lib/lighthouse/reference_resolver.rb +13 -15
- data/lib/lighthouse/relation_batch.rb +156 -0
- data/lib/lighthouse/resolver_dispatch.rb +26 -0
- data/lib/lighthouse/schema_cache.rb +120 -0
- data/lib/lighthouse/schema_factory.rb +27 -37
- data/lib/lighthouse/schema_generator.rb +196 -239
- data/lib/lighthouse/schema_implementation.rb +51 -48
- data/lib/lighthouse/sdl_loader.rb +17 -20
- data/lib/lighthouse/support/arguments.rb +119 -0
- data/lib/lighthouse/support/authorization.rb +18 -19
- data/lib/lighthouse/support/batching.rb +16 -0
- data/lib/lighthouse/support/directive_args.rb +26 -24
- data/lib/lighthouse/support/model_resolver.rb +56 -56
- data/lib/lighthouse/support/naming.rb +55 -59
- data/lib/lighthouse/tasks/lighthouse.rake +33 -0
- data/lib/lighthouse/where_conditions/operator_map.rb +25 -29
- data/sig/lighthouse/directives.rbs +158 -0
- data/sig/lighthouse/graphql.rbs +78 -0
- data/sig/lighthouse.rbs +142 -0
- metadata +56 -64
- data/lib/lighthouse/directive_resolvers/belongs_to_many_resolver.rb +0 -56
- data/lib/lighthouse/directive_resolvers/belongs_to_resolver.rb +0 -47
- data/lib/lighthouse/directive_resolvers/can_resolver.rb +0 -86
- data/lib/lighthouse/directive_resolvers/has_many_resolver.rb +0 -109
- data/lib/lighthouse/directive_resolvers/has_one_resolver.rb +0 -43
- data/lib/lighthouse/directives/relation_directive.rb +0 -332
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'lighthouse-graphql'
|
|
5
|
+
|
|
6
|
+
# `rails generate lighthouse:install` — sets a Rails app up for Lighthouse:
|
|
7
|
+
# the initializer, a starter SDL file, the schema class, a controller and
|
|
8
|
+
# its route, plus the graphql-config file IDEs read. Rerunning it skips the
|
|
9
|
+
# route, SDL and .gitignore entries that are already in place.
|
|
10
|
+
#
|
|
11
|
+
# It coexists with graphql-ruby's `graphql:install`, which writes the same
|
|
12
|
+
# controller path and route: an existing schema class or controller is kept
|
|
13
|
+
# (pass --force to replace it) instead of overwritten, so an app already
|
|
14
|
+
# serving a graphql-ruby schema keeps doing so until it is pointed at ours.
|
|
15
|
+
class Lighthouse::Generators::InstallGenerator < Rails::Generators::Base
|
|
16
|
+
CONTROLLER = 'app/controllers/graphql_controller.rb'
|
|
17
|
+
|
|
18
|
+
source_root File.expand_path('templates', __dir__)
|
|
19
|
+
|
|
20
|
+
desc 'Install Lighthouse: initializer, starter schema, schema class, controller, route and IDE config.'
|
|
21
|
+
|
|
22
|
+
class_option :schema, type: :string, default: 'AppSchema',
|
|
23
|
+
desc: 'Name of the schema constant to generate'
|
|
24
|
+
class_option :sdl_folder, type: :string, default: 'graphql',
|
|
25
|
+
desc: 'Folder, relative to the app root, holding the *.graphql files'
|
|
26
|
+
class_option :skip_controller, type: :boolean, default: false,
|
|
27
|
+
desc: 'Skip the GraphqlController and its route'
|
|
28
|
+
class_option :skip_ide, type: :boolean, default: false,
|
|
29
|
+
desc: 'Skip graphql.config.yml and the .gitignore entries for the IDE helper files'
|
|
30
|
+
|
|
31
|
+
def create_initializer
|
|
32
|
+
template 'initializer.rb.tt', 'config/initializers/lighthouse.rb'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def create_starter_schema
|
|
36
|
+
if Dir[File.join(destination_root, sdl_folder, '**', '*.graphql')].any?
|
|
37
|
+
say_status :skip, "#{sdl_folder}/ already has .graphql files", :yellow
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
template 'schema.graphql.tt', File.join(sdl_folder, 'schema.graphql')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def create_schema_class
|
|
45
|
+
path = File.join('app/graphql', "#{schema_name.underscore}.rb")
|
|
46
|
+
return if keep_existing?(path)
|
|
47
|
+
|
|
48
|
+
template 'schema.rb.tt', path
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def create_controller
|
|
52
|
+
return if options[:skip_controller]
|
|
53
|
+
return if (@kept_controller = keep_existing?(CONTROLLER))
|
|
54
|
+
|
|
55
|
+
template 'graphql_controller.rb.tt', CONTROLLER
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def add_route
|
|
59
|
+
return if options[:skip_controller]
|
|
60
|
+
|
|
61
|
+
routes = File.join(destination_root, 'config/routes.rb')
|
|
62
|
+
return unless File.exist?(routes)
|
|
63
|
+
return if File.read(routes).include?('graphql#execute')
|
|
64
|
+
|
|
65
|
+
# Byte-identical to graphql-ruby's install route, so whichever generator
|
|
66
|
+
# runs second finds it in place and does not add it again.
|
|
67
|
+
route 'post "/graphql", to: "graphql#execute"'
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def create_ide_config
|
|
71
|
+
return if options[:skip_ide]
|
|
72
|
+
|
|
73
|
+
template 'graphql.config.yml.tt', 'graphql.config.yml'
|
|
74
|
+
|
|
75
|
+
gitignore = File.join(destination_root, '.gitignore')
|
|
76
|
+
return unless File.exist?(gitignore)
|
|
77
|
+
|
|
78
|
+
entries = ide_helper_files.map { |file| "/#{file}" } - File.read(gitignore).lines.map(&:strip)
|
|
79
|
+
return if entries.empty?
|
|
80
|
+
|
|
81
|
+
append_to_file '.gitignore', "\n# Lighthouse IDE helper output (bin/rails lighthouse:ide_helper)\n#{entries.join("\n")}\n"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def show_next_steps
|
|
85
|
+
serve =
|
|
86
|
+
if @kept_controller
|
|
87
|
+
"Serve it: #{CONTROLLER} was kept — have it call #{schema_name}.execute(...) to serve Lighthouse's schema."
|
|
88
|
+
else
|
|
89
|
+
'Query it: POST /graphql {"query": "{ users { data { id name } paginatorInfo { total } } }"}'
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
say <<~MSG
|
|
93
|
+
|
|
94
|
+
Lighthouse is installed. Next steps:
|
|
95
|
+
1. Describe your API in #{sdl_folder}/*.graphql (the starter schema assumes a User model).
|
|
96
|
+
2. Generate the IDE helper files: bin/rails lighthouse:ide_helper
|
|
97
|
+
3. #{serve}
|
|
98
|
+
MSG
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
# Files another generator may own (graphql-ruby's install writes the same
|
|
104
|
+
# controller) are left alone unless --force is given.
|
|
105
|
+
def keep_existing?(path)
|
|
106
|
+
return false if options[:force] || !File.exist?(File.join(destination_root, path))
|
|
107
|
+
|
|
108
|
+
say_status :skip, "#{path} already exists (pass --force to replace it)", :yellow
|
|
109
|
+
true
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def schema_name
|
|
113
|
+
options[:schema].camelize
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def sdl_folder
|
|
117
|
+
options[:sdl_folder].delete_suffix('/')
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def default_sdl_folder?
|
|
121
|
+
sdl_folder == 'graphql'
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def ide_helper_files
|
|
125
|
+
[Lighthouse::IdeHelper::DIRECTIVES_FILE, Lighthouse::IdeHelper::PROGRAMMATIC_TYPES_FILE]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# graphql-config, read by the GraphQL plugins for JetBrains IDEs and VS Code.
|
|
2
|
+
# The two generated files describe Lighthouse's directives and the types it
|
|
3
|
+
# generates; refresh them after changing the schema:
|
|
4
|
+
# bin/rails lighthouse:ide_helper
|
|
5
|
+
schema:
|
|
6
|
+
- <%= sdl_folder %>/**/*.graphql
|
|
7
|
+
- <%= Lighthouse::IdeHelper::DIRECTIVES_FILE %>
|
|
8
|
+
- <%= Lighthouse::IdeHelper::PROGRAMMATIC_TYPES_FILE %>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class GraphqlController < ApplicationController
|
|
4
|
+
# If this endpoint is called from outside this domain, null the session
|
|
5
|
+
# instead of failing CSRF verification:
|
|
6
|
+
# protect_from_forgery with: :null_session
|
|
7
|
+
|
|
8
|
+
def execute
|
|
9
|
+
# PersistedQueries.execute resolves Apollo-style persisted queries when
|
|
10
|
+
# config.persisted_queries is on, and otherwise runs params[:query] as is.
|
|
11
|
+
result = Lighthouse::GraphQL::PersistedQueries.execute(
|
|
12
|
+
<%= schema_name %>,
|
|
13
|
+
query: params[:query],
|
|
14
|
+
extensions: params[:extensions],
|
|
15
|
+
variables: prepare_variables(params[:variables]),
|
|
16
|
+
operation_name: params[:operationName],
|
|
17
|
+
context: {
|
|
18
|
+
# Read by @guard and the @can* directives — see config/initializers/lighthouse.rb.
|
|
19
|
+
# current_user: current_user
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
# The Cache-Control header the selected fields' @cacheControl hints allow
|
|
23
|
+
# (a no-op when the schema has none).
|
|
24
|
+
Lighthouse::GraphQL::CacheControl.apply(response, result)
|
|
25
|
+
render json: result
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
raise e unless Rails.env.development?
|
|
28
|
+
|
|
29
|
+
handle_error_in_development(e)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Variables arrive as a JSON string (GET or form posts) or as parameters.
|
|
35
|
+
def prepare_variables(variables_param)
|
|
36
|
+
case variables_param
|
|
37
|
+
when String
|
|
38
|
+
variables_param.present? ? JSON.parse(variables_param) || {} : {}
|
|
39
|
+
when Hash
|
|
40
|
+
variables_param
|
|
41
|
+
when ActionController::Parameters
|
|
42
|
+
variables_param.to_unsafe_hash
|
|
43
|
+
when nil
|
|
44
|
+
{}
|
|
45
|
+
else
|
|
46
|
+
raise ArgumentError, "Unexpected parameter: #{variables_param}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def handle_error_in_development(error)
|
|
51
|
+
logger.error(error.message)
|
|
52
|
+
logger.error(error.backtrace.join("\n"))
|
|
53
|
+
|
|
54
|
+
render json: { errors: [{ message: error.message, backtrace: error.backtrace }], data: {} },
|
|
55
|
+
status: :internal_server_error
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Lighthouse configuration. Every option is optional; the defaults are shown.
|
|
4
|
+
# Reference: https://github.com/Vaz-Innovation/lighthouse-graphql/blob/main/docs/configuration.md
|
|
5
|
+
Lighthouse.configure do |config|
|
|
6
|
+
# Folder the *.graphql schema files are loaded from.
|
|
7
|
+
<% if default_sdl_folder? -%>
|
|
8
|
+
# config.sdl_folder = Rails.root.join('graphql').to_s
|
|
9
|
+
<% else -%>
|
|
10
|
+
config.sdl_folder = Rails.root.join('<%= sdl_folder %>').to_s
|
|
11
|
+
<% end -%>
|
|
12
|
+
|
|
13
|
+
# Where classes named by the schema are looked up, by kind. A Query or Mutation
|
|
14
|
+
# field without a resolving directive resolves by convention: Query.latestPosts
|
|
15
|
+
# to Queries::LatestPosts (a GraphQL::Schema::Resolver), Mutation.publishPost
|
|
16
|
+
# to Mutations::PublishPost (a GraphQL::Schema::Mutation). @builder classes
|
|
17
|
+
# come from :builders and @validator contracts from :validators. Generate them
|
|
18
|
+
# with `rails g lighthouse:query|mutation|builder|validator Name`.
|
|
19
|
+
# config.namespaces = {
|
|
20
|
+
# queries: ['Queries'], mutations: ['Mutations'], builders: ['Builders'], validators: ['Validators']
|
|
21
|
+
# }
|
|
22
|
+
|
|
23
|
+
# Namespaces tried, in order, for models (@all(model: "User")) and for
|
|
24
|
+
# @field(resolver:) on types other than Query and Mutation. Fully-qualified
|
|
25
|
+
# names (containing `::`) are used as-is.
|
|
26
|
+
# config.resolver_namespaces = ['App::Graphql']
|
|
27
|
+
|
|
28
|
+
# Store for @cache and @clearCache.
|
|
29
|
+
# config.cache_store = Rails.cache
|
|
30
|
+
|
|
31
|
+
# Run @create/@update/@createMany/@updateMany in a database transaction.
|
|
32
|
+
# config.transactional_mutations = true
|
|
33
|
+
|
|
34
|
+
# --- For large schemas and high traffic (all off by default) ---------------
|
|
35
|
+
# Cache the built schema document in tmp/cache, keyed by the SDL and
|
|
36
|
+
# everything that shapes it; warm it at deploy with `bin/rails lighthouse:cache`.
|
|
37
|
+
# config.schema_cache = Rails.env.production?
|
|
38
|
+
|
|
39
|
+
# Reload the app (and so the schema) when a .graphql file changes.
|
|
40
|
+
# config.watch_sdl = Rails.env.development?
|
|
41
|
+
|
|
42
|
+
# Automatic Persisted Queries (Apollo's protocol). Add a GET route too, so
|
|
43
|
+
# hashed queries can be cached by CDNs: get "/graphql", to: "graphql#execute"
|
|
44
|
+
# config.persisted_queries = true
|
|
45
|
+
# config.persisted_queries_ttl = 1.week.to_i
|
|
46
|
+
# A manifest of trusted operations from your client build, served by hash;
|
|
47
|
+
# with persisted_queries_only, nothing else may run.
|
|
48
|
+
# config.persisted_queries_manifest = Rails.root.join('persisted-queries.json')
|
|
49
|
+
# config.persisted_queries_only = Rails.env.production?
|
|
50
|
+
|
|
51
|
+
# Called when a directive or resolver swallows an error. The default raises in
|
|
52
|
+
# test and logs otherwise.
|
|
53
|
+
# config.on_error = ->(error, _context) { Rails.error.report(error) }
|
|
54
|
+
|
|
55
|
+
# Authorization seams used by @guard and the @can* directives (Pundit by default).
|
|
56
|
+
# config.authenticated_user = ->(ctx) { ctx[:current_user] }
|
|
57
|
+
# config.policy_user = ->(ctx) { ctx[:pundit_user] || ctx[:current_user] }
|
|
58
|
+
# config.authorizer = ->(user, record, ability, args = []) { Pundit.policy!(user, record).public_send("#{ability}?", *args) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Register app-defined directives and federation reference resolvers inside
|
|
62
|
+
# to_prepare, so they reference reloadable constants and run before the schema
|
|
63
|
+
# is built. See docs/custom-directives.md and docs/federation.md.
|
|
64
|
+
# Rails.application.config.to_prepare do
|
|
65
|
+
# Lighthouse::DirectiveRegistry.register(Directives::Upcase)
|
|
66
|
+
# Lighthouse::ReferenceResolver.register('User') { |ref, _ctx| User.find_by(id: ref['id']) }
|
|
67
|
+
# end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Starter schema generated by `rails generate lighthouse:install`. It assumes a
|
|
2
|
+
# User model with `name` and `email` columns — replace it with your own types.
|
|
3
|
+
# Directives reference: https://github.com/Vaz-Innovation/lighthouse-graphql/blob/main/docs/directives.md
|
|
4
|
+
|
|
5
|
+
"Indicates what fields are available at the top level of a query operation."
|
|
6
|
+
type Query {
|
|
7
|
+
"Find a single user by id."
|
|
8
|
+
user(id: ID! @eq): User @find
|
|
9
|
+
|
|
10
|
+
"List users, a page at a time (10 by default; pass first: and page:)."
|
|
11
|
+
users: [User!]! @paginate(defaultCount: 10)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
"Account of a person who uses this application."
|
|
15
|
+
type User {
|
|
16
|
+
id: ID!
|
|
17
|
+
name: String
|
|
18
|
+
email: String
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Executable schema built by Lighthouse from the SDL in <%= sdl_folder %>/. The
|
|
4
|
+
# Dataloader batches relationship loads (@hasMany, @belongsTo, ...) to avoid N+1.
|
|
5
|
+
<%= schema_name %> = Lighthouse::GraphQL::RbLightHouse.get_schema(
|
|
6
|
+
options: {
|
|
7
|
+
using: { GraphQL::Dataloader => {} }
|
|
8
|
+
# base_types: { object: Types::BaseObject } # your graphql-ruby base classes
|
|
9
|
+
}
|
|
10
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'class_generator'
|
|
4
|
+
|
|
5
|
+
# `rails generate lighthouse:mutation PublishPost` — a GraphQL::Schema::Mutation
|
|
6
|
+
# that `Mutation.publishPost` resolves to by convention.
|
|
7
|
+
class Lighthouse::Generators::MutationGenerator < Lighthouse::Generators::ClassGenerator
|
|
8
|
+
desc 'Generate a mutation class (GraphQL::Schema::Mutation) in app/graphql/mutations.'
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def kind = :mutations
|
|
13
|
+
|
|
14
|
+
def body
|
|
15
|
+
<<~RUBY
|
|
16
|
+
# Resolves Mutation.#{class_basename.camelize(:lower)}. Keyword arguments are the field's
|
|
17
|
+
# arguments in snake_case; `object` and `context` are available. Raise
|
|
18
|
+
# GraphQL::ExecutionError for failures the client should see.
|
|
19
|
+
class #{class_basename} < GraphQL::Schema::Mutation
|
|
20
|
+
def resolve(**args)
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
RUBY
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def usage
|
|
28
|
+
"Mutation.#{class_basename.camelize(:lower)} resolves to #{full_class_name} — add the field to your SDL without a resolving directive."
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'class_generator'
|
|
4
|
+
|
|
5
|
+
# `rails generate lighthouse:query LatestPosts` — a GraphQL::Schema::Resolver
|
|
6
|
+
# that `Query.latestPosts` resolves to by convention.
|
|
7
|
+
class Lighthouse::Generators::QueryGenerator < Lighthouse::Generators::ClassGenerator
|
|
8
|
+
desc 'Generate a query class (GraphQL::Schema::Resolver) in app/graphql/queries.'
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def kind = :queries
|
|
13
|
+
|
|
14
|
+
def body
|
|
15
|
+
<<~RUBY
|
|
16
|
+
# Resolves Query.#{class_basename.camelize(:lower)}. Keyword arguments are the field's
|
|
17
|
+
# arguments in snake_case; `object` and `context` are available.
|
|
18
|
+
class #{class_basename} < GraphQL::Schema::Resolver
|
|
19
|
+
def resolve(**args)
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
RUBY
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def usage
|
|
27
|
+
"Query.#{class_basename.camelize(:lower)} resolves to #{full_class_name} — add the field to your SDL without a resolving directive."
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'class_generator'
|
|
4
|
+
|
|
5
|
+
# `rails generate lighthouse:validator CreatePostInput` — a
|
|
6
|
+
# Dry::Validation::Contract for @validator. Name it after an input type
|
|
7
|
+
# (CreatePostInputValidator) or a field (Mutation::CreatePostValidator).
|
|
8
|
+
class Lighthouse::Generators::ValidatorGenerator < Lighthouse::Generators::ClassGenerator
|
|
9
|
+
desc 'Generate a validator (Dry::Validation::Contract) for @validator in app/graphql/validators.'
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def kind = :validators
|
|
14
|
+
|
|
15
|
+
def class_basename
|
|
16
|
+
base = super
|
|
17
|
+
base.end_with?('Validator') ? base : "#{base}Validator"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def body
|
|
21
|
+
<<~RUBY
|
|
22
|
+
# Arguments arrive with snake_case keys. Remove `option :graphql_context`
|
|
23
|
+
# if the rules don't need the GraphQL context.
|
|
24
|
+
class #{class_basename} < Dry::Validation::Contract
|
|
25
|
+
option :graphql_context, optional: true
|
|
26
|
+
|
|
27
|
+
params do
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
RUBY
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def usage
|
|
34
|
+
'Needs the dry-validation gem. Add @validator to the input type or field it is named after.'
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# @cacheControl — compute the HTTP Cache-Control header a response may carry
|
|
4
|
+
# from the fields a query selects, as PHP Lighthouse (after Apollo) does:
|
|
5
|
+
#
|
|
6
|
+
# - max-age is the lowest maxAge among the contributing fields; 0 means
|
|
7
|
+
# `no-cache`.
|
|
8
|
+
# - The response is `private` if any selected field's scope is PRIVATE.
|
|
9
|
+
# - Defaults: a root field is maxAge 0 and PRIVATE; any other field returning
|
|
10
|
+
# an object, interface or union is maxAge 0 unless its type carries
|
|
11
|
+
# @cacheControl; scalar fields inherit their parent and do not contribute.
|
|
12
|
+
# A directive on the field wins over one on its type, and its omitted
|
|
13
|
+
# arguments take the directive defaults (maxAge 0, PUBLIC).
|
|
14
|
+
# - `inheritMaxAge: true` uses the parent field's maxAge.
|
|
15
|
+
# - A mutation is never cacheable.
|
|
16
|
+
#
|
|
17
|
+
# The schema runs Analyzer on every query when its SDL uses @cacheControl;
|
|
18
|
+
# read the result with CacheControl.header(result) and set it on the
|
|
19
|
+
# response (see `apply`).
|
|
20
|
+
module Lighthouse::GraphQL::CacheControl
|
|
21
|
+
DEFINITION = 'directive @cacheControl(maxAge: Int! = 0, scope: CacheControlScope! = PUBLIC, ' \
|
|
22
|
+
'inheritMaxAge: Boolean! = false) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION'
|
|
23
|
+
|
|
24
|
+
TYPE_DEFINITIONS = {
|
|
25
|
+
'CacheControlScope' => <<~GRAPHQL.chomp
|
|
26
|
+
"Who a cached response may be shared with."
|
|
27
|
+
enum CacheControlScope {
|
|
28
|
+
"The same for every user: shared caches (CDNs, proxies) may store it."
|
|
29
|
+
PUBLIC
|
|
30
|
+
"Specific to the user: only the user's own client may store it."
|
|
31
|
+
PRIVATE
|
|
32
|
+
}
|
|
33
|
+
GRAPHQL
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
CONTEXT_KEY = :lighthouse_cache_control
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
# The Cache-Control value for an executed query (a GraphQL::Query::Result
|
|
41
|
+
# or GraphQL::Query), or nil when the schema does not use @cacheControl.
|
|
42
|
+
def header(result)
|
|
43
|
+
query = result.respond_to?(:query) ? result.query : result
|
|
44
|
+
query.respond_to?(:context) ? query.context[CONTEXT_KEY] : nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Set the header on a Rails (or Rack) response; a no-op without one.
|
|
48
|
+
def apply(response, result)
|
|
49
|
+
value = header(result)
|
|
50
|
+
response.headers['Cache-Control'] = value if value
|
|
51
|
+
value
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_header(max_age, private_scope)
|
|
55
|
+
visibility = private_scope ? 'private' : 'public'
|
|
56
|
+
max_age.positive? ? "max-age=#{max_age}, #{visibility}" : "no-cache#{', private' if private_scope}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class Analyzer < ::GraphQL::Analysis::Analyzer
|
|
60
|
+
def initialize(subject)
|
|
61
|
+
super
|
|
62
|
+
@parents = []
|
|
63
|
+
@max_age = nil
|
|
64
|
+
@private = false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def on_enter_field(node, _parent, visitor)
|
|
68
|
+
field = visitor.field_definition
|
|
69
|
+
return @parents.push(@parents.last) if field.nil? || visitor.skipping?
|
|
70
|
+
|
|
71
|
+
max_age, scope = hint_for(field)
|
|
72
|
+
unless max_age.nil?
|
|
73
|
+
@max_age = [@max_age, max_age].compact.min
|
|
74
|
+
@private ||= scope == 'PRIVATE'
|
|
75
|
+
end
|
|
76
|
+
@parents.push(max_age.nil? ? @parents.last : max_age)
|
|
77
|
+
node
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def on_leave_field(_node, _parent, _visitor)
|
|
81
|
+
@parents.pop
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def result
|
|
85
|
+
value =
|
|
86
|
+
if query.mutation?
|
|
87
|
+
'no-cache'
|
|
88
|
+
else
|
|
89
|
+
Lighthouse::GraphQL::CacheControl.to_header(@max_age || 0, @private)
|
|
90
|
+
end
|
|
91
|
+
query.context[CONTEXT_KEY] = value
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def root?
|
|
97
|
+
@parents.empty?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# [maxAge, scope] the field contributes, or [nil, nil] to inherit.
|
|
101
|
+
def hint_for(field)
|
|
102
|
+
directive = cache_control(field.ast_node)
|
|
103
|
+
return from_directive(directive) if directive
|
|
104
|
+
return [0, 'PRIVATE'] if root?
|
|
105
|
+
|
|
106
|
+
return_type = field.type.unwrap
|
|
107
|
+
return [nil, nil] if return_type.kind.leaf?
|
|
108
|
+
|
|
109
|
+
type_directive = cache_control(return_type.ast_node)
|
|
110
|
+
type_directive ? from_directive(type_directive) : [0, 'PUBLIC']
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def from_directive(directive)
|
|
114
|
+
scope = Lighthouse::Support::DirectiveArgs.literal(directive, 'scope') || 'PUBLIC'
|
|
115
|
+
if Lighthouse::Support::DirectiveArgs.literal(directive, 'inheritMaxAge')
|
|
116
|
+
[@parents.last || 0, scope]
|
|
117
|
+
else
|
|
118
|
+
[Lighthouse::Support::DirectiveArgs.literal(directive, 'maxAge') || 0, scope]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def cache_control(ast_node)
|
|
123
|
+
ast_node&.directives&.find { |d| d.name == 'cacheControl' }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
|
|
7
|
+
# Keys and invalidation for @cache / @clearCache over Config.cache_store
|
|
8
|
+
# (Rails.cache by default).
|
|
9
|
+
#
|
|
10
|
+
# PHP Lighthouse invalidates with cache tags, which Rails cache stores do not
|
|
11
|
+
# have (and `delete_matched` is not universal). Instead every entry key embeds
|
|
12
|
+
# four version tokens — the type, the type + parent id, the type + field, and
|
|
13
|
+
# the type + parent id + field — and @clearCache replaces the matching token.
|
|
14
|
+
# Old entries are never read again and expire on their own. A token that
|
|
15
|
+
# was evicted is re-created with a fresh value, so eviction only ever causes
|
|
16
|
+
# misses, never stale reads.
|
|
17
|
+
module Lighthouse::GraphQL::Caching
|
|
18
|
+
PREFIX = 'lighthouse'
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def store
|
|
23
|
+
Lighthouse::Config.cache_store
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def entry_key(type, id, field, args, user_key)
|
|
27
|
+
tokens = versions(version_keys(type, id, field))
|
|
28
|
+
[PREFIX, type, id, field, digest(args), user_key, *tokens].join(':')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @clearCache: invalidate everything cached for `type`, narrowed to one
|
|
32
|
+
# parent `id` and/or one `field` when given.
|
|
33
|
+
def clear(type, id: nil, field: nil)
|
|
34
|
+
key = version_key(type, id, field)
|
|
35
|
+
store.write(key, SecureRandom.hex(8))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def version_keys(type, id, field)
|
|
39
|
+
[version_key(type, nil, nil), version_key(type, id, nil), version_key(type, nil, field), version_key(type, id, field)]
|
|
40
|
+
.uniq
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def version_key(type, id, field)
|
|
44
|
+
[PREFIX, 'version', type, id, field].join(':')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def versions(keys)
|
|
48
|
+
found = store.read_multi(*keys)
|
|
49
|
+
keys.map do |key|
|
|
50
|
+
found[key] || begin
|
|
51
|
+
store.write(key, SecureRandom.hex(8), unless_exist: true)
|
|
52
|
+
store.read(key)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def digest(args)
|
|
58
|
+
Digest::SHA256.hexdigest(JSON.generate(canonical(Lighthouse::Support::Arguments.plain(args))))[0, 32]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def canonical(value)
|
|
62
|
+
case value
|
|
63
|
+
when Hash then value.map { |k, v| [k.to_s, canonical(v)] }.sort_by(&:first)
|
|
64
|
+
when Array then value.map { |v| canonical(v) }
|
|
65
|
+
else value
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|