@vantreeseba/drizzle-graphql 7.5.0 → 7.6.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.
- package/README.md +90 -0
- package/dist/README.md +90 -0
- package/dist/index.cjs +562 -188
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -2
- package/dist/index.d.ts +104 -2
- package/dist/index.js +563 -188
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -237,6 +237,96 @@ table becomes readable but not insertable — and warns on the console, since th
|
|
|
237
237
|
often a mistake than a plan.
|
|
238
238
|
|
|
239
239
|
> `exclude` is a static decision: the column is absent for every caller. Deciding per
|
|
240
|
+
> request *which fields* a caller may reach is a job for
|
|
241
|
+
> [`graphql-shield`](https://the-guild.dev/graphql/shield) or an equivalent layer above the
|
|
242
|
+
> schema. Deciding per request *which rows* a caller may reach is `scope`, below — that one a
|
|
243
|
+
> layer above cannot do, because a nested relation field never passes through a root resolver.
|
|
244
|
+
|
|
245
|
+
## Row scoping and multi-tenancy
|
|
246
|
+
|
|
247
|
+
`scope` attaches a predicate, derived from the request context, to every read and write of a
|
|
248
|
+
table:
|
|
249
|
+
|
|
250
|
+
```Typescript
|
|
251
|
+
import { and, eq } from 'drizzle-orm'
|
|
252
|
+
|
|
253
|
+
const { schema } = buildSchema(db, {
|
|
254
|
+
scope: {
|
|
255
|
+
posts: (ctx, table) => eq(table.orgId, ctx.orgId),
|
|
256
|
+
// A filter object works too — same shape as the generated `where` argument:
|
|
257
|
+
comments: (ctx) => ({ post: { orgId: { eq: ctx.orgId } } }),
|
|
258
|
+
},
|
|
259
|
+
})
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Keyed by the **Drizzle schema key**. `ctx` is your GraphQL context value; `table` is the table
|
|
263
|
+
the current statement runs against — for a relational read that is Drizzle's *aliased* copy, so
|
|
264
|
+
always build the predicate from that argument rather than from the imported table object.
|
|
265
|
+
|
|
266
|
+
The predicate is ANDed on **last**, after whatever the client sent, so a client filter can only
|
|
267
|
+
narrow the scope, never widen it. It reaches every path that touches the table:
|
|
268
|
+
|
|
269
|
+
| Path | Effect |
|
|
270
|
+
| --- | --- |
|
|
271
|
+
| `posts`, `postsSingle` | out-of-scope rows are not returned |
|
|
272
|
+
| `postsAggregate`, `postsGroupBy` | counted over in-scope rows only |
|
|
273
|
+
| `user.posts`, `user.postsAggregate` | scoped on both the batched and the eager (`with:`) path |
|
|
274
|
+
| cursor pages | the keyset page is taken within the scope |
|
|
275
|
+
| `updatePosts`, `updatePostsMany`, `deletePosts` | an out-of-scope row is simply not matched |
|
|
276
|
+
| `upsertPosts` (conflict branch) | a conflicting row outside the scope is left alone, not taken over |
|
|
277
|
+
| nested `connect` / `disconnect` / `set` | only in-scope rows can be attached or detached |
|
|
278
|
+
|
|
279
|
+
Two things it deliberately does not do:
|
|
280
|
+
|
|
281
|
+
- **It does not refuse.** A scoped `deletePosts(where: { id: { eq: 4 } })` on another
|
|
282
|
+
tenant's row returns `[]`, the same answer as a row that does not exist. Telling the caller
|
|
283
|
+
"that row exists but is not yours" is the leak the scope is there to close.
|
|
284
|
+
- **It does not apply to a plain insert.** There is no existing row to filter. `contextValues`
|
|
285
|
+
is the write-side half.
|
|
286
|
+
|
|
287
|
+
Returning `undefined` means *no restriction for this context*, so one hook can let an
|
|
288
|
+
administrator through:
|
|
289
|
+
|
|
290
|
+
```Typescript
|
|
291
|
+
scope: {
|
|
292
|
+
posts: (ctx, table) => (ctx.isAdmin ? undefined : eq(table.orgId, ctx.orgId)),
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
A table with no entry is unscoped, and a build with no `scope` at all emits exactly the SQL it
|
|
297
|
+
did before.
|
|
298
|
+
|
|
299
|
+
### Columns the server owns
|
|
300
|
+
|
|
301
|
+
`contextValues` supplies a column's value from the request context instead of accepting it from
|
|
302
|
+
the client:
|
|
303
|
+
|
|
304
|
+
```Typescript
|
|
305
|
+
const { schema } = buildSchema(db, {
|
|
306
|
+
contextValues: {
|
|
307
|
+
posts: {
|
|
308
|
+
orgId: (ctx) => ctx.orgId,
|
|
309
|
+
authorId: (ctx) => ctx.userId,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
- The column is **removed from `CreatePostsInput` and `UpdatePostsInput`**, so a client cannot
|
|
316
|
+
send it at all — the SDL itself says the column is server-owned. It stays readable on the
|
|
317
|
+
object type.
|
|
318
|
+
- Every insert stamps it, including rows created by a nested `create`.
|
|
319
|
+
- Updates never write it, so an update cannot hand a row to another owner.
|
|
320
|
+
|
|
321
|
+
Together the two close the loop: `contextValues` decides which tenant a new row belongs to, and
|
|
322
|
+
`scope` decides which rows a request can see and change afterwards.
|
|
323
|
+
|
|
324
|
+
> **MySQL caveat.** `ON DUPLICATE KEY UPDATE` takes no predicate, so a MySQL upsert's conflict
|
|
325
|
+
> branch cannot be scoped. Scope the table's `update` and rely on `contextValues` for the insert
|
|
326
|
+
> half.
|
|
327
|
+
|
|
328
|
+
Names that match nothing throw at build time, the same as `exclude` — a renamed column fails the
|
|
329
|
+
build instead of quietly ceasing to be scoped.
|
|
240
330
|
> request who may see what is a job for [`graphql-shield`](https://the-guild.dev/graphql/shield)
|
|
241
331
|
> or an equivalent layer above the schema, which has the request context this library
|
|
242
332
|
> does not.
|
package/dist/README.md
CHANGED
|
@@ -237,6 +237,96 @@ table becomes readable but not insertable — and warns on the console, since th
|
|
|
237
237
|
often a mistake than a plan.
|
|
238
238
|
|
|
239
239
|
> `exclude` is a static decision: the column is absent for every caller. Deciding per
|
|
240
|
+
> request *which fields* a caller may reach is a job for
|
|
241
|
+
> [`graphql-shield`](https://the-guild.dev/graphql/shield) or an equivalent layer above the
|
|
242
|
+
> schema. Deciding per request *which rows* a caller may reach is `scope`, below — that one a
|
|
243
|
+
> layer above cannot do, because a nested relation field never passes through a root resolver.
|
|
244
|
+
|
|
245
|
+
## Row scoping and multi-tenancy
|
|
246
|
+
|
|
247
|
+
`scope` attaches a predicate, derived from the request context, to every read and write of a
|
|
248
|
+
table:
|
|
249
|
+
|
|
250
|
+
```Typescript
|
|
251
|
+
import { and, eq } from 'drizzle-orm'
|
|
252
|
+
|
|
253
|
+
const { schema } = buildSchema(db, {
|
|
254
|
+
scope: {
|
|
255
|
+
posts: (ctx, table) => eq(table.orgId, ctx.orgId),
|
|
256
|
+
// A filter object works too — same shape as the generated `where` argument:
|
|
257
|
+
comments: (ctx) => ({ post: { orgId: { eq: ctx.orgId } } }),
|
|
258
|
+
},
|
|
259
|
+
})
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Keyed by the **Drizzle schema key**. `ctx` is your GraphQL context value; `table` is the table
|
|
263
|
+
the current statement runs against — for a relational read that is Drizzle's *aliased* copy, so
|
|
264
|
+
always build the predicate from that argument rather than from the imported table object.
|
|
265
|
+
|
|
266
|
+
The predicate is ANDed on **last**, after whatever the client sent, so a client filter can only
|
|
267
|
+
narrow the scope, never widen it. It reaches every path that touches the table:
|
|
268
|
+
|
|
269
|
+
| Path | Effect |
|
|
270
|
+
| --- | --- |
|
|
271
|
+
| `posts`, `postsSingle` | out-of-scope rows are not returned |
|
|
272
|
+
| `postsAggregate`, `postsGroupBy` | counted over in-scope rows only |
|
|
273
|
+
| `user.posts`, `user.postsAggregate` | scoped on both the batched and the eager (`with:`) path |
|
|
274
|
+
| cursor pages | the keyset page is taken within the scope |
|
|
275
|
+
| `updatePosts`, `updatePostsMany`, `deletePosts` | an out-of-scope row is simply not matched |
|
|
276
|
+
| `upsertPosts` (conflict branch) | a conflicting row outside the scope is left alone, not taken over |
|
|
277
|
+
| nested `connect` / `disconnect` / `set` | only in-scope rows can be attached or detached |
|
|
278
|
+
|
|
279
|
+
Two things it deliberately does not do:
|
|
280
|
+
|
|
281
|
+
- **It does not refuse.** A scoped `deletePosts(where: { id: { eq: 4 } })` on another
|
|
282
|
+
tenant's row returns `[]`, the same answer as a row that does not exist. Telling the caller
|
|
283
|
+
"that row exists but is not yours" is the leak the scope is there to close.
|
|
284
|
+
- **It does not apply to a plain insert.** There is no existing row to filter. `contextValues`
|
|
285
|
+
is the write-side half.
|
|
286
|
+
|
|
287
|
+
Returning `undefined` means *no restriction for this context*, so one hook can let an
|
|
288
|
+
administrator through:
|
|
289
|
+
|
|
290
|
+
```Typescript
|
|
291
|
+
scope: {
|
|
292
|
+
posts: (ctx, table) => (ctx.isAdmin ? undefined : eq(table.orgId, ctx.orgId)),
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
A table with no entry is unscoped, and a build with no `scope` at all emits exactly the SQL it
|
|
297
|
+
did before.
|
|
298
|
+
|
|
299
|
+
### Columns the server owns
|
|
300
|
+
|
|
301
|
+
`contextValues` supplies a column's value from the request context instead of accepting it from
|
|
302
|
+
the client:
|
|
303
|
+
|
|
304
|
+
```Typescript
|
|
305
|
+
const { schema } = buildSchema(db, {
|
|
306
|
+
contextValues: {
|
|
307
|
+
posts: {
|
|
308
|
+
orgId: (ctx) => ctx.orgId,
|
|
309
|
+
authorId: (ctx) => ctx.userId,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
- The column is **removed from `CreatePostsInput` and `UpdatePostsInput`**, so a client cannot
|
|
316
|
+
send it at all — the SDL itself says the column is server-owned. It stays readable on the
|
|
317
|
+
object type.
|
|
318
|
+
- Every insert stamps it, including rows created by a nested `create`.
|
|
319
|
+
- Updates never write it, so an update cannot hand a row to another owner.
|
|
320
|
+
|
|
321
|
+
Together the two close the loop: `contextValues` decides which tenant a new row belongs to, and
|
|
322
|
+
`scope` decides which rows a request can see and change afterwards.
|
|
323
|
+
|
|
324
|
+
> **MySQL caveat.** `ON DUPLICATE KEY UPDATE` takes no predicate, so a MySQL upsert's conflict
|
|
325
|
+
> branch cannot be scoped. Scope the table's `update` and rely on `contextValues` for the insert
|
|
326
|
+
> half.
|
|
327
|
+
|
|
328
|
+
Names that match nothing throw at build time, the same as `exclude` — a renamed column fails the
|
|
329
|
+
build instead of quietly ceasing to be scoped.
|
|
240
330
|
> request who may see what is a job for [`graphql-shield`](https://the-guild.dev/graphql/shield)
|
|
241
331
|
> or an equivalent layer above the schema, which has the request context this library
|
|
242
332
|
> does not.
|