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}/
|
|
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.
|
|
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}/
|
|
482
|
+
const path = basePath ? \`\${basePath}/each\` : '/each'
|
|
483
483
|
router.post(
|
|
484
484
|
path,
|
|
485
485
|
setShape(opConfig),
|