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
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,178 @@ 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
|
+
|
|
29
|
+
## [0.2.0] - 2026-10-17
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- `rails generate lighthouse:install` sets a Rails app up: the initializer (every option,
|
|
34
|
+
commented out at its default), a starter schema, the schema class with
|
|
35
|
+
`GraphQL::Dataloader`, a `GraphqlController` and its route, and a `graphql.config.yml`
|
|
36
|
+
for IDEs. `--schema`, `--sdl-folder`, `--skip-controller` and `--skip-ide` adjust it;
|
|
37
|
+
rerunning it adds the route and `.gitignore` entries only once. It coexists with
|
|
38
|
+
graphql-ruby's `graphql:install`: an existing controller or schema class is kept unless
|
|
39
|
+
`--force` is given, and the route is written exactly as graphql-ruby writes it, so it is
|
|
40
|
+
never added twice in either order.
|
|
41
|
+
- `bin/rails lighthouse:ide_helper` writes `schema-directives.graphql` (every registered
|
|
42
|
+
directive the SDL does not declare) and `programmatic-types.graphql` (the types the
|
|
43
|
+
manipulation phase generates) so IDEs can validate and autocomplete a Lighthouse
|
|
44
|
+
schema, as PHP Lighthouse's `lighthouse:ide-helper` does.
|
|
45
|
+
- `DirectiveRegistry#register_definition` contributes the SDL definition of a directive
|
|
46
|
+
that has no class of its own; `register_type_definition` / a class's `type_definitions`
|
|
47
|
+
contribute the types such definitions refer to, injected alongside them.
|
|
48
|
+
- **Queries, mutations and builders as separate kinds**, each with its own namespace
|
|
49
|
+
(`Config.namespaces`) and contract. A `Query` or `Mutation` field without a resolving
|
|
50
|
+
directive resolves by convention to `Queries::<FieldName>` (a `GraphQL::Schema::Resolver`)
|
|
51
|
+
or `Mutations::<FieldName>` (a `GraphQL::Schema::Mutation`, `RelayClassicMutation`
|
|
52
|
+
included) — graphql-ruby's contract: `resolve(**args)`, `object`, `context`, `ready?`,
|
|
53
|
+
`authorized?`. `@field(resolver:)` on those roots looks in the same namespaces. `@builder`
|
|
54
|
+
classes live in `Builders::`. Generators: `lighthouse:query`, `lighthouse:mutation`,
|
|
55
|
+
`lighthouse:builder`, `lighthouse:validator`.
|
|
56
|
+
- `@create`, `@createMany`, `@update`, `@updateMany`: save the returned model through
|
|
57
|
+
ActiveRecord, in a transaction (`Config.transactional_mutations`), reporting model
|
|
58
|
+
validation errors by argument path (`extensions.validation`, as in PHP Lighthouse).
|
|
59
|
+
`@spread` flattens an input into the arguments, `@inject(context:, name:)` sets
|
|
60
|
+
server-side values, `@rename` now also applies to arguments.
|
|
61
|
+
- `@validator` (field or input type) runs a contract before the field resolves:
|
|
62
|
+
a `Dry::Validation::Contract` (optional dependency) or any `call(values)` returning
|
|
63
|
+
errors. Input-type validators apply wherever the type is used, nested and in lists.
|
|
64
|
+
- The `@can*` family, Pundit by default: `@canFind`, `@canModel`, `@canQuery`,
|
|
65
|
+
`@canResolved`, `@canRoot`, with `injectArgs`, `args`, `action`
|
|
66
|
+
(`EXCEPTION_PASS` / `EXCEPTION_NOT_AUTHORIZED` / `RETURN_VALUE`) and `returnValue`. The
|
|
67
|
+
authorizer receives those arguments as a fourth parameter, only when there are any.
|
|
68
|
+
- `@cache(maxAge:, private:)`, `@cacheKey` and `@clearCache(type:, idSource:, field:)` over
|
|
69
|
+
`Config.cache_store` (`Rails.cache`), invalidating through versioned keys so it works on
|
|
70
|
+
any store, including ones without `delete_matched`.
|
|
71
|
+
- **Nested mutations**, PHP Lighthouse's API over ActiveRecord: an input field named after
|
|
72
|
+
an association carries `connect` / `create` / `update` / `upsert` / `disconnect` /
|
|
73
|
+
`delete` (belongs_to, polymorphic belongs_to, has_one, has_many) and `sync` /
|
|
74
|
+
`syncWithoutDetaching` (has_many :through, has_and_belongs_to_many — with pivot columns on a
|
|
75
|
+
`:through` join model), to any depth. Each record is saved once; errors are keyed by the full
|
|
76
|
+
input path. `@create` / `@update` / `@upsert` / `@delete(relation:)` on an argument run one
|
|
77
|
+
operation on an association.
|
|
78
|
+
- `@upsert` / `@upsertMany` (by primary key or `identifyingColumns`) and `@delete` (by the
|
|
79
|
+
field's filter arguments; refuses to delete without one), and the `@whereKey` filter.
|
|
80
|
+
- `@paginate(type: PAGINATOR | SIMPLE | CONNECTION, scopes:, defaultCount:, maxCount:)` and
|
|
81
|
+
`type:` on `@hasMany` / `@belongsToMany` / `@morphMany` / `@morphToMany`, generating PHP
|
|
82
|
+
Lighthouse's types and arguments (`first`, `page`, `after`; `PaginatorInfo`,
|
|
83
|
+
`SimplePaginatorInfo`, `PageInfo` with `total` / `count` / `currentPage` / `lastPage`). The total
|
|
84
|
+
is only counted when selected. `Config.pagination_default_count` / `pagination_max_count`.
|
|
85
|
+
- `@morphTo`, `@morphOne`, `@morphMany`, `@morphToMany` (ActiveRecord polymorphic
|
|
86
|
+
associations), and `scopes:` on every relationship directive.
|
|
87
|
+
- The integration suites of PHP Lighthouse for nested mutations (belongsTo, hasOne, hasMany,
|
|
88
|
+
belongsToMany, morphTo, morphOne, morphMany, morphToMany), the model mutation directives,
|
|
89
|
+
`@all` and `@paginate`, ported to RSpec against the same models (spec/integration).
|
|
90
|
+
- **Opt-in options for large schemas and high traffic**, all off by default (docs/performance.md):
|
|
91
|
+
- `config.schema_cache` caches the built schema document in `tmp/cache/lighthouse`, Bootsnap
|
|
92
|
+
style: content-addressed (SDL, versions, registered directives, manipulator source, config),
|
|
93
|
+
so it invalidates itself; atomic writes; `bin/rails lighthouse:cache` / `lighthouse:clear_cache`.
|
|
94
|
+
About 1.8x faster schema builds on large schemas.
|
|
95
|
+
- `config.watch_sdl` adds the SDL folder to Rails' watched dirs, so editing a `.graphql` file
|
|
96
|
+
reloads the schema in development (it previously needed a server restart).
|
|
97
|
+
- `config.persisted_queries`: Apollo-compatible Automatic Persisted Queries
|
|
98
|
+
(`PersistedQueries.execute`, used by the generated controller), with a TTL, a store, a
|
|
99
|
+
manifest of trusted operations (Apollo's format or `{ sha256 => query }`) served from memory,
|
|
100
|
+
`persisted_queries_only` as an allowlist, and `bin/rails lighthouse:persisted_queries:verify`
|
|
101
|
+
to check the manifest against the schema in CI.
|
|
102
|
+
- `@cacheControl(maxAge:, scope:, inheritMaxAge:)` computes the response's `Cache-Control`
|
|
103
|
+
header from the selected fields (PHP Lighthouse's / Apollo's rules);
|
|
104
|
+
`Lighthouse::GraphQL::CacheControl.apply(response, result)` sets it, and the generated
|
|
105
|
+
controller calls it.
|
|
106
|
+
- `example/`: a Rails 8.1 blog API built with Lighthouse, which is both an example to
|
|
107
|
+
read and the end-to-end suite. Its specs go through the real `/graphql` endpoint:
|
|
108
|
+
queries, pagination, relationships without N+1, `@guard` and the `@can*` family with
|
|
109
|
+
Pundit, mutations (nested ones included), validation, `@cache`/`@cacheControl`,
|
|
110
|
+
persisted queries over `GET`, and the rake tasks. CI runs it.
|
|
111
|
+
- RuboCop (with rubocop-rails, -rspec, -performance and -factory_bot) lints the gem and
|
|
112
|
+
the example app; CI runs it, and `rake` runs it after the suite.
|
|
113
|
+
- **Paginated relations no longer query once per parent.** `@hasMany`,
|
|
114
|
+
`@belongsToMany`, `@morphMany` and `@morphToMany` with `type:`, and `@paginate` on a
|
|
115
|
+
nested field, load every parent's page in one query (a `ROW_NUMBER()` window
|
|
116
|
+
partitioned by parent) and every total in one `GROUP BY`, only when a total is
|
|
117
|
+
selected. Databases without window functions keep loading one parent at a time. See
|
|
118
|
+
docs/relationships.md.
|
|
119
|
+
|
|
120
|
+
### Fixed
|
|
121
|
+
|
|
122
|
+
- `@whereConditions`' `AND`, `OR` and `HAS` were ignored when they came from a query.
|
|
123
|
+
graphql-ruby hands input objects over with Ruby-style keys (`:and`, `:or`, `:has`), and
|
|
124
|
+
only the upper-case keys were read, so a nested condition matched every record.
|
|
125
|
+
- `scopes:` was declared but ignored by `@count` and `@first`; `@all` now accepts it too.
|
|
126
|
+
- The default resolver discarded the result of a generated three-argument resolver
|
|
127
|
+
(`resolve_field_<name>(obj, args, ctx)`) and fell through to reading the attribute.
|
|
128
|
+
- Used standalone (without Rails), the gem missed ActiveSupport's `Enumerable` and
|
|
129
|
+
`String#exclude?` extensions it relies on (`index_by`, `pluck`, `exclude?`).
|
|
130
|
+
|
|
131
|
+
- Every built-in directive now contributes its SDL definition. `@all`, `@find`,
|
|
132
|
+
`@paginate`, `@hasMany`, `@belongsTo`, `@belongsToMany`, `@aggregate`, `@field`,
|
|
133
|
+
`@auth`, `@builder`, `@whereConditions` and `@whereHasConditions` had none, so a schema
|
|
134
|
+
that used one without declaring it failed to build — contrary to the docs.
|
|
135
|
+
- `@find` without `model:` found nothing: it inferred the model from the return type's
|
|
136
|
+
Ruby class name, which is `nil` for SDL-built types, instead of its GraphQL name.
|
|
137
|
+
- `@find` returned `null` for existing records when batched through the Dataloader: `ID`
|
|
138
|
+
arguments arrive as strings and were looked up against integer primary keys.
|
|
139
|
+
- Wrapping directives (`@guard`, ...) on a field without a resolving directive called
|
|
140
|
+
graphql-ruby's internal resolver with the wrong arity instead of the default reader.
|
|
141
|
+
- The gem now requires `graphql` itself instead of relying on the app to load it first.
|
|
142
|
+
- `@all` on a list field returned `nodes: null`: the field was rewritten to a connection the
|
|
143
|
+
resolver never built. `@all` now returns the list, as in PHP Lighthouse; it also resolved
|
|
144
|
+
`model:` ignoring `resolver_namespaces` and stripping the namespace (`Admin::User` → `User`).
|
|
145
|
+
- `@paginate` ignored the field's argument filters (`@eq`, `@like`, ...), took no `first` /
|
|
146
|
+
`page` arguments as a paginator, and returned `nodes: null` as a connection.
|
|
147
|
+
- `@find` ignored every argument but `id`; it now applies the argument filters and errors on
|
|
148
|
+
more than one match.
|
|
149
|
+
- The relationship directives' Dataloader source loaded each parent's association separately
|
|
150
|
+
(N+1); it now batches with ActiveRecord's preloader.
|
|
151
|
+
- The relationship directives and `@find` raised "Dataloader is not running" on schemas without
|
|
152
|
+
`GraphQL::Dataloader`; they now load directly there.
|
|
153
|
+
- Generated pagination wrappers made every paginated field `no-cache` under `@cacheControl`;
|
|
154
|
+
they now inherit the parent's maxAge, as in PHP Lighthouse.
|
|
155
|
+
- An explicit `null` directive argument (`@paginate(maxCount: null)`) failed to build the
|
|
156
|
+
schema: graphql-ruby passes it on as a NullValue node. Lighthouse maps it to nil.
|
|
157
|
+
|
|
158
|
+
### Removed
|
|
159
|
+
|
|
160
|
+
- **`@can`** — replaced by the `@can*` family. `@can(ability:)` becomes `@canResolved`,
|
|
161
|
+
`root: true` becomes `@canRoot`, `find:` / `model:` become `@canFind`; see the migration
|
|
162
|
+
table in docs/authorization.md. Schemas still using `@can` fail to build.
|
|
163
|
+
|
|
164
|
+
### Changed
|
|
165
|
+
|
|
166
|
+
- **`@hasMany` and `@belongsToMany` return plain lists**, as in PHP Lighthouse, instead of
|
|
167
|
+
connections. Pass `type: CONNECTION` (or `PAGINATOR` / `SIMPLE`) to paginate; clients that
|
|
168
|
+
selected `edges` / `nodes` on them need the `type:`.
|
|
169
|
+
- Paginated fields follow PHP Lighthouse: a paginator takes `first: Int!` (defaulted by
|
|
170
|
+
`defaultCount`) and `page`, a connection `first: Int!` and `after`, and returns non-null.
|
|
171
|
+
- `@paginate`'s `type:` is the `PaginateType` enum (`PAGINATOR`, `SIMPLE`, `CONNECTION`); the
|
|
172
|
+
string form still works at runtime.
|
|
173
|
+
- `@builder` classes are looked up in `Config.namespaces[:builders]` (`Builders::`) instead of
|
|
174
|
+
`resolver_namespaces`. Fully-qualified names work as before.
|
|
175
|
+
- `@field(resolver:)` on `Query` / `Mutation` looks in the queries / mutations namespaces;
|
|
176
|
+
elsewhere still in `resolver_namespaces`.
|
|
177
|
+
- `@aggregate`'s `function:` is declared as `String`. Enum
|
|
178
|
+
literals (`function: SUM`) still work at runtime, but IDEs expect strings
|
|
179
|
+
(`function: "SUM"`).
|
|
180
|
+
|
|
9
181
|
## [0.1.1] - 2026-09-23
|
|
10
182
|
|
|
11
183
|
Release tooling only: the library is unchanged from `0.1.0`. The packaged gem differs because
|
|
@@ -56,6 +228,8 @@ had been running inside a private monorepo as a path gem before being extracted.
|
|
|
56
228
|
- Config-driven extension seams: `Lighthouse.configure`, `resolver_namespaces`,
|
|
57
229
|
`on_error`, and `DirectiveRegistry.register` for app-defined directives.
|
|
58
230
|
|
|
59
|
-
[Unreleased]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.
|
|
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
|
|
233
|
+
[0.2.0]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.1.1...v0.2.0
|
|
60
234
|
[0.1.1]: https://github.com/Vaz-Innovation/lighthouse-graphql/compare/v0.1.0...v0.1.1
|
|
61
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
|
|
|
@@ -63,8 +63,36 @@ gem 'lighthouse-graphql'
|
|
|
63
63
|
It depends on `graphql ~> 2.5` and `activesupport`. For federation you also need
|
|
64
64
|
`apollo-federation` in your app (it is an optional, app-provided dependency).
|
|
65
65
|
|
|
66
|
+
Then let the generator set the app up:
|
|
67
|
+
|
|
68
|
+
```shell
|
|
69
|
+
bin/rails generate lighthouse:install
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
It creates the initializer, a starter schema in `graphql/schema.graphql`, the
|
|
73
|
+
`AppSchema` class, a `GraphqlController` with its `POST /graphql` route, and a
|
|
74
|
+
`graphql.config.yml` for your IDE. Run `bin/rails generate lighthouse:install --help`
|
|
75
|
+
for its options (`--schema`, `--sdl-folder`, `--skip-controller`, `--skip-ide`).
|
|
76
|
+
|
|
77
|
+
### IDE support
|
|
78
|
+
|
|
79
|
+
Lighthouse's directives and the types it generates (Paginator/Connection types,
|
|
80
|
+
`whereConditions` inputs, …) are defined in Ruby, so your IDE can't see them in
|
|
81
|
+
your `.graphql` files. Write them out for it:
|
|
82
|
+
|
|
83
|
+
```shell
|
|
84
|
+
bin/rails lighthouse:ide_helper
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
This writes `schema-directives.graphql` and `programmatic-types.graphql` to the app
|
|
88
|
+
root; the generated `graphql.config.yml` already points the GraphQL plugins for
|
|
89
|
+
JetBrains IDEs and VS Code at them. Rerun it after changing your schema or
|
|
90
|
+
registering a directive. See [docs/getting-started.md](docs/getting-started.md#ide-support).
|
|
91
|
+
|
|
66
92
|
## Quick start
|
|
67
93
|
|
|
94
|
+
Prefer to wire it by hand? These are the steps the generator performs.
|
|
95
|
+
|
|
68
96
|
1. **Put your SDL** in `app/graphql` or any folder (default: `RAILS_ROOT/graphql`),
|
|
69
97
|
split across as many `.graphql` files as you like:
|
|
70
98
|
|
|
@@ -103,17 +131,33 @@ It depends on `graphql ~> 2.5` and `activesupport`. For federation you also need
|
|
|
103
131
|
See **[docs/getting-started.md](docs/getting-started.md)** for the full setup,
|
|
104
132
|
including base types and configuration.
|
|
105
133
|
|
|
134
|
+
### Example app
|
|
135
|
+
|
|
136
|
+
[`example/`](example) is a small blog API built with Lighthouse on Rails 8.1. It shows
|
|
137
|
+
the schema, the generated `Queries::`/`Mutations::`/`Builders::`/`Validators::` classes,
|
|
138
|
+
Pundit policies and the opt-in performance features working together, and its specs
|
|
139
|
+
are the library's end-to-end suite. See [example/README.md](example/README.md).
|
|
140
|
+
|
|
106
141
|
---
|
|
107
142
|
|
|
108
143
|
## Documentation
|
|
109
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
|
+
|
|
110
148
|
| Guide | What it covers |
|
|
111
149
|
| --- | --- |
|
|
150
|
+
| [Motivation](docs/motivation.md) | Where Lighthouse comes from, why the schema comes first, and why directives fit Active Record |
|
|
112
151
|
| [Getting started](docs/getting-started.md) | Install, base types, building & serving the schema |
|
|
113
152
|
| [Directives reference](docs/directives.md) | Every built-in directive, grouped by purpose |
|
|
114
|
-
| [Relationships](docs/relationships.md) | `@hasMany`, `@belongsTo`, `@belongsToMany`, `@hasOne
|
|
153
|
+
| [Relationships](docs/relationships.md) | `@hasMany`, `@belongsTo`, `@belongsToMany`, `@hasOne`, the `@morph*` variants, batching and `type:` pagination |
|
|
115
154
|
| [Filtering & ordering](docs/filtering-and-ordering.md) | `@eq`/`@where`/`@in`/`@like`, `@whereConditions`, `@orderBy` |
|
|
116
|
-
| [
|
|
155
|
+
| [Queries, mutations & builders](docs/resolvers.md) | `Queries::` / `Mutations::` classes (graphql-ruby's contract), builders, generators |
|
|
156
|
+
| [Mutations](docs/mutations.md) | `@create`, `@update`, `@upsert`, `@delete` (and `*Many`), nested mutations on every kind of association, `@spread`, `@inject` |
|
|
157
|
+
| [Validation](docs/validation.md) | Model validations, `@validator` with Dry::Validation contracts |
|
|
158
|
+
| [Authentication & authorization](docs/authorization.md) | `@guard`, `@canFind` / `@canModel` / `@canQuery` / `@canResolved` / `@canRoot` (Pundit by default) |
|
|
159
|
+
| [Performance at scale](docs/performance.md) | Opt-in schema cache, SDL reloading in development, persisted queries (APQ) and trusted-document allowlists |
|
|
160
|
+
| [Caching](docs/caching.md) | `@cache`, `@cacheKey`, `@clearCache` (Rails.cache), `@cacheControl` (HTTP) |
|
|
117
161
|
| [Apollo Federation](docs/federation.md) | `@key`, `resolve_reference`, the `ReferenceResolver` registry |
|
|
118
162
|
| [Custom directives](docs/custom-directives.md) | The contracts, the registry, writing your own directive |
|
|
119
163
|
| [Configuration](docs/configuration.md) | `Lighthouse.configure`, namespaces, error handling |
|
data/docs/authorization.md
CHANGED
|
@@ -1,109 +1,168 @@
|
|
|
1
1
|
# Authentication & Authorization
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
in
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
Enforce **authentication** and **authorization** declaratively in your schema,
|
|
4
|
+
instead of in resolvers whose only job is a policy check. `@guard` requires a
|
|
5
|
+
user. The `@can*` family checks a policy ability, Pundit by default. This is PHP
|
|
6
|
+
Lighthouse's v6 design, which replaced its single `@can` with one directive per
|
|
7
|
+
*thing being authorized*.
|
|
8
8
|
|
|
9
9
|
## Configuration
|
|
10
10
|
|
|
11
|
-
The
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
**typically no configuration is needed**:
|
|
11
|
+
The defaults work with a Rails app that puts `current_user` (or `pundit_user`)
|
|
12
|
+
in the GraphQL context and uses Pundit, so **typically no configuration is
|
|
13
|
+
needed**:
|
|
15
14
|
|
|
16
15
|
```ruby
|
|
17
16
|
Lighthouse.configure do |config|
|
|
18
17
|
# The authenticated user (nil if none). Default:
|
|
19
18
|
config.authenticated_user = ->(ctx) { ctx[:current_user] }
|
|
20
19
|
|
|
21
|
-
# The "user"
|
|
22
|
-
# to the authenticated user.
|
|
20
|
+
# The "user" handed to policies. Default: ctx[:pundit_user], else the authenticated user.
|
|
23
21
|
config.policy_user = ->(ctx) { ctx[:pundit_user] }
|
|
24
22
|
|
|
25
|
-
# How an ability is checked. Default
|
|
26
|
-
config.authorizer = ->(user, record, ability
|
|
23
|
+
# How an ability is checked. Default (Pundit):
|
|
24
|
+
config.authorizer = ->(user, record, ability, args = []) do
|
|
25
|
+
Pundit.policy!(user, record).public_send("#{ability}?", *args)
|
|
26
|
+
end
|
|
27
27
|
end
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
Point `authorizer` at any system (CanCanCan, ActionPolicy, a
|
|
31
|
-
something
|
|
30
|
+
Point `authorizer` at any system (CanCanCan, ActionPolicy, a lambda) to use
|
|
31
|
+
something else. It receives `args` only when a directive passes some, so a
|
|
32
|
+
three-argument authorizer keeps working for checks without them.
|
|
32
33
|
|
|
33
34
|
## `@guard` — require authentication
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
Raises `Unauthenticated.` when there is no user. It runs before everything else
|
|
37
|
+
on the field.
|
|
37
38
|
|
|
38
39
|
```graphql
|
|
39
40
|
type Query {
|
|
40
|
-
me: User @
|
|
41
|
+
me: User @guard
|
|
41
42
|
}
|
|
42
43
|
```
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
## The `@can*` family
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
Each directive checks `ability` with the policy, e.g. `PostPolicy#update?` for
|
|
48
|
+
`ability: "update"`. They differ in what they authorize:
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
| Directive | Authorizes | Typical use |
|
|
51
|
+
| --- | --- | --- |
|
|
52
|
+
| `@canModel` | the model **class** | `create?` before `@create` |
|
|
53
|
+
| `@canFind` | the models whose keys an argument holds | `update?` before `@update` |
|
|
54
|
+
| `@canQuery` | the models the field's argument filters select | bulk reads or writes |
|
|
55
|
+
| `@canResolved` | what the field resolved to (each item of a list) | read-only fields |
|
|
56
|
+
| `@canRoot` | the parent object | a sensitive field on a type |
|
|
51
57
|
|
|
52
58
|
```graphql
|
|
53
59
|
type Query {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
post(id: ID! @eq): Post @find @canResolved(ability: "show")
|
|
61
|
+
draftCount(authorId: ID @eq): Int @count(model: "Post") @canQuery(ability: "show")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type Mutation {
|
|
65
|
+
createPost(input: CreatePostInput! @spread): Post @create @canModel(ability: "create")
|
|
66
|
+
updatePost(input: UpdatePostInput! @spread): Post @update @canFind(ability: "update", find: "input.id")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type User {
|
|
70
|
+
email: String @canRoot(ability: "view_email")
|
|
58
71
|
}
|
|
59
72
|
```
|
|
60
73
|
|
|
74
|
+
With Pundit, `@canFind(ability: "update", find: "input.id")` loads the post and
|
|
75
|
+
calls `PostPolicy.new(pundit_user, post).update?`. `@canModel(ability: "create")`
|
|
76
|
+
calls `PostPolicy.new(pundit_user, Post).create?`.
|
|
77
|
+
|
|
78
|
+
All of them run **before** the field resolves, except `@canResolved`. That one
|
|
79
|
+
needs the result, so use it only on fields that do not change data.
|
|
80
|
+
|
|
81
|
+
### Arguments
|
|
82
|
+
|
|
83
|
+
Every `@can*` directive takes:
|
|
84
|
+
|
|
85
|
+
| Argument | Meaning |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `ability: String!` | The policy method, without `?` |
|
|
88
|
+
| `injectArgs: Boolean = false` | Pass the field's arguments (a Hash) to the policy method |
|
|
89
|
+
| `args: CanArgs` | Static values passed after them, e.g. `args: 2` or `args: ["a", "b"]` |
|
|
90
|
+
| `action: CanAction = EXCEPTION_PASS` | What a denial does (below) |
|
|
91
|
+
| `returnValue: CanArgs` | The value returned for `action: RETURN_VALUE` |
|
|
92
|
+
|
|
61
93
|
```ruby
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
94
|
+
class PostPolicy < ApplicationPolicy
|
|
95
|
+
def publish?(args) # @canFind(ability: "publish", find: "id", injectArgs: true)
|
|
96
|
+
user.admin? && args[:title] != record.title
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def edit?(level) # @canFind(ability: "edit", find: "id", args: 2)
|
|
100
|
+
user.level >= level
|
|
69
101
|
end
|
|
70
102
|
end
|
|
71
103
|
```
|
|
72
104
|
|
|
73
|
-
|
|
105
|
+
Plus, per directive:
|
|
74
106
|
|
|
75
|
-
|
|
107
|
+
- `@canFind(find: String!, model: String, findOrFail: Boolean = true, scopes: [String!])`.
|
|
108
|
+
`find` is the argument holding the primary key or keys, and may be a dot path
|
|
109
|
+
(`"input.id"`). A key that matches no record fails with
|
|
110
|
+
`Couldn't find Post with 'id'=…`, unless `findOrFail: false`. `scopes` are model
|
|
111
|
+
scopes applied to the lookup.
|
|
112
|
+
- `@canModel(model: String)`.
|
|
113
|
+
- `@canQuery(model: String, scopes: [String!])`. `scopes` narrow the records
|
|
114
|
+
being authorized, not the field's result.
|
|
76
115
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
type User {
|
|
80
|
-
email: String! @can(ability: "view_email", root: true)
|
|
81
|
-
}
|
|
116
|
+
The model defaults to the `model:` of another directive on the field (as in
|
|
117
|
+
`@count(model: "Post") @canQuery`), then to the field's return type.
|
|
82
118
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
119
|
+
### `action`: what a denial does
|
|
120
|
+
|
|
121
|
+
| `action` | Result |
|
|
122
|
+
| --- | --- |
|
|
123
|
+
| `EXCEPTION_PASS` (default) | The error reaches the client: `This action is unauthorized.`, or `@canFind`'s not-found message |
|
|
124
|
+
| `EXCEPTION_NOT_AUTHORIZED` | Always `This action is unauthorized.`, which hides whether a record exists |
|
|
125
|
+
| `RETURN_VALUE` | The field returns `returnValue` instead of resolving |
|
|
126
|
+
|
|
127
|
+
```graphql
|
|
128
|
+
post(id: ID!): Post @canFind(ability: "show", find: "id", action: EXCEPTION_NOT_AUTHORIZED)
|
|
87
129
|
```
|
|
88
130
|
|
|
89
|
-
|
|
131
|
+
`action` applies to the directive's own checks only. Errors from the resolver
|
|
132
|
+
itself are untouched.
|
|
133
|
+
|
|
134
|
+
## Ordering
|
|
135
|
+
|
|
136
|
+
The order you write directives in does not matter. They run as:
|
|
137
|
+
|
|
138
|
+
1. `@guard` / `@auth` (authentication)
|
|
139
|
+
2. `@canFind`, `@canModel`, `@canQuery`, `@canRoot` (and the check of `@canResolved`, after the resolver)
|
|
140
|
+
3. `@validator` (see [validation.md](validation.md))
|
|
141
|
+
4. `@cache`, then the resolver
|
|
142
|
+
|
|
143
|
+
So a cached value is still authorized on every request.
|
|
90
144
|
|
|
91
145
|
## Multi-tenancy vs. authorization
|
|
92
146
|
|
|
93
147
|
Keep the two layers distinct:
|
|
94
148
|
|
|
95
|
-
- **Tenancy / scoping**
|
|
96
|
-
|
|
97
|
-
- **Authorization**
|
|
98
|
-
`@can
|
|
149
|
+
- **Tenancy / scoping** decides which account's records are visible. It belongs in
|
|
150
|
+
a [builder](resolvers.md#builders) or query class, e.g. `account.posts`.
|
|
151
|
+
- **Authorization** decides whether this user may do this to this record. It
|
|
152
|
+
belongs in `@can*` and your policies.
|
|
99
153
|
|
|
100
|
-
For
|
|
101
|
-
|
|
154
|
+
For lists, scope with a builder (e.g. Pundit's `policy_scope`) rather than
|
|
155
|
+
authorizing item by item.
|
|
102
156
|
|
|
103
|
-
##
|
|
157
|
+
## Migrating from `@can`
|
|
158
|
+
|
|
159
|
+
`@can` is gone. Its modes map one to one:
|
|
104
160
|
|
|
105
|
-
|
|
161
|
+
| Before | After |
|
|
162
|
+
| --- | --- |
|
|
163
|
+
| `@can(ability: "show")` | `@canResolved(ability: "show")` |
|
|
164
|
+
| `@can(ability: "view_email", root: true)` | `@canRoot(ability: "view_email")` |
|
|
165
|
+
| `@can(ability: "update", find: "id", model: "Post")` | `@canFind(ability: "update", find: "id", model: "Post")` |
|
|
106
166
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
3. the field's data resolver (`@field`, `@all`, `@hasMany`, …) — innermost
|
|
167
|
+
One difference: `@canResolved` authorizes **each item** of a list. `@can` skipped
|
|
168
|
+
lists.
|
data/docs/best-practices.md
CHANGED
|
@@ -40,8 +40,21 @@ Without it, nested relationships re-introduce N+1 queries.
|
|
|
40
40
|
|
|
41
41
|
## Authorize at the field, early
|
|
42
42
|
|
|
43
|
-
Use `@
|
|
44
|
-
|
|
43
|
+
Use `@guard` and the `@can*` directives on fields that require them. They wrap the
|
|
44
|
+
resolver, so the check runs before any data is loaded (only `@canResolved`, which
|
|
45
|
+
needs the result, runs after — keep it to fields that don't write).
|
|
46
|
+
|
|
47
|
+
## Keep queries, mutations and builders apart
|
|
48
|
+
|
|
49
|
+
A custom `Query` field is a `GraphQL::Schema::Resolver` in `app/graphql/queries`,
|
|
50
|
+
a `Mutation` field a `GraphQL::Schema::Mutation` in `app/graphql/mutations`, and a
|
|
51
|
+
query refinement a builder in `app/graphql/builders`. They have different jobs and
|
|
52
|
+
contracts; don't fold them into one "resolvers" folder. See [resolvers.md](resolvers.md).
|
|
53
|
+
|
|
54
|
+
## Validate in the model first
|
|
55
|
+
|
|
56
|
+
Model validations already cover `@create` / `@update`. Reach for `@validator` only
|
|
57
|
+
for rules that aren't the model's. See [validation.md](validation.md).
|
|
45
58
|
|
|
46
59
|
## Mutations: prefer throwable errors
|
|
47
60
|
|
|
@@ -62,7 +75,7 @@ end
|
|
|
62
75
|
|
|
63
76
|
## One configuration seam
|
|
64
77
|
|
|
65
|
-
Keep all Lighthouse wiring — `resolver_namespaces`, custom directive
|
|
78
|
+
Keep all Lighthouse wiring — `namespaces`, `resolver_namespaces`, custom directive
|
|
66
79
|
registration, federation reference resolvers, `on_error` — in initializers, not
|
|
67
80
|
scattered through the app. This keeps the library a clean dependency and makes the
|
|
68
81
|
app's GraphQL setup easy to find.
|