prisma-generator-express 1.39.0 → 1.41.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 +842 -89
- package/dist/constants.d.ts +1 -1
- package/dist/generators/generateHonoHandler.d.ts +4 -0
- package/dist/generators/generateHonoHandler.js +94 -0
- package/dist/generators/generateHonoHandler.js.map +1 -0
- package/dist/generators/generateOperationCore.d.ts +3 -1
- package/dist/generators/generateOperationCore.js +266 -160
- package/dist/generators/generateOperationCore.js.map +1 -1
- package/dist/generators/generateRouteConfigType.d.ts +3 -1
- package/dist/generators/generateRouteConfigType.js +36 -31
- package/dist/generators/generateRouteConfigType.js.map +1 -1
- package/dist/generators/generateRouter.d.ts +4 -2
- package/dist/generators/generateRouter.js +130 -119
- package/dist/generators/generateRouter.js.map +1 -1
- package/dist/generators/generateRouterFastify.d.ts +3 -1
- package/dist/generators/generateRouterFastify.js +12 -10
- package/dist/generators/generateRouterFastify.js.map +1 -1
- package/dist/generators/generateRouterHono.d.ts +8 -0
- package/dist/generators/generateRouterHono.js +371 -0
- package/dist/generators/generateRouterHono.js.map +1 -0
- package/dist/generators/generateUnifiedDocs.d.ts +2 -1
- package/dist/generators/generateUnifiedDocs.js +55 -7
- package/dist/generators/generateUnifiedDocs.js.map +1 -1
- package/dist/generators/generateUnifiedScalarUI.js +55 -2
- package/dist/generators/generateUnifiedScalarUI.js.map +1 -1
- package/dist/index.js +32 -20
- package/dist/index.js.map +1 -1
- package/dist/utils/copyFiles.d.ts +2 -1
- package/dist/utils/copyFiles.js +39 -34
- package/dist/utils/copyFiles.js.map +1 -1
- package/dist/utils/importExt.d.ts +2 -0
- package/dist/utils/importExt.js +11 -0
- package/dist/utils/importExt.js.map +1 -0
- package/dist/utils/resolveImportStyle.d.ts +3 -0
- package/dist/utils/resolveImportStyle.js +211 -0
- package/dist/utils/resolveImportStyle.js.map +1 -0
- package/dist/utils/writeFileSafely.js +6 -9
- package/dist/utils/writeFileSafely.js.map +1 -1
- package/package.json +4 -2
- package/src/constants.ts +1 -1
- package/src/copy/routeConfig.express.ts +39 -5
- package/src/copy/routeConfig.fastify.ts +8 -4
- package/src/copy/routeConfig.hono.ts +25 -0
- package/src/copy/routeConfig.ts +42 -2
- package/src/generators/generateHonoHandler.ts +104 -0
- package/src/generators/generateOperationCore.ts +273 -169
- package/src/generators/generateRouteConfigType.ts +42 -35
- package/src/generators/generateRouter.ts +134 -121
- package/src/generators/generateRouterFastify.ts +14 -9
- package/src/generators/generateRouterHono.ts +386 -0
- package/src/generators/generateUnifiedDocs.ts +59 -6
- package/src/generators/generateUnifiedScalarUI.ts +56 -2
- package/src/index.ts +39 -44
- package/src/utils/copyFiles.ts +45 -45
- package/src/utils/importExt.ts +7 -0
- package/src/utils/resolveImportStyle.ts +187 -0
- package/src/utils/writeFileSafely.ts +6 -22
package/README.md
CHANGED
|
@@ -5,32 +5,36 @@
|
|
|
5
5
|
[](https://codecov.io/gh/multipliedtwice/prisma-generator-express)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
Prisma generator that creates Express or
|
|
8
|
+
Prisma generator that creates Express, Fastify, or Hono CRUD API routes with OpenAPI documentation from your Prisma schema.
|
|
9
9
|
|
|
10
10
|
Running `npx prisma generate` produces:
|
|
11
11
|
|
|
12
12
|
- Handler functions for all Prisma operations (findMany, create, update, delete, etc.)
|
|
13
13
|
- Router generator with middleware support (before/after hooks per operation)
|
|
14
14
|
- POST read endpoints for all read operations (for complex queries exceeding URL length limits)
|
|
15
|
+
- Express-only progressive read streaming over Server-Sent Events (SSE) for staged page-level responses
|
|
15
16
|
- OpenAPI 3.1 spec (JSON and YAML endpoints registered automatically per router)
|
|
16
17
|
- Documentation helpers for contract view and Scalar UI (require manual mounting)
|
|
17
18
|
- Client-side query parameter encoder
|
|
18
19
|
- Guard/variant shape enforcement via prisma-guard integration
|
|
19
20
|
|
|
20
|
-
Supports
|
|
21
|
+
Supports **Express**, **Fastify**, and **Hono** targets via the `target` configuration option.
|
|
21
22
|
|
|
22
23
|
## Table of contents
|
|
23
24
|
|
|
24
25
|
- [Compatibility](#compatibility)
|
|
25
26
|
- [Installation](#installation)
|
|
26
27
|
- [Setup](#setup)
|
|
28
|
+
- [Path casing in generated endpoints](#path-casing-in-generated-endpoints)
|
|
27
29
|
- [Usage (Express)](#usage-express)
|
|
28
30
|
- [Usage (Fastify)](#usage-fastify)
|
|
31
|
+
- [Usage (Hono)](#usage-hono)
|
|
29
32
|
- [Selective routes with middleware](#selective-routes-with-middleware)
|
|
30
33
|
- [Guard shapes (prisma-guard integration)](#guard-shapes-prisma-guard-integration)
|
|
31
34
|
- [Request body format](#request-body-format)
|
|
32
35
|
- [Query encoding (client side)](#query-encoding-client-side)
|
|
33
36
|
- [POST read endpoints](#post-read-endpoints)
|
|
37
|
+
- [Progressive Endpoint Composition (Express SSE)](#progressive-endpoint-composition-express-sse)
|
|
34
38
|
- [Response shaping: select, include, omit](#response-shaping-select-include-omit)
|
|
35
39
|
- [BigInt and Decimal handling](#bigint-and-decimal-handling)
|
|
36
40
|
- [Pagination](#pagination)
|
|
@@ -64,6 +68,11 @@ Some operations require newer versions:
|
|
|
64
68
|
| --------- | ------------ | ---------------- |
|
|
65
69
|
| Express | `"express"` | `express.Router()` factory function per model |
|
|
66
70
|
| Fastify | `"fastify"` | Fastify plugin function per model |
|
|
71
|
+
| Hono | `"hono"` | `Hono` instance factory function per model |
|
|
72
|
+
|
|
73
|
+
The Hono target v1 is tested on Node.js runtimes only. See [Cloudflare Workers and edge runtimes](#cloudflare-workers-and-edge-runtimes).
|
|
74
|
+
|
|
75
|
+
Progressive Endpoint Composition over Server-Sent Events is currently supported by the Express target only. Fastify and Hono continue to support normal JSON read and write routes.
|
|
67
76
|
|
|
68
77
|
### Database provider support
|
|
69
78
|
|
|
@@ -95,12 +104,18 @@ Peer dependencies for Fastify:
|
|
|
95
104
|
npm install @prisma/client fastify
|
|
96
105
|
```
|
|
97
106
|
|
|
107
|
+
Peer dependencies for Hono:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npm install @prisma/client hono
|
|
111
|
+
```
|
|
112
|
+
|
|
98
113
|
Optional peer dependencies:
|
|
99
114
|
|
|
100
115
|
```bash
|
|
101
116
|
npm install prisma-sql # SQL optimization
|
|
102
117
|
npm install prisma-guard zod # Guard shape enforcement
|
|
103
|
-
npm install prisma-query-builder-ui # Visual query playground
|
|
118
|
+
npm install prisma-query-builder-ui # Visual query playground (Express/Fastify only — not auto-started for Hono)
|
|
104
119
|
```
|
|
105
120
|
|
|
106
121
|
## Setup
|
|
@@ -117,7 +132,7 @@ generator express {
|
|
|
117
132
|
}
|
|
118
133
|
```
|
|
119
134
|
|
|
120
|
-
To
|
|
135
|
+
To target Fastify or Hono, set the `target` config:
|
|
121
136
|
|
|
122
137
|
```prisma
|
|
123
138
|
generator express {
|
|
@@ -126,7 +141,14 @@ generator express {
|
|
|
126
141
|
}
|
|
127
142
|
```
|
|
128
143
|
|
|
129
|
-
|
|
144
|
+
```prisma
|
|
145
|
+
generator express {
|
|
146
|
+
provider = "prisma-generator-express"
|
|
147
|
+
target = "hono"
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Valid `target` values are `"express"` (default), `"fastify"`, and `"hono"`.
|
|
130
152
|
|
|
131
153
|
The generator detects the Prisma client generator automatically. All standard provider values are supported: `prisma-client-js`, `@prisma/client`, and `prisma-client`.
|
|
132
154
|
|
|
@@ -136,6 +158,26 @@ Generate:
|
|
|
136
158
|
npx prisma generate
|
|
137
159
|
```
|
|
138
160
|
|
|
161
|
+
## Path casing in generated endpoints
|
|
162
|
+
|
|
163
|
+
Model names are converted to **flat lowercase** in URL paths. There is no kebab-case or snake_case conversion — the model name is lowercased character by character.
|
|
164
|
+
|
|
165
|
+
| Model name | URL path |
|
|
166
|
+
| ----------------- | -------------------- |
|
|
167
|
+
| `User` | `/user` |
|
|
168
|
+
| `BlogPost` | `/blogpost` |
|
|
169
|
+
| `OrderItem` | `/orderitem` |
|
|
170
|
+
| `INVOICE_RECORDS` | `/invoice_records` |
|
|
171
|
+
| `apiKey` | `/apikey` |
|
|
172
|
+
|
|
173
|
+
Underscores in model names are preserved. Camel-case word boundaries are not preserved.
|
|
174
|
+
|
|
175
|
+
Throughout this README, `{modelname}` (lowercase) represents the converted path segment. For example, the path `/{modelname}/first` refers to `/user/first` for a `User` model, or `/blogpost/first` for a `BlogPost` model.
|
|
176
|
+
|
|
177
|
+
The generated directory structure preserves the original model casing — e.g. `generated/BlogPost/BlogPostRouter.ts` — but the runtime URL is `/blogpost`.
|
|
178
|
+
|
|
179
|
+
To remove the model prefix entirely, set `addModelPrefix: false` in the route config. To replace it with a custom prefix, use `customUrlPrefix`.
|
|
180
|
+
|
|
139
181
|
## Usage (Express)
|
|
140
182
|
|
|
141
183
|
```ts
|
|
@@ -146,6 +188,8 @@ import { UserRouter } from './generated/User/UserRouter'
|
|
|
146
188
|
const prisma = new PrismaClient()
|
|
147
189
|
const app = express()
|
|
148
190
|
|
|
191
|
+
app.use(express.json())
|
|
192
|
+
|
|
149
193
|
app.use((req, res, next) => {
|
|
150
194
|
req.prisma = prisma
|
|
151
195
|
next()
|
|
@@ -162,6 +206,8 @@ app.listen(3000, () => {
|
|
|
162
206
|
})
|
|
163
207
|
```
|
|
164
208
|
|
|
209
|
+
`express.json()` is required because write endpoints (`create`, `update`, `delete`, `upsert`) and POST read endpoints accept JSON request bodies.
|
|
210
|
+
|
|
165
211
|
## Usage (Fastify)
|
|
166
212
|
|
|
167
213
|
When `target = "fastify"`, each model produces a Fastify plugin function instead of an Express router.
|
|
@@ -195,16 +241,138 @@ fastify.listen({ port: 3000 }, () => {
|
|
|
195
241
|
|
|
196
242
|
The generated function signature is `async function ModelRoutes(fastify: FastifyInstance, config?: RouteConfig)`. It registers routes directly on the provided Fastify instance.
|
|
197
243
|
|
|
198
|
-
|
|
244
|
+
## Usage (Hono)
|
|
245
|
+
|
|
246
|
+
When `target = "hono"`, each model produces a function that returns a Hono instance.
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import { Hono } from 'hono'
|
|
250
|
+
import { PrismaClient } from '@prisma/client'
|
|
251
|
+
import { UserRouter } from './generated/User/UserRouter'
|
|
252
|
+
|
|
253
|
+
type Env = {
|
|
254
|
+
Variables: {
|
|
255
|
+
prisma: PrismaClient
|
|
256
|
+
}
|
|
257
|
+
}
|
|
199
258
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
259
|
+
const prisma = new PrismaClient()
|
|
260
|
+
const app = new Hono<Env>()
|
|
261
|
+
|
|
262
|
+
app.use('*', async (c, next) => {
|
|
263
|
+
c.set('prisma', prisma)
|
|
264
|
+
await next()
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
const userConfig = {
|
|
268
|
+
enableAll: true,
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
app.route('/', UserRouter(userConfig))
|
|
272
|
+
|
|
273
|
+
export default app
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
The generated function signature is `UserRouter(config?: RouteConfig): Hono`. Mount with `app.route(prefix, UserRouter(config))`.
|
|
277
|
+
|
|
278
|
+
PrismaClient is injected via `c.set('prisma', prismaInstance)` in middleware that runs before the router. Declare `prisma` (and any optional connectors like `postgres` / `sqlite`) in your Hono app's `Variables` type so TypeScript can verify the injection. The same pattern applies to optional `postgres` / `sqlite` connectors for [prisma-sql integration](#prisma-sql-integration).
|
|
279
|
+
|
|
280
|
+
### Hooks (Hono)
|
|
281
|
+
|
|
282
|
+
Hono hooks are native Hono middleware functions:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import type { HonoHookHandler } from './generated/routeConfig.target'
|
|
286
|
+
|
|
287
|
+
const auth: HonoHookHandler = async (c, next) => {
|
|
288
|
+
const token = c.req.header('authorization')
|
|
289
|
+
if (!token) return c.json({ message: 'Unauthorized' }, 401)
|
|
290
|
+
await next()
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const userConfig = {
|
|
294
|
+
findMany: {
|
|
295
|
+
before: [auth],
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Call `await next()` to continue the chain. Return a `Response` to short-circuit — subsequent hooks, the main handler, and the response middleware will not run.
|
|
301
|
+
|
|
302
|
+
### HTTPException normalization
|
|
303
|
+
|
|
304
|
+
Throwing Hono's `HTTPException` from a hook short-circuits to a JSON error response. The router's `app.onError` catches the exception, preserves the status code, and **normalizes the response body** to `{ "message": err.message }`.
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
import { HTTPException } from 'hono/http-exception'
|
|
308
|
+
|
|
309
|
+
const auth: HonoHookHandler = async (c, next) => {
|
|
310
|
+
const token = c.req.header('authorization')
|
|
311
|
+
if (!token) {
|
|
312
|
+
throw new HTTPException(401, { message: 'Unauthorized' })
|
|
313
|
+
}
|
|
314
|
+
await next()
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Custom response bodies attached to `HTTPException` are **not preserved** — the router always returns `{ message: err.message }` with the exception's status code. If you need a custom response body, return a `Response` directly from the hook instead of throwing.
|
|
319
|
+
|
|
320
|
+
This normalization ensures all errors from generated routes share a single shape, so clients only need to handle one error format.
|
|
321
|
+
|
|
322
|
+
### Cloudflare Workers and edge runtimes
|
|
323
|
+
|
|
324
|
+
The Hono target v1 is tested on Node.js runtimes only. The route layer may be portable to edge runtimes (Cloudflare Workers, Deno Deploy, Vercel Edge), but **production edge support is not guaranteed**. Prisma Client edge usage requires compatible Prisma setup, driver adapters, or Prisma Accelerate / Prisma Postgres depending on the database. `prisma-guard` edge compatibility is unverified.
|
|
325
|
+
|
|
326
|
+
On Cloudflare Workers, you must construct an edge-compatible Prisma client yourself and expose it through your runtime environment. Cloudflare does not provide a built-in Prisma binding — the exact setup depends on your database and Prisma adapter (Prisma Accelerate, `@prisma/adapter-d1`, etc.).
|
|
327
|
+
|
|
328
|
+
A minimal pattern, assuming you've already wired up an edge-compatible client behind a `PRISMA` binding:
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
type Env = {
|
|
332
|
+
Bindings: {
|
|
333
|
+
PRISMA: any
|
|
334
|
+
}
|
|
335
|
+
Variables: {
|
|
336
|
+
prisma: any
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const app = new Hono<Env>()
|
|
341
|
+
|
|
342
|
+
app.use('*', async (c, next) => {
|
|
343
|
+
c.set('prisma', c.env.PRISMA)
|
|
344
|
+
await next()
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
app.route('/', UserRouter({ enableAll: true }))
|
|
348
|
+
|
|
349
|
+
export default app
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Both `Bindings` (what the runtime injects) and `Variables` (what your middleware sets via `c.set`) need to be declared on the app's `Env` type.
|
|
353
|
+
|
|
354
|
+
### Query Builder
|
|
355
|
+
|
|
356
|
+
The Query Builder playground is Node-only and **not auto-started** by the Hono target. The generated `?ui=playground` route can render the playground iframe, but the Hono router does not start the Query Builder server. Start `prisma-query-builder-ui` manually in a separate process and point the config to that server when needed.
|
|
357
|
+
|
|
358
|
+
### Query string differences
|
|
359
|
+
|
|
360
|
+
Hono's `c.req.query()` returns a flat `Record<string, string>` — duplicate query keys collapse to the last value. For example, `?take=10&take=20` becomes `{ take: '20' }`. This differs from Express, which parses `?a=1&a=2` into `{ a: ['1', '2'] }`.
|
|
361
|
+
|
|
362
|
+
The `encodeQueryParams` client utility does not emit duplicate keys, so this only matters for hand-built query strings. All complex Prisma arguments are JSON-encoded into single query values.
|
|
363
|
+
|
|
364
|
+
### Key differences between targets
|
|
365
|
+
|
|
366
|
+
| Aspect | Express | Fastify | Hono |
|
|
367
|
+
| ------ | ------- | ------- | ---- |
|
|
368
|
+
| Generated function | `ModelRouter(config)` returns `express.Router` | `ModelRoutes(fastify, config)` registers on instance | `ModelRouter(config)` returns `Hono` instance |
|
|
369
|
+
| Mounting | `app.use('/', ModelRouter(config))` | `fastify.register(async (i) => { await ModelRoutes(i, config) })` | `app.route('/', ModelRouter(config))` |
|
|
370
|
+
| Hook types | `RequestHandler[]` | `FastifyHookHandler[]` | `HonoHookHandler[]` (native middleware) |
|
|
371
|
+
| Hook signature | `(req, res, next)` | `(request, reply)` | `(c, next)` |
|
|
372
|
+
| Guard resolveVariant | `express.Request` | `FastifyRequest` | Hono `Context` |
|
|
373
|
+
| PrismaClient injection | `req.prisma = prisma` | `request.prisma = prisma` | `c.set('prisma', prisma)` |
|
|
374
|
+
| Error handling | Express error middleware | `setErrorHandler` | `app.onError` |
|
|
375
|
+
| Query Builder auto-start | Yes (Node only) | Yes (Node only) | No (manual start) |
|
|
208
376
|
|
|
209
377
|
## Selective routes with middleware
|
|
210
378
|
|
|
@@ -242,15 +410,33 @@ fastify.register(async (instance) => {
|
|
|
242
410
|
})
|
|
243
411
|
```
|
|
244
412
|
|
|
245
|
-
Only operations listed in the config (or all when `enableAll: true`) are registered. Operations not listed produce no routes.
|
|
246
|
-
|
|
247
413
|
Fastify hooks receive `(request: FastifyRequest, reply: FastifyReply)`. If a hook sends a reply (via `reply.send()`), subsequent hooks and the handler are skipped.
|
|
248
414
|
|
|
415
|
+
### Hono
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
const userConfig = {
|
|
419
|
+
findMany: {
|
|
420
|
+
before: [async (c, next) => { /* auth check */ await next() }],
|
|
421
|
+
},
|
|
422
|
+
create: {
|
|
423
|
+
before: [async (c, next) => { /* auth + validation */ await next() }],
|
|
424
|
+
},
|
|
425
|
+
findUnique: {},
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
app.route('/', UserRouter(userConfig))
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Hono hooks are native middleware functions. Call `await next()` to continue the chain. Return a `Response` (e.g. `c.json({...}, 403)`) or throw `HTTPException` to short-circuit — subsequent hooks and the handler will not run.
|
|
432
|
+
|
|
433
|
+
Only operations listed in the config (or all when `enableAll: true`) are registered. Operations not listed produce no routes.
|
|
434
|
+
|
|
249
435
|
## Guard shapes (prisma-guard integration)
|
|
250
436
|
|
|
251
437
|
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)`.
|
|
252
438
|
|
|
253
|
-
Guard shapes work identically
|
|
439
|
+
Guard shapes work identically across all three targets. The only difference is the type of the `resolveVariant` callback parameter (`Request` for Express, `FastifyRequest` for Fastify, `Context` for Hono).
|
|
254
440
|
|
|
255
441
|
### Guard setup
|
|
256
442
|
|
|
@@ -275,7 +461,7 @@ generator express {
|
|
|
275
461
|
}
|
|
276
462
|
```
|
|
277
463
|
|
|
278
|
-
Run `npx prisma generate` to emit both the
|
|
464
|
+
Run `npx prisma generate` to emit both the routes and the guard artifacts.
|
|
279
465
|
|
|
280
466
|
Extend PrismaClient with the guard extension and attach it to requests:
|
|
281
467
|
|
|
@@ -293,6 +479,8 @@ const prisma = new PrismaClient().$extends(
|
|
|
293
479
|
|
|
294
480
|
const app = express()
|
|
295
481
|
|
|
482
|
+
app.use(express.json())
|
|
483
|
+
|
|
296
484
|
app.use((req, res, next) => {
|
|
297
485
|
req.prisma = prisma
|
|
298
486
|
next()
|
|
@@ -312,16 +500,20 @@ app.use('/', UserRouter({
|
|
|
312
500
|
app.listen(3000)
|
|
313
501
|
```
|
|
314
502
|
|
|
503
|
+
For Fastify and Hono, attach the extended client the same way — via `request.prisma = prisma` (Fastify) or `c.set('prisma', prisma)` (Hono).
|
|
504
|
+
|
|
315
505
|
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().`
|
|
316
506
|
|
|
317
507
|
### How guard integration works
|
|
318
508
|
|
|
319
509
|
Each operation config accepts an optional `shape` property. When present, the generated handler:
|
|
320
510
|
|
|
321
|
-
1. Stores the shape on the request context via middleware (Express: `res.locals.guardShape`, Fastify: `request.guardShape`)
|
|
511
|
+
1. Stores the shape on the request context via middleware (Express: `res.locals.guardShape = shape`, Fastify: `request.guardShape = shape`, Hono: `c.set('guardShape', shape)`)
|
|
322
512
|
2. Resolves the caller from `config.guard.resolveVariant(req)`, then from the configured header (default `x-api-variant`), falling back to `undefined`
|
|
323
513
|
3. Calls `prisma.model.guard(shape, caller).method(args)` instead of `prisma.model.method(args)`
|
|
324
514
|
|
|
515
|
+
The downstream handler reads these values (`res.locals.guardShape`, `request.guardShape`, `c.get('guardShape')`) when constructing the Prisma call.
|
|
516
|
+
|
|
325
517
|
When `shape` is absent, the handler calls Prisma directly with no guard enforcement.
|
|
326
518
|
|
|
327
519
|
Generated route config types treat `shape` as a named shape map. Use `default` for the normal single-shape case, and add other keys only when you need caller-based variants. The runtime still passes the map to `prisma-guard`; the `default` variant is selected when no caller is provided or no variant matches.
|
|
@@ -463,7 +655,7 @@ If the caller is missing or doesn't match any key, the request is rejected with
|
|
|
463
655
|
|
|
464
656
|
### Custom caller resolution
|
|
465
657
|
|
|
466
|
-
Use `resolveVariant` for caller logic beyond a simple header
|
|
658
|
+
Use `resolveVariant` for caller logic beyond a simple header. The callback parameter type depends on the target.
|
|
467
659
|
|
|
468
660
|
```ts
|
|
469
661
|
// Express
|
|
@@ -501,6 +693,27 @@ const userConfig = {
|
|
|
501
693
|
}
|
|
502
694
|
```
|
|
503
695
|
|
|
696
|
+
```ts
|
|
697
|
+
// Hono
|
|
698
|
+
const userConfig = {
|
|
699
|
+
findMany: {
|
|
700
|
+
shape: {
|
|
701
|
+
admin: { /* ... */ },
|
|
702
|
+
public: { /* ... */ },
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
guard: {
|
|
706
|
+
resolveVariant: (c) => {
|
|
707
|
+
const user = c.get('user')
|
|
708
|
+
if (user?.role === 'admin') return 'admin'
|
|
709
|
+
return 'public'
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
}
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
When using `c.get('user')` or other custom context values in TypeScript, add them to the `Variables` type of your Hono app so the call is typed correctly. For example: `Hono<{ Variables: { prisma: PrismaClient; user?: { role: string } } }>`.
|
|
716
|
+
|
|
504
717
|
`resolveVariant` takes priority over the header. If both are configured, the header is checked only when `resolveVariant` returns `undefined`.
|
|
505
718
|
|
|
506
719
|
### Parameterized caller patterns
|
|
@@ -837,6 +1050,8 @@ const prisma = new PrismaClient().$extends(
|
|
|
837
1050
|
}))
|
|
838
1051
|
)
|
|
839
1052
|
|
|
1053
|
+
app.use(express.json())
|
|
1054
|
+
|
|
840
1055
|
app.use((req, res, next) => {
|
|
841
1056
|
const tenantId = req.headers['x-tenant-id'] as string
|
|
842
1057
|
store.run({ tenantId }, () => {
|
|
@@ -883,7 +1098,7 @@ For upsert: `where`, `create`, `update`, `select`, `include`
|
|
|
883
1098
|
|
|
884
1099
|
### Guard error handling
|
|
885
1100
|
|
|
886
|
-
Guard errors are mapped to HTTP status codes by the generated error
|
|
1101
|
+
Guard errors are mapped to HTTP status codes by the generated error handler:
|
|
887
1102
|
|
|
888
1103
|
| Error type | HTTP status | When |
|
|
889
1104
|
| ------------- | ----------- | ----------------------------------------------------------------- |
|
|
@@ -914,6 +1129,8 @@ const prisma = new PrismaClient().$extends(
|
|
|
914
1129
|
|
|
915
1130
|
const app = express()
|
|
916
1131
|
|
|
1132
|
+
app.use(express.json())
|
|
1133
|
+
|
|
917
1134
|
app.use((req, res, next) => {
|
|
918
1135
|
const tenantId = req.headers['x-tenant-id'] as string
|
|
919
1136
|
const role = req.headers['x-role'] as string || 'viewer'
|
|
@@ -1013,6 +1230,8 @@ All write operations accept the full Prisma args object as the JSON request body
|
|
|
1013
1230
|
|
|
1014
1231
|
Write operations that return records (create, update, delete, upsert, createManyAndReturn, updateManyAndReturn) support `select`, `include`, and `omit` in the request body to control the response shape.
|
|
1015
1232
|
|
|
1233
|
+
For Express, mount `express.json()` before the router so request bodies are parsed. For Hono, malformed JSON bodies are rejected with 400 (`{ "message": "Invalid JSON in request body" }`) before reaching the handler.
|
|
1234
|
+
|
|
1016
1235
|
### Bulk operations
|
|
1017
1236
|
|
|
1018
1237
|
`createMany`, `createManyAndReturn`, `updateMany`, and `updateManyAndReturn` accept scalar-only data inputs. Nested relation writes are not supported in bulk operations.
|
|
@@ -1047,17 +1266,19 @@ POST read endpoints are enabled by default. Disable them with `disablePostReads:
|
|
|
1047
1266
|
|
|
1048
1267
|
Most read operations use the same path for both GET and POST. The only exception is `findMany`, which uses a `/read` suffix to avoid conflicting with `POST /` (create).
|
|
1049
1268
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
|
1053
|
-
|
|
|
1054
|
-
|
|
|
1055
|
-
|
|
|
1056
|
-
|
|
|
1057
|
-
|
|
|
1058
|
-
|
|
|
1059
|
-
|
|
|
1060
|
-
|
|
|
1269
|
+
`{modelname}` in the paths below is the lowercased model name. See [Path casing in generated endpoints](#path-casing-in-generated-endpoints).
|
|
1270
|
+
|
|
1271
|
+
| Operation | GET path | POST path |
|
|
1272
|
+
| ----------------- | ---------------------------- | ---------------------------- |
|
|
1273
|
+
| findMany | `/{modelname}/` | `/{modelname}/read` |
|
|
1274
|
+
| findFirst | `/{modelname}/first` | `/{modelname}/first` |
|
|
1275
|
+
| findFirstOrThrow | `/{modelname}/first/strict` | `/{modelname}/first/strict` |
|
|
1276
|
+
| findUnique | `/{modelname}/unique` | `/{modelname}/unique` |
|
|
1277
|
+
| findUniqueOrThrow | `/{modelname}/unique/strict` | `/{modelname}/unique/strict` |
|
|
1278
|
+
| findManyPaginated | `/{modelname}/paginated` | `/{modelname}/paginated` |
|
|
1279
|
+
| count | `/{modelname}/count` | `/{modelname}/count` |
|
|
1280
|
+
| aggregate | `/{modelname}/aggregate` | `/{modelname}/aggregate` |
|
|
1281
|
+
| groupBy | `/{modelname}/groupby` | `/{modelname}/groupby` |
|
|
1061
1282
|
|
|
1062
1283
|
### Usage
|
|
1063
1284
|
|
|
@@ -1106,6 +1327,385 @@ app.use('/', UserRouter({
|
|
|
1106
1327
|
}))
|
|
1107
1328
|
```
|
|
1108
1329
|
|
|
1330
|
+
## Progressive Endpoint Composition (Express SSE)
|
|
1331
|
+
|
|
1332
|
+
Progressive Endpoint Composition lets an Express read endpoint stream partial response fields over Server-Sent Events while still ending with a final result event.
|
|
1333
|
+
|
|
1334
|
+
This is useful for page-level endpoints where different UI sections need different slices of data. For example, a dashboard can render profile basics first, then saved jobs, applications, invitations, and activity as each stage finishes.
|
|
1335
|
+
|
|
1336
|
+
This feature is **Express-only** in v1.
|
|
1337
|
+
|
|
1338
|
+
### Mental model
|
|
1339
|
+
|
|
1340
|
+
Progressive Endpoint Composition is explicit staged data loading for a specific operation variant.
|
|
1341
|
+
|
|
1342
|
+
It is **not** automatic Prisma include streaming. The generator does not split arbitrary `select` or `include` trees. You define stages yourself and each stage decides what query to run and which field path to patch.
|
|
1343
|
+
|
|
1344
|
+
### Request format
|
|
1345
|
+
|
|
1346
|
+
Use the same generated GET read endpoint and request SSE with the `Accept` header:
|
|
1347
|
+
|
|
1348
|
+
```http
|
|
1349
|
+
GET /user/first
|
|
1350
|
+
Accept: text/event-stream
|
|
1351
|
+
x-api-variant: /talent/dashboard
|
|
1352
|
+
```
|
|
1353
|
+
|
|
1354
|
+
No new endpoint is generated. The variant is resolved the same way as guard variants: `guard.resolveVariant(req)` first, then the configured header, defaulting to `x-api-variant`.
|
|
1355
|
+
|
|
1356
|
+
If a GET read request accepts `text/event-stream` but the matched variant has no progressive config, the router runs the normal read query and returns a single SSE `result` event.
|
|
1357
|
+
|
|
1358
|
+
POST read endpoints remain JSON-only.
|
|
1359
|
+
|
|
1360
|
+
### Supported operations
|
|
1361
|
+
|
|
1362
|
+
Progressive SSE can be configured on Express GET read operations only:
|
|
1363
|
+
|
|
1364
|
+
- `findMany`
|
|
1365
|
+
- `findUnique`
|
|
1366
|
+
- `findUniqueOrThrow`
|
|
1367
|
+
- `findFirst`
|
|
1368
|
+
- `findFirstOrThrow`
|
|
1369
|
+
- `findManyPaginated`
|
|
1370
|
+
- `count`
|
|
1371
|
+
- `aggregate`
|
|
1372
|
+
- `groupBy`
|
|
1373
|
+
|
|
1374
|
+
Write operations do not support progressive SSE.
|
|
1375
|
+
|
|
1376
|
+
### Event protocol
|
|
1377
|
+
|
|
1378
|
+
Each event is sent as a normal SSE `data:` line containing JSON.
|
|
1379
|
+
|
|
1380
|
+
Progress event:
|
|
1381
|
+
|
|
1382
|
+
```json
|
|
1383
|
+
{ "type": "progress", "stage": "profileBasics", "completed": 1, "total": 4 }
|
|
1384
|
+
```
|
|
1385
|
+
|
|
1386
|
+
Field event:
|
|
1387
|
+
|
|
1388
|
+
```json
|
|
1389
|
+
{ "type": "field", "key": "profile", "value": { "id": "profile-id" } }
|
|
1390
|
+
```
|
|
1391
|
+
|
|
1392
|
+
Nested field event:
|
|
1393
|
+
|
|
1394
|
+
```json
|
|
1395
|
+
{ "type": "field", "key": "profile.appliedTo", "value": [] }
|
|
1396
|
+
```
|
|
1397
|
+
|
|
1398
|
+
Final result event:
|
|
1399
|
+
|
|
1400
|
+
```json
|
|
1401
|
+
{ "type": "result", "data": { "id": "user-id", "profile": {}, "savedJobAds": [] } }
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
Error event:
|
|
1405
|
+
|
|
1406
|
+
```json
|
|
1407
|
+
{ "type": "error", "message": "Could not load progressive response" }
|
|
1408
|
+
```
|
|
1409
|
+
|
|
1410
|
+
The final `result.data` is the accumulated object built from all applied patches, unless a stage returns a stop result.
|
|
1411
|
+
|
|
1412
|
+
### Route config example
|
|
1413
|
+
|
|
1414
|
+
Progressive config lives on an Express read operation. It is keyed by the resolved variant.
|
|
1415
|
+
|
|
1416
|
+
```ts
|
|
1417
|
+
import type { ProgressiveStage } from './generated/routeConfig.target'
|
|
1418
|
+
|
|
1419
|
+
const dashboardIdentity: ProgressiveStage<{ userId: string }> = async ({
|
|
1420
|
+
ctx,
|
|
1421
|
+
prisma,
|
|
1422
|
+
}) => {
|
|
1423
|
+
const user = await prisma.user.findFirst({
|
|
1424
|
+
select: { id: true },
|
|
1425
|
+
where: { id: ctx.userId },
|
|
1426
|
+
})
|
|
1427
|
+
|
|
1428
|
+
if (!user) {
|
|
1429
|
+
return {
|
|
1430
|
+
stop: true,
|
|
1431
|
+
data: null,
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
return {
|
|
1436
|
+
key: 'id',
|
|
1437
|
+
value: user.id,
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
const dashboardProfileBasics: ProgressiveStage<{ userId: string }> = async ({
|
|
1442
|
+
ctx,
|
|
1443
|
+
prisma,
|
|
1444
|
+
}) => {
|
|
1445
|
+
const user = await prisma.user.findFirst({
|
|
1446
|
+
select: {
|
|
1447
|
+
profile: {
|
|
1448
|
+
select: {
|
|
1449
|
+
id: true,
|
|
1450
|
+
profileName: true,
|
|
1451
|
+
profilePicture: true,
|
|
1452
|
+
jobTitle: true,
|
|
1453
|
+
location: true,
|
|
1454
|
+
skills: true,
|
|
1455
|
+
isAvailableForHire: true,
|
|
1456
|
+
_count: {
|
|
1457
|
+
select: {
|
|
1458
|
+
profileViews: true,
|
|
1459
|
+
savedAt: true,
|
|
1460
|
+
},
|
|
1461
|
+
},
|
|
1462
|
+
boost: {
|
|
1463
|
+
select: {
|
|
1464
|
+
boostedUntil: true,
|
|
1465
|
+
},
|
|
1466
|
+
},
|
|
1467
|
+
},
|
|
1468
|
+
},
|
|
1469
|
+
},
|
|
1470
|
+
where: { id: ctx.userId },
|
|
1471
|
+
})
|
|
1472
|
+
|
|
1473
|
+
return {
|
|
1474
|
+
key: 'profile',
|
|
1475
|
+
value: user?.profile
|
|
1476
|
+
? {
|
|
1477
|
+
...user.profile,
|
|
1478
|
+
appliedTo: [],
|
|
1479
|
+
invitationsFor: [],
|
|
1480
|
+
jobAdViews: [],
|
|
1481
|
+
campaign_clicks: [],
|
|
1482
|
+
jobAssignments: [],
|
|
1483
|
+
resourceOfCompanyTalentRoster: [],
|
|
1484
|
+
}
|
|
1485
|
+
: null,
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
const dashboardApplications: ProgressiveStage<{ userId: string }> = async ({
|
|
1490
|
+
ctx,
|
|
1491
|
+
prisma,
|
|
1492
|
+
accumulated,
|
|
1493
|
+
}) => {
|
|
1494
|
+
if (accumulated.profile == null) return
|
|
1495
|
+
|
|
1496
|
+
const profile = await prisma.talentProfile.findFirst({
|
|
1497
|
+
select: {
|
|
1498
|
+
appliedTo: {
|
|
1499
|
+
orderBy: { createdAt: 'desc' },
|
|
1500
|
+
take: 50,
|
|
1501
|
+
where: { deletedAt: null },
|
|
1502
|
+
select: {
|
|
1503
|
+
id: true,
|
|
1504
|
+
createdAt: true,
|
|
1505
|
+
viewedAt: true,
|
|
1506
|
+
details: true,
|
|
1507
|
+
jobAd: true,
|
|
1508
|
+
},
|
|
1509
|
+
},
|
|
1510
|
+
},
|
|
1511
|
+
where: { userId: ctx.userId },
|
|
1512
|
+
})
|
|
1513
|
+
|
|
1514
|
+
return {
|
|
1515
|
+
key: 'profile.appliedTo',
|
|
1516
|
+
value: profile?.appliedTo ?? [],
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
const userConfig = {
|
|
1521
|
+
resolveContext: (req) => ({
|
|
1522
|
+
userId: req.user.id,
|
|
1523
|
+
}),
|
|
1524
|
+
|
|
1525
|
+
guard: {
|
|
1526
|
+
variantHeader: 'x-api-variant',
|
|
1527
|
+
},
|
|
1528
|
+
|
|
1529
|
+
findFirst: {
|
|
1530
|
+
shape: {
|
|
1531
|
+
'/talent/dashboard': dashboardShape,
|
|
1532
|
+
me: meShape,
|
|
1533
|
+
},
|
|
1534
|
+
progressive: {
|
|
1535
|
+
'/talent/dashboard': {
|
|
1536
|
+
enabled: true,
|
|
1537
|
+
stages: [
|
|
1538
|
+
'dashboardIdentity',
|
|
1539
|
+
'dashboardProfileBasics',
|
|
1540
|
+
'dashboardApplications',
|
|
1541
|
+
],
|
|
1542
|
+
},
|
|
1543
|
+
},
|
|
1544
|
+
progressiveStages: {
|
|
1545
|
+
dashboardIdentity,
|
|
1546
|
+
dashboardProfileBasics,
|
|
1547
|
+
dashboardApplications,
|
|
1548
|
+
},
|
|
1549
|
+
},
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
app.use('/', UserRouter(userConfig))
|
|
1553
|
+
```
|
|
1554
|
+
|
|
1555
|
+
`resolveContext` is required for a variant with `progressive.enabled !== false`. It is not required for ordinary JSON requests or for single-result SSE fallback.
|
|
1556
|
+
|
|
1557
|
+
### Stage function API
|
|
1558
|
+
|
|
1559
|
+
```ts
|
|
1560
|
+
type ProgressiveStageContext<TContext = unknown, TPrisma = any> = {
|
|
1561
|
+
ctx: TContext
|
|
1562
|
+
req: Request
|
|
1563
|
+
res: Response
|
|
1564
|
+
prisma: TPrisma
|
|
1565
|
+
variant: string
|
|
1566
|
+
accumulated: Record<string, unknown>
|
|
1567
|
+
signal: AbortSignal
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
type ProgressivePatch = {
|
|
1571
|
+
key: string
|
|
1572
|
+
value: unknown
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
type ProgressiveStopResult<T = unknown> = {
|
|
1576
|
+
stop: true
|
|
1577
|
+
data: T
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
type ProgressiveStageResult<T = unknown> =
|
|
1581
|
+
| void
|
|
1582
|
+
| ProgressivePatch
|
|
1583
|
+
| ProgressivePatch[]
|
|
1584
|
+
| ProgressiveStopResult<T>
|
|
1585
|
+
|
|
1586
|
+
type ProgressiveStage<TContext = unknown, TPrisma = any, T = unknown> = (
|
|
1587
|
+
context: ProgressiveStageContext<TContext, TPrisma>,
|
|
1588
|
+
) => Promise<ProgressiveStageResult<T>>
|
|
1589
|
+
```
|
|
1590
|
+
|
|
1591
|
+
A stage may return:
|
|
1592
|
+
|
|
1593
|
+
- `void` — no patch for this stage
|
|
1594
|
+
- one `{ key, value }` patch
|
|
1595
|
+
- an array of patches
|
|
1596
|
+
- `{ stop: true, data }` to immediately send a final `result` event and stop executing later stages
|
|
1597
|
+
|
|
1598
|
+
### Patch path behavior
|
|
1599
|
+
|
|
1600
|
+
Patch keys use dot paths, for example `profile.appliedTo`.
|
|
1601
|
+
|
|
1602
|
+
Nested patches require the parent object to already exist in `accumulated`. If a stage tries to patch through `null`, `undefined`, an array, a primitive, or a non-plain object, the patch is dropped and no `field` event is sent.
|
|
1603
|
+
|
|
1604
|
+
This means parent objects should be initialized by earlier stages:
|
|
1605
|
+
|
|
1606
|
+
```ts
|
|
1607
|
+
return {
|
|
1608
|
+
key: 'profile',
|
|
1609
|
+
value: {
|
|
1610
|
+
...profileBasics,
|
|
1611
|
+
appliedTo: [],
|
|
1612
|
+
invitationsFor: [],
|
|
1613
|
+
},
|
|
1614
|
+
}
|
|
1615
|
+
```
|
|
1616
|
+
|
|
1617
|
+
Patch path segments `__proto__`, `constructor`, `prototype`, and empty path segments are rejected.
|
|
1618
|
+
|
|
1619
|
+
### Hooks and guard behavior
|
|
1620
|
+
|
|
1621
|
+
For SSE requests:
|
|
1622
|
+
|
|
1623
|
+
- `before` hooks run before streaming starts
|
|
1624
|
+
- `after` hooks do not run, because the SSE middleware handles the response and does not continue to the normal handler
|
|
1625
|
+
- progressive stages receive `req.prisma` directly
|
|
1626
|
+
- guard shapes are not automatically applied to stage queries
|
|
1627
|
+
|
|
1628
|
+
Stage authors are responsible for using the resolved context and enforcing ownership or tenant constraints in their stage queries.
|
|
1629
|
+
|
|
1630
|
+
For variants without progressive config, the single-result SSE fallback uses the normal generated core read handler, so guard shape behavior matches the JSON endpoint.
|
|
1631
|
+
|
|
1632
|
+
### Client-side usage
|
|
1633
|
+
|
|
1634
|
+
Use `fetch` with streaming. Native browser `EventSource` cannot send custom headers like `x-api-variant`.
|
|
1635
|
+
|
|
1636
|
+
Minimal example:
|
|
1637
|
+
|
|
1638
|
+
```ts
|
|
1639
|
+
const response = await fetch('/user/first', {
|
|
1640
|
+
headers: {
|
|
1641
|
+
Accept: 'text/event-stream',
|
|
1642
|
+
'x-api-variant': '/talent/dashboard',
|
|
1643
|
+
},
|
|
1644
|
+
})
|
|
1645
|
+
|
|
1646
|
+
if (!response.body) {
|
|
1647
|
+
throw new Error('ReadableStream is not available')
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
const reader = response.body.getReader()
|
|
1651
|
+
const decoder = new TextDecoder()
|
|
1652
|
+
let buffer = ''
|
|
1653
|
+
|
|
1654
|
+
while (true) {
|
|
1655
|
+
const { value, done } = await reader.read()
|
|
1656
|
+
if (done) break
|
|
1657
|
+
|
|
1658
|
+
buffer += decoder.decode(value, { stream: true })
|
|
1659
|
+
const parts = buffer.split('\n\n')
|
|
1660
|
+
buffer = parts.pop() ?? ''
|
|
1661
|
+
|
|
1662
|
+
for (const part of parts) {
|
|
1663
|
+
const line = part
|
|
1664
|
+
.split('\n')
|
|
1665
|
+
.find((entry) => entry.startsWith('data: '))
|
|
1666
|
+
|
|
1667
|
+
if (!line) continue
|
|
1668
|
+
|
|
1669
|
+
const event = JSON.parse(line.slice('data: '.length))
|
|
1670
|
+
|
|
1671
|
+
if (event.type === 'field') {
|
|
1672
|
+
// patch local field state
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
if (event.type === 'result') {
|
|
1676
|
+
// replace with final result
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
```
|
|
1681
|
+
|
|
1682
|
+
For React Query, include the variant and mode in the query key:
|
|
1683
|
+
|
|
1684
|
+
```ts
|
|
1685
|
+
['user', 'first', { variant: '/talent/dashboard', mode: 'sse' }]
|
|
1686
|
+
```
|
|
1687
|
+
|
|
1688
|
+
Do not reuse the same query key as the JSON endpoint because the same URL can return different shapes depending on `x-api-variant`.
|
|
1689
|
+
|
|
1690
|
+
### Runtime notes
|
|
1691
|
+
|
|
1692
|
+
The SSE response sets:
|
|
1693
|
+
|
|
1694
|
+
```http
|
|
1695
|
+
Content-Type: text/event-stream
|
|
1696
|
+
Cache-Control: no-cache, no-transform
|
|
1697
|
+
Connection: keep-alive
|
|
1698
|
+
X-Accel-Buffering: no
|
|
1699
|
+
```
|
|
1700
|
+
|
|
1701
|
+
The server sends keepalive comments periodically:
|
|
1702
|
+
|
|
1703
|
+
```txt
|
|
1704
|
+
: keepalive
|
|
1705
|
+
```
|
|
1706
|
+
|
|
1707
|
+
If compression middleware is used, configure it to skip `text/event-stream`, or ensure `res.flush()` is available so events are flushed promptly.
|
|
1708
|
+
|
|
1109
1709
|
## Response shaping: select, include, omit
|
|
1110
1710
|
|
|
1111
1711
|
Read and single-record write operations support three response shaping parameters:
|
|
@@ -1157,7 +1757,9 @@ All errors are returned as JSON with a `message` field:
|
|
|
1157
1757
|
{ "message": "Unique constraint violation" }
|
|
1158
1758
|
```
|
|
1159
1759
|
|
|
1160
|
-
Each generated router installs
|
|
1760
|
+
Each generated router installs error handling (Express middleware, Fastify `setErrorHandler`, or Hono `app.onError`) 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.
|
|
1761
|
+
|
|
1762
|
+
For the Hono target, thrown `HTTPException` instances are caught by `app.onError` and converted to `{ "message": err.message }` with the exception's status code. Custom response bodies attached to `HTTPException` are not preserved — see [HTTPException normalization](#httpexception-normalization).
|
|
1161
1763
|
|
|
1162
1764
|
| Status | Description |
|
|
1163
1765
|
| ------ | ------------------------------------------ |
|
|
@@ -1179,12 +1781,12 @@ All incoming JSON bodies and query parameters are sanitized to reject `__proto__
|
|
|
1179
1781
|
|
|
1180
1782
|
Each router automatically registers OpenAPI spec endpoints when not in production:
|
|
1181
1783
|
|
|
1182
|
-
| Endpoint
|
|
1183
|
-
|
|
|
1184
|
-
| `/{
|
|
1185
|
-
| `/{
|
|
1784
|
+
| Endpoint | Description |
|
|
1785
|
+
| ------------------------------- | --------------------- |
|
|
1786
|
+
| `/{modelname}/openapi.json` | OpenAPI 3.1 JSON spec |
|
|
1787
|
+
| `/{modelname}/openapi.yaml` | OpenAPI 3.1 YAML spec |
|
|
1186
1788
|
|
|
1187
|
-
Actual paths depend on `customUrlPrefix` and `addModelPrefix` configuration.
|
|
1789
|
+
Actual paths depend on `customUrlPrefix` and `addModelPrefix` configuration. `{modelname}` is the lowercased model name (see [Path casing](#path-casing-in-generated-endpoints)).
|
|
1188
1790
|
|
|
1189
1791
|
The OpenAPI spec includes POST read endpoints when they are enabled (default). Each POST read operation appears with its own `operationId` and request body schema documenting the native JSON argument types.
|
|
1190
1792
|
|
|
@@ -1210,6 +1812,8 @@ const postConfig = {
|
|
|
1210
1812
|
enableAll: true,
|
|
1211
1813
|
}
|
|
1212
1814
|
|
|
1815
|
+
app.use(express.json())
|
|
1816
|
+
|
|
1213
1817
|
app.use('/', UserRouter(userConfig))
|
|
1214
1818
|
app.use('/', PostRouter(postConfig))
|
|
1215
1819
|
|
|
@@ -1270,14 +1874,70 @@ fastify.get('/docs', generateCombinedDocs({
|
|
|
1270
1874
|
}))
|
|
1271
1875
|
```
|
|
1272
1876
|
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1877
|
+
#### Hono
|
|
1878
|
+
|
|
1879
|
+
```ts
|
|
1880
|
+
import { Hono } from 'hono'
|
|
1881
|
+
import { PrismaClient } from '@prisma/client'
|
|
1882
|
+
import {
|
|
1883
|
+
generateCombinedDocs,
|
|
1884
|
+
registerModelDocs,
|
|
1885
|
+
} from './generated/combinedDocs'
|
|
1886
|
+
import { UserRouter } from './generated/User/UserRouter'
|
|
1887
|
+
import { PostRouter } from './generated/Post/PostRouter'
|
|
1888
|
+
|
|
1889
|
+
type Env = {
|
|
1890
|
+
Variables: {
|
|
1891
|
+
prisma: PrismaClient
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
const prisma = new PrismaClient()
|
|
1896
|
+
|
|
1897
|
+
const userConfig = {
|
|
1898
|
+
findMany: { before: [async (c, next) => { /* auth */ await next() }] },
|
|
1899
|
+
create: {},
|
|
1900
|
+
findUnique: {},
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
const postConfig = {
|
|
1904
|
+
enableAll: true,
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
const app = new Hono<Env>()
|
|
1908
|
+
|
|
1909
|
+
app.use('*', async (c, next) => {
|
|
1910
|
+
c.set('prisma', prisma)
|
|
1911
|
+
await next()
|
|
1912
|
+
})
|
|
1913
|
+
|
|
1914
|
+
app.route('/', UserRouter(userConfig))
|
|
1915
|
+
app.route('/', PostRouter(postConfig))
|
|
1916
|
+
|
|
1917
|
+
registerModelDocs(app, '/docs', {
|
|
1918
|
+
User: userConfig,
|
|
1919
|
+
Post: postConfig,
|
|
1920
|
+
})
|
|
1921
|
+
|
|
1922
|
+
app.get('/docs', generateCombinedDocs({
|
|
1923
|
+
title: 'My API',
|
|
1924
|
+
modelConfigs: {
|
|
1925
|
+
User: userConfig,
|
|
1926
|
+
Post: postConfig,
|
|
1927
|
+
},
|
|
1928
|
+
}))
|
|
1929
|
+
```
|
|
1930
|
+
|
|
1931
|
+
| Endpoint | Description |
|
|
1932
|
+
| --------------------------------- | ----------------------- |
|
|
1933
|
+
| `/docs` | Combined index page |
|
|
1934
|
+
| `/docs/{modelname}` | Contract view (default) |
|
|
1935
|
+
| `/docs/{modelname}?ui=scalar` | Scalar interactive UI |
|
|
1936
|
+
| `/docs/{modelname}?ui=json` | Raw JSON |
|
|
1937
|
+
| `/docs/{modelname}?ui=yaml` | Raw YAML |
|
|
1938
|
+
| `/docs/{modelname}?ui=playground` | Query playground |
|
|
1939
|
+
|
|
1940
|
+
The `?ui=playground` endpoint requires `prisma-query-builder-ui`. For Express and Fastify, the builder is auto-started in development. For Hono, the builder must be started manually in a separate process (see [Query Builder](#query-builder)).
|
|
1281
1941
|
|
|
1282
1942
|
Disable in production via `NODE_ENV=production` or `DISABLE_OPENAPI=true`. Override with `disableOpenApi: false` in config to force-enable.
|
|
1283
1943
|
|
|
@@ -1298,18 +1958,20 @@ When `specBasePath` is not set, `customUrlPrefix` is used for both runtime route
|
|
|
1298
1958
|
|
|
1299
1959
|
## prisma-sql integration
|
|
1300
1960
|
|
|
1301
|
-
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
|
|
1961
|
+
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 context.
|
|
1302
1962
|
|
|
1303
|
-
Set
|
|
1963
|
+
Set the connector in your middleware to activate the extension:
|
|
1304
1964
|
|
|
1305
1965
|
```ts
|
|
1306
1966
|
import { PrismaClient } from '@prisma/client'
|
|
1307
1967
|
import postgres from 'postgres'
|
|
1968
|
+
import { Hono } from 'hono'
|
|
1308
1969
|
|
|
1309
1970
|
const prisma = new PrismaClient()
|
|
1310
1971
|
const sql = postgres(process.env.DATABASE_URL!)
|
|
1311
1972
|
|
|
1312
1973
|
// Express
|
|
1974
|
+
app.use(express.json())
|
|
1313
1975
|
app.use((req, res, next) => {
|
|
1314
1976
|
req.prisma = prisma
|
|
1315
1977
|
req.postgres = sql
|
|
@@ -1321,9 +1983,27 @@ fastify.addHook('onRequest', async (request) => {
|
|
|
1321
1983
|
request.prisma = prisma
|
|
1322
1984
|
request.postgres = sql
|
|
1323
1985
|
})
|
|
1986
|
+
|
|
1987
|
+
// Hono
|
|
1988
|
+
type Env = {
|
|
1989
|
+
Variables: {
|
|
1990
|
+
prisma: PrismaClient
|
|
1991
|
+
postgres: ReturnType<typeof postgres>
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
const app = new Hono<Env>()
|
|
1996
|
+
|
|
1997
|
+
app.use('*', async (c, next) => {
|
|
1998
|
+
c.set('prisma', prisma)
|
|
1999
|
+
c.set('postgres', sql)
|
|
2000
|
+
await next()
|
|
2001
|
+
})
|
|
1324
2002
|
```
|
|
1325
2003
|
|
|
1326
|
-
Without a connector on the request, the handlers use the standard PrismaClient. Set `DEBUG=true` in the environment to enable prisma-sql debug logging.
|
|
2004
|
+
Without a connector on the request context, the handlers use the standard PrismaClient. Set `DEBUG=true` in the environment to enable prisma-sql debug logging.
|
|
2005
|
+
|
|
2006
|
+
For SQLite, use `c.set('sqlite', sqliteConnector)` (Hono) or the equivalent on Express/Fastify, and add `sqlite` to the `Variables` type.
|
|
1327
2007
|
|
|
1328
2008
|
## Query parameter parsing
|
|
1329
2009
|
|
|
@@ -1331,42 +2011,48 @@ GET query values are parsed server-side. Strings starting with `{`, `[`, or `"`
|
|
|
1331
2011
|
|
|
1332
2012
|
POST read endpoints bypass this parsing entirely — the JSON body is used as-is with native types.
|
|
1333
2013
|
|
|
2014
|
+
On the Hono target, duplicate query keys collapse to the last value (`?a=1&a=2` → `a=2`). `encodeQueryParams` does not emit duplicate keys, so this only matters for hand-built query strings.
|
|
2015
|
+
|
|
1334
2016
|
## Router schema
|
|
1335
2017
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
|
1339
|
-
|
|
|
1340
|
-
|
|
|
1341
|
-
|
|
|
1342
|
-
|
|
|
1343
|
-
|
|
|
1344
|
-
|
|
|
1345
|
-
|
|
|
1346
|
-
|
|
|
1347
|
-
|
|
|
1348
|
-
|
|
|
1349
|
-
|
|
|
1350
|
-
|
|
|
1351
|
-
|
|
|
1352
|
-
|
|
|
1353
|
-
|
|
|
1354
|
-
|
|
|
1355
|
-
|
|
|
1356
|
-
|
|
|
1357
|
-
|
|
|
1358
|
-
|
|
|
1359
|
-
|
|
|
1360
|
-
|
|
|
1361
|
-
|
|
|
1362
|
-
|
|
|
1363
|
-
|
|
|
1364
|
-
|
|
|
2018
|
+
`{modelname}` in the paths below is the lowercased model name. For a `User` model, `/{modelname}/first` becomes `/user/first`. For `BlogPost`, it becomes `/blogpost/first`. See [Path casing in generated endpoints](#path-casing-in-generated-endpoints).
|
|
2019
|
+
|
|
2020
|
+
| Operation | Method | Path | Notes |
|
|
2021
|
+
| ------------------- | ------ | ---------------------------- | ---------------------------------- |
|
|
2022
|
+
| findMany | GET | `/{modelname}/` | |
|
|
2023
|
+
| findMany | POST | `/{modelname}/read` | POST read alternative |
|
|
2024
|
+
| findFirst | GET | `/{modelname}/first` | |
|
|
2025
|
+
| findFirst | POST | `/{modelname}/first` | POST read alternative |
|
|
2026
|
+
| findFirstOrThrow | GET | `/{modelname}/first/strict` | |
|
|
2027
|
+
| findFirstOrThrow | POST | `/{modelname}/first/strict` | POST read alternative |
|
|
2028
|
+
| findUnique | GET | `/{modelname}/unique` | |
|
|
2029
|
+
| findUnique | POST | `/{modelname}/unique` | POST read alternative |
|
|
2030
|
+
| findUniqueOrThrow | GET | `/{modelname}/unique/strict` | |
|
|
2031
|
+
| findUniqueOrThrow | POST | `/{modelname}/unique/strict` | POST read alternative |
|
|
2032
|
+
| findManyPaginated | GET | `/{modelname}/paginated` | |
|
|
2033
|
+
| findManyPaginated | POST | `/{modelname}/paginated` | POST read alternative |
|
|
2034
|
+
| count | GET | `/{modelname}/count` | |
|
|
2035
|
+
| count | POST | `/{modelname}/count` | POST read alternative |
|
|
2036
|
+
| aggregate | GET | `/{modelname}/aggregate` | |
|
|
2037
|
+
| aggregate | POST | `/{modelname}/aggregate` | POST read alternative |
|
|
2038
|
+
| groupBy | GET | `/{modelname}/groupby` | |
|
|
2039
|
+
| groupBy | POST | `/{modelname}/groupby` | POST read alternative |
|
|
2040
|
+
| create | POST | `/{modelname}/` | |
|
|
2041
|
+
| createMany | POST | `/{modelname}/many` | |
|
|
2042
|
+
| createManyAndReturn | POST | `/{modelname}/many/return` | |
|
|
2043
|
+
| update | PUT | `/{modelname}/` | |
|
|
2044
|
+
| updateMany | PUT | `/{modelname}/many` | |
|
|
2045
|
+
| updateManyAndReturn | PUT | `/{modelname}/many/return` | |
|
|
2046
|
+
| upsert | PATCH | `/{modelname}/` | |
|
|
2047
|
+
| delete | DELETE | `/{modelname}/` | |
|
|
2048
|
+
| deleteMany | DELETE | `/{modelname}/many` | |
|
|
1365
2049
|
|
|
1366
2050
|
Paths shown are relative suffixes. Actual paths include the model prefix (e.g., `/user/first`) unless `addModelPrefix: false`, and any `customUrlPrefix`.
|
|
1367
2051
|
|
|
1368
2052
|
POST read endpoints are enabled by default. Set `disablePostReads: true` to remove them.
|
|
1369
2053
|
|
|
2054
|
+
For the Express target, GET read endpoints can also stream SSE events when the request sends `Accept: text/event-stream`. SSE uses the same GET paths shown above; no additional routes are generated. See [Progressive Endpoint Composition](#progressive-endpoint-composition-express-sse).
|
|
2055
|
+
|
|
1370
2056
|
## Skipping models
|
|
1371
2057
|
|
|
1372
2058
|
Add `/// generator off` to a model's documentation to skip generation:
|
|
@@ -1383,7 +2069,7 @@ model InternalLog {
|
|
|
1383
2069
|
### Express
|
|
1384
2070
|
|
|
1385
2071
|
```ts
|
|
1386
|
-
interface RouteConfig {
|
|
2072
|
+
interface RouteConfig<TCtx = unknown> {
|
|
1387
2073
|
enableAll?: boolean
|
|
1388
2074
|
addModelPrefix?: boolean // default: true
|
|
1389
2075
|
customUrlPrefix?: string
|
|
@@ -1404,6 +2090,8 @@ interface RouteConfig {
|
|
|
1404
2090
|
variantHeader?: string // default: 'x-api-variant'
|
|
1405
2091
|
}
|
|
1406
2092
|
|
|
2093
|
+
resolveContext?: (req: Request) => TCtx | Promise<TCtx>
|
|
2094
|
+
|
|
1407
2095
|
queryBuilder?: QueryBuilderConfig | false
|
|
1408
2096
|
|
|
1409
2097
|
pagination?: {
|
|
@@ -1412,13 +2100,18 @@ interface RouteConfig {
|
|
|
1412
2100
|
distinctCountLimit?: number // default: 100000
|
|
1413
2101
|
}
|
|
1414
2102
|
|
|
1415
|
-
//
|
|
1416
|
-
findMany?:
|
|
1417
|
-
findUnique?:
|
|
1418
|
-
findUniqueOrThrow?:
|
|
1419
|
-
findFirst?:
|
|
1420
|
-
findFirstOrThrow?:
|
|
1421
|
-
findManyPaginated?:
|
|
2103
|
+
// read operation config
|
|
2104
|
+
findMany?: ReadOperationConfig<TCtx>
|
|
2105
|
+
findUnique?: ReadOperationConfig<TCtx>
|
|
2106
|
+
findUniqueOrThrow?: ReadOperationConfig<TCtx>
|
|
2107
|
+
findFirst?: ReadOperationConfig<TCtx>
|
|
2108
|
+
findFirstOrThrow?: ReadOperationConfig<TCtx>
|
|
2109
|
+
findManyPaginated?: ReadOperationConfig<TCtx>
|
|
2110
|
+
aggregate?: ReadOperationConfig<TCtx>
|
|
2111
|
+
count?: ReadOperationConfig<TCtx>
|
|
2112
|
+
groupBy?: ReadOperationConfig<TCtx>
|
|
2113
|
+
|
|
2114
|
+
// write operation config
|
|
1422
2115
|
create?: OperationConfig
|
|
1423
2116
|
createMany?: OperationConfig
|
|
1424
2117
|
createManyAndReturn?: OperationConfig
|
|
@@ -1428,9 +2121,6 @@ interface RouteConfig {
|
|
|
1428
2121
|
upsert?: OperationConfig
|
|
1429
2122
|
delete?: OperationConfig
|
|
1430
2123
|
deleteMany?: OperationConfig
|
|
1431
|
-
aggregate?: OperationConfig
|
|
1432
|
-
count?: OperationConfig
|
|
1433
|
-
groupBy?: OperationConfig
|
|
1434
2124
|
}
|
|
1435
2125
|
|
|
1436
2126
|
interface OperationConfig {
|
|
@@ -1439,6 +2129,46 @@ interface OperationConfig {
|
|
|
1439
2129
|
shape?: Record<string, any>
|
|
1440
2130
|
}
|
|
1441
2131
|
|
|
2132
|
+
interface ReadOperationConfig<TCtx = unknown> extends OperationConfig {
|
|
2133
|
+
progressive?: Record<string, ProgressiveVariantConfig>
|
|
2134
|
+
progressiveStages?: Record<string, ProgressiveStage<TCtx>>
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
type ProgressiveVariantConfig = {
|
|
2138
|
+
enabled?: boolean
|
|
2139
|
+
stages: string[]
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
type ProgressiveStageContext<TContext = unknown, TPrisma = any> = {
|
|
2143
|
+
ctx: TContext
|
|
2144
|
+
req: Request
|
|
2145
|
+
res: Response
|
|
2146
|
+
prisma: TPrisma
|
|
2147
|
+
variant: string
|
|
2148
|
+
accumulated: Record<string, unknown>
|
|
2149
|
+
signal: AbortSignal
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
type ProgressivePatch = {
|
|
2153
|
+
key: string
|
|
2154
|
+
value: unknown
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
type ProgressiveStopResult<T = unknown> = {
|
|
2158
|
+
stop: true
|
|
2159
|
+
data: T
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
type ProgressiveStageResult<T = unknown> =
|
|
2163
|
+
| void
|
|
2164
|
+
| ProgressivePatch
|
|
2165
|
+
| ProgressivePatch[]
|
|
2166
|
+
| ProgressiveStopResult<T>
|
|
2167
|
+
|
|
2168
|
+
type ProgressiveStage<TContext = unknown, TPrisma = any, T = unknown> = (
|
|
2169
|
+
context: ProgressiveStageContext<TContext, TPrisma>,
|
|
2170
|
+
) => Promise<ProgressiveStageResult<T>>
|
|
2171
|
+
|
|
1442
2172
|
interface QueryBuilderConfig {
|
|
1443
2173
|
enabled?: boolean
|
|
1444
2174
|
port?: number
|
|
@@ -1467,6 +2197,27 @@ type FastifyHookHandler = (
|
|
|
1467
2197
|
|
|
1468
2198
|
The `guard.resolveVariant` callback receives `FastifyRequest` instead of `Request`.
|
|
1469
2199
|
|
|
2200
|
+
### Hono
|
|
2201
|
+
|
|
2202
|
+
The Hono config is identical except for hook and resolver types:
|
|
2203
|
+
|
|
2204
|
+
```ts
|
|
2205
|
+
interface OperationConfig {
|
|
2206
|
+
before?: HonoHookHandler[]
|
|
2207
|
+
after?: HonoHookHandler[]
|
|
2208
|
+
shape?: Record<string, any>
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
type HonoHookHandler<Env extends { Variables: any } = any> = (
|
|
2212
|
+
c: Context<Env>,
|
|
2213
|
+
next: Next,
|
|
2214
|
+
) => Promise<Response | void> | Response | void
|
|
2215
|
+
```
|
|
2216
|
+
|
|
2217
|
+
The `guard.resolveVariant` callback receives Hono's `Context`. Hooks are native Hono middleware — call `await next()` to continue the chain, return a `Response` (or throw `HTTPException`) to short-circuit.
|
|
2218
|
+
|
|
2219
|
+
The Hono router does not auto-start the Query Builder. Set `queryBuilder: false` to make the playground route return 404, or run `prisma-query-builder-ui` manually for development.
|
|
2220
|
+
|
|
1470
2221
|
### Shared options
|
|
1471
2222
|
|
|
1472
2223
|
`customUrlPrefix` is normalized to ensure a leading slash and strip trailing slashes.
|
|
@@ -1475,6 +2226,8 @@ The `guard.resolveVariant` callback receives `FastifyRequest` instead of `Reques
|
|
|
1475
2226
|
|
|
1476
2227
|
`disablePostReads` removes all POST read endpoints when set to `true`. POST read endpoints are enabled by default. This is a global setting — there is no per-operation toggle.
|
|
1477
2228
|
|
|
2229
|
+
`resolveContext` is Express-only and is required for enabled progressive SSE variants. It is called before progressive stages run and its return value is passed to each stage as `ctx`.
|
|
2230
|
+
|
|
1478
2231
|
`openApiServers` sets the `servers` array in the OpenAPI spec:
|
|
1479
2232
|
|
|
1480
2233
|
```ts
|