lighthouse-graphql 0.1.0 → 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 +168 -1
  3. data/README.md +46 -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 +52 -64
  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
data/docs/resolvers.md ADDED
@@ -0,0 +1,179 @@
1
+ # Queries, Mutations and Builders
2
+
3
+ Most fields need no Ruby: a directive (`@all`, `@find`, `@paginate`, `@create`,
4
+ …) or the default attribute reader resolves them. For the rest, Lighthouse looks
5
+ up a class by name. There are three kinds of class, and they are not
6
+ interchangeable. Each kind has its own contract and its own folder:
7
+
8
+ | Kind | Folder / namespace | Contract | Used by |
9
+ | --- | --- | --- | --- |
10
+ | Query | `app/graphql/queries` · `Queries::` | `GraphQL::Schema::Resolver` | `Query` fields |
11
+ | Mutation | `app/graphql/mutations` · `Mutations::` | `GraphQL::Schema::Mutation` | `Mutation` fields |
12
+ | Builder | `app/graphql/builders` · `Builders::` | `call(relation, args, context)` → relation | `@builder` |
13
+
14
+ Validators (`app/graphql/validators`) are the fourth kind. They are covered in
15
+ [validation.md](validation.md).
16
+
17
+ ## Queries and mutations: graphql-ruby's contract
18
+
19
+ A query or mutation class is a plain graphql-ruby
20
+ [resolver](https://graphql-ruby.org/fields/resolvers.html) or
21
+ [mutation](https://graphql-ruby.org/mutations/mutation_classes.html), so everything
22
+ you know from graphql-ruby applies:
23
+
24
+ - the field's arguments arrive as `snake_case` keyword arguments to `resolve`;
25
+ - `object` and `context` are available;
26
+ - `ready?` and `authorized?` run before `resolve`.
27
+
28
+ The SDL already declares the field's arguments and return type, so the class
29
+ declares neither.
30
+
31
+ ```graphql
32
+ type Query {
33
+ latestPosts(limit: Int): [Post!]!
34
+ }
35
+
36
+ type Mutation {
37
+ publishPost(id: ID!): Post
38
+ }
39
+ ```
40
+
41
+ ```ruby
42
+ # app/graphql/queries/latest_posts.rb
43
+ module Queries
44
+ class LatestPosts < GraphQL::Schema::Resolver
45
+ def resolve(limit: 10)
46
+ Post.order(created_at: :desc).limit(limit)
47
+ end
48
+ end
49
+ end
50
+
51
+ # app/graphql/mutations/publish_post.rb
52
+ module Mutations
53
+ class PublishPost < GraphQL::Schema::Mutation
54
+ def authorized?(id:)
55
+ context[:current_user]&.admin? || raise(GraphQL::ExecutionError, 'Admins only.')
56
+ end
57
+
58
+ def resolve(id:)
59
+ Post.find(id).tap { |post| post.update!(published: true) }
60
+ end
61
+ end
62
+ end
63
+ ```
64
+
65
+ ### Resolution by convention
66
+
67
+ A field on `Query` or `Mutation` with **no resolving directive** resolves to the
68
+ class named after it, as in PHP Lighthouse:
69
+
70
+ - `Query.latestPosts` resolves to `Queries::LatestPosts`;
71
+ - `Mutation.publishPost` resolves to `Mutations::PublishPost`.
72
+
73
+ The class is looked up on each call, so code reloading works. Classes found by
74
+ convention must inherit from `GraphQL::Schema::Resolver` (or `Mutation`, which
75
+ is one). Otherwise Lighthouse raises a `Lighthouse::DefinitionError`. A root
76
+ field with no class is reported through [`on_error`](configuration.md#on_error):
77
+ it raises in test and logs elsewhere. Lighthouse never falls back to a top-level
78
+ constant, so a `post` field is never mistaken for the `Post` model.
79
+
80
+ graphql-ruby's `RelayClassicMutation` (what `rails g graphql:install` generates
81
+ `Mutations::BaseMutation` from) works too, as long as the SDL field takes the
82
+ `input:` argument it expects.
83
+
84
+ Wrapping directives (`@guard`, the `@can*` family, `@validator`, `@cache`,
85
+ `@clearCache`) are not resolvers, so a field carrying only those still resolves by
86
+ convention:
87
+
88
+ ```graphql
89
+ type Mutation {
90
+ publishPost(id: ID!): Post @guard @canFind(ability: "publish", find: "id")
91
+ }
92
+ ```
93
+
94
+ ### Naming a class explicitly
95
+
96
+ `@field(resolver:)` points a field at a class by name. On `Query` and `Mutation`
97
+ the name is looked up in the queries and mutations namespaces; on any other type,
98
+ in [`resolver_namespaces`](configuration.md#resolver_namespaces).
99
+
100
+ ```graphql
101
+ type Query {
102
+ feed: [Post!]! @field(resolver: "Feeds::Home") # Queries::Feeds::Home
103
+ }
104
+ ```
105
+
106
+ Besides graphql-ruby resolver classes, `@field` also accepts a plain callable:
107
+ anything with `call(object, args, context)`.
108
+
109
+ ### `@spread` and `@inject`
110
+
111
+ Both shape the keyword arguments a class receives. They do the same for
112
+ [`@create` / `@update`](mutations.md).
113
+
114
+ ```graphql
115
+ type Mutation {
116
+ createDraft(input: CreateDraftInput! @spread): Post
117
+ @inject(context: "current_user.id", name: "author_id")
118
+ }
119
+ ```
120
+
121
+ `resolve(title:, body:, author_id:)` receives the input's fields at the top level.
122
+ `author_id` comes from `context[:current_user].id`, so clients cannot set it.
123
+ `@inject` is repeatable, and `name:` may be a dot path into an input
124
+ (`"input.author_id"`).
125
+
126
+ ## Builders
127
+
128
+ A builder is not a resolver: it does not produce the field's value. It refines the
129
+ query of a field that a model directive resolves. It receives the relation, the
130
+ field's arguments and the context, and returns a relation:
131
+
132
+ ```graphql
133
+ type Query {
134
+ posts: [Post!]! @paginate @builder(class: "Published")
135
+ }
136
+ ```
137
+
138
+ ```ruby
139
+ # app/graphql/builders/published.rb
140
+ module Builders
141
+ class Published
142
+ def self.call(relation, args, context)
143
+ relation.where(published: true)
144
+ end
145
+ end
146
+ end
147
+ ```
148
+
149
+ `@builder` works with `@all`, `@paginate`, `@first`, `@count` and `@aggregate`.
150
+ It is repeatable, and `method:` names a method other than `call`. Builder classes
151
+ are looked up in the builders namespace only. Raising a `GraphQL::ExecutionError`
152
+ from a builder (e.g. a tenancy check) always reaches the client; it never falls
153
+ back to the unscoped relation.
154
+
155
+ ## Generators
156
+
157
+ ```shell
158
+ bin/rails generate lighthouse:query LatestPosts # app/graphql/queries/latest_posts.rb
159
+ bin/rails generate lighthouse:mutation PublishPost # app/graphql/mutations/publish_post.rb
160
+ bin/rails generate lighthouse:builder Published # app/graphql/builders/published.rb
161
+ bin/rails generate lighthouse:validator CreatePostInput # app/graphql/validators/create_post_input_validator.rb
162
+ ```
163
+
164
+ Each one writes the class with its contract, into the first namespace configured
165
+ for its kind (or `--namespace=…`). Namespaced names nest modules:
166
+ `lighthouse:mutation Blog::Publish` writes `Mutations::Blog::Publish`. These
167
+ generators do not collide with graphql-ruby's `graphql:*` generators. A
168
+ `lighthouse:mutation` and a `graphql:mutation` of the same name target the same
169
+ file, and Rails asks before overwriting it.
170
+
171
+ ## Changing the namespaces
172
+
173
+ ```ruby
174
+ Lighthouse.configure do |config|
175
+ config.namespaces = { queries: ['Api::Queries'], mutations: ['Api::Mutations'] }
176
+ end
177
+ ```
178
+
179
+ Kinds you leave out keep their defaults. Each kind takes a list, tried in order.
@@ -0,0 +1,132 @@
1
+ # Validation
2
+
3
+ Rails puts validation in the model, and so does Lighthouse: the
4
+ [mutation directives](mutations.md) save through ActiveRecord, so `validates`
5
+ rules apply with no configuration. Not every rule belongs to a model, though:
6
+ a password confirmation, a rule across two arguments, the arguments of a query, or
7
+ input that never reaches a model at all. For those, `@validator` runs a contract
8
+ class before the field resolves.
9
+
10
+ Both report errors the same way, in PHP Lighthouse's format: messages keyed by
11
+ argument path under `extensions.validation`.
12
+
13
+ ```json
14
+ {
15
+ "data": { "signUp": null },
16
+ "errors": [{
17
+ "message": "Validation failed for the field [signUp].",
18
+ "extensions": { "validation": { "passwordConfirmation": ["must match password"] } }
19
+ }]
20
+ }
21
+ ```
22
+
23
+ ## Model validations
24
+
25
+ Nothing to do. A failed `save` in `@create`, `@update`, `@createMany` or
26
+ `@updateMany` reports the model's `full_message`s, keyed by the argument each
27
+ attribute came from (see [mutations.md](mutations.md#validation-errors)).
28
+
29
+ ## `@validator`
30
+
31
+ ```graphql
32
+ directive @validator(class: String) on FIELD_DEFINITION | INPUT_OBJECT
33
+ ```
34
+
35
+ ### On a field
36
+
37
+ The contract receives all of the field's arguments:
38
+
39
+ ```graphql
40
+ type Mutation {
41
+ signUp(email: String!, password: String!, passwordConfirmation: String): User @validator
42
+ }
43
+ ```
44
+
45
+ ```ruby
46
+ # app/graphql/validators/mutation/sign_up_validator.rb
47
+ module Validators
48
+ module Mutation
49
+ class SignUpValidator < Dry::Validation::Contract
50
+ params do
51
+ required(:email).filled(:string)
52
+ required(:password).filled(:string, min_size?: 12)
53
+ optional(:password_confirmation).maybe(:string)
54
+ end
55
+
56
+ rule(:password_confirmation) do
57
+ key.failure('must match password') if value && value != values[:password]
58
+ end
59
+ end
60
+ end
61
+ end
62
+ ```
63
+
64
+ The class defaults to `Validators::<ParentType>::<FieldName>Validator`. Name
65
+ another with `@validator(class: "SignUpRules")`; it is looked up in the validators
66
+ namespace.
67
+
68
+ ### On an input type
69
+
70
+ The contract receives that input's values. It runs **wherever the type is used**:
71
+ as an argument, nested inside another input, or as each item of a list. The
72
+ fields that take the input need no directive.
73
+
74
+ ```graphql
75
+ input AddressInput @validator {
76
+ street: String
77
+ zipCode: String
78
+ }
79
+
80
+ type Mutation {
81
+ ship(to: AddressInput!, billing: BillingInput): Shipment
82
+ shipMany(to: [AddressInput!]!): [Shipment!]!
83
+ }
84
+ ```
85
+
86
+ The class defaults to `Validators::<InputType>Validator`, here
87
+ `Validators::AddressInputValidator`. Errors are prefixed with the path to the
88
+ input: `to.zipCode`, `billing.address.street`, `to.1.street`.
89
+
90
+ ### Contracts
91
+
92
+ A contract is a [Dry::Validation](https://dry-rb.org/gems/dry-validation/)
93
+ contract:
94
+
95
+ - Values arrive with `snake_case` symbol keys, and the error keys are mapped back
96
+ to the GraphQL argument names.
97
+ - Declare `option :graphql_context, optional: true` to read the GraphQL context in
98
+ rules (e.g. `graphql_context[:current_user]`).
99
+ - `dry-validation` is an optional dependency. Add it to your Gemfile to use these
100
+ contracts.
101
+
102
+ Any other object works if its `call(values)` returns a Dry-style result, or a
103
+ `{ key => [messages] }` Hash (empty when valid):
104
+
105
+ ```ruby
106
+ module Validators
107
+ class NoSpam
108
+ def call(values)
109
+ values[:title].to_s.include?('spam') ? { title: ['looks like spam'] } : {}
110
+ end
111
+ end
112
+ end
113
+ ```
114
+
115
+ A missing class raises `Lighthouse::DefinitionError`. Lighthouse never silently
116
+ skips validation.
117
+
118
+ ### Order
119
+
120
+ Validation runs after authentication and authorization (`@guard`, `@can*`), so
121
+ anonymous clients cannot probe your rules, and before `@cache` and the resolver.
122
+ All contracts on a field run, and their errors are reported together.
123
+
124
+ Generate a contract with `bin/rails generate lighthouse:validator AddressInput`.
125
+
126
+ ## Why not `@rules`?
127
+
128
+ PHP Lighthouse's `@rules(apply: ["email", "max:280"])` embeds Laravel's
129
+ string-based rule language in the schema. Rails has no equivalent to port: its
130
+ validations live in models, and Dry contracts express the rest as Ruby with
131
+ proper types. So Lighthouse for Rails supports those two, and does not invent a
132
+ third rule syntax.
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'class_generator'
4
+
5
+ # `rails generate lighthouse:builder PublishedPosts` — a query builder for
6
+ # `@builder(class: "PublishedPosts")`.
7
+ class Lighthouse::Generators::BuilderGenerator < Lighthouse::Generators::ClassGenerator
8
+ desc 'Generate a query builder for @builder in app/graphql/builders.'
9
+
10
+ private
11
+
12
+ def kind = :builders
13
+
14
+ def body
15
+ <<~RUBY
16
+ # Applied by @builder(class: "#{class_name}") before the field resolves:
17
+ # receives the relation, the field's arguments and the GraphQL context, and
18
+ # returns the relation to use.
19
+ class #{class_basename}
20
+ def self.call(relation, args, context)
21
+ relation
22
+ end
23
+ end
24
+ RUBY
25
+ end
26
+
27
+ def usage
28
+ "Use it with @builder(class: \"#{class_name}\") on a field resolved by @all, @paginate, @first, @count or @aggregate."
29
+ end
30
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'lighthouse-graphql'
5
+
6
+ # Base for the generators that scaffold the classes a schema refers to by
7
+ # name — queries, mutations, builders and validators — each in its own
8
+ # namespace (Config.namespaces) and with its own contract.
9
+ class Lighthouse::Generators::ClassGenerator < Rails::Generators::NamedBase
10
+ hide!
11
+
12
+ class_option :namespace, type: :string,
13
+ desc: 'Namespace to generate into (default: the first of Config.namespaces for this kind)'
14
+
15
+ def create_class
16
+ create_file File.join('app/graphql', *modules.map(&:underscore), "#{class_basename.underscore}.rb"), source
17
+ end
18
+
19
+ def show_usage
20
+ say_status :usage, usage, :blue if usage
21
+ end
22
+
23
+ private
24
+
25
+ # :queries, :mutations, :builders or :validators.
26
+ def kind
27
+ raise NotImplementedError
28
+ end
29
+
30
+ def body
31
+ raise NotImplementedError
32
+ end
33
+
34
+ def usage
35
+ nil
36
+ end
37
+
38
+ def class_basename
39
+ class_name.demodulize
40
+ end
41
+
42
+ def namespace_name
43
+ options[:namespace] || Lighthouse::Config.namespaces[kind].first
44
+ end
45
+
46
+ def modules
47
+ namespace_name.split('::') + class_name.split('::')[0...-1]
48
+ end
49
+
50
+ def full_class_name
51
+ (modules + [class_basename]).join('::')
52
+ end
53
+
54
+ def source
55
+ lines = ['# frozen_string_literal: true', '']
56
+ modules.each_with_index { |mod, depth| lines << "#{' ' * depth}module #{mod}" }
57
+ body.each_line { |line| lines << (line.strip.empty? ? '' : "#{' ' * modules.size}#{line.chomp}") }
58
+ modules.size.times.reverse_each { |depth| lines << "#{' ' * depth}end" }
59
+ "#{lines.join("\n")}\n"
60
+ end
61
+ end
@@ -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
+ }