prisma-generator-express 1.49.0 → 1.50.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 CHANGED
@@ -13,6 +13,7 @@ Running `npx prisma generate` produces:
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
15
  - Express-only progressive read streaming over Server-Sent Events (SSE), using manual stages or auto-include splitting for single-record relation reads
16
+ - Express-only standalone materialized view router for read-only access to registered PostgreSQL materialized views
16
17
  - OpenAPI 3.1 spec (JSON and YAML endpoints registered automatically per router)
17
18
  - Documentation helpers for contract view and Scalar UI (require manual mounting)
18
19
  - Client-side query parameter encoder
@@ -34,6 +35,7 @@ Supports **Express**, **Fastify**, and **Hono** targets via the `target` configu
34
35
  - [Request body format](#request-body-format)
35
36
  - [Query encoding (client side)](#query-encoding-client-side)
36
37
  - [POST read endpoints](#post-read-endpoints)
38
+ - [Materialized views router (Express)](#materialized-views-router-express)
37
39
  - [Progressive Endpoint Composition (Express SSE)](#progressive-endpoint-composition-express-sse)
38
40
  - [Response shaping: select, include, omit](#response-shaping-select-include-omit)
39
41
  - [BigInt and Decimal handling](#bigint-and-decimal-handling)
@@ -1327,6 +1329,220 @@ app.use('/', UserRouter({
1327
1329
  }))
1328
1330
  ```
1329
1331
 
1332
+ ## Materialized views router (Express)
1333
+
1334
+ The Express target includes a standalone helper for read-only access to PostgreSQL materialized views.
1335
+
1336
+ Materialized views are not Prisma models and do not have Prisma delegates, so they are not generated as normal per-model routers. Instead, mount one standalone router and provide an explicit registry of allowed views.
1337
+
1338
+ This feature is **Express-only**.
1339
+
1340
+ ### Usage
1341
+
1342
+ ```ts
1343
+ import express from 'express'
1344
+ import { PrismaClient } from '@prisma/client'
1345
+ import { materializedViewsRouter } from './generated/materializedRouter'
1346
+
1347
+ const prisma = new PrismaClient()
1348
+ const app = express()
1349
+
1350
+ app.use('/api', materializedViewsRouter({
1351
+ prisma,
1352
+ basePath: '/materialized',
1353
+ views: {
1354
+ jobAdStats: {
1355
+ relation: 'mv_jobad_stats',
1356
+ orderBy: {
1357
+ field: 'updatedAt',
1358
+ direction: 'desc',
1359
+ },
1360
+ },
1361
+ companyStats: {
1362
+ relation: 'mv_company_stats',
1363
+ },
1364
+ },
1365
+ }))
1366
+
1367
+ app.listen(3000)
1368
+ ```
1369
+
1370
+ This registers:
1371
+
1372
+ ```http
1373
+ GET /api/materialized/jobAdStats?take=50&skip=0
1374
+ GET /api/materialized/companyStats?take=50
1375
+ ```
1376
+
1377
+ ### View registry
1378
+
1379
+ Each key in `views` is the public API name. The `relation` value is the actual database relation name.
1380
+
1381
+ ```ts
1382
+ views: {
1383
+ jobAdStats: {
1384
+ relation: 'mv_jobad_stats',
1385
+ schema: 'public',
1386
+ defaultLimit: 50,
1387
+ maxLimit: 500,
1388
+ orderBy: {
1389
+ field: 'updatedAt',
1390
+ direction: 'desc',
1391
+ nulls: 'last',
1392
+ },
1393
+ },
1394
+ }
1395
+ ```
1396
+
1397
+ Supported view options:
1398
+
1399
+ | Option | Type | Description |
1400
+ | ------ | ---- | ----------- |
1401
+ | `relation` | `string` | Database materialized view name |
1402
+ | `schema` | `string` | Optional schema name |
1403
+ | `defaultLimit` | `number` | Default page size for this view |
1404
+ | `maxLimit` | `number` | Maximum page size for this view |
1405
+ | `orderBy` | `string \| object` | Deterministic sort column |
1406
+ | `authorize` | `function` | Optional per-view authorization hook |
1407
+
1408
+ `orderBy` can be a string:
1409
+
1410
+ ```ts
1411
+ orderBy: 'updatedAt'
1412
+ ```
1413
+
1414
+ Or an object:
1415
+
1416
+ ```ts
1417
+ orderBy: {
1418
+ field: 'updatedAt',
1419
+ direction: 'desc',
1420
+ nulls: 'last',
1421
+ }
1422
+ ```
1423
+
1424
+ ### Router options
1425
+
1426
+ ```ts
1427
+ materializedViewsRouter({
1428
+ prisma,
1429
+ basePath: '/materialized',
1430
+ defaultLimit: 50,
1431
+ maxLimit: 1000,
1432
+ before: [authMiddleware],
1433
+ after: [auditMiddleware],
1434
+ views: {
1435
+ jobAdStats: { relation: 'mv_jobad_stats' },
1436
+ },
1437
+ })
1438
+ ```
1439
+
1440
+ Supported router options:
1441
+
1442
+ | Option | Type | Description |
1443
+ | ------ | ---- | ----------- |
1444
+ | `prisma` | Prisma client-like object | Must expose `$queryRawUnsafe` |
1445
+ | `views` | `Record<string, ViewDef>` | Registry of allowed materialized views |
1446
+ | `basePath` | `string` | Path inside this router, default `''` |
1447
+ | `defaultLimit` | `number` | Global default page size, default `50` |
1448
+ | `maxLimit` | `number` | Global max page size, default `1000` |
1449
+ | `before` | `RequestHandler[]` | Express middleware before the query |
1450
+ | `after` | `RequestHandler[]` | Express middleware after the query |
1451
+
1452
+ ### Authorization
1453
+
1454
+ Use `before` for shared middleware and `authorize` for per-view checks.
1455
+
1456
+ ```ts
1457
+ const forbidden = (message: string) => Object.assign(new Error(message), { status: 403 })
1458
+
1459
+ app.use('/api', materializedViewsRouter({
1460
+ prisma,
1461
+ basePath: '/materialized',
1462
+ before: [requireAuth],
1463
+ views: {
1464
+ publicStats: {
1465
+ relation: 'mv_public_stats',
1466
+ },
1467
+ adminStats: {
1468
+ relation: 'mv_admin_stats',
1469
+ authorize: (req) => {
1470
+ if (req.user?.role !== 'admin') {
1471
+ throw forbidden('Forbidden')
1472
+ }
1473
+ },
1474
+ },
1475
+ },
1476
+ }))
1477
+ ```
1478
+
1479
+ If a view is not in the registry, the router returns:
1480
+
1481
+ ```json
1482
+ { "message": "unknown view" }
1483
+ ```
1484
+
1485
+ ### Pagination
1486
+
1487
+ The router supports `take` and `skip` query parameters:
1488
+
1489
+ ```http
1490
+ GET /materialized/jobAdStats?take=100&skip=200
1491
+ ```
1492
+
1493
+ `take` is clamped to the configured max limit. `skip` is clamped to zero or greater.
1494
+
1495
+ When `skip > 0`, the view must define `orderBy`. This prevents unstable offset pagination.
1496
+
1497
+ ```ts
1498
+ views: {
1499
+ jobAdStats: {
1500
+ relation: 'mv_jobad_stats',
1501
+ orderBy: {
1502
+ field: 'updatedAt',
1503
+ direction: 'desc',
1504
+ },
1505
+ },
1506
+ }
1507
+ ```
1508
+
1509
+ ### Identifier safety
1510
+
1511
+ Only registered view names can be queried. Database identifiers such as `schema`, `relation`, and `orderBy.field` must match this pattern:
1512
+
1513
+ ```txt
1514
+ ^[A-Za-z_][A-Za-z0-9_]*$
1515
+ ```
1516
+
1517
+ Identifiers are double-quoted before being used in SQL.
1518
+
1519
+ This means camel-case database columns must exist as quoted identifiers. For example, `orderBy: 'updatedAt'` queries `"updatedAt"`. If the materialized view was created without a quoted alias, PostgreSQL stores the column as `updatedat`.
1520
+
1521
+ ### Response serialization
1522
+
1523
+ Responses use the same serialization behavior as generated routers:
1524
+
1525
+ - `BigInt` values become strings
1526
+ - `Decimal` values become strings
1527
+ - `Buffer` and `Uint8Array` values become base64 strings
1528
+ - `DateTime` values become ISO 8601 strings
1529
+
1530
+ ### Limitations
1531
+
1532
+ The materialized views router is intentionally small and read-only.
1533
+
1534
+ It does not support:
1535
+
1536
+ - Prisma `where`
1537
+ - Prisma `select`
1538
+ - Prisma `include`
1539
+ - Prisma guard shapes
1540
+ - OpenAPI generation
1541
+ - Fastify or Hono targets
1542
+ - refreshing materialized views
1543
+
1544
+ Use it for explicit read-only endpoints over known materialized views. For normal Prisma models, use the generated model routers.
1545
+
1330
1546
  ## Progressive Endpoint Composition (Express SSE)
1331
1547
 
1332
1548
  Progressive Endpoint Composition lets an Express read endpoint stream partial response fields over Server-Sent Events while still ending with a final result event.
@@ -49,6 +49,7 @@ const SHARED_FILES = [
49
49
  const EXPRESS_ONLY_FILES = [
50
50
  'autoIncludePlanner.ts',
51
51
  'autoIncludeRuntime.ts',
52
+ 'materializedRouter.ts',
52
53
  ];
53
54
  function resolveTemplateDir(subpath) {
54
55
  const fromSrc = path.join(__dirname, '..', '..', 'src', subpath);
@@ -1 +1 @@
1
- {"version":3,"file":"copyFiles.js","sourceRoot":"","sources":["../../src/utils/copyFiles.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"copyFiles.js","sourceRoot":"","sources":["../../src/utils/copyFiles.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "prisma-generator-express",
3
3
  "description": "Prisma generator for Express, Fastify, and Hono CRUD APIs with OpenAPI documentation",
4
- "version": "1.49.0",
4
+ "version": "1.50.0",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "license": "MIT",
@@ -1,5 +1,11 @@
1
1
  import express from 'express'
2
- import type { NextFunction, Request, RequestHandler, Response, Router } from 'express'
2
+ import type {
3
+ NextFunction,
4
+ Request,
5
+ RequestHandler,
6
+ Response,
7
+ Router,
8
+ } from 'express'
3
9
  import { HttpError, mapError, transformResult } from './operationRuntime'
4
10
 
5
11
  type SortDirection = 'asc' | 'desc'
@@ -7,11 +13,7 @@ type NullsOrder = 'first' | 'last'
7
13
 
8
14
  type OrderByDef =
9
15
  | string
10
- | {
11
- field: string
12
- direction?: SortDirection
13
- nulls?: NullsOrder
14
- }
16
+ | { field: string; direction?: SortDirection; nulls?: NullsOrder }
15
17
 
16
18
  type ViewDef = {
17
19
  relation: string
@@ -19,11 +21,18 @@ type ViewDef = {
19
21
  defaultLimit?: number
20
22
  maxLimit?: number
21
23
  orderBy?: OrderByDef
22
- authorize?: (req: Request, viewName: string, def: ViewDef) => void | Promise<void>
24
+ authorize?: (
25
+ req: Request,
26
+ viewName: string,
27
+ def: ViewDef,
28
+ ) => void | Promise<void>
23
29
  }
24
30
 
25
31
  type PrismaRawClient = {
26
- $queryRawUnsafe: <T = unknown>(sql: string, ...values: unknown[]) => Promise<T>
32
+ $queryRawUnsafe: <T = unknown>(
33
+ sql: string,
34
+ ...values: unknown[]
35
+ ) => Promise<T>
27
36
  }
28
37
 
29
38
  type MaterializedRouterOptions = {
@@ -39,7 +48,8 @@ type MaterializedRouterOptions = {
39
48
  const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/
40
49
 
41
50
  const quoteIdent = (name: string): string => {
42
- if (!IDENT_RE.test(name)) throw new Error('invalid identifier: ' + name)
51
+ if (!IDENT_RE.test(name))
52
+ throw new HttpError(400, 'invalid identifier: ' + name)
43
53
  return '"' + name.replace(/"/g, '""') + '"'
44
54
  }
45
55
 
@@ -54,7 +64,12 @@ const buildFqn = (def: ViewDef): string =>
54
64
  ? quoteIdent(def.schema) + '.' + quoteIdent(def.relation)
55
65
  : quoteIdent(def.relation)
56
66
 
57
- const clampInt = (v: unknown, fallback: number, min: number, max: number): number => {
67
+ const clampInt = (
68
+ v: unknown,
69
+ fallback: number,
70
+ min: number,
71
+ max: number,
72
+ ): number => {
58
73
  const n = Number(v ?? fallback)
59
74
  if (!Number.isFinite(n)) return fallback
60
75
  return Math.min(Math.max(Math.trunc(n), min), max)
@@ -63,23 +78,21 @@ const clampInt = (v: unknown, fallback: number, min: number, max: number): numbe
63
78
  const normalizeDirection = (value: unknown): 'ASC' | 'DESC' => {
64
79
  if (value === undefined || value === 'asc' || value === 'ASC') return 'ASC'
65
80
  if (value === 'desc' || value === 'DESC') return 'DESC'
66
- throw new Error('invalid sort direction')
81
+ throw new HttpError(400, 'invalid sort direction')
67
82
  }
68
83
 
69
- const normalizeNulls = (value: unknown): '' | ' NULLS FIRST' | ' NULLS LAST' => {
84
+ const normalizeNulls = (
85
+ value: unknown,
86
+ ): '' | ' NULLS FIRST' | ' NULLS LAST' => {
70
87
  if (value === undefined) return ''
71
88
  if (value === 'first' || value === 'FIRST') return ' NULLS FIRST'
72
89
  if (value === 'last' || value === 'LAST') return ' NULLS LAST'
73
- throw new Error('invalid nulls order')
90
+ throw new HttpError(400, 'invalid nulls order')
74
91
  }
75
92
 
76
93
  const buildOrderBy = (orderBy?: OrderByDef): string => {
77
94
  if (!orderBy) return ''
78
-
79
- if (typeof orderBy === 'string') {
80
- return ' ORDER BY ' + quoteIdent(orderBy)
81
- }
82
-
95
+ if (typeof orderBy === 'string') return ' ORDER BY ' + quoteIdent(orderBy)
83
96
  return (
84
97
  ' ORDER BY ' +
85
98
  quoteIdent(orderBy.field) +
@@ -89,7 +102,9 @@ const buildOrderBy = (orderBy?: OrderByDef): string => {
89
102
  )
90
103
  }
91
104
 
92
- export const materializedViewsRouter = (opts: MaterializedRouterOptions): Router => {
105
+ export const materializedViewsRouter = (
106
+ opts: MaterializedRouterOptions,
107
+ ): Router => {
93
108
  const router = express.Router()
94
109
  const basePath = normalizeBasePath(opts.basePath)
95
110
  const defaultLimit = opts.defaultLimit ?? 50
@@ -104,14 +119,8 @@ export const materializedViewsRouter = (opts: MaterializedRouterOptions): Router
104
119
  try {
105
120
  const viewName = req.params.viewName
106
121
  const def = opts.views[viewName]
107
-
108
- if (!def) {
109
- throw new HttpError(404, 'unknown view')
110
- }
111
-
112
- if (def.authorize) {
113
- await def.authorize(req, viewName, def)
114
- }
122
+ if (!def) throw new HttpError(404, 'unknown view')
123
+ if (def.authorize) await def.authorize(req, viewName, def)
115
124
 
116
125
  const take = clampInt(
117
126
  req.query.take,
@@ -119,11 +128,13 @@ export const materializedViewsRouter = (opts: MaterializedRouterOptions): Router
119
128
  1,
120
129
  def.maxLimit ?? maxLimit,
121
130
  )
122
-
123
131
  const skip = clampInt(req.query.skip, 0, 0, Number.MAX_SAFE_INTEGER)
124
132
 
125
133
  if (skip > 0 && !def.orderBy) {
126
- throw new HttpError(400, 'skip requires orderBy for deterministic pagination')
134
+ throw new HttpError(
135
+ 400,
136
+ 'skip requires orderBy for deterministic pagination',
137
+ )
127
138
  }
128
139
 
129
140
  const sql =
@@ -132,8 +143,11 @@ export const materializedViewsRouter = (opts: MaterializedRouterOptions): Router
132
143
  buildOrderBy(def.orderBy) +
133
144
  ' LIMIT $1 OFFSET $2'
134
145
 
135
- const rows = await opts.prisma.$queryRawUnsafe<unknown[]>(sql, take, skip)
136
-
146
+ const rows = await opts.prisma.$queryRawUnsafe<unknown[]>(
147
+ sql,
148
+ take,
149
+ skip,
150
+ )
137
151
  res.locals.data = transformResult(rows)
138
152
  next()
139
153
  } catch (err) {
@@ -146,23 +160,28 @@ export const materializedViewsRouter = (opts: MaterializedRouterOptions): Router
146
160
  },
147
161
  )
148
162
 
149
- router.use((err: unknown, _req: Request, res: Response, next: NextFunction) => {
150
- const httpError =
151
- err instanceof HttpError
152
- ? err
153
- : err && typeof err === 'object' && typeof (err as { status?: number }).status === 'number'
154
- ? new HttpError(
155
- (err as { status: number }).status,
156
- (err as { message?: string }).message || 'Internal server error',
157
- )
158
- : mapError(err)
159
-
160
- if (!res.headersSent) {
161
- return res.status(httpError.status).json({ message: httpError.message })
162
- }
163
+ router.use(
164
+ (err: unknown, _req: Request, res: Response, next: NextFunction) => {
165
+ const httpError =
166
+ err instanceof HttpError
167
+ ? err
168
+ : err &&
169
+ typeof err === 'object' &&
170
+ typeof (err as { status?: number }).status === 'number'
171
+ ? new HttpError(
172
+ (err as { status: number }).status,
173
+ (err as { message?: string }).message ||
174
+ 'Internal server error',
175
+ )
176
+ : mapError(err)
177
+
178
+ if (!res.headersSent) {
179
+ return res.status(httpError.status).json({ message: httpError.message })
180
+ }
163
181
 
164
- next(err)
165
- })
182
+ next(err)
183
+ },
184
+ )
166
185
 
167
186
  return router
168
- }
187
+ }
@@ -18,6 +18,7 @@ const SHARED_FILES = [
18
18
  const EXPRESS_ONLY_FILES = [
19
19
  'autoIncludePlanner.ts',
20
20
  'autoIncludeRuntime.ts',
21
+ 'materializedRouter.ts',
21
22
  ]
22
23
 
23
24
  interface CopyFileOptions {