prisma-generator-express 1.20.0 → 1.21.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.
- package/README.md +657 -12
- package/dist/client/encodeQueryParams.js +2 -2
- package/dist/client/encodeQueryParams.js.map +1 -1
- package/dist/generators/generateUnifiedHandler.js +48 -4
- package/dist/generators/generateUnifiedHandler.js.map +1 -1
- package/package.json +1 -1
- package/src/client/encodeQueryParams.ts +1 -1
- package/src/generators/generateUnifiedHandler.ts +48 -4
package/README.md
CHANGED
|
@@ -16,6 +16,30 @@ Running `npx prisma generate` produces:
|
|
|
16
16
|
- Client-side query parameter encoder
|
|
17
17
|
- Guard/variant shape enforcement via prisma-guard integration
|
|
18
18
|
|
|
19
|
+
## Table of contents
|
|
20
|
+
|
|
21
|
+
- [Compatibility](#compatibility)
|
|
22
|
+
- [Installation](#installation)
|
|
23
|
+
- [Setup](#setup)
|
|
24
|
+
- [Usage](#usage)
|
|
25
|
+
- [Selective routes with middleware](#selective-routes-with-middleware)
|
|
26
|
+
- [Guard shapes (prisma-guard integration)](#guard-shapes-prisma-guard-integration)
|
|
27
|
+
- [Request body format](#request-body-format)
|
|
28
|
+
- [Query encoding (client side)](#query-encoding-client-side)
|
|
29
|
+
- [Response shaping: select, include, omit](#response-shaping-select-include-omit)
|
|
30
|
+
- [BigInt and Decimal handling](#bigint-and-decimal-handling)
|
|
31
|
+
- [Pagination](#pagination)
|
|
32
|
+
- [Error handling](#error-handling)
|
|
33
|
+
- [Security](#security)
|
|
34
|
+
- [Documentation endpoints](#documentation-endpoints)
|
|
35
|
+
- [prisma-sql integration](#prisma-sql-integration)
|
|
36
|
+
- [Query parameter parsing](#query-parameter-parsing)
|
|
37
|
+
- [Router schema](#router-schema)
|
|
38
|
+
- [Skipping models](#skipping-models)
|
|
39
|
+
- [Configuration](#configuration)
|
|
40
|
+
- [Environment variables](#environment-variables)
|
|
41
|
+
- [License](#license)
|
|
42
|
+
|
|
19
43
|
## Compatibility
|
|
20
44
|
|
|
21
45
|
### Prisma version
|
|
@@ -54,7 +78,7 @@ npm install @prisma/client express
|
|
|
54
78
|
Optional peer dependencies:
|
|
55
79
|
```bash
|
|
56
80
|
npm install prisma-sql # SQL optimization
|
|
57
|
-
npm install prisma-guard
|
|
81
|
+
npm install prisma-guard zod # Guard shape enforcement
|
|
58
82
|
npm install prisma-query-builder-ui # Visual query playground
|
|
59
83
|
```
|
|
60
84
|
|
|
@@ -120,30 +144,171 @@ app.use('/', UserRouter(userConfig))
|
|
|
120
144
|
|
|
121
145
|
Only operations listed in the config (or all when `enableAll: true`) are registered. Operations not listed produce no routes.
|
|
122
146
|
|
|
123
|
-
## Guard shapes (
|
|
147
|
+
## Guard shapes (prisma-guard integration)
|
|
148
|
+
|
|
149
|
+
prisma-generator-express integrates with [prisma-guard](https://github.com/multipliedtwice/prisma-guard) to enforce input validation, query shape restrictions, and tenant isolation on generated routes. When a `shape` is configured on an operation, the handler calls `prisma.model.guard(shape, caller).method(args)` instead of `prisma.model.method(args)`.
|
|
124
150
|
|
|
125
|
-
Guard
|
|
151
|
+
### Guard setup
|
|
126
152
|
|
|
127
|
-
|
|
153
|
+
Install prisma-guard and add its generator to your schema:
|
|
128
154
|
```bash
|
|
129
|
-
npm install prisma-guard
|
|
155
|
+
npm install prisma-guard zod
|
|
156
|
+
```
|
|
157
|
+
```prisma
|
|
158
|
+
generator client {
|
|
159
|
+
provider = "prisma-client-js"
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
generator guard {
|
|
163
|
+
provider = "prisma-guard"
|
|
164
|
+
output = "generated/guard"
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
generator express {
|
|
168
|
+
provider = "prisma-generator-express"
|
|
169
|
+
}
|
|
130
170
|
```
|
|
131
171
|
|
|
132
|
-
|
|
172
|
+
Run `npx prisma generate` to emit both the express routes and the guard artifacts.
|
|
173
|
+
|
|
174
|
+
Extend PrismaClient with the guard extension and attach it to requests:
|
|
133
175
|
```ts
|
|
176
|
+
import express from 'express'
|
|
134
177
|
import { PrismaClient } from '@prisma/client'
|
|
135
|
-
import {
|
|
178
|
+
import { guard } from './generated/guard/client'
|
|
179
|
+
import { UserRouter } from './generated/User/UserRouter'
|
|
180
|
+
|
|
181
|
+
const prisma = new PrismaClient().$extends(
|
|
182
|
+
guard.extension(() => ({
|
|
183
|
+
// scope context, caller, or any other values
|
|
184
|
+
}))
|
|
185
|
+
)
|
|
136
186
|
|
|
137
|
-
const
|
|
187
|
+
const app = express()
|
|
188
|
+
|
|
189
|
+
app.use((req, res, next) => {
|
|
190
|
+
req.prisma = prisma
|
|
191
|
+
next()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
app.use('/', UserRouter({
|
|
195
|
+
findMany: {
|
|
196
|
+
shape: {
|
|
197
|
+
where: { name: { contains: true } },
|
|
198
|
+
take: { max: 50, default: 20 },
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
}))
|
|
202
|
+
|
|
203
|
+
app.listen(3000)
|
|
138
204
|
```
|
|
139
205
|
|
|
140
|
-
|
|
206
|
+
If prisma-guard is not installed or the client is not extended with the guard extension, requests to guarded routes return 500 with the message: `Guard shapes require prisma-guard extension on PrismaClient. Install: npm install prisma-guard, then extend your client with guardExtension().`
|
|
207
|
+
|
|
208
|
+
### How guard integration works
|
|
209
|
+
|
|
210
|
+
Each operation config accepts an optional `shape` property. When present, the generated handler:
|
|
211
|
+
|
|
212
|
+
1. Stores the shape on `res.locals.guardShape` via middleware
|
|
213
|
+
2. Resolves the caller from `config.guard.resolveVariant(req)`, then from the configured header (default `x-api-variant`), falling back to `undefined`
|
|
214
|
+
3. Calls `prisma.model.guard(shape, caller).method(args)` instead of `prisma.model.method(args)`
|
|
215
|
+
|
|
216
|
+
When `shape` is absent, the handler calls Prisma directly with no guard enforcement.
|
|
217
|
+
|
|
218
|
+
### Single shape per operation
|
|
219
|
+
|
|
220
|
+
A single shape object restricts what the client can do on that operation. No caller routing is needed.
|
|
221
|
+
```ts
|
|
222
|
+
const userConfig = {
|
|
223
|
+
findMany: {
|
|
224
|
+
shape: {
|
|
225
|
+
where: { email: { contains: true }, role: { equals: true } },
|
|
226
|
+
orderBy: { createdAt: true },
|
|
227
|
+
take: { max: 100, default: 25 },
|
|
228
|
+
skip: true,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
create: {
|
|
232
|
+
shape: {
|
|
233
|
+
data: { email: true, name: true, role: 'user' },
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
update: {
|
|
237
|
+
shape: {
|
|
238
|
+
data: { name: true },
|
|
239
|
+
where: { id: { equals: true } },
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
delete: {
|
|
243
|
+
shape: {
|
|
244
|
+
where: { id: { equals: true } },
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
app.use('/', UserRouter(userConfig))
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
In this example:
|
|
253
|
+
|
|
254
|
+
- `findMany` allows filtering by `email` (contains) and `role` (equals), sorting by `createdAt`, pagination via `take`/`skip`. All other where fields, orderBy fields, and include/select are rejected.
|
|
255
|
+
- `create` accepts `email` and `name` from the client. `role` is forced to `'user'` regardless of what the client sends.
|
|
256
|
+
- `update` only allows changing `name`, and requires a unique `id` in `where`.
|
|
257
|
+
- `delete` requires a unique `id` in `where`.
|
|
258
|
+
|
|
259
|
+
### Shape value types in data
|
|
260
|
+
|
|
261
|
+
Each field in a `data` shape accepts one of four value types:
|
|
262
|
+
```ts
|
|
263
|
+
import { force } from 'prisma-guard'
|
|
264
|
+
|
|
265
|
+
const config = {
|
|
266
|
+
create: {
|
|
267
|
+
shape: {
|
|
268
|
+
data: {
|
|
269
|
+
email: true, // client-controlled, @zod chains apply
|
|
270
|
+
name: true, // client-controlled
|
|
271
|
+
role: 'member', // forced to 'member', client cannot override
|
|
272
|
+
isActive: force(true), // forced to boolean true (force() needed to distinguish from client-controlled)
|
|
273
|
+
bio: (base) => base.max(500), // client-controlled with inline validation override
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
- `true` — client provides the value; `@zod` schema directives from the Prisma schema apply
|
|
281
|
+
- literal value — server forces this value; client input is ignored
|
|
282
|
+
- `force(value)` — same as literal, but required when the forced value is literally `true` (since bare `true` means client-controlled)
|
|
283
|
+
- `(base) => schema` — client provides the value; the function receives the base Zod type and returns a refined schema, bypassing `@zod` chains
|
|
284
|
+
|
|
285
|
+
### Named shapes (variant-based routing)
|
|
286
|
+
|
|
287
|
+
Different API consumers often need different shapes for the same operation. Named shapes use a caller value to route to the correct shape.
|
|
141
288
|
```ts
|
|
142
289
|
const userConfig = {
|
|
143
290
|
findMany: {
|
|
144
291
|
shape: {
|
|
145
|
-
admin: {
|
|
146
|
-
|
|
292
|
+
admin: {
|
|
293
|
+
where: { email: { contains: true }, role: { equals: true }, isActive: { equals: true } },
|
|
294
|
+
include: { posts: true, profile: true },
|
|
295
|
+
take: { max: 200 },
|
|
296
|
+
},
|
|
297
|
+
public: {
|
|
298
|
+
where: { name: { contains: true } },
|
|
299
|
+
select: { id: true, name: true },
|
|
300
|
+
take: { max: 20, default: 10 },
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
create: {
|
|
305
|
+
shape: {
|
|
306
|
+
admin: {
|
|
307
|
+
data: { email: true, name: true, role: true, isActive: true },
|
|
308
|
+
},
|
|
309
|
+
editor: {
|
|
310
|
+
data: { email: true, name: true, role: 'member' },
|
|
311
|
+
},
|
|
147
312
|
},
|
|
148
313
|
},
|
|
149
314
|
guard: {
|
|
@@ -154,7 +319,487 @@ const userConfig = {
|
|
|
154
319
|
app.use('/', UserRouter(userConfig))
|
|
155
320
|
```
|
|
156
321
|
|
|
157
|
-
|
|
322
|
+
The client sends the variant in the configured header:
|
|
323
|
+
```ts
|
|
324
|
+
// Admin frontend
|
|
325
|
+
fetch('/user', {
|
|
326
|
+
headers: { 'x-api-variant': 'admin' },
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
// Public frontend
|
|
330
|
+
fetch('/user', {
|
|
331
|
+
headers: { 'x-api-variant': 'public' },
|
|
332
|
+
})
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
If the caller is missing or doesn't match any key, the request is rejected with 400 (`CallerError`).
|
|
336
|
+
|
|
337
|
+
### Custom caller resolution
|
|
338
|
+
|
|
339
|
+
Use `resolveVariant` for caller logic beyond a simple header:
|
|
340
|
+
```ts
|
|
341
|
+
const userConfig = {
|
|
342
|
+
findMany: {
|
|
343
|
+
shape: {
|
|
344
|
+
admin: { /* ... */ },
|
|
345
|
+
public: { /* ... */ },
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
guard: {
|
|
349
|
+
resolveVariant: (req) => {
|
|
350
|
+
if (req.user?.role === 'admin') return 'admin'
|
|
351
|
+
return 'public'
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
`resolveVariant` takes priority over the header. If both are configured, the header is checked only when `resolveVariant` returns `undefined`.
|
|
358
|
+
|
|
359
|
+
### Parameterized caller patterns
|
|
360
|
+
|
|
361
|
+
Caller keys support parameterized path patterns:
|
|
362
|
+
```ts
|
|
363
|
+
const projectConfig = {
|
|
364
|
+
update: {
|
|
365
|
+
shape: {
|
|
366
|
+
'/admin/projects/:id': {
|
|
367
|
+
data: { title: true, status: true, priority: true },
|
|
368
|
+
where: { id: { equals: true } },
|
|
369
|
+
},
|
|
370
|
+
'/editor/projects/:id': {
|
|
371
|
+
data: { title: true },
|
|
372
|
+
where: { id: { equals: true } },
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
guard: {
|
|
377
|
+
variantHeader: 'x-caller',
|
|
378
|
+
},
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
The client sends the full path:
|
|
383
|
+
```ts
|
|
384
|
+
fetch('/project', {
|
|
385
|
+
method: 'PUT',
|
|
386
|
+
headers: {
|
|
387
|
+
'x-caller': '/admin/projects/abc123',
|
|
388
|
+
'Content-Type': 'application/json',
|
|
389
|
+
},
|
|
390
|
+
body: JSON.stringify({
|
|
391
|
+
where: { id: { equals: 'abc123' } },
|
|
392
|
+
data: { title: 'Updated', status: 'active' },
|
|
393
|
+
}),
|
|
394
|
+
})
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Exact matches are checked first. Parameters (`:id`) are routing-only and are not extracted.
|
|
398
|
+
|
|
399
|
+
### Forced where conditions
|
|
400
|
+
|
|
401
|
+
Literal values in `where` shapes are forced server-side and cannot be overridden by the client:
|
|
402
|
+
```ts
|
|
403
|
+
import { force } from 'prisma-guard'
|
|
404
|
+
|
|
405
|
+
const projectConfig = {
|
|
406
|
+
findMany: {
|
|
407
|
+
shape: {
|
|
408
|
+
where: {
|
|
409
|
+
status: { equals: 'published' }, // always filter to published
|
|
410
|
+
isDeleted: { equals: false }, // always exclude deleted
|
|
411
|
+
isActive: { equals: force(true) }, // force() needed for boolean true
|
|
412
|
+
title: { contains: true }, // client-controlled
|
|
413
|
+
},
|
|
414
|
+
take: { max: 50 },
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
A request with `{ where: { title: { contains: 'demo' } } }` produces:
|
|
421
|
+
```
|
|
422
|
+
WHERE status = 'published'
|
|
423
|
+
AND isDeleted = false
|
|
424
|
+
AND isActive = true
|
|
425
|
+
AND title LIKE '%demo%'
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
The client cannot bypass the forced conditions.
|
|
429
|
+
|
|
430
|
+
### Logical combinators (AND, OR, NOT)
|
|
431
|
+
|
|
432
|
+
Where shapes support `AND`, `OR`, and `NOT`. The combinator value defines which fields are allowed inside it:
|
|
433
|
+
```ts
|
|
434
|
+
const config = {
|
|
435
|
+
findMany: {
|
|
436
|
+
shape: {
|
|
437
|
+
where: {
|
|
438
|
+
OR: {
|
|
439
|
+
title: { contains: true },
|
|
440
|
+
description: { contains: true },
|
|
441
|
+
},
|
|
442
|
+
status: { equals: 'published' }, // forced, always applied
|
|
443
|
+
},
|
|
444
|
+
take: { max: 50 },
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Client sends:
|
|
451
|
+
```json
|
|
452
|
+
{
|
|
453
|
+
"where": {
|
|
454
|
+
"OR": [
|
|
455
|
+
{ "title": { "contains": "demo" } },
|
|
456
|
+
{ "description": { "contains": "demo" } }
|
|
457
|
+
]
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
The forced `status = 'published'` is always merged as an AND condition. Forced values inside combinators are lifted to the top-level query, regardless of the combinator type.
|
|
463
|
+
|
|
464
|
+
### Relation filters in where
|
|
465
|
+
|
|
466
|
+
Where shapes support relation-level filters. To-many relations use `some`, `every`, `none`. To-one relations use `is`, `isNot`.
|
|
467
|
+
```ts
|
|
468
|
+
const userConfig = {
|
|
469
|
+
findMany: {
|
|
470
|
+
shape: {
|
|
471
|
+
where: {
|
|
472
|
+
posts: {
|
|
473
|
+
some: {
|
|
474
|
+
title: { contains: true },
|
|
475
|
+
published: { equals: true }, // forced inside the relation
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
take: { max: 50 },
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
The client can filter by `title` inside the relation, but `published = true` is always enforced.
|
|
486
|
+
|
|
487
|
+
### Select, include, and omit in shapes
|
|
488
|
+
|
|
489
|
+
Shapes can restrict which response fields and relations the client may request:
|
|
490
|
+
```ts
|
|
491
|
+
const userConfig = {
|
|
492
|
+
findMany: {
|
|
493
|
+
shape: {
|
|
494
|
+
where: { role: { equals: true } },
|
|
495
|
+
select: {
|
|
496
|
+
id: true,
|
|
497
|
+
email: true,
|
|
498
|
+
name: true,
|
|
499
|
+
posts: {
|
|
500
|
+
select: { id: true, title: true },
|
|
501
|
+
},
|
|
502
|
+
_count: {
|
|
503
|
+
select: { posts: true },
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
take: { max: 50 },
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
The client can only select from the whitelisted fields and relations. Attempting to select unlisted fields (e.g. `passwordHash`) is rejected.
|
|
513
|
+
|
|
514
|
+
`select` and `include` are mutually exclusive at the same level in both the shape and the client request.
|
|
515
|
+
|
|
516
|
+
### Nested include with forced where and pagination
|
|
517
|
+
|
|
518
|
+
Nested includes on to-many relations support `where`, `orderBy`, `cursor`, `take`, and `skip`:
|
|
519
|
+
```ts
|
|
520
|
+
import { force } from 'prisma-guard'
|
|
521
|
+
|
|
522
|
+
const userConfig = {
|
|
523
|
+
findMany: {
|
|
524
|
+
shape: {
|
|
525
|
+
include: {
|
|
526
|
+
posts: {
|
|
527
|
+
where: { isDeleted: { equals: false } }, // forced: never return deleted posts
|
|
528
|
+
orderBy: { createdAt: true },
|
|
529
|
+
take: { max: 20, default: 10 },
|
|
530
|
+
skip: true,
|
|
531
|
+
},
|
|
532
|
+
profile: true, // simple include, no constraints
|
|
533
|
+
_count: {
|
|
534
|
+
select: {
|
|
535
|
+
posts: {
|
|
536
|
+
where: { isDeleted: { equals: false } }, // count only non-deleted
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
take: { max: 50 },
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Mutation return projection
|
|
548
|
+
|
|
549
|
+
Write operations that return records (`create`, `update`, `upsert`, `delete`, `createManyAndReturn`, `updateManyAndReturn`) support `select` and `include` in the shape:
|
|
550
|
+
```ts
|
|
551
|
+
const userConfig = {
|
|
552
|
+
create: {
|
|
553
|
+
shape: {
|
|
554
|
+
data: { email: true, name: true },
|
|
555
|
+
include: {
|
|
556
|
+
profile: true,
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
update: {
|
|
561
|
+
shape: {
|
|
562
|
+
data: { name: true },
|
|
563
|
+
where: { id: { equals: true } },
|
|
564
|
+
select: {
|
|
565
|
+
id: true,
|
|
566
|
+
name: true,
|
|
567
|
+
updatedAt: true,
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
The client can include `include` or `select` in the request body. If the shape does not define projection, the client cannot request one. Batch methods (`createMany`, `updateMany`, `deleteMany`) do not support projection.
|
|
575
|
+
|
|
576
|
+
### Upsert
|
|
577
|
+
|
|
578
|
+
Upsert uses `create` and `update` shape keys instead of `data`:
|
|
579
|
+
```ts
|
|
580
|
+
import { force } from 'prisma-guard'
|
|
581
|
+
|
|
582
|
+
const projectConfig = {
|
|
583
|
+
upsert: {
|
|
584
|
+
shape: {
|
|
585
|
+
where: { id: { equals: true } },
|
|
586
|
+
create: {
|
|
587
|
+
title: true,
|
|
588
|
+
status: 'draft',
|
|
589
|
+
isActive: force(true),
|
|
590
|
+
},
|
|
591
|
+
update: {
|
|
592
|
+
title: true,
|
|
593
|
+
},
|
|
594
|
+
select: { id: true, title: true, status: true },
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
All three (`where`, `create`, `update`) are required. Using `data` instead of `create`/`update` is rejected.
|
|
601
|
+
|
|
602
|
+
### Bulk mutation safety
|
|
603
|
+
|
|
604
|
+
`updateMany`, `updateManyAndReturn`, and `deleteMany` require `where` in the shape:
|
|
605
|
+
```ts
|
|
606
|
+
const userConfig = {
|
|
607
|
+
deleteMany: {
|
|
608
|
+
shape: {
|
|
609
|
+
where: { isActive: { equals: true }, role: { equals: true } },
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
updateMany: {
|
|
613
|
+
shape: {
|
|
614
|
+
data: { isActive: true },
|
|
615
|
+
where: { role: { equals: true } },
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
A shape without `where` on these methods is rejected. Empty resolved where at runtime is also rejected.
|
|
622
|
+
|
|
623
|
+
### Tenant isolation with guard shapes
|
|
624
|
+
|
|
625
|
+
When the guard extension is configured with scope context, tenant filters are injected automatically into all top-level operations on scoped models. Guard shapes and scope work together:
|
|
626
|
+
```prisma
|
|
627
|
+
/// @scope-root
|
|
628
|
+
model Tenant {
|
|
629
|
+
id String @id @default(cuid())
|
|
630
|
+
name String
|
|
631
|
+
projects Project[]
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
model Project {
|
|
635
|
+
id String @id @default(cuid())
|
|
636
|
+
title String
|
|
637
|
+
tenantId String
|
|
638
|
+
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
639
|
+
}
|
|
640
|
+
```
|
|
641
|
+
```ts
|
|
642
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
643
|
+
import { guard } from './generated/guard/client'
|
|
644
|
+
|
|
645
|
+
const store = new AsyncLocalStorage<{ tenantId: string }>()
|
|
646
|
+
|
|
647
|
+
const prisma = new PrismaClient().$extends(
|
|
648
|
+
guard.extension(() => ({
|
|
649
|
+
Tenant: store.getStore()?.tenantId,
|
|
650
|
+
}))
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
app.use((req, res, next) => {
|
|
654
|
+
const tenantId = req.headers['x-tenant-id'] as string
|
|
655
|
+
store.run({ tenantId }, () => {
|
|
656
|
+
req.prisma = prisma
|
|
657
|
+
next()
|
|
658
|
+
})
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
app.use('/', ProjectRouter({
|
|
662
|
+
findMany: {
|
|
663
|
+
shape: {
|
|
664
|
+
where: { title: { contains: true } },
|
|
665
|
+
take: { max: 50 },
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
create: {
|
|
669
|
+
shape: {
|
|
670
|
+
data: { title: true },
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
}))
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
The scope extension handles tenant isolation at the query level:
|
|
677
|
+
|
|
678
|
+
- Reads: `AND tenantId = ?` is injected into where
|
|
679
|
+
- Creates: `tenantId` is injected into data (the scope FK does not need to be in the data shape)
|
|
680
|
+
- Updates/deletes: `tenantId` condition is merged into where, scope FK is stripped from data
|
|
681
|
+
- Upsert: scope condition in where, FK injected into create data, FK stripped from update data
|
|
682
|
+
|
|
683
|
+
The data shape for `create` above only lists `title`. The `tenantId` field is injected by the scope extension automatically — the create completeness check accounts for scope foreign keys.
|
|
684
|
+
|
|
685
|
+
### Supported shape keys
|
|
686
|
+
|
|
687
|
+
For reads: `where`, `include`, `select`, `orderBy`, `cursor`, `take`, `skip`, `distinct`, `_count`, `_avg`, `_sum`, `_min`, `_max`, `by`, `having`
|
|
688
|
+
|
|
689
|
+
For writes: `data`, `where`, `select`, `include` (select/include only on methods that return records)
|
|
690
|
+
|
|
691
|
+
For upsert: `where`, `create`, `update`, `select`, `include`
|
|
692
|
+
|
|
693
|
+
### Guard error handling
|
|
694
|
+
|
|
695
|
+
Guard errors are mapped to HTTP status codes by the generated error-handling middleware:
|
|
696
|
+
|
|
697
|
+
| Error type | HTTP status | When |
|
|
698
|
+
| ------------- | ----------- | ----------------------------------------------------------------- |
|
|
699
|
+
| `ShapeError` | 400 | Invalid shape config, unknown fields, body validation, type errors |
|
|
700
|
+
| `CallerError` | 400 | Missing/unknown/ambiguous caller, caller in body |
|
|
701
|
+
| `PolicyError` | 403 | Scope denied, missing tenant context, rejected findUnique |
|
|
702
|
+
|
|
703
|
+
All errors return `{ "message": "..." }` in the response body.
|
|
704
|
+
|
|
705
|
+
### Complete guard example
|
|
706
|
+
```ts
|
|
707
|
+
import express from 'express'
|
|
708
|
+
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
709
|
+
import { PrismaClient } from '@prisma/client'
|
|
710
|
+
import { guard } from './generated/guard/client'
|
|
711
|
+
import { force } from 'prisma-guard'
|
|
712
|
+
import { UserRouter } from './generated/User/UserRouter'
|
|
713
|
+
import { ProjectRouter } from './generated/Project/ProjectRouter'
|
|
714
|
+
|
|
715
|
+
const store = new AsyncLocalStorage<{ tenantId: string; role: string }>()
|
|
716
|
+
|
|
717
|
+
const prisma = new PrismaClient().$extends(
|
|
718
|
+
guard.extension(() => ({
|
|
719
|
+
Tenant: store.getStore()?.tenantId,
|
|
720
|
+
}))
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
const app = express()
|
|
724
|
+
|
|
725
|
+
app.use((req, res, next) => {
|
|
726
|
+
const tenantId = req.headers['x-tenant-id'] as string
|
|
727
|
+
const role = req.headers['x-role'] as string || 'viewer'
|
|
728
|
+
store.run({ tenantId, role }, () => {
|
|
729
|
+
req.prisma = prisma
|
|
730
|
+
next()
|
|
731
|
+
})
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
app.use('/', ProjectRouter({
|
|
735
|
+
findMany: {
|
|
736
|
+
shape: {
|
|
737
|
+
admin: {
|
|
738
|
+
where: { title: { contains: true }, status: { equals: true } },
|
|
739
|
+
include: { members: true },
|
|
740
|
+
orderBy: { createdAt: true },
|
|
741
|
+
take: { max: 200 },
|
|
742
|
+
skip: true,
|
|
743
|
+
},
|
|
744
|
+
viewer: {
|
|
745
|
+
where: {
|
|
746
|
+
title: { contains: true },
|
|
747
|
+
status: { equals: 'published' },
|
|
748
|
+
isDeleted: { equals: false },
|
|
749
|
+
},
|
|
750
|
+
select: { id: true, title: true, createdAt: true },
|
|
751
|
+
take: { max: 50, default: 20 },
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
create: {
|
|
756
|
+
shape: {
|
|
757
|
+
admin: {
|
|
758
|
+
data: { title: true, status: true, priority: true },
|
|
759
|
+
include: { members: true },
|
|
760
|
+
},
|
|
761
|
+
viewer: {
|
|
762
|
+
data: { title: true, status: 'draft', priority: 1 },
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
update: {
|
|
767
|
+
shape: {
|
|
768
|
+
admin: {
|
|
769
|
+
data: { title: true, status: true, priority: true },
|
|
770
|
+
where: { id: { equals: true } },
|
|
771
|
+
},
|
|
772
|
+
viewer: {
|
|
773
|
+
data: { title: true },
|
|
774
|
+
where: { id: { equals: true } },
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
delete: {
|
|
779
|
+
shape: {
|
|
780
|
+
admin: {
|
|
781
|
+
where: { id: { equals: true } },
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
},
|
|
785
|
+
guard: {
|
|
786
|
+
resolveVariant: (req) => {
|
|
787
|
+
const ctx = store.getStore()
|
|
788
|
+
return ctx?.role === 'admin' ? 'admin' : 'viewer'
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
}))
|
|
792
|
+
|
|
793
|
+
app.listen(3000)
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
In this setup:
|
|
797
|
+
|
|
798
|
+
- Admins can filter by any allowed field, include relations, and take up to 200 rows
|
|
799
|
+
- Viewers can only see published, non-deleted projects with a restricted field set
|
|
800
|
+
- Create: admins set any allowed field; viewers always create drafts with priority 1
|
|
801
|
+
- Delete: only admins can delete; viewers hitting the delete endpoint get a `CallerError` because there is no `viewer` shape for delete
|
|
802
|
+
- Tenant isolation is automatic — every query is scoped to the tenant from `x-tenant-id`
|
|
158
803
|
|
|
159
804
|
## Request body format
|
|
160
805
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.encodeQueryParams = void 0;
|
|
4
|
-
const
|
|
4
|
+
const misc_js_1 = require("../copy/misc.js");
|
|
5
5
|
function replacer(_key, value) {
|
|
6
6
|
if (typeof value === 'bigint') {
|
|
7
7
|
return value.toString();
|
|
@@ -21,7 +21,7 @@ const encodeQueryParams = (params) => {
|
|
|
21
21
|
entries.push(`${encodeURIComponent(key)}=${encodeURIComponent(value.toString())}`);
|
|
22
22
|
continue;
|
|
23
23
|
}
|
|
24
|
-
if (Array.isArray(value) || (0,
|
|
24
|
+
if (Array.isArray(value) || (0, misc_js_1.isObject)(value)) {
|
|
25
25
|
entries.push(`${encodeURIComponent(key)}=${encodeURIComponent(JSON.stringify(value, replacer))}`);
|
|
26
26
|
continue;
|
|
27
27
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"encodeQueryParams.js","sourceRoot":"","sources":["../../src/client/encodeQueryParams.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"encodeQueryParams.js","sourceRoot":"","sources":["../../src/client/encodeQueryParams.ts"],"names":[],"mappings":";;;AAAA,6CAA0C;AAiB1C,SAAS,QAAQ,CAAC,IAAY,EAAE,KAAc;IAC5C,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAA;IACzB,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAEM,MAAM,iBAAiB,GAAG,CAAC,MAA+B,EAAU,EAAE;IAC3E,MAAM,OAAO,GAAa,EAAE,CAAA;IAE5B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,KAAK,KAAK,SAAS;YAAE,SAAQ;QAEjC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,OAAO,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YAC/C,SAAQ;QACV,CAAC;QAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,OAAO,CAAC,IAAI,CACV,GAAG,kBAAkB,CAAC,GAAG,CAAC,IAAI,kBAAkB,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,EAAE,CACrE,CAAA;YACD,SAAQ;QACV,CAAC;QAED,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,IAAA,kBAAQ,EAAC,KAAK,CAAC,EAAE,CAAC;YAC5C,OAAO,CAAC,IAAI,CACV,GAAG,kBAAkB,CAAC,GAAG,CAAC,IAAI,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,EAAE,CACpF,CAAA;YACD,SAAQ;QACV,CAAC;QAED,OAAO,CAAC,IAAI,CACV,GAAG,kBAAkB,CAAC,GAAG,CAAC,IAAI,kBAAkB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAClE,CAAA;IACH,CAAC;IAED,OAAO,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAC1B,CAAC,CAAA;AA/BY,QAAA,iBAAiB,qBA+B7B"}
|
|
@@ -210,6 +210,48 @@ function assertGuard(delegate: any): void {
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
const COUNT_PROJECTION_KEYS = new Set(['select', 'include'])
|
|
214
|
+
|
|
215
|
+
const GUARD_SHAPE_CONFIG_KEYS = new Set([
|
|
216
|
+
'data', 'create', 'update', 'where', 'include', 'select', 'orderBy',
|
|
217
|
+
'cursor', 'take', 'skip', 'distinct', 'having', '_count', '_avg',
|
|
218
|
+
'_sum', '_min', '_max', 'by',
|
|
219
|
+
])
|
|
220
|
+
|
|
221
|
+
function stripProjectionKeys(obj: Record<string, any>): Record<string, any> {
|
|
222
|
+
const result: Record<string, any> = {}
|
|
223
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
224
|
+
if (COUNT_PROJECTION_KEYS.has(key)) continue
|
|
225
|
+
result[key] = value
|
|
226
|
+
}
|
|
227
|
+
return result
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function stripProjectionForCount(shape: Record<string, any>): Record<string, any> {
|
|
231
|
+
if (typeof shape === 'function') {
|
|
232
|
+
return (...args: any[]) => stripProjectionKeys((shape as Function)(...args))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const keys = Object.keys(shape)
|
|
236
|
+
const isSingleShape = keys.length === 0 || keys.every(k => GUARD_SHAPE_CONFIG_KEYS.has(k))
|
|
237
|
+
|
|
238
|
+
if (isSingleShape) {
|
|
239
|
+
return stripProjectionKeys(shape)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const result: Record<string, any> = {}
|
|
243
|
+
for (const [key, variant] of Object.entries(shape)) {
|
|
244
|
+
if (typeof variant === 'function') {
|
|
245
|
+
result[key] = (...args: any[]) => stripProjectionKeys(variant(...args))
|
|
246
|
+
} else if (typeof variant === 'object' && variant !== null) {
|
|
247
|
+
result[key] = stripProjectionKeys(variant)
|
|
248
|
+
} else {
|
|
249
|
+
result[key] = variant
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return result
|
|
253
|
+
}
|
|
254
|
+
|
|
213
255
|
${generateReadHandlers(modelName, modelNameLower)}
|
|
214
256
|
|
|
215
257
|
${generateWriteHandlers(modelName, modelNameLower)}
|
|
@@ -349,6 +391,8 @@ async function countForPagination(
|
|
|
349
391
|
const distinctFields = normalizeDistinct(query.distinct)
|
|
350
392
|
const hasDistinct = distinctFields.length > 0
|
|
351
393
|
|
|
394
|
+
const countShape = shape ? stripProjectionForCount(shape) : undefined
|
|
395
|
+
|
|
352
396
|
if (hasDistinct) {
|
|
353
397
|
const selectField = distinctFields[0]
|
|
354
398
|
const distinctArgs: Record<string, any> = {
|
|
@@ -366,8 +410,8 @@ async function countForPagination(
|
|
|
366
410
|
console.warn('[prisma-generator-express] Distinct count exceeds ' + DISTINCT_COUNT_LIMIT + ', falling back to approximate total')
|
|
367
411
|
const countArgs: Record<string, any> = {}
|
|
368
412
|
if (query.where) countArgs.where = query.where
|
|
369
|
-
return
|
|
370
|
-
? await delegate.guard(
|
|
413
|
+
return countShape
|
|
414
|
+
? await delegate.guard(countShape, caller).count(countArgs)
|
|
371
415
|
: await delegate.count(countArgs)
|
|
372
416
|
}
|
|
373
417
|
|
|
@@ -377,8 +421,8 @@ async function countForPagination(
|
|
|
377
421
|
const countArgs: Record<string, any> = {}
|
|
378
422
|
if (query.where) countArgs.where = query.where
|
|
379
423
|
|
|
380
|
-
return
|
|
381
|
-
? await delegate.guard(
|
|
424
|
+
return countShape
|
|
425
|
+
? await delegate.guard(countShape, caller).count(countArgs)
|
|
382
426
|
: await delegate.count(countArgs)
|
|
383
427
|
}
|
|
384
428
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generateUnifiedHandler.js","sourceRoot":"","sources":["../../src/generators/generateUnifiedHandler.ts"],"names":[],"mappings":";;AAOA,
|
|
1
|
+
{"version":3,"file":"generateUnifiedHandler.js","sourceRoot":"","sources":["../../src/generators/generateUnifiedHandler.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -216,6 +216,48 @@ function assertGuard(delegate: any): void {
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
const COUNT_PROJECTION_KEYS = new Set(['select', 'include'])
|
|
220
|
+
|
|
221
|
+
const GUARD_SHAPE_CONFIG_KEYS = new Set([
|
|
222
|
+
'data', 'create', 'update', 'where', 'include', 'select', 'orderBy',
|
|
223
|
+
'cursor', 'take', 'skip', 'distinct', 'having', '_count', '_avg',
|
|
224
|
+
'_sum', '_min', '_max', 'by',
|
|
225
|
+
])
|
|
226
|
+
|
|
227
|
+
function stripProjectionKeys(obj: Record<string, any>): Record<string, any> {
|
|
228
|
+
const result: Record<string, any> = {}
|
|
229
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
230
|
+
if (COUNT_PROJECTION_KEYS.has(key)) continue
|
|
231
|
+
result[key] = value
|
|
232
|
+
}
|
|
233
|
+
return result
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function stripProjectionForCount(shape: Record<string, any>): Record<string, any> {
|
|
237
|
+
if (typeof shape === 'function') {
|
|
238
|
+
return (...args: any[]) => stripProjectionKeys((shape as Function)(...args))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const keys = Object.keys(shape)
|
|
242
|
+
const isSingleShape = keys.length === 0 || keys.every(k => GUARD_SHAPE_CONFIG_KEYS.has(k))
|
|
243
|
+
|
|
244
|
+
if (isSingleShape) {
|
|
245
|
+
return stripProjectionKeys(shape)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const result: Record<string, any> = {}
|
|
249
|
+
for (const [key, variant] of Object.entries(shape)) {
|
|
250
|
+
if (typeof variant === 'function') {
|
|
251
|
+
result[key] = (...args: any[]) => stripProjectionKeys(variant(...args))
|
|
252
|
+
} else if (typeof variant === 'object' && variant !== null) {
|
|
253
|
+
result[key] = stripProjectionKeys(variant)
|
|
254
|
+
} else {
|
|
255
|
+
result[key] = variant
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return result
|
|
259
|
+
}
|
|
260
|
+
|
|
219
261
|
${generateReadHandlers(modelName, modelNameLower)}
|
|
220
262
|
|
|
221
263
|
${generateWriteHandlers(modelName, modelNameLower)}
|
|
@@ -374,6 +416,8 @@ async function countForPagination(
|
|
|
374
416
|
const distinctFields = normalizeDistinct(query.distinct)
|
|
375
417
|
const hasDistinct = distinctFields.length > 0
|
|
376
418
|
|
|
419
|
+
const countShape = shape ? stripProjectionForCount(shape) : undefined
|
|
420
|
+
|
|
377
421
|
if (hasDistinct) {
|
|
378
422
|
const selectField = distinctFields[0]
|
|
379
423
|
const distinctArgs: Record<string, any> = {
|
|
@@ -391,8 +435,8 @@ async function countForPagination(
|
|
|
391
435
|
console.warn('[prisma-generator-express] Distinct count exceeds ' + DISTINCT_COUNT_LIMIT + ', falling back to approximate total')
|
|
392
436
|
const countArgs: Record<string, any> = {}
|
|
393
437
|
if (query.where) countArgs.where = query.where
|
|
394
|
-
return
|
|
395
|
-
? await delegate.guard(
|
|
438
|
+
return countShape
|
|
439
|
+
? await delegate.guard(countShape, caller).count(countArgs)
|
|
396
440
|
: await delegate.count(countArgs)
|
|
397
441
|
}
|
|
398
442
|
|
|
@@ -402,8 +446,8 @@ async function countForPagination(
|
|
|
402
446
|
const countArgs: Record<string, any> = {}
|
|
403
447
|
if (query.where) countArgs.where = query.where
|
|
404
448
|
|
|
405
|
-
return
|
|
406
|
-
? await delegate.guard(
|
|
449
|
+
return countShape
|
|
450
|
+
? await delegate.guard(countShape, caller).count(countArgs)
|
|
407
451
|
: await delegate.count(countArgs)
|
|
408
452
|
}
|
|
409
453
|
|