prisma-generator-express 1.28.0 → 1.30.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 +244 -14
- package/dist/constants.d.ts +1 -0
- package/dist/generators/generateFastifyHandler.d.ts +4 -0
- package/dist/generators/generateFastifyHandler.js +78 -0
- package/dist/generators/generateFastifyHandler.js.map +1 -0
- package/dist/generators/generateOperationCore.d.ts +6 -0
- package/dist/generators/generateOperationCore.js +534 -0
- package/dist/generators/generateOperationCore.js.map +1 -0
- package/dist/generators/generateQueryBuilderHelper.js +85 -69
- package/dist/generators/generateQueryBuilderHelper.js.map +1 -1
- package/dist/generators/generateRouter.js +1 -25
- package/dist/generators/generateRouter.js.map +1 -1
- package/dist/generators/generateRouterFastify.d.ts +5 -0
- package/dist/generators/generateRouterFastify.js +512 -0
- package/dist/generators/generateRouterFastify.js.map +1 -0
- package/dist/generators/generateUnifiedDocs.d.ts +2 -1
- package/dist/generators/generateUnifiedDocs.js +147 -82
- package/dist/generators/generateUnifiedDocs.js.map +1 -1
- package/dist/generators/generateUnifiedHandler.d.ts +0 -1
- package/dist/generators/generateUnifiedHandler.js +47 -516
- package/dist/generators/generateUnifiedHandler.js.map +1 -1
- package/dist/generators/generateUnifiedScalarUI.d.ts +2 -0
- package/dist/generators/generateUnifiedScalarUI.js +127 -1324
- package/dist/generators/generateUnifiedScalarUI.js.map +1 -1
- package/dist/index.js +33 -8
- package/dist/index.js.map +1 -1
- package/dist/utils/copyFiles.d.ts +2 -1
- package/dist/utils/copyFiles.js +73 -39
- package/dist/utils/copyFiles.js.map +1 -1
- package/dist/utils/writeFileSafely.js +3 -0
- package/dist/utils/writeFileSafely.js.map +1 -1
- package/package.json +4 -1
- package/src/client/encodeQueryParams.ts +1 -1
- package/src/constants.ts +2 -0
- package/src/copy/createOutputValidatorMiddleware.ts +9 -12
- package/src/copy/docsRenderer.ts +1285 -0
- package/src/copy/parseQueryParams.ts +4 -8
- package/src/copy/routeConfig.ts +10 -4
- package/src/generators/generateFastifyHandler.ts +86 -0
- package/src/generators/generateOperationCore.ts +545 -0
- package/src/generators/generateQueryBuilderHelper.ts +86 -70
- package/src/generators/generateRouter.ts +1 -25
- package/src/generators/generateRouterFastify.ts +522 -0
- package/src/generators/generateUnifiedDocs.ts +164 -81
- package/src/generators/generateUnifiedHandler.ts +45 -533
- package/src/generators/generateUnifiedScalarUI.ts +134 -1323
- package/src/index.ts +45 -9
- package/src/utils/copyFiles.ts +88 -44
- package/src/utils/writeFileSafely.ts +4 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://codecov.io/gh/multipliedtwice/prisma-generator-express)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
Prisma generator that creates Express CRUD API routes with OpenAPI documentation from your Prisma schema.
|
|
8
|
+
Prisma generator that creates Express or Fastify CRUD API routes with OpenAPI documentation from your Prisma schema.
|
|
9
9
|
|
|
10
10
|
Running `npx prisma generate` produces:
|
|
11
11
|
|
|
@@ -16,12 +16,15 @@ 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
|
+
Supports both **Express** and **Fastify** targets via the `target` configuration option.
|
|
20
|
+
|
|
19
21
|
## Table of contents
|
|
20
22
|
|
|
21
23
|
- [Compatibility](#compatibility)
|
|
22
24
|
- [Installation](#installation)
|
|
23
25
|
- [Setup](#setup)
|
|
24
|
-
- [Usage](#usage)
|
|
26
|
+
- [Usage (Express)](#usage-express)
|
|
27
|
+
- [Usage (Fastify)](#usage-fastify)
|
|
25
28
|
- [Selective routes with middleware](#selective-routes-with-middleware)
|
|
26
29
|
- [Guard shapes (prisma-guard integration)](#guard-shapes-prisma-guard-integration)
|
|
27
30
|
- [Request body format](#request-body-format)
|
|
@@ -53,6 +56,13 @@ Some operations require newer versions:
|
|
|
53
56
|
| `omit` parameter | 6.2.0 | Returns 400 on versions 6.0.x–6.1.x |
|
|
54
57
|
| `updateManyAndReturn` | 6.2.0 | PostgreSQL, CockroachDB, SQLite only |
|
|
55
58
|
|
|
59
|
+
### Framework support
|
|
60
|
+
|
|
61
|
+
| Framework | Target value | Generated output |
|
|
62
|
+
| --------- | ------------ | ---------------- |
|
|
63
|
+
| Express | `"express"` | `express.Router()` factory function per model |
|
|
64
|
+
| Fastify | `"fastify"` | Fastify plugin function per model |
|
|
65
|
+
|
|
56
66
|
### Database provider support
|
|
57
67
|
|
|
58
68
|
Most operations work across all Prisma-supported providers. Exceptions:
|
|
@@ -66,16 +76,25 @@ Most operations work across all Prisma-supported providers. Exceptions:
|
|
|
66
76
|
Operations not supported by your database provider return `501 Not Implemented` at runtime. The generator emits handlers for all operations regardless of provider — use selective route configuration to expose only supported operations.
|
|
67
77
|
|
|
68
78
|
## Installation
|
|
79
|
+
|
|
69
80
|
```bash
|
|
70
81
|
npm install -D prisma-generator-express
|
|
71
82
|
```
|
|
72
83
|
|
|
73
|
-
Peer dependencies:
|
|
84
|
+
Peer dependencies for Express:
|
|
85
|
+
|
|
74
86
|
```bash
|
|
75
87
|
npm install @prisma/client express
|
|
76
88
|
```
|
|
77
89
|
|
|
90
|
+
Peer dependencies for Fastify:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm install @prisma/client fastify
|
|
94
|
+
```
|
|
95
|
+
|
|
78
96
|
Optional peer dependencies:
|
|
97
|
+
|
|
79
98
|
```bash
|
|
80
99
|
npm install prisma-sql # SQL optimization
|
|
81
100
|
npm install prisma-guard zod # Guard shape enforcement
|
|
@@ -85,6 +104,7 @@ npm install prisma-query-builder-ui # Visual query playground
|
|
|
85
104
|
## Setup
|
|
86
105
|
|
|
87
106
|
Add the generator to your `schema.prisma`:
|
|
107
|
+
|
|
88
108
|
```prisma
|
|
89
109
|
generator client {
|
|
90
110
|
provider = "prisma-client-js"
|
|
@@ -95,14 +115,27 @@ generator express {
|
|
|
95
115
|
}
|
|
96
116
|
```
|
|
97
117
|
|
|
118
|
+
To generate Fastify routes instead of Express, set the `target` config:
|
|
119
|
+
|
|
120
|
+
```prisma
|
|
121
|
+
generator express {
|
|
122
|
+
provider = "prisma-generator-express"
|
|
123
|
+
target = "fastify"
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Valid `target` values are `"express"` (default) and `"fastify"`.
|
|
128
|
+
|
|
98
129
|
The generator detects the Prisma client generator automatically. All standard provider values are supported: `prisma-client-js`, `@prisma/client`, and `prisma-client`.
|
|
99
130
|
|
|
100
131
|
Generate:
|
|
132
|
+
|
|
101
133
|
```bash
|
|
102
134
|
npx prisma generate
|
|
103
135
|
```
|
|
104
136
|
|
|
105
|
-
## Usage
|
|
137
|
+
## Usage (Express)
|
|
138
|
+
|
|
106
139
|
```ts
|
|
107
140
|
import express from 'express'
|
|
108
141
|
import { PrismaClient } from '@prisma/client'
|
|
@@ -127,7 +160,54 @@ app.listen(3000, () => {
|
|
|
127
160
|
})
|
|
128
161
|
```
|
|
129
162
|
|
|
163
|
+
## Usage (Fastify)
|
|
164
|
+
|
|
165
|
+
When `target = "fastify"`, each model produces a Fastify plugin function instead of an Express router.
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import Fastify from 'fastify'
|
|
169
|
+
import { PrismaClient } from '@prisma/client'
|
|
170
|
+
import { UserRoutes } from './generated/User/UserRouter'
|
|
171
|
+
|
|
172
|
+
const prisma = new PrismaClient()
|
|
173
|
+
const fastify = Fastify()
|
|
174
|
+
|
|
175
|
+
fastify.decorateRequest('prisma', null)
|
|
176
|
+
|
|
177
|
+
fastify.addHook('onRequest', async (request) => {
|
|
178
|
+
request.prisma = prisma
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const userConfig = {
|
|
182
|
+
enableAll: true,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fastify.register(async (instance) => {
|
|
186
|
+
await UserRoutes(instance, userConfig)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
fastify.listen({ port: 3000 }, () => {
|
|
190
|
+
console.log('Server is running on http://localhost:3000')
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The generated function signature is `async function ModelRoutes(fastify: FastifyInstance, config?: RouteConfig)`. It registers routes directly on the provided Fastify instance.
|
|
195
|
+
|
|
196
|
+
### Key differences between Express and Fastify targets
|
|
197
|
+
|
|
198
|
+
| Aspect | Express | Fastify |
|
|
199
|
+
| ------ | ------- | ------- |
|
|
200
|
+
| Generated function | `ModelRouter(config)` returns `express.Router` | `ModelRoutes(fastify, config)` registers on instance |
|
|
201
|
+
| Mounting | `app.use('/', ModelRouter(config))` | `fastify.register(async (i) => { await ModelRoutes(i, config) })` |
|
|
202
|
+
| Hook types | `before`/`after` are Express `RequestHandler[]` | `before`/`after` are `FastifyHookHandler[]` |
|
|
203
|
+
| Guard resolveVariant | Receives `express.Request` | Receives `FastifyRequest` |
|
|
204
|
+
| Request data | `req.prisma`, `res.locals` | `request.prisma`, `request` properties |
|
|
205
|
+
| Error handling | Express error middleware on the router | Fastify `setErrorHandler` on the instance |
|
|
206
|
+
|
|
130
207
|
## Selective routes with middleware
|
|
208
|
+
|
|
209
|
+
### Express
|
|
210
|
+
|
|
131
211
|
```ts
|
|
132
212
|
const userConfig = {
|
|
133
213
|
findMany: {
|
|
@@ -142,18 +222,42 @@ const userConfig = {
|
|
|
142
222
|
app.use('/', UserRouter(userConfig))
|
|
143
223
|
```
|
|
144
224
|
|
|
225
|
+
### Fastify
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
const userConfig = {
|
|
229
|
+
findMany: {
|
|
230
|
+
before: [async (request, reply) => { /* auth check */ }],
|
|
231
|
+
},
|
|
232
|
+
create: {
|
|
233
|
+
before: [async (request, reply) => { /* auth + validation */ }],
|
|
234
|
+
},
|
|
235
|
+
findUnique: {},
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
fastify.register(async (instance) => {
|
|
239
|
+
await UserRoutes(instance, userConfig)
|
|
240
|
+
})
|
|
241
|
+
```
|
|
242
|
+
|
|
145
243
|
Only operations listed in the config (or all when `enableAll: true`) are registered. Operations not listed produce no routes.
|
|
146
244
|
|
|
245
|
+
Fastify hooks receive `(request: FastifyRequest, reply: FastifyReply)`. If a hook sends a reply (via `reply.send()`), subsequent hooks and the handler are skipped.
|
|
246
|
+
|
|
147
247
|
## Guard shapes (prisma-guard integration)
|
|
148
248
|
|
|
149
249
|
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)`.
|
|
150
250
|
|
|
251
|
+
Guard shapes work identically for both Express and Fastify targets. The only difference is the type of the `resolveVariant` callback parameter (`Request` vs `FastifyRequest`).
|
|
252
|
+
|
|
151
253
|
### Guard setup
|
|
152
254
|
|
|
153
255
|
Install prisma-guard and add its generator to your schema:
|
|
256
|
+
|
|
154
257
|
```bash
|
|
155
258
|
npm install prisma-guard zod
|
|
156
259
|
```
|
|
260
|
+
|
|
157
261
|
```prisma
|
|
158
262
|
generator client {
|
|
159
263
|
provider = "prisma-client-js"
|
|
@@ -172,6 +276,7 @@ generator express {
|
|
|
172
276
|
Run `npx prisma generate` to emit both the express routes and the guard artifacts.
|
|
173
277
|
|
|
174
278
|
Extend PrismaClient with the guard extension and attach it to requests:
|
|
279
|
+
|
|
175
280
|
```ts
|
|
176
281
|
import express from 'express'
|
|
177
282
|
import { PrismaClient } from '@prisma/client'
|
|
@@ -209,7 +314,7 @@ If prisma-guard is not installed or the client is not extended with the guard ex
|
|
|
209
314
|
|
|
210
315
|
Each operation config accepts an optional `shape` property. When present, the generated handler:
|
|
211
316
|
|
|
212
|
-
1. Stores the shape on `res.locals.guardShape
|
|
317
|
+
1. Stores the shape on the request context via middleware (Express: `res.locals.guardShape`, Fastify: `request.guardShape`)
|
|
213
318
|
2. Resolves the caller from `config.guard.resolveVariant(req)`, then from the configured header (default `x-api-variant`), falling back to `undefined`
|
|
214
319
|
3. Calls `prisma.model.guard(shape, caller).method(args)` instead of `prisma.model.method(args)`
|
|
215
320
|
|
|
@@ -218,6 +323,7 @@ When `shape` is absent, the handler calls Prisma directly with no guard enforcem
|
|
|
218
323
|
### Single shape per operation
|
|
219
324
|
|
|
220
325
|
A single shape object restricts what the client can do on that operation. No caller routing is needed.
|
|
326
|
+
|
|
221
327
|
```ts
|
|
222
328
|
const userConfig = {
|
|
223
329
|
findMany: {
|
|
@@ -259,6 +365,7 @@ In this example:
|
|
|
259
365
|
### Shape value types in data
|
|
260
366
|
|
|
261
367
|
Each field in a `data` shape accepts one of four value types:
|
|
368
|
+
|
|
262
369
|
```ts
|
|
263
370
|
import { force } from 'prisma-guard'
|
|
264
371
|
|
|
@@ -285,6 +392,7 @@ const config = {
|
|
|
285
392
|
### Named shapes (variant-based routing)
|
|
286
393
|
|
|
287
394
|
Different API consumers often need different shapes for the same operation. Named shapes use a caller value to route to the correct shape.
|
|
395
|
+
|
|
288
396
|
```ts
|
|
289
397
|
const userConfig = {
|
|
290
398
|
findMany: {
|
|
@@ -320,6 +428,7 @@ app.use('/', UserRouter(userConfig))
|
|
|
320
428
|
```
|
|
321
429
|
|
|
322
430
|
The client sends the variant in the configured header:
|
|
431
|
+
|
|
323
432
|
```ts
|
|
324
433
|
// Admin frontend
|
|
325
434
|
fetch('/user', {
|
|
@@ -337,7 +446,9 @@ If the caller is missing or doesn't match any key, the request is rejected with
|
|
|
337
446
|
### Custom caller resolution
|
|
338
447
|
|
|
339
448
|
Use `resolveVariant` for caller logic beyond a simple header:
|
|
449
|
+
|
|
340
450
|
```ts
|
|
451
|
+
// Express
|
|
341
452
|
const userConfig = {
|
|
342
453
|
findMany: {
|
|
343
454
|
shape: {
|
|
@@ -354,11 +465,30 @@ const userConfig = {
|
|
|
354
465
|
}
|
|
355
466
|
```
|
|
356
467
|
|
|
468
|
+
```ts
|
|
469
|
+
// Fastify
|
|
470
|
+
const userConfig = {
|
|
471
|
+
findMany: {
|
|
472
|
+
shape: {
|
|
473
|
+
admin: { /* ... */ },
|
|
474
|
+
public: { /* ... */ },
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
guard: {
|
|
478
|
+
resolveVariant: (request) => {
|
|
479
|
+
if (request.user?.role === 'admin') return 'admin'
|
|
480
|
+
return 'public'
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
357
486
|
`resolveVariant` takes priority over the header. If both are configured, the header is checked only when `resolveVariant` returns `undefined`.
|
|
358
487
|
|
|
359
488
|
### Parameterized caller patterns
|
|
360
489
|
|
|
361
490
|
Caller keys support parameterized path patterns:
|
|
491
|
+
|
|
362
492
|
```ts
|
|
363
493
|
const projectConfig = {
|
|
364
494
|
update: {
|
|
@@ -380,6 +510,7 @@ const projectConfig = {
|
|
|
380
510
|
```
|
|
381
511
|
|
|
382
512
|
The client sends the full path:
|
|
513
|
+
|
|
383
514
|
```ts
|
|
384
515
|
fetch('/project', {
|
|
385
516
|
method: 'PUT',
|
|
@@ -399,6 +530,7 @@ Exact matches are checked first. Parameters (`:id`) are routing-only and are not
|
|
|
399
530
|
### Forced where conditions
|
|
400
531
|
|
|
401
532
|
Literal values in `where` shapes are forced server-side and cannot be overridden by the client:
|
|
533
|
+
|
|
402
534
|
```ts
|
|
403
535
|
import { force } from 'prisma-guard'
|
|
404
536
|
|
|
@@ -421,9 +553,9 @@ A request with `{ where: { title: { contains: 'demo' } } }` produces:
|
|
|
421
553
|
|
|
422
554
|
```
|
|
423
555
|
WHERE status = 'published'
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
556
|
+
AND isDeleted = false
|
|
557
|
+
AND isActive = true
|
|
558
|
+
AND title LIKE '%demo%'
|
|
427
559
|
```
|
|
428
560
|
|
|
429
561
|
The client cannot bypass the forced conditions.
|
|
@@ -431,6 +563,7 @@ The client cannot bypass the forced conditions.
|
|
|
431
563
|
### Logical combinators (AND, OR, NOT)
|
|
432
564
|
|
|
433
565
|
Where shapes support `AND`, `OR`, and `NOT`. The combinator value defines which fields are allowed inside it:
|
|
566
|
+
|
|
434
567
|
```ts
|
|
435
568
|
const config = {
|
|
436
569
|
findMany: {
|
|
@@ -449,6 +582,7 @@ const config = {
|
|
|
449
582
|
```
|
|
450
583
|
|
|
451
584
|
Client sends:
|
|
585
|
+
|
|
452
586
|
```json
|
|
453
587
|
{
|
|
454
588
|
"where": {
|
|
@@ -465,6 +599,7 @@ The forced `status = 'published'` is always merged as an AND condition. Forced v
|
|
|
465
599
|
### Relation filters in where
|
|
466
600
|
|
|
467
601
|
Where shapes support relation-level filters. To-many relations use `some`, `every`, `none`. To-one relations use `is`, `isNot`.
|
|
602
|
+
|
|
468
603
|
```ts
|
|
469
604
|
const userConfig = {
|
|
470
605
|
findMany: {
|
|
@@ -488,6 +623,7 @@ The client can filter by `title` inside the relation, but `published = true` is
|
|
|
488
623
|
### Select and include in shapes
|
|
489
624
|
|
|
490
625
|
Shapes can restrict which response fields and relations the client may request:
|
|
626
|
+
|
|
491
627
|
```ts
|
|
492
628
|
const userConfig = {
|
|
493
629
|
findMany: {
|
|
@@ -521,6 +657,7 @@ This means a single shape declaration like the example above defines both the se
|
|
|
521
657
|
### Nested include with forced where and pagination
|
|
522
658
|
|
|
523
659
|
Nested includes on to-many relations support `where`, `orderBy`, `cursor`, `take`, and `skip`:
|
|
660
|
+
|
|
524
661
|
```ts
|
|
525
662
|
import { force } from 'prisma-guard'
|
|
526
663
|
|
|
@@ -552,6 +689,7 @@ const userConfig = {
|
|
|
552
689
|
### Mutation return projection
|
|
553
690
|
|
|
554
691
|
Write operations that return records (`create`, `update`, `upsert`, `delete`, `createManyAndReturn`, `updateManyAndReturn`) support `select` and `include` in the shape:
|
|
692
|
+
|
|
555
693
|
```ts
|
|
556
694
|
const userConfig = {
|
|
557
695
|
create: {
|
|
@@ -583,6 +721,7 @@ For mutations, projection shapes only validate and constrain client-requested pr
|
|
|
583
721
|
### Upsert
|
|
584
722
|
|
|
585
723
|
Upsert uses `create` and `update` shape keys instead of `data`:
|
|
724
|
+
|
|
586
725
|
```ts
|
|
587
726
|
import { force } from 'prisma-guard'
|
|
588
727
|
|
|
@@ -609,6 +748,7 @@ All three (`where`, `create`, `update`) are required. Using `data` instead of `c
|
|
|
609
748
|
### Bulk mutation safety
|
|
610
749
|
|
|
611
750
|
`updateMany`, `updateManyAndReturn`, and `deleteMany` require `where` in the shape:
|
|
751
|
+
|
|
612
752
|
```ts
|
|
613
753
|
const userConfig = {
|
|
614
754
|
deleteMany: {
|
|
@@ -630,6 +770,7 @@ A shape without `where` on these methods is rejected. Empty resolved where at ru
|
|
|
630
770
|
### Tenant isolation with guard shapes
|
|
631
771
|
|
|
632
772
|
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:
|
|
773
|
+
|
|
633
774
|
```prisma
|
|
634
775
|
/// @scope-root
|
|
635
776
|
model Tenant {
|
|
@@ -645,6 +786,7 @@ model Project {
|
|
|
645
786
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
646
787
|
}
|
|
647
788
|
```
|
|
789
|
+
|
|
648
790
|
```ts
|
|
649
791
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
650
792
|
import { guard } from './generated/guard/client'
|
|
@@ -710,6 +852,7 @@ Guard errors are mapped to HTTP status codes by the generated error-handling mid
|
|
|
710
852
|
All errors return `{ "message": "..." }` in the response body.
|
|
711
853
|
|
|
712
854
|
### Complete guard example
|
|
855
|
+
|
|
713
856
|
```ts
|
|
714
857
|
import express from 'express'
|
|
715
858
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
@@ -811,6 +954,7 @@ In this setup:
|
|
|
811
954
|
## Request body format
|
|
812
955
|
|
|
813
956
|
All write operations accept the full Prisma args object as the JSON request body. The body must be a JSON object — sending `null`, arrays, or other non-object values returns 400.
|
|
957
|
+
|
|
814
958
|
```ts
|
|
815
959
|
// Create
|
|
816
960
|
{ "data": { "name": "Alice", "email": "alice@example.com" }, "select": { "id": true } }
|
|
@@ -836,6 +980,7 @@ Write operations that return records (create, update, delete, upsert, createMany
|
|
|
836
980
|
`deleteMany`, `updateMany`, and `updateManyAndReturn` require a `where` field in the request body. Requests without `where` are rejected with 400 to prevent accidental mass operations. Sending `{ "where": {} }` is valid and matches all records — this protection catches accidental omission, not intentional broad operations.
|
|
837
981
|
|
|
838
982
|
## Query encoding (client side)
|
|
983
|
+
|
|
839
984
|
```ts
|
|
840
985
|
import { encodeQueryParams } from './generated/client/encodeQueryParams'
|
|
841
986
|
|
|
@@ -876,27 +1021,32 @@ On the client side, `encodeQueryParams` handles BigInt serialization automatical
|
|
|
876
1021
|
|
|
877
1022
|
The `hasMore` field is reliable for forward offset pagination (`skip` + `take`) only. When using cursor-based pagination or negative `take` (backward pagination), `hasMore` may be inaccurate.
|
|
878
1023
|
|
|
879
|
-
|
|
1024
|
+
When `distinct` is used with `findManyPaginated`, the total count is determined by executing a distinct query up to the configured limit (default: 100,000 rows). If the number of distinct values exceeds this limit, the total falls back to an approximate non-distinct count. When guard shapes are active, the distinct counting query respects the guard's where restrictions.
|
|
1025
|
+
|
|
1026
|
+
Configure default and maximum page sizes and the distinct count limit:
|
|
1027
|
+
|
|
880
1028
|
```ts
|
|
881
1029
|
UserRouter({
|
|
882
1030
|
findManyPaginated: {},
|
|
883
1031
|
pagination: {
|
|
884
1032
|
defaultLimit: 20,
|
|
885
1033
|
maxLimit: 100,
|
|
1034
|
+
distinctCountLimit: 50000,
|
|
886
1035
|
},
|
|
887
1036
|
})
|
|
888
1037
|
```
|
|
889
1038
|
|
|
890
|
-
`pagination.defaultLimit` is applied when the client omits `take`. `pagination.maxLimit` caps `take` by absolute value.
|
|
1039
|
+
`pagination.defaultLimit` is applied when the client omits `take`. `pagination.maxLimit` caps `take` by absolute value. `pagination.distinctCountLimit` overrides the default 100,000 row threshold for distinct count estimation. All settings apply to `findMany` and `findManyPaginated`.
|
|
891
1040
|
|
|
892
1041
|
## Error handling
|
|
893
1042
|
|
|
894
1043
|
All errors are returned as JSON with a `message` field:
|
|
1044
|
+
|
|
895
1045
|
```json
|
|
896
1046
|
{ "message": "Unique constraint violation" }
|
|
897
1047
|
```
|
|
898
1048
|
|
|
899
|
-
Each generated router installs an error-handling middleware that normalizes errors. Prisma error codes are mapped to appropriate HTTP status codes. Guard errors are mapped as follows: `ShapeError` and `CallerError` → 400, `PolicyError` → 403.
|
|
1049
|
+
Each generated router installs an error-handling middleware (Express) or error handler (Fastify) that normalizes errors. Prisma error codes are mapped to appropriate HTTP status codes. Guard errors are mapped as follows: `ShapeError` and `CallerError` → 400, `PolicyError` → 403.
|
|
900
1050
|
|
|
901
1051
|
| Status | Description |
|
|
902
1052
|
| ------ | ------------------------------------------ |
|
|
@@ -927,7 +1077,10 @@ Actual paths depend on `customUrlPrefix` and `addModelPrefix` configuration.
|
|
|
927
1077
|
|
|
928
1078
|
### Manual (generated helpers, require mounting)
|
|
929
1079
|
|
|
930
|
-
The generator produces helper functions that you mount yourself. Pass the same config object used for the router to keep docs and runtime in sync
|
|
1080
|
+
The generator produces helper functions that you mount yourself. Pass the same config object used for the router to keep docs and runtime in sync.
|
|
1081
|
+
|
|
1082
|
+
#### Express
|
|
1083
|
+
|
|
931
1084
|
```ts
|
|
932
1085
|
import {
|
|
933
1086
|
generateCombinedDocs,
|
|
@@ -964,6 +1117,46 @@ app.get(
|
|
|
964
1117
|
)
|
|
965
1118
|
```
|
|
966
1119
|
|
|
1120
|
+
#### Fastify
|
|
1121
|
+
|
|
1122
|
+
```ts
|
|
1123
|
+
import {
|
|
1124
|
+
generateCombinedDocs,
|
|
1125
|
+
registerModelDocs,
|
|
1126
|
+
} from './generated/combinedDocs'
|
|
1127
|
+
|
|
1128
|
+
const userConfig = {
|
|
1129
|
+
findMany: { before: [async (request, reply) => { /* auth */ }] },
|
|
1130
|
+
create: {},
|
|
1131
|
+
findUnique: {},
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const postConfig = {
|
|
1135
|
+
enableAll: true,
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
fastify.register(async (instance) => {
|
|
1139
|
+
await UserRoutes(instance, userConfig)
|
|
1140
|
+
})
|
|
1141
|
+
|
|
1142
|
+
fastify.register(async (instance) => {
|
|
1143
|
+
await PostRoutes(instance, postConfig)
|
|
1144
|
+
})
|
|
1145
|
+
|
|
1146
|
+
registerModelDocs(fastify, '/docs', {
|
|
1147
|
+
User: userConfig,
|
|
1148
|
+
Post: postConfig,
|
|
1149
|
+
})
|
|
1150
|
+
|
|
1151
|
+
fastify.get('/docs', generateCombinedDocs({
|
|
1152
|
+
title: 'My API',
|
|
1153
|
+
modelConfigs: {
|
|
1154
|
+
User: userConfig,
|
|
1155
|
+
Post: postConfig,
|
|
1156
|
+
},
|
|
1157
|
+
}))
|
|
1158
|
+
```
|
|
1159
|
+
|
|
967
1160
|
| Endpoint | Description |
|
|
968
1161
|
| ----------------------------- | ----------------------- |
|
|
969
1162
|
| `/docs` | Combined index page |
|
|
@@ -978,6 +1171,7 @@ Disable in production via `NODE_ENV=production` or `DISABLE_OPENAPI=true`. Overr
|
|
|
978
1171
|
### Spec paths and mount prefixes
|
|
979
1172
|
|
|
980
1173
|
Use `specBasePath` to set the base path for OpenAPI spec and docs independently of route registration:
|
|
1174
|
+
|
|
981
1175
|
```ts
|
|
982
1176
|
const userConfig = {
|
|
983
1177
|
enableAll: true,
|
|
@@ -993,7 +1187,8 @@ When `specBasePath` is not set, `customUrlPrefix` is used for both runtime route
|
|
|
993
1187
|
|
|
994
1188
|
When `prisma-sql` is installed, the generated handlers automatically attempt to use its `speedExtension` for optimized SQL execution. The extension activates only when a database connector is provided on the request object.
|
|
995
1189
|
|
|
996
|
-
Set `req.postgres` or `req.sqlite` in your middleware to activate the extension:
|
|
1190
|
+
Set `req.postgres` or `req.sqlite` (Express) / `request.postgres` or `request.sqlite` (Fastify) in your middleware to activate the extension:
|
|
1191
|
+
|
|
997
1192
|
```ts
|
|
998
1193
|
import { PrismaClient } from '@prisma/client'
|
|
999
1194
|
import postgres from 'postgres'
|
|
@@ -1001,18 +1196,25 @@ import postgres from 'postgres'
|
|
|
1001
1196
|
const prisma = new PrismaClient()
|
|
1002
1197
|
const sql = postgres(process.env.DATABASE_URL!)
|
|
1003
1198
|
|
|
1199
|
+
// Express
|
|
1004
1200
|
app.use((req, res, next) => {
|
|
1005
1201
|
req.prisma = prisma
|
|
1006
1202
|
req.postgres = sql
|
|
1007
1203
|
next()
|
|
1008
1204
|
})
|
|
1205
|
+
|
|
1206
|
+
// Fastify
|
|
1207
|
+
fastify.addHook('onRequest', async (request) => {
|
|
1208
|
+
request.prisma = prisma
|
|
1209
|
+
request.postgres = sql
|
|
1210
|
+
})
|
|
1009
1211
|
```
|
|
1010
1212
|
|
|
1011
1213
|
Without a connector on the request, the handlers use the standard PrismaClient. Set `DEBUG=true` in the environment to enable prisma-sql debug logging.
|
|
1012
1214
|
|
|
1013
1215
|
## Query parameter parsing
|
|
1014
1216
|
|
|
1015
|
-
GET query values are parsed server-side. Strings starting with `{`, `[`, or `"` are JSON-parsed. The strings `true`, `false`, `null` are converted to their JS equivalents. Numeric conversion only
|
|
1217
|
+
GET query values are parsed server-side. Strings starting with `{`, `[`, or `"` are JSON-parsed. The strings `true`, `false`, `null` are converted to their JS equivalents. Numeric conversion applies only to `take` and `skip`, and only when the value is a valid integer (e.g., `"10"` is parsed, `"10.5"` and `""` are not). Use `encodeQueryParams` on the client side to avoid encoding issues.
|
|
1016
1218
|
|
|
1017
1219
|
## Router schema
|
|
1018
1220
|
|
|
@@ -1042,6 +1244,7 @@ Paths shown are relative suffixes. Actual paths include the model prefix (e.g.,
|
|
|
1042
1244
|
## Skipping models
|
|
1043
1245
|
|
|
1044
1246
|
Add `/// generator off` to a model's documentation to skip generation:
|
|
1247
|
+
|
|
1045
1248
|
```prisma
|
|
1046
1249
|
/// generator off
|
|
1047
1250
|
model InternalLog {
|
|
@@ -1050,6 +1253,9 @@ model InternalLog {
|
|
|
1050
1253
|
```
|
|
1051
1254
|
|
|
1052
1255
|
## Configuration
|
|
1256
|
+
|
|
1257
|
+
### Express
|
|
1258
|
+
|
|
1053
1259
|
```ts
|
|
1054
1260
|
interface RouteConfig {
|
|
1055
1261
|
enableAll?: boolean
|
|
@@ -1076,6 +1282,7 @@ interface RouteConfig {
|
|
|
1076
1282
|
pagination?: {
|
|
1077
1283
|
defaultLimit?: number
|
|
1078
1284
|
maxLimit?: number
|
|
1285
|
+
distinctCountLimit?: number // default: 100000
|
|
1079
1286
|
}
|
|
1080
1287
|
|
|
1081
1288
|
// per-operation config
|
|
@@ -1114,11 +1321,33 @@ interface QueryBuilderConfig {
|
|
|
1114
1321
|
}
|
|
1115
1322
|
```
|
|
1116
1323
|
|
|
1324
|
+
### Fastify
|
|
1325
|
+
|
|
1326
|
+
The Fastify config is identical except for hook and resolver types:
|
|
1327
|
+
|
|
1328
|
+
```ts
|
|
1329
|
+
interface OperationConfig {
|
|
1330
|
+
before?: FastifyHookHandler[]
|
|
1331
|
+
after?: FastifyHookHandler[]
|
|
1332
|
+
shape?: Record<string, any>
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
type FastifyHookHandler = (
|
|
1336
|
+
request: FastifyRequest,
|
|
1337
|
+
reply: FastifyReply,
|
|
1338
|
+
) => Promise<void> | void
|
|
1339
|
+
```
|
|
1340
|
+
|
|
1341
|
+
The `guard.resolveVariant` callback receives `FastifyRequest` instead of `Request`.
|
|
1342
|
+
|
|
1343
|
+
### Shared options
|
|
1344
|
+
|
|
1117
1345
|
`customUrlPrefix` is normalized to ensure a leading slash and strip trailing slashes.
|
|
1118
1346
|
|
|
1119
1347
|
`specBasePath` controls the base path used in OpenAPI spec paths and docs examples, independent of `customUrlPrefix`.
|
|
1120
1348
|
|
|
1121
1349
|
`openApiServers` sets the `servers` array in the OpenAPI spec:
|
|
1350
|
+
|
|
1122
1351
|
```ts
|
|
1123
1352
|
UserRouter({
|
|
1124
1353
|
enableAll: true,
|
|
@@ -1129,6 +1358,7 @@ UserRouter({
|
|
|
1129
1358
|
```
|
|
1130
1359
|
|
|
1131
1360
|
`openApiSecuritySchemes` and `openApiSecurity` set the security configuration in the OpenAPI spec:
|
|
1361
|
+
|
|
1132
1362
|
```ts
|
|
1133
1363
|
UserRouter({
|
|
1134
1364
|
enableAll: true,
|
package/dist/constants.d.ts
CHANGED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateFastifyHandler = generateFastifyHandler;
|
|
4
|
+
const READ_OPS = [
|
|
5
|
+
'findMany',
|
|
6
|
+
'findFirst',
|
|
7
|
+
'findFirstOrThrow',
|
|
8
|
+
'findUnique',
|
|
9
|
+
'findUniqueOrThrow',
|
|
10
|
+
'findManyPaginated',
|
|
11
|
+
'aggregate',
|
|
12
|
+
'count',
|
|
13
|
+
'groupBy',
|
|
14
|
+
];
|
|
15
|
+
const WRITE_OPS = [
|
|
16
|
+
'create',
|
|
17
|
+
'createMany',
|
|
18
|
+
'createManyAndReturn',
|
|
19
|
+
'update',
|
|
20
|
+
'updateMany',
|
|
21
|
+
'updateManyAndReturn',
|
|
22
|
+
'upsert',
|
|
23
|
+
'delete',
|
|
24
|
+
'deleteMany',
|
|
25
|
+
];
|
|
26
|
+
const CREATED_OPS = new Set([
|
|
27
|
+
'create',
|
|
28
|
+
'createMany',
|
|
29
|
+
'createManyAndReturn',
|
|
30
|
+
]);
|
|
31
|
+
function generateFastifyHandler(options) {
|
|
32
|
+
const modelName = options.model.name;
|
|
33
|
+
const readHandlers = READ_OPS.map((op) => {
|
|
34
|
+
const exportName = `${modelName}${op.charAt(0).toUpperCase() + op.slice(1)}`;
|
|
35
|
+
return `
|
|
36
|
+
export async function ${exportName}(
|
|
37
|
+
request: FastifyRequest,
|
|
38
|
+
_reply: FastifyReply,
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
const data = await core.${op}(buildContext(request))
|
|
41
|
+
;(request as any).resultData = data
|
|
42
|
+
}`;
|
|
43
|
+
}).join('\n');
|
|
44
|
+
const writeHandlers = WRITE_OPS.map((op) => {
|
|
45
|
+
const exportName = `${modelName}${op.charAt(0).toUpperCase() + op.slice(1)}`;
|
|
46
|
+
const statusCode = CREATED_OPS.has(op) ? 201 : 200;
|
|
47
|
+
return `
|
|
48
|
+
export async function ${exportName}(
|
|
49
|
+
request: FastifyRequest,
|
|
50
|
+
_reply: FastifyReply,
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
const data = await core.${op}(buildContext(request))
|
|
53
|
+
;(request as any).resultData = data
|
|
54
|
+
;(request as any).resultStatus = ${statusCode}
|
|
55
|
+
}`;
|
|
56
|
+
}).join('\n');
|
|
57
|
+
return `import type { FastifyRequest, FastifyReply } from 'fastify'
|
|
58
|
+
import * as core from './${modelName}Core.js'
|
|
59
|
+
import type { OperationContext } from '../operationRuntime.js'
|
|
60
|
+
|
|
61
|
+
function buildContext(request: FastifyRequest): OperationContext {
|
|
62
|
+
const req = request as any
|
|
63
|
+
return {
|
|
64
|
+
prisma: req.prisma,
|
|
65
|
+
postgres: req.postgres,
|
|
66
|
+
sqlite: req.sqlite,
|
|
67
|
+
parsedQuery: req.parsedQuery,
|
|
68
|
+
body: request.body,
|
|
69
|
+
guardShape: req.guardShape,
|
|
70
|
+
guardCaller: req.guardCaller,
|
|
71
|
+
paginationConfig: req.routeConfig?.pagination,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
${readHandlers}
|
|
75
|
+
${writeHandlers}
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=generateFastifyHandler.js.map
|