prisma-generator-express 1.48.0 → 1.49.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.
@@ -463,7 +463,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
463
463
  if (config.updateEach) {
464
464
  const opConfig: OperationConfigLike = (config.updateEach as OperationConfigLike | undefined) ?? defaultOpConfig
465
465
  const { before = [], after = [] } = opConfig
466
- const path = basePath ? \`\${basePath}/updateEach\` : '/updateEach'
466
+ const path = basePath ? \`\${basePath}/each\` : '/each'
467
467
  router.post(
468
468
  path,
469
469
  setShape(opConfig),
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.48.0",
4
+ "version": "1.49.0",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "license": "MIT",
@@ -0,0 +1,168 @@
1
+ import express from 'express'
2
+ import type { NextFunction, Request, RequestHandler, Response, Router } from 'express'
3
+ import { HttpError, mapError, transformResult } from './operationRuntime'
4
+
5
+ type SortDirection = 'asc' | 'desc'
6
+ type NullsOrder = 'first' | 'last'
7
+
8
+ type OrderByDef =
9
+ | string
10
+ | {
11
+ field: string
12
+ direction?: SortDirection
13
+ nulls?: NullsOrder
14
+ }
15
+
16
+ type ViewDef = {
17
+ relation: string
18
+ schema?: string
19
+ defaultLimit?: number
20
+ maxLimit?: number
21
+ orderBy?: OrderByDef
22
+ authorize?: (req: Request, viewName: string, def: ViewDef) => void | Promise<void>
23
+ }
24
+
25
+ type PrismaRawClient = {
26
+ $queryRawUnsafe: <T = unknown>(sql: string, ...values: unknown[]) => Promise<T>
27
+ }
28
+
29
+ type MaterializedRouterOptions = {
30
+ prisma: PrismaRawClient
31
+ views: Record<string, ViewDef>
32
+ basePath?: string
33
+ defaultLimit?: number
34
+ maxLimit?: number
35
+ before?: RequestHandler[]
36
+ after?: RequestHandler[]
37
+ }
38
+
39
+ const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/
40
+
41
+ const quoteIdent = (name: string): string => {
42
+ if (!IDENT_RE.test(name)) throw new Error('invalid identifier: ' + name)
43
+ return '"' + name.replace(/"/g, '""') + '"'
44
+ }
45
+
46
+ const normalizeBasePath = (value?: string): string => {
47
+ if (!value || value === '/') return ''
48
+ const prefixed = value.startsWith('/') ? value : '/' + value
49
+ return prefixed.replace(/\/+$/, '')
50
+ }
51
+
52
+ const buildFqn = (def: ViewDef): string =>
53
+ def.schema
54
+ ? quoteIdent(def.schema) + '.' + quoteIdent(def.relation)
55
+ : quoteIdent(def.relation)
56
+
57
+ const clampInt = (v: unknown, fallback: number, min: number, max: number): number => {
58
+ const n = Number(v ?? fallback)
59
+ if (!Number.isFinite(n)) return fallback
60
+ return Math.min(Math.max(Math.trunc(n), min), max)
61
+ }
62
+
63
+ const normalizeDirection = (value: unknown): 'ASC' | 'DESC' => {
64
+ if (value === undefined || value === 'asc' || value === 'ASC') return 'ASC'
65
+ if (value === 'desc' || value === 'DESC') return 'DESC'
66
+ throw new Error('invalid sort direction')
67
+ }
68
+
69
+ const normalizeNulls = (value: unknown): '' | ' NULLS FIRST' | ' NULLS LAST' => {
70
+ if (value === undefined) return ''
71
+ if (value === 'first' || value === 'FIRST') return ' NULLS FIRST'
72
+ if (value === 'last' || value === 'LAST') return ' NULLS LAST'
73
+ throw new Error('invalid nulls order')
74
+ }
75
+
76
+ const buildOrderBy = (orderBy?: OrderByDef): string => {
77
+ if (!orderBy) return ''
78
+
79
+ if (typeof orderBy === 'string') {
80
+ return ' ORDER BY ' + quoteIdent(orderBy)
81
+ }
82
+
83
+ return (
84
+ ' ORDER BY ' +
85
+ quoteIdent(orderBy.field) +
86
+ ' ' +
87
+ normalizeDirection(orderBy.direction) +
88
+ normalizeNulls(orderBy.nulls)
89
+ )
90
+ }
91
+
92
+ export const materializedViewsRouter = (opts: MaterializedRouterOptions): Router => {
93
+ const router = express.Router()
94
+ const basePath = normalizeBasePath(opts.basePath)
95
+ const defaultLimit = opts.defaultLimit ?? 50
96
+ const maxLimit = opts.maxLimit ?? 1000
97
+ const before = opts.before ?? []
98
+ const after = opts.after ?? []
99
+
100
+ router.get(
101
+ basePath + '/:viewName',
102
+ ...before,
103
+ async (req: Request, res: Response, next: NextFunction) => {
104
+ try {
105
+ const viewName = req.params.viewName
106
+ 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
+ }
115
+
116
+ const take = clampInt(
117
+ req.query.take,
118
+ def.defaultLimit ?? defaultLimit,
119
+ 1,
120
+ def.maxLimit ?? maxLimit,
121
+ )
122
+
123
+ const skip = clampInt(req.query.skip, 0, 0, Number.MAX_SAFE_INTEGER)
124
+
125
+ if (skip > 0 && !def.orderBy) {
126
+ throw new HttpError(400, 'skip requires orderBy for deterministic pagination')
127
+ }
128
+
129
+ const sql =
130
+ 'SELECT * FROM ' +
131
+ buildFqn(def) +
132
+ buildOrderBy(def.orderBy) +
133
+ ' LIMIT $1 OFFSET $2'
134
+
135
+ const rows = await opts.prisma.$queryRawUnsafe<unknown[]>(sql, take, skip)
136
+
137
+ res.locals.data = transformResult(rows)
138
+ next()
139
+ } catch (err) {
140
+ next(mapError(err))
141
+ }
142
+ },
143
+ ...after,
144
+ (_req: Request, res: Response) => {
145
+ res.json(res.locals.data)
146
+ },
147
+ )
148
+
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
+
164
+ next(err)
165
+ })
166
+
167
+ return router
168
+ }
@@ -479,7 +479,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
479
479
  if (config.updateEach) {
480
480
  const opConfig: OperationConfigLike = (config.updateEach as OperationConfigLike | undefined) ?? defaultOpConfig
481
481
  const { before = [], after = [] } = opConfig
482
- const path = basePath ? \`\${basePath}/updateEach\` : '/updateEach'
482
+ const path = basePath ? \`\${basePath}/each\` : '/each'
483
483
  router.post(
484
484
  path,
485
485
  setShape(opConfig),