prisma-generator-express 1.19.0 → 1.20.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/package.json +4 -6
- package/src/bin.ts +2 -0
- package/src/constants.ts +1 -0
- package/src/generators/generateImportPrismaStatement.ts +78 -0
- package/src/generators/generateQueryBuilderHelper.ts +138 -0
- package/src/generators/generateRouter.ts +352 -0
- package/src/generators/generateUnifiedDocs.ts +168 -0
- package/src/generators/generateUnifiedHandler.ts +469 -0
- package/src/generators/generateUnifiedScalarUI.ts +1409 -0
- package/src/index.ts +100 -0
- package/src/utils/copyFiles.ts +134 -0
- package/src/utils/strings.ts +7 -0
- package/src/utils/writeFileSafely.ts +83 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
export function generateUnifiedDocs(models: string[]): string {
|
|
2
|
+
const imports = models
|
|
3
|
+
.map((model) => `import { ${model}Docs } from './${model}/${model}Docs'`)
|
|
4
|
+
.join('\n')
|
|
5
|
+
|
|
6
|
+
return `${imports}
|
|
7
|
+
import { Request, Response, RequestHandler } from 'express'
|
|
8
|
+
import type { RouteConfig } from './routeConfig'
|
|
9
|
+
|
|
10
|
+
const _env = typeof process !== 'undefined' && process.env ? process.env : {} as Record<string, string | undefined>
|
|
11
|
+
|
|
12
|
+
const docsHandlers: Record<string, (config: any) => (req: Request, res: Response) => any> = {
|
|
13
|
+
${models.map((model) => ` ${model}: ${model}Docs`).join(',\n')}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type DocsUI = 'docs' | 'scalar' | 'json' | 'yaml' | 'playground'
|
|
17
|
+
|
|
18
|
+
interface ModelDocsConfig extends RouteConfig {
|
|
19
|
+
docsTitle?: string
|
|
20
|
+
docsUi?: DocsUI
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CombinedDocsConfig {
|
|
24
|
+
disableOpenApi?: boolean
|
|
25
|
+
title?: string
|
|
26
|
+
description?: string
|
|
27
|
+
basePath?: string
|
|
28
|
+
version?: string
|
|
29
|
+
modelConfigs: {
|
|
30
|
+
[modelName: string]: ModelDocsConfig
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function escapeHtml(input: string) {
|
|
35
|
+
return input
|
|
36
|
+
.replaceAll('&', '&')
|
|
37
|
+
.replaceAll('<', '<')
|
|
38
|
+
.replaceAll('>', '>')
|
|
39
|
+
.replaceAll('"', '"')
|
|
40
|
+
.replaceAll("'", ''')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function removeTrailingSlash(p: string): string {
|
|
44
|
+
if (p === '/') return ''
|
|
45
|
+
return p.endsWith('/') ? p.slice(0, -1) : p
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isOpenApiDisabled(disableOpenApi?: boolean) {
|
|
49
|
+
if (disableOpenApi === true) return true
|
|
50
|
+
if (disableOpenApi === false) return false
|
|
51
|
+
return _env.DISABLE_OPENAPI === 'true' || _env.NODE_ENV === 'production'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isPlaygroundAvailable(config?: ModelDocsConfig) {
|
|
55
|
+
if (_env.NODE_ENV === 'production') return false
|
|
56
|
+
if (!config) return true
|
|
57
|
+
if (config.queryBuilder === false) return false
|
|
58
|
+
if (typeof config.queryBuilder === 'object' && config.queryBuilder.enabled === false) return false
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function generateCombinedDocs(config: CombinedDocsConfig) {
|
|
63
|
+
const title = config.title || 'API Documentation'
|
|
64
|
+
const description = config.description || ''
|
|
65
|
+
const version = config.version || ''
|
|
66
|
+
|
|
67
|
+
return (req: Request, res: Response) => {
|
|
68
|
+
const registeredModels = Object.keys(config.modelConfigs).filter((m) => {
|
|
69
|
+
const cfg = config.modelConfigs[m]
|
|
70
|
+
return m in docsHandlers && !isOpenApiDisabled(cfg?.disableOpenApi ?? config.disableOpenApi)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
if (registeredModels.length === 0) {
|
|
74
|
+
return res.status(404).send('OpenAPI documentation is disabled')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const basePath = removeTrailingSlash(config.basePath || '/docs')
|
|
78
|
+
const generatedAt = new Date().toISOString()
|
|
79
|
+
|
|
80
|
+
const html = \`<!DOCTYPE html>
|
|
81
|
+
<html lang="en">
|
|
82
|
+
<head>
|
|
83
|
+
<meta charset="utf-8" />
|
|
84
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
85
|
+
<title>\${escapeHtml(title)}</title>
|
|
86
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
87
|
+
</head>
|
|
88
|
+
<body class="m-0 bg-white text-gray-900 font-serif leading-normal">
|
|
89
|
+
<div class="max-w-[980px] mx-auto px-7 pt-10 pb-16">
|
|
90
|
+
<div class="border-b-2 border-gray-900 pb-3.5 mb-[18px]">
|
|
91
|
+
<div class="text-[28px] font-bold tracking-wide">\${escapeHtml(title)}</div>
|
|
92
|
+
\${description ? '<div class="mt-1.5 text-gray-500 text-sm">' + escapeHtml(description) + '</div>' : ''}
|
|
93
|
+
<div class="mt-3 flex gap-x-5 text-[13px] text-gray-500">
|
|
94
|
+
\${version ? '<div>Version: ' + escapeHtml(version) + '</div>' : ''}
|
|
95
|
+
<div>Generated: \${escapeHtml(generatedAt)}</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div class="mt-[22px]">
|
|
100
|
+
<h2 class="m-0 mb-2.5 text-lg border-t border-gray-300 pt-3.5">Models</h2>
|
|
101
|
+
<table class="w-full border-collapse text-[13px]">
|
|
102
|
+
<thead>
|
|
103
|
+
<tr>
|
|
104
|
+
<th class="text-left py-2 px-2 border-b border-gray-300 align-top font-bold">Model</th>
|
|
105
|
+
<th class="text-left py-2 px-2 border-b border-gray-300 align-top font-bold">Documentation</th>
|
|
106
|
+
<th class="text-left py-2 px-2 border-b border-gray-300 align-top font-bold">Views</th>
|
|
107
|
+
</tr>
|
|
108
|
+
</thead>
|
|
109
|
+
<tbody>
|
|
110
|
+
\${registeredModels.map((m) => {
|
|
111
|
+
const lower = m.toLowerCase()
|
|
112
|
+
const docsUrl = \\\`\\\${basePath}/\\\${lower}\\\`
|
|
113
|
+
const scalarUrl = \\\`\\\${basePath}/\\\${lower}?ui=scalar\\\`
|
|
114
|
+
const jsonUrl = \\\`\\\${basePath}/\\\${lower}?ui=json\\\`
|
|
115
|
+
const yamlUrl = \\\`\\\${basePath}/\\\${lower}?ui=yaml\\\`
|
|
116
|
+
const playgroundUrl = \\\`\\\${basePath}/\\\${lower}?ui=playground\\\`
|
|
117
|
+
const modelCfg = config.modelConfigs[m]
|
|
118
|
+
const modelPlayground = isPlaygroundAvailable(modelCfg)
|
|
119
|
+
const playgroundLink = modelPlayground
|
|
120
|
+
? \\\`, <a href="\\\${playgroundUrl}" class="text-inherit underline">playground</a>\\\`
|
|
121
|
+
: ''
|
|
122
|
+
return \\\`
|
|
123
|
+
<tr>
|
|
124
|
+
<td class="text-left py-2 px-2 border-b border-gray-300 align-top">\\\${escapeHtml(m)}</td>
|
|
125
|
+
<td class="text-left py-2 px-2 border-b border-gray-300 align-top"><a href="\\\${docsUrl}" class="text-inherit underline">\\\${escapeHtml(docsUrl)}</a></td>
|
|
126
|
+
<td class="text-left py-2 px-2 border-b border-gray-300 align-top">
|
|
127
|
+
<a href="\\\${scalarUrl}" class="text-inherit underline">scalar</a>,
|
|
128
|
+
<a href="\\\${jsonUrl}" class="text-inherit underline">json</a>,
|
|
129
|
+
<a href="\\\${yamlUrl}" class="text-inherit underline">yaml</a>\\\${playgroundLink}
|
|
130
|
+
</td>
|
|
131
|
+
</tr>
|
|
132
|
+
\\\`
|
|
133
|
+
}).join('')}
|
|
134
|
+
</tbody>
|
|
135
|
+
</table>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</body>
|
|
139
|
+
</html>\`
|
|
140
|
+
|
|
141
|
+
res.type('html').send(html)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function registerModelDocs(
|
|
146
|
+
app: any,
|
|
147
|
+
basePath: string = '/docs',
|
|
148
|
+
configs: CombinedDocsConfig['modelConfigs'] = {},
|
|
149
|
+
options?: { disableOpenApi?: boolean }
|
|
150
|
+
) {
|
|
151
|
+
const normalizedBase = removeTrailingSlash(basePath)
|
|
152
|
+
const registeredModels = Object.keys(configs).filter((m) => {
|
|
153
|
+
const cfg = configs[m]
|
|
154
|
+
return m in docsHandlers && !isOpenApiDisabled(cfg?.disableOpenApi ?? options?.disableOpenApi)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
if (registeredModels.length === 0) return
|
|
158
|
+
|
|
159
|
+
registeredModels.forEach((model) => {
|
|
160
|
+
const handler = docsHandlers[model]
|
|
161
|
+
const cfg = configs[model] || {}
|
|
162
|
+
const path = \\\`\\\${normalizedBase}/\\\${model.toLowerCase()}\\\`
|
|
163
|
+
console.log(\\\` Registered docs: \\\${path}\\\`)
|
|
164
|
+
app.get(path, handler(cfg))
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
`
|
|
168
|
+
}
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { DMMF } from '@prisma/generator-helper'
|
|
2
|
+
|
|
3
|
+
export interface UnifiedHandlerOptions {
|
|
4
|
+
model: DMMF.Model
|
|
5
|
+
prismaImportStatement: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function generateUnifiedHandler(options: UnifiedHandlerOptions): string {
|
|
9
|
+
const { model, prismaImportStatement } = options
|
|
10
|
+
const modelName = model.name
|
|
11
|
+
const modelNameLower = modelName.charAt(0).toLowerCase() + modelName.slice(1)
|
|
12
|
+
const importPath =
|
|
13
|
+
prismaImportStatement.match(/from ['"](.+?)['"]/)?.[1] || ''
|
|
14
|
+
|
|
15
|
+
return `
|
|
16
|
+
import { Prisma, PrismaClient } from '${importPath}'
|
|
17
|
+
import { Request, Response, NextFunction } from 'express'
|
|
18
|
+
import { sanitizeKeys } from '../misc'
|
|
19
|
+
|
|
20
|
+
let _speedExtension: ((opts: any) => any) | null = null
|
|
21
|
+
|
|
22
|
+
const _prismasqlModule = 'prisma-' + 'sql'
|
|
23
|
+
const _prismasqlReady = (async () => {
|
|
24
|
+
try {
|
|
25
|
+
const mod = await import(_prismasqlModule)
|
|
26
|
+
_speedExtension = mod.speedExtension ?? mod.default?.speedExtension ?? null
|
|
27
|
+
} catch (err: any) {
|
|
28
|
+
const code = err?.code
|
|
29
|
+
if (code !== 'MODULE_NOT_FOUND' && code !== 'ERR_MODULE_NOT_FOUND') {
|
|
30
|
+
console.warn('[prisma-generator-express] prisma-sql initialization failed:', err)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
})()
|
|
34
|
+
|
|
35
|
+
const _extendedClients = new WeakMap<object, WeakMap<object, PrismaClient>>()
|
|
36
|
+
|
|
37
|
+
const DISTINCT_COUNT_LIMIT = 100000
|
|
38
|
+
|
|
39
|
+
export class HttpError extends Error {
|
|
40
|
+
status: number
|
|
41
|
+
constructor(status: number, message: string) {
|
|
42
|
+
super(message)
|
|
43
|
+
this.name = 'HttpError'
|
|
44
|
+
this.status = status
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const PRISMA_ERROR_MAP: Record<string, { status: number; message: string }> = {
|
|
49
|
+
P2000: { status: 400, message: 'Value too long for column' },
|
|
50
|
+
P2001: { status: 404, message: 'Record not found' },
|
|
51
|
+
P2002: { status: 409, message: 'Unique constraint violation' },
|
|
52
|
+
P2003: { status: 400, message: 'Foreign key constraint failed' },
|
|
53
|
+
P2004: { status: 400, message: 'Constraint failed on the database' },
|
|
54
|
+
P2005: { status: 400, message: 'Invalid field value' },
|
|
55
|
+
P2006: { status: 400, message: 'Invalid value provided' },
|
|
56
|
+
P2007: { status: 400, message: 'Data validation error' },
|
|
57
|
+
P2008: { status: 400, message: 'Failed to parse the query' },
|
|
58
|
+
P2009: { status: 400, message: 'Failed to validate the query' },
|
|
59
|
+
P2010: { status: 500, message: 'Raw query failed' },
|
|
60
|
+
P2011: { status: 400, message: 'Null constraint violation' },
|
|
61
|
+
P2012: { status: 400, message: 'Missing required value' },
|
|
62
|
+
P2013: { status: 400, message: 'Missing required argument' },
|
|
63
|
+
P2014: { status: 400, message: 'Required relation violation' },
|
|
64
|
+
P2015: { status: 404, message: 'Related record not found' },
|
|
65
|
+
P2016: { status: 400, message: 'Query interpretation error' },
|
|
66
|
+
P2017: { status: 400, message: 'Records not connected' },
|
|
67
|
+
P2018: { status: 404, message: 'Required connected record not found' },
|
|
68
|
+
P2019: { status: 400, message: 'Input error' },
|
|
69
|
+
P2020: { status: 400, message: 'Value out of range for the field type' },
|
|
70
|
+
P2021: { status: 500, message: 'Table does not exist in the database' },
|
|
71
|
+
P2022: { status: 500, message: 'Column does not exist in the database' },
|
|
72
|
+
P2023: { status: 500, message: 'Inconsistent column data' },
|
|
73
|
+
P2024: { status: 503, message: 'Connection pool timeout' },
|
|
74
|
+
P2025: { status: 404, message: 'Record not found' },
|
|
75
|
+
P2026: { status: 501, message: 'Feature not supported by the current database provider' },
|
|
76
|
+
P2028: { status: 500, message: 'Transaction API error' },
|
|
77
|
+
P2030: { status: 400, message: 'Cannot find a fulltext index for the search' },
|
|
78
|
+
P2033: { status: 400, message: 'Number out of range for the field type' },
|
|
79
|
+
P2034: { status: 409, message: 'Transaction conflict, please retry' },
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function getExtendedClient(req: Request): Promise<PrismaClient> {
|
|
83
|
+
const base = (req as any).prisma as PrismaClient
|
|
84
|
+
if (!base) {
|
|
85
|
+
throw new HttpError(500, 'PrismaClient not found on request. Set req.prisma in middleware.')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await _prismasqlReady
|
|
89
|
+
|
|
90
|
+
if (!_speedExtension) return base
|
|
91
|
+
|
|
92
|
+
const connector = (req as any).postgres || (req as any).sqlite
|
|
93
|
+
if (!connector) return base
|
|
94
|
+
|
|
95
|
+
if (typeof connector === 'object' && connector !== null) {
|
|
96
|
+
const innerMap = _extendedClients.get(connector)
|
|
97
|
+
if (innerMap) {
|
|
98
|
+
const cached = innerMap.get(base)
|
|
99
|
+
if (cached) return cached
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const extended = base.$extends(_speedExtension({
|
|
105
|
+
postgres: (req as any).postgres,
|
|
106
|
+
sqlite: (req as any).sqlite,
|
|
107
|
+
debug: process.env.DEBUG === 'true'
|
|
108
|
+
})) as unknown as PrismaClient
|
|
109
|
+
|
|
110
|
+
if (typeof connector === 'object' && connector !== null) {
|
|
111
|
+
let innerMap = _extendedClients.get(connector)
|
|
112
|
+
if (!innerMap) {
|
|
113
|
+
innerMap = new WeakMap<object, PrismaClient>()
|
|
114
|
+
_extendedClients.set(connector, innerMap)
|
|
115
|
+
}
|
|
116
|
+
innerMap.set(base, extended)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return extended
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.warn('[speedExtension] Failed to initialize, using base client:', error)
|
|
122
|
+
return base
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function handleError(error: unknown, next: NextFunction): void {
|
|
127
|
+
if (error instanceof HttpError) {
|
|
128
|
+
next(error)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (error && typeof error === 'object' && 'name' in error && error.name === 'ShapeError') {
|
|
133
|
+
next(new HttpError(400, (error as Error).message))
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (error && typeof error === 'object' && 'name' in error && error.name === 'CallerError') {
|
|
138
|
+
next(new HttpError(400, (error as Error).message))
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (error && typeof error === 'object' && 'name' in error && error.name === 'PolicyError') {
|
|
143
|
+
next(new HttpError(403, (error as Error).message))
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (error && typeof error === 'object' && 'code' in error) {
|
|
148
|
+
const code = (error as any).code as string
|
|
149
|
+
const mapped = PRISMA_ERROR_MAP[code]
|
|
150
|
+
if (mapped) {
|
|
151
|
+
next(new HttpError(mapped.status, mapped.message))
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
if (typeof code === 'string' && code.startsWith('P')) {
|
|
155
|
+
next(new HttpError(500, 'Database operation failed'))
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (error && typeof error === 'object' && 'name' in error) {
|
|
161
|
+
const name = (error as any).name
|
|
162
|
+
if (name === 'PrismaClientValidationError') {
|
|
163
|
+
next(new HttpError(400, 'Invalid query parameters'))
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.error('[prisma-generator-express] Unhandled error:', error)
|
|
169
|
+
next(new HttpError(500, 'Internal server error'))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function safeParseBody(req: Request): Record<string, any> {
|
|
173
|
+
const body = req.body
|
|
174
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
175
|
+
throw new HttpError(400, 'Request body must be a JSON object')
|
|
176
|
+
}
|
|
177
|
+
return sanitizeKeys(body as Record<string, any>)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function requireBodyField(body: Record<string, any>, field: string): void {
|
|
181
|
+
if (!(field in body) || body[field] === undefined) {
|
|
182
|
+
throw new HttpError(400, 'Missing required field: ' + field)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function applyPaginationLimits(query: Record<string, any>, res: Response): Record<string, any> {
|
|
187
|
+
const routeConfig = res.locals.routeConfig
|
|
188
|
+
const pagination = routeConfig?.pagination
|
|
189
|
+
if (!pagination) return query
|
|
190
|
+
|
|
191
|
+
const result = { ...query }
|
|
192
|
+
|
|
193
|
+
if (result.take === undefined && pagination.defaultLimit !== undefined) {
|
|
194
|
+
result.take = pagination.defaultLimit
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (pagination.maxLimit !== undefined && result.take !== undefined) {
|
|
198
|
+
const takeNum = Number(result.take)
|
|
199
|
+
if (Math.abs(takeNum) > pagination.maxLimit) {
|
|
200
|
+
result.take = takeNum < 0 ? -pagination.maxLimit : pagination.maxLimit
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return result
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizeDistinct(value: unknown): string[] {
|
|
208
|
+
if (typeof value === 'string') return [value]
|
|
209
|
+
if (Array.isArray(value)) return value.filter((v): v is string => typeof v === 'string')
|
|
210
|
+
return []
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function assertGuard(delegate: any): void {
|
|
214
|
+
if (typeof delegate.guard !== 'function') {
|
|
215
|
+
throw new HttpError(500, 'Guard shapes require prisma-guard extension on PrismaClient. Install: npm install prisma-guard, then extend your client with guardExtension().')
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
${generateReadHandlers(modelName, modelNameLower)}
|
|
220
|
+
|
|
221
|
+
${generateWriteHandlers(modelName, modelNameLower)}
|
|
222
|
+
`
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function generateReadHandlers(
|
|
226
|
+
modelName: string,
|
|
227
|
+
modelNameLower: string,
|
|
228
|
+
): string {
|
|
229
|
+
const standardReadOps = [
|
|
230
|
+
'findFirst',
|
|
231
|
+
'findUnique',
|
|
232
|
+
'findUniqueOrThrow',
|
|
233
|
+
'findFirstOrThrow',
|
|
234
|
+
'count',
|
|
235
|
+
'aggregate',
|
|
236
|
+
'groupBy',
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
const standardHandlers = standardReadOps
|
|
240
|
+
.map((op) => {
|
|
241
|
+
const functionName = `${modelName}${op.charAt(0).toUpperCase() + op.slice(1)}`
|
|
242
|
+
|
|
243
|
+
return `
|
|
244
|
+
export async function ${functionName}(
|
|
245
|
+
req: Request,
|
|
246
|
+
res: Response,
|
|
247
|
+
next: NextFunction
|
|
248
|
+
) {
|
|
249
|
+
try {
|
|
250
|
+
const query = res.locals.parsedQuery || {}
|
|
251
|
+
const extended = await getExtendedClient(req)
|
|
252
|
+
const shape = res.locals.guardShape
|
|
253
|
+
|
|
254
|
+
let data
|
|
255
|
+
if (shape) {
|
|
256
|
+
assertGuard(extended.${modelNameLower})
|
|
257
|
+
const caller = res.locals.guardCaller
|
|
258
|
+
data = await extended.${modelNameLower}.guard(shape, caller).${op}(query)
|
|
259
|
+
} else {
|
|
260
|
+
data = await extended.${modelNameLower}.${op}(query)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
res.locals.data = data
|
|
264
|
+
next()
|
|
265
|
+
} catch (error: unknown) {
|
|
266
|
+
handleError(error, next)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
`
|
|
270
|
+
})
|
|
271
|
+
.join('\n')
|
|
272
|
+
|
|
273
|
+
const findManyHandler = `
|
|
274
|
+
export async function ${modelName}FindMany(
|
|
275
|
+
req: Request,
|
|
276
|
+
res: Response,
|
|
277
|
+
next: NextFunction
|
|
278
|
+
) {
|
|
279
|
+
try {
|
|
280
|
+
const rawQuery = res.locals.parsedQuery || {}
|
|
281
|
+
const query = applyPaginationLimits(rawQuery, res)
|
|
282
|
+
const extended = await getExtendedClient(req)
|
|
283
|
+
const shape = res.locals.guardShape
|
|
284
|
+
|
|
285
|
+
let data
|
|
286
|
+
if (shape) {
|
|
287
|
+
assertGuard(extended.${modelNameLower})
|
|
288
|
+
const caller = res.locals.guardCaller
|
|
289
|
+
data = await extended.${modelNameLower}.guard(shape, caller).findMany(query)
|
|
290
|
+
} else {
|
|
291
|
+
data = await extended.${modelNameLower}.findMany(query)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
res.locals.data = data
|
|
295
|
+
next()
|
|
296
|
+
} catch (error: unknown) {
|
|
297
|
+
handleError(error, next)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
`
|
|
301
|
+
|
|
302
|
+
return findManyHandler + '\n' + standardHandlers
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function generateWriteHandlers(
|
|
306
|
+
modelName: string,
|
|
307
|
+
modelNameLower: string,
|
|
308
|
+
): string {
|
|
309
|
+
const writeOps: {
|
|
310
|
+
name: string
|
|
311
|
+
method: string
|
|
312
|
+
requiredFields?: string[]
|
|
313
|
+
}[] = [
|
|
314
|
+
{ name: 'Create', method: 'create' },
|
|
315
|
+
{ name: 'CreateMany', method: 'createMany' },
|
|
316
|
+
{ name: 'CreateManyAndReturn', method: 'createManyAndReturn' },
|
|
317
|
+
{ name: 'Update', method: 'update' },
|
|
318
|
+
{
|
|
319
|
+
name: 'UpdateMany',
|
|
320
|
+
method: 'updateMany',
|
|
321
|
+
requiredFields: ['where', 'data'],
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: 'UpdateManyAndReturn',
|
|
325
|
+
method: 'updateManyAndReturn',
|
|
326
|
+
requiredFields: ['where', 'data'],
|
|
327
|
+
},
|
|
328
|
+
{ name: 'Delete', method: 'delete' },
|
|
329
|
+
{ name: 'DeleteMany', method: 'deleteMany', requiredFields: ['where'] },
|
|
330
|
+
{ name: 'Upsert', method: 'upsert' },
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
writeOps
|
|
335
|
+
.map((op) => {
|
|
336
|
+
const functionName = `${modelName}${op.name}`
|
|
337
|
+
const validationLines = (op.requiredFields || [])
|
|
338
|
+
.map((field) => ` requireBodyField(body, '${field}')`)
|
|
339
|
+
.join('\n')
|
|
340
|
+
|
|
341
|
+
return `
|
|
342
|
+
export async function ${functionName}(req: Request, res: Response, next: NextFunction) {
|
|
343
|
+
try {
|
|
344
|
+
const body = safeParseBody(req)
|
|
345
|
+
${validationLines ? validationLines + '\n' : ''} const extended = await getExtendedClient(req)
|
|
346
|
+
const shape = res.locals.guardShape
|
|
347
|
+
|
|
348
|
+
let data
|
|
349
|
+
if (shape) {
|
|
350
|
+
assertGuard(extended.${modelNameLower})
|
|
351
|
+
const caller = res.locals.guardCaller
|
|
352
|
+
data = await extended.${modelNameLower}.guard(shape, caller).${op.method}(body)
|
|
353
|
+
} else {
|
|
354
|
+
data = await extended.${modelNameLower}.${op.method}(body)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
res.locals.data = data
|
|
358
|
+
next()
|
|
359
|
+
} catch (error: unknown) {
|
|
360
|
+
handleError(error, next)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
`
|
|
364
|
+
})
|
|
365
|
+
.join('\n') +
|
|
366
|
+
`
|
|
367
|
+
|
|
368
|
+
async function countForPagination(
|
|
369
|
+
delegate: any,
|
|
370
|
+
query: Record<string, any>,
|
|
371
|
+
shape: Record<string, any> | undefined,
|
|
372
|
+
caller: string | undefined,
|
|
373
|
+
): Promise<number> {
|
|
374
|
+
const distinctFields = normalizeDistinct(query.distinct)
|
|
375
|
+
const hasDistinct = distinctFields.length > 0
|
|
376
|
+
|
|
377
|
+
if (hasDistinct) {
|
|
378
|
+
const selectField = distinctFields[0]
|
|
379
|
+
const distinctArgs: Record<string, any> = {
|
|
380
|
+
where: query.where,
|
|
381
|
+
distinct: distinctFields,
|
|
382
|
+
select: { [selectField]: true },
|
|
383
|
+
take: DISTINCT_COUNT_LIMIT + 1,
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const results = shape
|
|
387
|
+
? await delegate.guard(shape, caller).findMany(distinctArgs)
|
|
388
|
+
: await delegate.findMany(distinctArgs)
|
|
389
|
+
|
|
390
|
+
if (results.length > DISTINCT_COUNT_LIMIT) {
|
|
391
|
+
console.warn('[prisma-generator-express] Distinct count exceeds ' + DISTINCT_COUNT_LIMIT + ', falling back to approximate total')
|
|
392
|
+
const countArgs: Record<string, any> = {}
|
|
393
|
+
if (query.where) countArgs.where = query.where
|
|
394
|
+
return shape
|
|
395
|
+
? await delegate.guard(shape, caller).count(countArgs)
|
|
396
|
+
: await delegate.count(countArgs)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return results.length
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const countArgs: Record<string, any> = {}
|
|
403
|
+
if (query.where) countArgs.where = query.where
|
|
404
|
+
|
|
405
|
+
return shape
|
|
406
|
+
? await delegate.guard(shape, caller).count(countArgs)
|
|
407
|
+
: await delegate.count(countArgs)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export async function ${modelName}FindManyPaginated(req: Request, res: Response, next: NextFunction) {
|
|
411
|
+
try {
|
|
412
|
+
const rawQuery = res.locals.parsedQuery || {}
|
|
413
|
+
const query = applyPaginationLimits(rawQuery, res)
|
|
414
|
+
const extended = await getExtendedClient(req)
|
|
415
|
+
const shape = res.locals.guardShape
|
|
416
|
+
const caller = res.locals.guardCaller
|
|
417
|
+
|
|
418
|
+
if (shape) {
|
|
419
|
+
assertGuard(extended.${modelNameLower})
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let items: any[]
|
|
423
|
+
let total: number
|
|
424
|
+
|
|
425
|
+
if (typeof extended.$transaction === 'function') {
|
|
426
|
+
try {
|
|
427
|
+
const txResult = await extended.$transaction(async (tx: any) => {
|
|
428
|
+
const d = shape
|
|
429
|
+
? await tx.${modelNameLower}.guard(shape, caller).findMany(query)
|
|
430
|
+
: await tx.${modelNameLower}.findMany(query)
|
|
431
|
+
const t = await countForPagination(tx.${modelNameLower}, query, shape, caller)
|
|
432
|
+
return { d, t }
|
|
433
|
+
})
|
|
434
|
+
items = txResult.d
|
|
435
|
+
total = txResult.t
|
|
436
|
+
} catch (txError: any) {
|
|
437
|
+
if (
|
|
438
|
+
txError?.message?.includes?.('interactive transactions') ||
|
|
439
|
+
txError?.code === 'P2028'
|
|
440
|
+
) {
|
|
441
|
+
console.warn('[prisma-generator-express] Interactive transactions not available, pagination queries are non-atomic')
|
|
442
|
+
items = shape
|
|
443
|
+
? await extended.${modelNameLower}.guard(shape, caller).findMany(query)
|
|
444
|
+
: await extended.${modelNameLower}.findMany(query)
|
|
445
|
+
total = await countForPagination(extended.${modelNameLower}, query, shape, caller)
|
|
446
|
+
} else {
|
|
447
|
+
throw txError
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
items = shape
|
|
452
|
+
? await extended.${modelNameLower}.guard(shape, caller).findMany(query)
|
|
453
|
+
: await extended.${modelNameLower}.findMany(query)
|
|
454
|
+
total = await countForPagination(extended.${modelNameLower}, query, shape, caller)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const skip = (query.skip as number) ?? 0
|
|
458
|
+
const absTake = Math.abs((query.take as number) ?? items.length)
|
|
459
|
+
const hasMore = items.length >= absTake && skip + items.length < total
|
|
460
|
+
|
|
461
|
+
res.locals.data = { data: items, total, hasMore }
|
|
462
|
+
next()
|
|
463
|
+
} catch (error: unknown) {
|
|
464
|
+
handleError(error, next)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
`
|
|
468
|
+
)
|
|
469
|
+
}
|