lighthouse-graphql 0.2.0 → 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 +22 -1
- data/README.md +5 -1
- data/docs/motivation.md +184 -0
- data/docs/resolvers.md +10 -0
- data/lib/generators/lighthouse/builder_generator.rb +1 -1
- data/lib/lighthouse/graphql/version.rb +1 -1
- data/sig/lighthouse/directives.rbs +158 -0
- data/sig/lighthouse/graphql.rbs +78 -0
- data/sig/lighthouse.rbs +142 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 844e3fc4942a637b4353427932e031c210ad52d4d425161a58f45e2e36f1d931
|
|
4
|
+
data.tar.gz: 9b0b05109d1fcac2f330b7266fe91eb502d521dabce399adfd98828137119468
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef2d22dd33355e36e3b40725cea55ebf5923a06ea2c94660228d8cc6b3a6e222bd7da6f42f796d52275a2130c1ebd9e54844f85febf31d66a7a4c759d310723e
|
|
7
|
+
data.tar.gz: 7334b3e2b4ecb745fea9a4f9c08860d8e5c6245654323b0161a0b97c367d8ea022a3b23f7bd7e5bbd8c4e8614939b16e82fa737e9e133e4c5cba555199088c1d
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,26 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.1] - 2026-10-17
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- RBS signatures in `sig/`, shipped with the gem, for the parts an app implements or calls:
|
|
14
|
+
`Lighthouse::_Builder` (what a `@builder` class answers) and `_Validator`, every
|
|
15
|
+
`Lighthouse::Config` setting with the callables it takes (`_ErrorHandler`, `_UserReader`,
|
|
16
|
+
`_Authorizer`), the directive extension points (`DirectiveRegistry`, the contracts,
|
|
17
|
+
`BaseDirectiveResolver`, `Directives::Arguments::Base`), the entrypoints (`get_schema`,
|
|
18
|
+
`SchemaFactory`, `PersistedQueries`, `CacheControl`, `SchemaCache`, `IdeHelper`) and the
|
|
19
|
+
errors. `rbs collection` (and so Steep) loads them from the installed gem, as it does for any
|
|
20
|
+
gem that ships `sig/`; nothing changes at runtime.
|
|
21
|
+
CI validates them and runs the suite with every Lighthouse method checked against its
|
|
22
|
+
signature, so they cannot drift from the code.
|
|
23
|
+
- A documentation site, built from `docs/` with VuePress as PHP Lighthouse builds its own,
|
|
24
|
+
and published to GitHub Pages on every push to `main`:
|
|
25
|
+
<https://vaz-innovation.github.io/lighthouse-graphql/>.
|
|
26
|
+
- `docs/motivation.md`: where Lighthouse comes from, why the schema comes first, and why
|
|
27
|
+
directives work so well on Active Record — and when something else fits better.
|
|
28
|
+
|
|
9
29
|
## [0.2.0] - 2026-10-17
|
|
10
30
|
|
|
11
31
|
### Added
|
|
@@ -208,7 +228,8 @@ had been running inside a private monorepo as a path gem before being extracted.
|
|
|
208
228
|
- Config-driven extension seams: `Lighthouse.configure`, `resolver_namespaces`,
|
|
209
229
|
`on_error`, and `DirectiveRegistry.register` for app-defined directives.
|
|
210
230
|
|
|
211
|
-
[Unreleased]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.2.
|
|
231
|
+
[Unreleased]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.2.1...HEAD
|
|
232
|
+
[0.2.1]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.2.0...v0.2.1
|
|
212
233
|
[0.2.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.1.1...v0.2.0
|
|
213
234
|
[0.1.1]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.1.0...v0.1.1
|
|
214
235
|
[0.1.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -39,7 +39,7 @@ paginate, walk a relationship, or read an attribute. Lighthouse — like its PHP
|
|
|
39
39
|
inspiration — captures those patterns as **directives** you attach in the schema,
|
|
40
40
|
and falls back to a sensible default reader for plain fields. You write Ruby only
|
|
41
41
|
for the genuinely custom parts (`@field(resolver: "...")`), and your schema stays
|
|
42
|
-
the single source of truth.
|
|
42
|
+
the single source of truth. [Motivation](docs/motivation.md) explains the idea in depth.
|
|
43
43
|
|
|
44
44
|
### Inspirations
|
|
45
45
|
|
|
@@ -142,8 +142,12 @@ are the library's end-to-end suite. See [example/README.md](example/README.md).
|
|
|
142
142
|
|
|
143
143
|
## Documentation
|
|
144
144
|
|
|
145
|
+
**Read it at [vaz-innovation.github.io/lighthouse-graphql](https://vaz-innovation.github.io/lighthouse-graphql/)**,
|
|
146
|
+
or as Markdown in [`docs/`](docs):
|
|
147
|
+
|
|
145
148
|
| Guide | What it covers |
|
|
146
149
|
| --- | --- |
|
|
150
|
+
| [Motivation](docs/motivation.md) | Where Lighthouse comes from, why the schema comes first, and why directives fit Active Record |
|
|
147
151
|
| [Getting started](docs/getting-started.md) | Install, base types, building & serving the schema |
|
|
148
152
|
| [Directives reference](docs/directives.md) | Every built-in directive, grouped by purpose |
|
|
149
153
|
| [Relationships](docs/relationships.md) | `@hasMany`, `@belongsTo`, `@belongsToMany`, `@hasOne`, the `@morph*` variants, batching and `type:` pagination |
|
data/docs/motivation.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Motivation
|
|
2
|
+
|
|
3
|
+
Lighthouse GraphQL Rails brings the ideas of [Lighthouse](https://lighthouse-php.com),
|
|
4
|
+
the GraphQL framework for Laravel, to Ruby on Rails. This page explains those ideas:
|
|
5
|
+
why the schema comes first, why behaviour lives in directives, and why that works so
|
|
6
|
+
well on top of an opinionated model layer like Active Record.
|
|
7
|
+
|
|
8
|
+
## Where it comes from
|
|
9
|
+
|
|
10
|
+
Lighthouse ([nuwave/lighthouse](https://github.com/nuwave/lighthouse)) changed how many
|
|
11
|
+
Laravel teams build GraphQL APIs. Instead of a resolver class for every field, you write
|
|
12
|
+
the schema in GraphQL's own language and attach **directives** that say what a field
|
|
13
|
+
does: `@paginate` it, follow a `@hasMany` relationship, `@create` a model, check a policy
|
|
14
|
+
with `@canFind`. Lighthouse reads those directives and generates efficient queries against
|
|
15
|
+
Eloquent, Laravel's ORM.
|
|
16
|
+
|
|
17
|
+
The idea does not depend on PHP. It depends on two things: a schema written first, and a
|
|
18
|
+
model layer with strong conventions. Rails has the second in Active Record, and
|
|
19
|
+
[graphql-ruby](https://graphql-ruby.org) gives Ruby a mature GraphQL engine to build the
|
|
20
|
+
first on. Lighthouse GraphQL Rails puts them together. It keeps Lighthouse's directive
|
|
21
|
+
names, arguments and error formats wherever they make sense, so its documentation, its
|
|
22
|
+
examples and the experience of Laravel teams carry over.
|
|
23
|
+
|
|
24
|
+
## The problem with writing every resolver
|
|
25
|
+
|
|
26
|
+
Most GraphQL fields in a Rails app do one of a handful of things: list records, find one,
|
|
27
|
+
paginate, follow an association, filter and sort, read an attribute, or create, update
|
|
28
|
+
and delete a record. Written by hand, each of these is a resolver. Each resolver is a
|
|
29
|
+
place where:
|
|
30
|
+
|
|
31
|
+
- **the API's shape hides in Ruby.** To know what `posts` accepts and returns, you read
|
|
32
|
+
a type class, its fields, their resolver methods and the scopes they call.
|
|
33
|
+
- **N+1 queries creep in.** Every association needs its own batch loader, and every
|
|
34
|
+
paginated association needs a separate count, page and cursor logic.
|
|
35
|
+
- **the same decisions are made again**, slightly differently each time: how pages are
|
|
36
|
+
numbered, what a cursor contains, how validation errors are reported, which filter
|
|
37
|
+
arguments are allowed.
|
|
38
|
+
|
|
39
|
+
With [graphql-ruby](https://graphql-ruby.org) alone, a list of published posts with their
|
|
40
|
+
author and comments, filterable by author, looks roughly like this:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
class Types::PostType < Types::BaseObject
|
|
44
|
+
field :id, ID, null: false
|
|
45
|
+
field :title, String, null: false
|
|
46
|
+
field :author, Types::UserType, null: false
|
|
47
|
+
field :comments, Types::CommentType.connection_type, null: false
|
|
48
|
+
|
|
49
|
+
def author
|
|
50
|
+
dataloader.with(Sources::RecordById, User).load(object.author_id)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def comments
|
|
54
|
+
object.comments.order(:created_at) # one query per post
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class Types::QueryType < Types::BaseObject
|
|
59
|
+
field :posts, Types::PostType.connection_type, null: false do
|
|
60
|
+
argument :author_id, ID, required: false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def posts(author_id: nil)
|
|
64
|
+
scope = Post.published.order(created_at: :desc)
|
|
65
|
+
author_id ? scope.where(author_id: author_id) : scope
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
…plus the `Sources::RecordById` batch loader, and more code to batch the comments. With
|
|
71
|
+
Lighthouse, it is the schema:
|
|
72
|
+
|
|
73
|
+
```graphql
|
|
74
|
+
type Query {
|
|
75
|
+
posts(authorId: ID @eq): [Post!]! @paginate(type: CONNECTION) @builder(class: "Published")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type Post {
|
|
79
|
+
id: ID!
|
|
80
|
+
title: String!
|
|
81
|
+
author: User! @belongsTo
|
|
82
|
+
comments: [Comment!]! @hasMany(type: CONNECTION, scopes: ["chronological"])
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The `author` of every post loads in one query, and so does every post's page of
|
|
87
|
+
comments, with one more for their totals if the client asks for `pageInfo { total }`. This isn't a
|
|
88
|
+
special case: it is how every relationship directive works.
|
|
89
|
+
|
|
90
|
+
## Why the schema comes first
|
|
91
|
+
|
|
92
|
+
In a schema-first API, the `.graphql` files *are* the API's definition, not something
|
|
93
|
+
generated from Ruby classes:
|
|
94
|
+
|
|
95
|
+
- **It is a contract you can read.** Frontend developers, mobile developers and reviewers
|
|
96
|
+
read the same SDL that clients query. A pull request that changes the API shows the
|
|
97
|
+
change in the schema diff.
|
|
98
|
+
- **It is designed before it is implemented.** You can agree on the types and fields
|
|
99
|
+
first, as a team, and the server follows the design rather than leaking its internals.
|
|
100
|
+
- **Tools understand it directly.** IDE plugins, code generators and schema registries
|
|
101
|
+
read SDL. Lighthouse's `lighthouse:ide_helper` task adds its directives and generated
|
|
102
|
+
types so editors validate and autocomplete the schema.
|
|
103
|
+
- **It says what each field does.** `posts: [Post!]! @paginate @guard @cacheControl(maxAge: 60)`
|
|
104
|
+
tells a reader, without opening any Ruby, that the field is paginated, requires a
|
|
105
|
+
signed-in user and can be cached for a minute.
|
|
106
|
+
|
|
107
|
+
## Why directives
|
|
108
|
+
|
|
109
|
+
Directives are a small vocabulary that composes. Each one does one job, and a field
|
|
110
|
+
combines them:
|
|
111
|
+
|
|
112
|
+
```graphql
|
|
113
|
+
type Mutation {
|
|
114
|
+
updatePost(input: UpdatePostInput! @spread): Post!
|
|
115
|
+
@guard # require a user
|
|
116
|
+
@canFind(ability: "update", find: "input.id") # PostPolicy#update?
|
|
117
|
+
@update # save through Active Record, in a transaction
|
|
118
|
+
@clearCache(type: "Post", idSource: { argument: "input.id" })
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Because the behaviour is declared, Lighthouse controls the order it runs in
|
|
123
|
+
(authentication, then authorization, then validation, then the save, then cache
|
|
124
|
+
invalidation) and handles the details the same way everywhere: pagination types and
|
|
125
|
+
cursors, validation errors keyed by argument path, `Cache-Control` headers. That
|
|
126
|
+
consistency is what a hand-written resolver per field rarely achieves.
|
|
127
|
+
|
|
128
|
+
## Why it fits Active Record
|
|
129
|
+
|
|
130
|
+
A directive can only be this short if something else knows the details. In Lighthouse,
|
|
131
|
+
that is the model layer. Active Record is opinionated, and every convention it imposes is
|
|
132
|
+
information Lighthouse can use:
|
|
133
|
+
|
|
134
|
+
| Active Record already knows | So Lighthouse can |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| Associations, their foreign keys, `:through` and polymorphic links | Resolve `@hasMany`, `@belongsTo`, `@belongsToMany` and `@morph*` from the name alone, batch them, and paginate many parents in one query |
|
|
137
|
+
| Scopes | Narrow any query with `scopes: ["published"]` |
|
|
138
|
+
| Validations | Report a failed `@create` or `@update` by argument path, with the model's own messages and locale |
|
|
139
|
+
| Transactions and callbacks | Run nested mutations (create a post, attach tags, add comments) atomically, with your callbacks |
|
|
140
|
+
| Naming conventions (`Post`, `posts`, `author_id`) | Map `Post` to its model, `authorId` to `author_id`, and a field to its association, with no configuration |
|
|
141
|
+
|
|
142
|
+
The schema says **what** the API offers; the models already say **how** the data
|
|
143
|
+
behaves. Nothing is described twice, and your domain logic stays where Rails puts it,
|
|
144
|
+
in models, scopes, validations and policies, instead of moving into GraphQL resolvers.
|
|
145
|
+
That is the same reason Lighthouse works so well on Eloquent: both are opinionated
|
|
146
|
+
Active Record ORMs, and conventions are what make a declarative layer on top possible.
|
|
147
|
+
|
|
148
|
+
## Ruby where there is real logic
|
|
149
|
+
|
|
150
|
+
Schema-first doesn't mean schema-only. When a field does something no directive
|
|
151
|
+
describes, write Ruby, using graphql-ruby's own classes:
|
|
152
|
+
|
|
153
|
+
- A `Query` or `Mutation` field without a resolving directive resolves by convention to
|
|
154
|
+
`Queries::FieldName` (a `GraphQL::Schema::Resolver`) or `Mutations::FieldName` (a
|
|
155
|
+
`GraphQL::Schema::Mutation`). See [Queries, Mutations and Builders](resolvers.md).
|
|
156
|
+
- `@builder` classes adjust a query; `@validator` contracts check arguments.
|
|
157
|
+
- Your own directives plug in through the registry, with the same contracts as the
|
|
158
|
+
built-in ones. See [Custom Directives](custom-directives.md).
|
|
159
|
+
|
|
160
|
+
Lighthouse is built on graphql-ruby, not instead of it: its Dataloader, its execution
|
|
161
|
+
engine and its classes are all there when you need them.
|
|
162
|
+
|
|
163
|
+
## From small apps to large ones
|
|
164
|
+
|
|
165
|
+
The same schema serves a side project and a busy API. When it grows, opt-in features keep
|
|
166
|
+
it fast without changing the schema: a cached schema build for quick boots, Apollo's
|
|
167
|
+
persisted queries for CDN caching, and an allowlist of trusted operations. They are all
|
|
168
|
+
off by default. See [Performance at scale](performance.md).
|
|
169
|
+
|
|
170
|
+
## When something else fits better
|
|
171
|
+
|
|
172
|
+
Lighthouse is at its best when the API is close to your models. Consider plain
|
|
173
|
+
graphql-ruby, or using Lighthouse only for part of the schema, when:
|
|
174
|
+
|
|
175
|
+
- the API is mostly computed, aggregated or fetched from other services, rather than read
|
|
176
|
+
from and written to your database;
|
|
177
|
+
- your team prefers to define the schema in Ruby and generate the SDL from it
|
|
178
|
+
(code-first);
|
|
179
|
+
- types differ so much from your tables that most fields would need their own resolver.
|
|
180
|
+
|
|
181
|
+
Both approaches run on graphql-ruby, and a Lighthouse schema can call graphql-ruby
|
|
182
|
+
classes anywhere, so you can start with directives and write Ruby where it pays off.
|
|
183
|
+
|
|
184
|
+
Ready to try it? Continue with [Getting Started](getting-started.md).
|
data/docs/resolvers.md
CHANGED
|
@@ -152,6 +152,16 @@ are looked up in the builders namespace only. Raising a `GraphQL::ExecutionError
|
|
|
152
152
|
from a builder (e.g. a tenancy check) always reaches the client; it never falls
|
|
153
153
|
back to the unscoped relation.
|
|
154
154
|
|
|
155
|
+
The gem ships RBS signatures in `sig/`, so with Steep (or anything that reads
|
|
156
|
+
`rbs collection`) a builder can be checked against `Lighthouse::_Builder`:
|
|
157
|
+
|
|
158
|
+
```rbs
|
|
159
|
+
# sig/builders/published.rbs
|
|
160
|
+
class Builders::Published
|
|
161
|
+
extend Lighthouse::_Builder
|
|
162
|
+
end
|
|
163
|
+
```
|
|
164
|
+
|
|
155
165
|
## Generators
|
|
156
166
|
|
|
157
167
|
```shell
|
|
@@ -15,7 +15,7 @@ class Lighthouse::Generators::BuilderGenerator < Lighthouse::Generators::ClassGe
|
|
|
15
15
|
<<~RUBY
|
|
16
16
|
# Applied by @builder(class: "#{class_name}") before the field resolves:
|
|
17
17
|
# receives the relation, the field's arguments and the GraphQL context, and
|
|
18
|
-
# returns the relation to use.
|
|
18
|
+
# returns the relation to use. Its type is Lighthouse::_Builder.
|
|
19
19
|
class #{class_basename}
|
|
20
20
|
def self.call(relation, args, context)
|
|
21
21
|
relation
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# The extension points for custom directives: the contracts a directive class
|
|
2
|
+
# includes, the registry it is registered in, and the base classes it can
|
|
3
|
+
# subclass. See docs/custom-directives.md.
|
|
4
|
+
module Lighthouse
|
|
5
|
+
# What the registry asks a self-describing directive class: its SDL name
|
|
6
|
+
# (without the @) and its `directive @name(...) on ...` definition.
|
|
7
|
+
interface _DirectiveClass
|
|
8
|
+
def directive_name: () -> String
|
|
9
|
+
def definition: () -> String?
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Marker modules: the registry detects a class's capabilities by which of
|
|
13
|
+
# them it includes. Only FieldResolver and ArgBuilder change how a class is
|
|
14
|
+
# registered; the others document intent.
|
|
15
|
+
module Contracts
|
|
16
|
+
module Directive
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Its instances, built with (field_def, type_def, directive_node, opts),
|
|
20
|
+
# install the field's resolver in `define_resolver(target_type)`.
|
|
21
|
+
module FieldResolver
|
|
22
|
+
include Directive
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Its instances, built with (arg_node, directive_node), answer
|
|
26
|
+
# `handle_builder(relation, value)` with the constrained relation.
|
|
27
|
+
module ArgBuilder
|
|
28
|
+
include Directive
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module FieldMiddleware
|
|
32
|
+
include Directive
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module FieldManipulator
|
|
36
|
+
include Directive
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
module ArgManipulator
|
|
40
|
+
include Directive
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class DirectiveRegistry
|
|
45
|
+
RESOLUTION_ORDER: Hash[String, Integer]
|
|
46
|
+
NAMED_RESOLVERS: Hash[String, Symbol]
|
|
47
|
+
SELF_DESCRIBING_RESOLVERS: Array[Symbol]
|
|
48
|
+
ARG_BUILDERS: Array[Symbol]
|
|
49
|
+
PASSIVE_DEFINITIONS: Hash[String, String]
|
|
50
|
+
|
|
51
|
+
def self.default: () -> DirectiveRegistry
|
|
52
|
+
|
|
53
|
+
# register(MyDirective) when the class answers `directive_name`, else
|
|
54
|
+
# register('name', MyDirective).
|
|
55
|
+
def self.register: (Class | String | Symbol name_or_klass, ?Class? klass) -> DirectiveRegistry
|
|
56
|
+
|
|
57
|
+
def initialize: () -> void
|
|
58
|
+
def register: (Class | String | Symbol name_or_klass, ?Class? klass) -> self
|
|
59
|
+
def register_defaults: () -> self
|
|
60
|
+
def register_definition: (String | Symbol name, String definition) -> self
|
|
61
|
+
def register_type_definition: (String | Symbol name, String definition) -> self
|
|
62
|
+
def register_manipulator: (_SdlManipulator manipulator) -> self
|
|
63
|
+
|
|
64
|
+
def resolution_class: (String | Symbol name) -> Class?
|
|
65
|
+
def resolution_order: (String | Symbol name) -> Integer
|
|
66
|
+
def wrapper?: (String | Symbol name) -> bool
|
|
67
|
+
def resolution_names: () -> Array[String]
|
|
68
|
+
def arg_builder_class: (String | Symbol name) -> Class?
|
|
69
|
+
def arg_builder_names: () -> Array[String]
|
|
70
|
+
|
|
71
|
+
# { "name" => "directive @name(...) on ..." }
|
|
72
|
+
def directive_definitions: () -> Hash[String, String]
|
|
73
|
+
# { "Name" => "enum Name { ... }" }
|
|
74
|
+
def type_definitions: () -> Hash[String, String]
|
|
75
|
+
|
|
76
|
+
attr_reader sdl_manipulators: Array[_SdlManipulator]
|
|
77
|
+
def manipulate_sdl: (String sdl) -> String
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Writes the SDL an IDE needs next to the app's own: `bin/rails lighthouse:ide_helper`.
|
|
81
|
+
module IdeHelper
|
|
82
|
+
DIRECTIVES_FILE: String
|
|
83
|
+
PROGRAMMATIC_TYPES_FILE: String
|
|
84
|
+
|
|
85
|
+
# Writes both files into `output_dir` and returns their paths.
|
|
86
|
+
def self?.write: (output_dir: path, ?sdl_folder: path?, ?registry: DirectiveRegistry) -> Array[String]
|
|
87
|
+
def self?.directives_sdl: (?String sdl, ?registry: DirectiveRegistry) -> String
|
|
88
|
+
def self?.programmatic_types_sdl: (String sdl, ?registry: DirectiveRegistry) -> String
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
module GraphQL
|
|
92
|
+
module DirectiveResolvers
|
|
93
|
+
# The base of the built-in field directives, and of a custom one:
|
|
94
|
+
# subclass it and implement `define_resolver`.
|
|
95
|
+
class BaseDirectiveResolver
|
|
96
|
+
# graphql-ruby's field and type definitions, and the directive's AST node.
|
|
97
|
+
@field_def: untyped
|
|
98
|
+
@type_def: untyped
|
|
99
|
+
@directive_node: untyped
|
|
100
|
+
@opts: Hash[Symbol, untyped]
|
|
101
|
+
|
|
102
|
+
def initialize: (untyped field_def, untyped type_def, untyped directive_node, ?Hash[Symbol, untyped] opts) -> void
|
|
103
|
+
|
|
104
|
+
# Install the field's resolver on `target_type_class` (a graphql-ruby type class).
|
|
105
|
+
def define_resolver: (untyped target_type_class) -> untyped
|
|
106
|
+
|
|
107
|
+
def apply_where_filters: (relation relation, untyped args) -> relation
|
|
108
|
+
def apply_arg_builders: (relation relation, arguments args_h) -> relation
|
|
109
|
+
def extract_relation_name_from_directive: (untyped arg_node) -> String?
|
|
110
|
+
def constantize_model: (String? name) -> Class?
|
|
111
|
+
def apply_scopes: (relation relation) -> relation
|
|
112
|
+
|
|
113
|
+
# The literal value of an argument of this directive.
|
|
114
|
+
def directive_arg: (String name) -> untyped
|
|
115
|
+
def resolver_method_name: () -> String
|
|
116
|
+
# The field's resolver so far, for a directive that wraps it.
|
|
117
|
+
def capture_inner: (untyped target_type_class) -> untyped
|
|
118
|
+
def install: (untyped target_type_class, untyped callable) -> untyped
|
|
119
|
+
# The model the directive acts on; raises DefinitionError when there is none.
|
|
120
|
+
def model_class: () -> Class
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Applies the field's @builder directives, in SDL order, to its relation.
|
|
124
|
+
class BuilderApplier
|
|
125
|
+
DEFINITION: String
|
|
126
|
+
|
|
127
|
+
def self.apply: (relation relation, untyped field_def_ast_node, arguments args, context ctx, untyped resolver_instance) -> relation
|
|
128
|
+
def self.apply_directive: (relation relation, untyped directive_node, arguments args, context ctx, untyped resolver_instance) -> relation
|
|
129
|
+
def self.extract_directive_arg: (untyped directive_node, String name) -> untyped
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
module Directives
|
|
134
|
+
module Arguments
|
|
135
|
+
# The base of the argument filters (@eq, @in, @orderBy, ...), and of a
|
|
136
|
+
# custom one: subclass it and implement `handle_builder`.
|
|
137
|
+
class Base
|
|
138
|
+
include Contracts::ArgBuilder
|
|
139
|
+
|
|
140
|
+
# graphql-ruby's AST nodes for the argument and the directive on it.
|
|
141
|
+
@arg_node: untyped
|
|
142
|
+
@directive_node: untyped
|
|
143
|
+
|
|
144
|
+
def initialize: (untyped arg_node, untyped directive_node) -> void
|
|
145
|
+
|
|
146
|
+
# Constrain the relation by the value the client gave the argument.
|
|
147
|
+
def handle_builder: (relation relation, untyped value) -> relation
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
# The directive's `key:`, else the argument's name in snake_case.
|
|
152
|
+
def column: () -> String
|
|
153
|
+
def arel_column: (relation relation) -> untyped
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# The entrypoints: building the schema from SDL and running operations.
|
|
2
|
+
module Lighthouse
|
|
3
|
+
module GraphQL
|
|
4
|
+
VERSION: String
|
|
5
|
+
|
|
6
|
+
Config: singleton(Lighthouse::Config)
|
|
7
|
+
|
|
8
|
+
# A GraphQL::Schema subclass.
|
|
9
|
+
type schema = untyped
|
|
10
|
+
|
|
11
|
+
module RbLightHouse
|
|
12
|
+
# The schema built from the SDL in `sdl_folder` (default: Config.sdl_folder).
|
|
13
|
+
# `options` takes :base_types, :base_schema and the schema's own options.
|
|
14
|
+
def self.get_schema: (?sdl_folder: path?, ?options: Hash[Symbol, untyped]) -> schema
|
|
15
|
+
def self.configure: () ?{ (singleton(Lighthouse::Config) config) -> void } -> void
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module SchemaFactory
|
|
19
|
+
def self?.build_base_schema: (?sdl_folder: path?, ?sdl: String?, ?base_types: Hash[untyped, untyped],
|
|
20
|
+
?schema_options: Hash[Symbol, untyped], **untyped extra_options) -> schema
|
|
21
|
+
def self?.load_sdl: (path? sdl_folder) -> String
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Apollo's Automatic Persisted Queries, and a manifest of trusted operations.
|
|
25
|
+
module PersistedQueries
|
|
26
|
+
HASH: Regexp
|
|
27
|
+
NOT_FOUND: Array[String]
|
|
28
|
+
NOT_SUPPORTED: Array[String]
|
|
29
|
+
HASH_MISMATCH: Array[String]
|
|
30
|
+
NOT_IN_LIST: Array[String]
|
|
31
|
+
|
|
32
|
+
# A request the protocol rejects, rendered with Apollo's error codes.
|
|
33
|
+
class Error < StandardError
|
|
34
|
+
attr_reader code: String
|
|
35
|
+
|
|
36
|
+
def initialize: (String message, String code) -> void
|
|
37
|
+
def to_h: () -> Hash[String, untyped]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# `extensions` as the request carries it: a Hash, a JSON String (GET) or
|
|
41
|
+
# ActionController::Parameters.
|
|
42
|
+
#
|
|
43
|
+
# `schema.execute` with the query the request names: its result, or a
|
|
44
|
+
# protocol error as a ready-to-render { "errors" => [...] } Hash.
|
|
45
|
+
def self?.execute: (schema schema, query: String?, ?extensions: untyped, **untyped options) -> untyped
|
|
46
|
+
# The query text to run for a request's `query` and `extensions`.
|
|
47
|
+
def self?.resolve: (String? query, untyped extensions) -> String?
|
|
48
|
+
def self?.enabled?: () -> bool
|
|
49
|
+
def self?.digest: (String query) -> String
|
|
50
|
+
# Store a query ahead of time (e.g. from a deploy task); returns its hash.
|
|
51
|
+
def self?.register: (String query) -> String
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The Cache-Control header the selected fields' @cacheControl hints allow.
|
|
55
|
+
module CacheControl
|
|
56
|
+
DEFINITION: String
|
|
57
|
+
TYPE_DEFINITIONS: Hash[String, String]
|
|
58
|
+
CONTEXT_KEY: Symbol
|
|
59
|
+
|
|
60
|
+
# For an executed query (a GraphQL::Query::Result or GraphQL::Query);
|
|
61
|
+
# nil when the schema does not use @cacheControl.
|
|
62
|
+
def self?.header: (untyped result) -> String?
|
|
63
|
+
# Set the header on a Rails (or Rack) response, and return it.
|
|
64
|
+
def self?.apply: (untyped response, untyped result) -> String?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The opt-in cache of the manipulated schema document, in tmp/cache.
|
|
68
|
+
module SchemaCache
|
|
69
|
+
PREFIX: String
|
|
70
|
+
|
|
71
|
+
def self?.enabled?: () -> bool
|
|
72
|
+
def self?.directory: () -> path
|
|
73
|
+
# Build the schema document now and return the cache file's path.
|
|
74
|
+
def self?.warm!: (?String? sdl) -> String
|
|
75
|
+
def self?.clear!: () -> void
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
data/sig/lighthouse.rbs
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Signatures for the parts of Lighthouse an application implements or calls:
|
|
2
|
+
# the classes a schema names (builders, validators), the configuration, the
|
|
3
|
+
# directive extension points and the entrypoints. Internals are left out.
|
|
4
|
+
#
|
|
5
|
+
# graphql-ruby and Active Record ship no RBS every app has, so the values that
|
|
6
|
+
# come from them are aliases of untyped, named for what they hold.
|
|
7
|
+
module Lighthouse
|
|
8
|
+
# An ActiveRecord::Relation, or anything with its query interface.
|
|
9
|
+
type relation = untyped
|
|
10
|
+
|
|
11
|
+
# A field's arguments, with snake_case Symbol keys as graphql-ruby passes them.
|
|
12
|
+
type arguments = Hash[Symbol, untyped]
|
|
13
|
+
|
|
14
|
+
# The request's GraphQL::Query::Context.
|
|
15
|
+
type context = untyped
|
|
16
|
+
|
|
17
|
+
# A path on disk: a String or a Pathname.
|
|
18
|
+
type path = String | _ToPath
|
|
19
|
+
|
|
20
|
+
# A class `@builder(class: "Name")` names, looked up in Config.namespaces[:builders].
|
|
21
|
+
# It refines the relation of a field resolved by @all, @paginate, @first,
|
|
22
|
+
# @count or @aggregate and returns the relation to use; nil keeps the one it
|
|
23
|
+
# was given. `@builder(method:)` names a method other than `call`, with the
|
|
24
|
+
# same signature. The class answers it, or else an instance built with no
|
|
25
|
+
# arguments does. A GraphQL::ExecutionError it raises reaches the client.
|
|
26
|
+
interface _Builder
|
|
27
|
+
def call: (relation relation, arguments args, context context) -> relation?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# A @validator contract other than a Dry::Validation::Contract: it gets the
|
|
31
|
+
# arguments (snake_case Symbol keys) and answers the errors by key,
|
|
32
|
+
# { title: ["is missing"] }, nested for input objects; empty when valid.
|
|
33
|
+
interface _Validator
|
|
34
|
+
def call: (Hash[Symbol, untyped] values) -> Hash[Symbol | String, untyped]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Config.on_error: called with an error a directive or resolver swallowed.
|
|
38
|
+
interface _ErrorHandler
|
|
39
|
+
def call: (StandardError error, context? context) -> void
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Config.authenticated_user and Config.policy_user: the user of a request.
|
|
43
|
+
interface _UserReader
|
|
44
|
+
def call: (context? context) -> untyped
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Config.authorizer: may `user` perform `ability` on `record` (a model
|
|
48
|
+
# instance, or a model class for @canModel)? `args` are the @can* `args:`,
|
|
49
|
+
# after the field's arguments with `injectArgs: true`.
|
|
50
|
+
interface _Authorizer
|
|
51
|
+
def call: (untyped user, untyped record, String | Symbol ability, ?Array[untyped] args) -> boolish
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# What DirectiveRegistry#register_manipulator takes: it transforms the SDL
|
|
55
|
+
# before the schema is built.
|
|
56
|
+
interface _SdlManipulator
|
|
57
|
+
def manipulate: (String sdl) -> String
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.configure: () ?{ (singleton(Config) config) -> void } -> void
|
|
61
|
+
|
|
62
|
+
module Config
|
|
63
|
+
DEFAULT_NAMESPACES: Hash[Symbol, Array[String]]
|
|
64
|
+
SETTINGS: Array[Symbol]
|
|
65
|
+
|
|
66
|
+
def self.configure: () ?{ (singleton(Config) config) -> void } -> void
|
|
67
|
+
def self.[]: (Symbol | String key) -> untyped
|
|
68
|
+
def self.reset!: () -> void
|
|
69
|
+
|
|
70
|
+
def self.sdl_folder: () -> path?
|
|
71
|
+
def self.sdl_folder=: (path? folder) -> path?
|
|
72
|
+
|
|
73
|
+
# { queries:, mutations:, builders:, validators: } => [namespace, ...].
|
|
74
|
+
# Assigning a partial hash overrides only the kinds it names.
|
|
75
|
+
def self.namespaces: () -> Hash[Symbol, Array[String]]
|
|
76
|
+
def self.namespaces=: (Hash[Symbol | String, String | Array[String]] overrides) -> Hash[Symbol | String, String | Array[String]]
|
|
77
|
+
|
|
78
|
+
def self.resolver_namespaces: () -> Array[String]
|
|
79
|
+
def self.resolver_namespaces=: (Array[String] namespaces) -> Array[String]
|
|
80
|
+
|
|
81
|
+
def self.directive_map: () -> Hash[untyped, untyped]
|
|
82
|
+
|
|
83
|
+
# An ActiveSupport::Cache::Store; Rails.cache by default.
|
|
84
|
+
def self.cache_store: () -> untyped
|
|
85
|
+
def self.cache_store=: (untyped store) -> untyped
|
|
86
|
+
|
|
87
|
+
def self.transactional_mutations: () -> bool
|
|
88
|
+
def self.transactional_mutations=: (bool? enabled) -> bool?
|
|
89
|
+
|
|
90
|
+
def self.pagination_default_count: () -> Integer?
|
|
91
|
+
def self.pagination_default_count=: (Integer? count) -> Integer?
|
|
92
|
+
def self.pagination_max_count: () -> Integer?
|
|
93
|
+
def self.pagination_max_count=: (Integer? count) -> Integer?
|
|
94
|
+
|
|
95
|
+
def self.schema_cache: () -> bool
|
|
96
|
+
def self.schema_cache=: (bool? enabled) -> bool?
|
|
97
|
+
def self.schema_cache_path: () -> path?
|
|
98
|
+
def self.schema_cache_path=: (path? path) -> path?
|
|
99
|
+
def self.watch_sdl: () -> bool
|
|
100
|
+
def self.watch_sdl=: (bool? enabled) -> bool?
|
|
101
|
+
|
|
102
|
+
def self.persisted_queries: () -> bool
|
|
103
|
+
def self.persisted_queries=: (bool? enabled) -> bool?
|
|
104
|
+
def self.persisted_queries_store: () -> untyped
|
|
105
|
+
def self.persisted_queries_store=: (untyped store) -> untyped
|
|
106
|
+
def self.persisted_queries_ttl: () -> Numeric?
|
|
107
|
+
def self.persisted_queries_ttl=: (Numeric? seconds) -> Numeric?
|
|
108
|
+
def self.persisted_queries_manifest: () -> path?
|
|
109
|
+
def self.persisted_queries_manifest=: (path? path) -> path?
|
|
110
|
+
def self.persisted_queries_only: () -> bool
|
|
111
|
+
def self.persisted_queries_only=: (bool? enabled) -> bool?
|
|
112
|
+
|
|
113
|
+
def self.on_error: () -> _ErrorHandler
|
|
114
|
+
def self.on_error=: (_ErrorHandler? handler) -> _ErrorHandler?
|
|
115
|
+
def self.handle_error: (StandardError error, ?context? context) -> void
|
|
116
|
+
|
|
117
|
+
def self.authenticated_user: () -> _UserReader
|
|
118
|
+
def self.authenticated_user=: (_UserReader? reader) -> _UserReader?
|
|
119
|
+
def self.policy_user: () -> _UserReader
|
|
120
|
+
def self.policy_user=: (_UserReader? reader) -> _UserReader?
|
|
121
|
+
def self.authorizer: () -> _Authorizer
|
|
122
|
+
def self.authorizer=: (_Authorizer? authorizer) -> _Authorizer?
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The schema refers to something that does not exist or breaks a contract.
|
|
126
|
+
class DefinitionError < StandardError
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# A GraphQL::ExecutionError (declared from StandardError, which it descends
|
|
130
|
+
# from, as graphql-ruby ships no RBS): the @can* family denied the ability.
|
|
131
|
+
class AuthorizationError < StandardError
|
|
132
|
+
def initialize: (?String message, **untyped options) -> void
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# A GraphQL::ExecutionError: invalid arguments, keyed by argument path
|
|
136
|
+
# ({ "input.title" => ["Title can't be blank"] }) under extensions.validation.
|
|
137
|
+
class ValidationError < StandardError
|
|
138
|
+
attr_reader messages: Hash[String, Array[String]]
|
|
139
|
+
|
|
140
|
+
def initialize: (String | Symbol field_name, Hash[String, Array[String]] messages) -> void
|
|
141
|
+
end
|
|
142
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lighthouse-graphql
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vaz Innovation
|
|
@@ -62,6 +62,7 @@ files:
|
|
|
62
62
|
- docs/federation.md
|
|
63
63
|
- docs/filtering-and-ordering.md
|
|
64
64
|
- docs/getting-started.md
|
|
65
|
+
- docs/motivation.md
|
|
65
66
|
- docs/mutations.md
|
|
66
67
|
- docs/performance.md
|
|
67
68
|
- docs/relationships.md
|
|
@@ -155,13 +156,16 @@ files:
|
|
|
155
156
|
- lib/lighthouse/support/naming.rb
|
|
156
157
|
- lib/lighthouse/tasks/lighthouse.rake
|
|
157
158
|
- lib/lighthouse/where_conditions/operator_map.rb
|
|
159
|
+
- sig/lighthouse.rbs
|
|
160
|
+
- sig/lighthouse/directives.rbs
|
|
161
|
+
- sig/lighthouse/graphql.rbs
|
|
158
162
|
homepage: https://github.com/Vaz-Innovation/lighthouse-graphql
|
|
159
163
|
licenses:
|
|
160
164
|
- MIT
|
|
161
165
|
metadata:
|
|
162
166
|
source_code_uri: https://github.com/Vaz-Innovation/lighthouse-graphql
|
|
163
167
|
changelog_uri: https://github.com/Vaz-Innovation/lighthouse-graphql/blob/main/CHANGELOG.md
|
|
164
|
-
documentation_uri: https://github.
|
|
168
|
+
documentation_uri: https://vaz-innovation.github.io/lighthouse-graphql/
|
|
165
169
|
rubygems_mfa_required: 'true'
|
|
166
170
|
rdoc_options: []
|
|
167
171
|
require_paths:
|