prisma-generator-express 1.57.0 → 1.58.1
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 +111 -29
- package/dist/copy/misc.js +25 -6
- package/dist/copy/misc.js.map +1 -1
- package/dist/generators/generateFastifyHandler.js +11 -0
- package/dist/generators/generateFastifyHandler.js.map +1 -1
- package/dist/generators/generateHonoHandler.js +14 -20
- package/dist/generators/generateHonoHandler.js.map +1 -1
- package/dist/generators/generateImportPrismaStatement.js +7 -1
- package/dist/generators/generateImportPrismaStatement.js.map +1 -1
- package/dist/generators/generateOperationCore.js +58 -17
- package/dist/generators/generateOperationCore.js.map +1 -1
- package/dist/generators/generateRouteConfigType.js +12 -7
- package/dist/generators/generateRouteConfigType.js.map +1 -1
- package/dist/generators/generateRouter.js +57 -32
- package/dist/generators/generateRouter.js.map +1 -1
- package/dist/generators/generateRouterFastify.js +235 -191
- package/dist/generators/generateRouterFastify.js.map +1 -1
- package/dist/generators/generateRouterHono.d.ts +2 -3
- package/dist/generators/generateRouterHono.js +131 -89
- package/dist/generators/generateRouterHono.js.map +1 -1
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/copy/autoIncludeRuntime.ts +9 -5
- package/src/copy/buildModelOpenApi.ts +96 -0
- package/src/copy/docsRenderer.ts +577 -174
- package/src/copy/materializedRouter.ts +40 -1
- package/src/copy/misc.ts +23 -6
- package/src/copy/operationDefinitions.ts +10 -0
- package/src/copy/operationRuntime.ts +28 -9
- package/src/copy/routeConfig.express.ts +9 -9
- package/src/copy/routeConfig.hono.ts +63 -5
- package/src/copy/routeConfig.ts +44 -22
- package/src/generators/generateFastifyHandler.ts +12 -0
- package/src/generators/generateHonoHandler.ts +15 -20
- package/src/generators/generateImportPrismaStatement.ts +10 -1
- package/src/generators/generateOperationCore.ts +58 -17
- package/src/generators/generateRouteConfigType.ts +13 -8
- package/src/generators/generateRouter.ts +57 -32
- package/src/generators/generateRouterFastify.ts +235 -191
- package/src/generators/generateRouterHono.ts +131 -91
- package/src/index.ts +11 -7
|
@@ -48,6 +48,8 @@ type MaterializedRouterOptions = {
|
|
|
48
48
|
|
|
49
49
|
const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
50
50
|
|
|
51
|
+
export const forbidden = (message: string): HttpError => new HttpError(403, message)
|
|
52
|
+
|
|
51
53
|
const quoteIdent = (name: string): string => {
|
|
52
54
|
if (!IDENT_RE.test(name))
|
|
53
55
|
throw new HttpError(400, 'invalid identifier: ' + name)
|
|
@@ -144,9 +146,46 @@ const enforceAllowlist = (orderBy: OrderByDef, allowed?: string[]): void => {
|
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
const validateViewIdentifier = (
|
|
150
|
+
viewName: string,
|
|
151
|
+
fieldName: string,
|
|
152
|
+
value: string,
|
|
153
|
+
): void => {
|
|
154
|
+
if (!IDENT_RE.test(value)) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
'materializedViewsRouter: invalid ' + fieldName + ' identifier for view "' +
|
|
157
|
+
viewName + '": ' + value,
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const validateViewConfig = (viewName: string, def: ViewDef): void => {
|
|
163
|
+
validateViewIdentifier(viewName, 'relation', def.relation)
|
|
164
|
+
if (def.schema) validateViewIdentifier(viewName, 'schema', def.schema)
|
|
165
|
+
const ob = def.orderBy
|
|
166
|
+
if (typeof ob === 'string') validateViewIdentifier(viewName, 'orderBy', ob)
|
|
167
|
+
else if (ob) validateViewIdentifier(viewName, 'orderBy.field', ob.field)
|
|
168
|
+
if (def.allowedOrderBy) {
|
|
169
|
+
for (const field of def.allowedOrderBy) {
|
|
170
|
+
validateViewIdentifier(viewName, 'allowedOrderBy[]', field)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!def.allowedOrderBy && !def.orderBy) {
|
|
174
|
+
console.warn(
|
|
175
|
+
'[materializedViewsRouter] view "' + viewName +
|
|
176
|
+
'" has neither `orderBy` nor `allowedOrderBy` set. ' +
|
|
177
|
+
'Clients can sort by any valid identifier, which may cause full table scans.',
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
147
182
|
export const materializedViewsRouter = (
|
|
148
183
|
opts: MaterializedRouterOptions,
|
|
149
184
|
): Router => {
|
|
185
|
+
for (const [name, def] of Object.entries(opts.views)) {
|
|
186
|
+
validateViewConfig(name, def)
|
|
187
|
+
}
|
|
188
|
+
|
|
150
189
|
const router = express.Router()
|
|
151
190
|
const basePath = normalizeBasePath(opts.basePath)
|
|
152
191
|
const defaultLimit = opts.defaultLimit ?? 50
|
|
@@ -230,4 +269,4 @@ export const materializedViewsRouter = (
|
|
|
230
269
|
)
|
|
231
270
|
|
|
232
271
|
return router
|
|
233
|
-
}
|
|
272
|
+
}
|
package/src/copy/misc.ts
CHANGED
|
@@ -17,15 +17,32 @@ export function isSafeKey(key: string): boolean {
|
|
|
17
17
|
|
|
18
18
|
export function sanitizeKeys<T>(value: T): T {
|
|
19
19
|
if (Array.isArray(value)) {
|
|
20
|
-
|
|
20
|
+
let mutated = false
|
|
21
|
+
const out: unknown[] = new Array(value.length)
|
|
22
|
+
for (let i = 0; i < value.length; i++) {
|
|
23
|
+
const sanitized = sanitizeKeys(value[i])
|
|
24
|
+
if (sanitized !== value[i]) mutated = true
|
|
25
|
+
out[i] = sanitized
|
|
26
|
+
}
|
|
27
|
+
return (mutated ? out : value) as T
|
|
21
28
|
}
|
|
22
29
|
if (isPlainObject(value)) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
const keys = Object.keys(value)
|
|
31
|
+
let hasUnsafe = false
|
|
32
|
+
let childrenMutated = false
|
|
33
|
+
const sanitizedChildren: Record<string, unknown> = {}
|
|
34
|
+
for (const key of keys) {
|
|
35
|
+
if (!isSafeKey(key)) {
|
|
36
|
+
hasUnsafe = true
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
const original = (value as Record<string, unknown>)[key]
|
|
40
|
+
const sanitized = sanitizeKeys(original)
|
|
41
|
+
if (sanitized !== original) childrenMutated = true
|
|
42
|
+
sanitizedChildren[key] = sanitized
|
|
27
43
|
}
|
|
28
|
-
|
|
44
|
+
if (!hasUnsafe && !childrenMutated) return value
|
|
45
|
+
return sanitizedChildren as T
|
|
29
46
|
}
|
|
30
47
|
return value
|
|
31
48
|
}
|
|
@@ -5,6 +5,7 @@ export interface OperationDef {
|
|
|
5
5
|
method: HttpMethod
|
|
6
6
|
pathSuffix: string
|
|
7
7
|
configKey: string
|
|
8
|
+
excludeFromEnableAll?: boolean
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export const OPERATION_DEFS: OperationDef[] = [
|
|
@@ -86,6 +87,13 @@ export const OPERATION_DEFS: OperationDef[] = [
|
|
|
86
87
|
pathSuffix: '/groupby',
|
|
87
88
|
configKey: 'groupBy',
|
|
88
89
|
},
|
|
90
|
+
{
|
|
91
|
+
name: 'updateEach',
|
|
92
|
+
method: 'post',
|
|
93
|
+
pathSuffix: '/each',
|
|
94
|
+
configKey: 'updateEach',
|
|
95
|
+
excludeFromEnableAll: true,
|
|
96
|
+
},
|
|
89
97
|
]
|
|
90
98
|
|
|
91
99
|
export const READ_OPERATION_NAMES = new Set([
|
|
@@ -110,5 +118,7 @@ export function isOperationEnabled(
|
|
|
110
118
|
config: Record<string, any>,
|
|
111
119
|
def: OperationDef,
|
|
112
120
|
): boolean {
|
|
121
|
+
if (config[def.configKey] === false) return false
|
|
122
|
+
if (def.excludeFromEnableAll) return !!config[def.configKey]
|
|
113
123
|
return !!(config.enableAll || config[def.configKey])
|
|
114
124
|
}
|
|
@@ -104,6 +104,7 @@ const PRISMA_ERROR_MAP: Record<string, { status: number; message: string }> = {
|
|
|
104
104
|
P2024: { status: 503, message: 'Connection pool timeout' },
|
|
105
105
|
P2025: { status: 404, message: 'Record not found' },
|
|
106
106
|
P2026: { status: 501, message: 'Feature not supported by the current database provider' },
|
|
107
|
+
P2027: { status: 400, message: 'Multiple errors occurred during transaction execution' },
|
|
107
108
|
P2028: { status: 500, message: 'Transaction API error' },
|
|
108
109
|
P2030: { status: 400, message: 'Cannot find a fulltext index for the search' },
|
|
109
110
|
P2033: { status: 400, message: 'Number out of range for the field type' },
|
|
@@ -122,9 +123,15 @@ function asErrorShape(error: unknown): ErrorShape {
|
|
|
122
123
|
return {}
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
function isProduction(): boolean {
|
|
127
|
+
return typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production'
|
|
128
|
+
}
|
|
129
|
+
|
|
125
130
|
export function mapError(error: unknown): HttpError {
|
|
126
131
|
if (error instanceof HttpError) return error
|
|
127
132
|
const e = asErrorShape(error)
|
|
133
|
+
const isProd = isProduction()
|
|
134
|
+
|
|
128
135
|
if (e.name === 'ShapeError') return new HttpError(400, e.message || 'Shape validation failed')
|
|
129
136
|
if (e.name === 'CallerError') return new HttpError(400, e.message || 'Caller validation failed')
|
|
130
137
|
if (e.name === 'PolicyError') return new HttpError(403, e.message || 'Policy denied')
|
|
@@ -139,24 +146,34 @@ export function mapError(error: unknown): HttpError {
|
|
|
139
146
|
const mapped = PRISMA_ERROR_MAP[e.code]
|
|
140
147
|
if (mapped) {
|
|
141
148
|
const detail = e.message
|
|
142
|
-
|
|
149
|
+
const shouldStripDetail = isProd && mapped.status >= 500
|
|
150
|
+
return new HttpError(
|
|
151
|
+
mapped.status,
|
|
152
|
+
!shouldStripDetail && detail ? mapped.message + ': ' + detail : mapped.message,
|
|
153
|
+
)
|
|
143
154
|
}
|
|
144
155
|
if (e.code.startsWith('P')) {
|
|
145
156
|
const msg = e.message || 'Database operation failed'
|
|
146
157
|
console.warn('[prisma-generator-express] Unmapped Prisma error code:', e.code, msg)
|
|
147
|
-
return new HttpError(500, msg)
|
|
158
|
+
return new HttpError(500, isProd ? 'Internal server error' : msg)
|
|
148
159
|
}
|
|
149
160
|
}
|
|
150
161
|
if (typeof e.name === 'string') {
|
|
151
162
|
if (e.name === 'PrismaClientValidationError') return new HttpError(400, e.message || 'Invalid query parameters')
|
|
152
163
|
if (e.name === 'PrismaClientKnownRequestError') return new HttpError(400, e.message || 'Database request error')
|
|
153
|
-
if (e.name === 'PrismaClientInitializationError')
|
|
154
|
-
|
|
155
|
-
|
|
164
|
+
if (e.name === 'PrismaClientInitializationError') {
|
|
165
|
+
return new HttpError(503, isProd ? 'Service unavailable' : (e.message || 'Database connection failed'))
|
|
166
|
+
}
|
|
167
|
+
if (e.name === 'PrismaClientRustPanicError') {
|
|
168
|
+
return new HttpError(500, isProd ? 'Internal server error' : (e.message || 'Internal database engine error'))
|
|
169
|
+
}
|
|
170
|
+
if (e.name === 'PrismaClientUnknownRequestError') {
|
|
171
|
+
return new HttpError(500, isProd ? 'Internal server error' : (e.message || 'Unknown database error'))
|
|
172
|
+
}
|
|
156
173
|
}
|
|
157
174
|
const msg = error instanceof Error ? error.message : String(error)
|
|
158
175
|
console.error('[prisma-generator-express] Unhandled error:', error)
|
|
159
|
-
return new HttpError(500, msg || 'Internal server error')
|
|
176
|
+
return new HttpError(500, isProd ? 'Internal server error' : (msg || 'Internal server error'))
|
|
160
177
|
}
|
|
161
178
|
|
|
162
179
|
type SpeedExtensionFactory = (opts: { postgres?: unknown; sqlite?: unknown; debug?: boolean }) => unknown
|
|
@@ -248,14 +265,16 @@ export function applyPaginationLimits(
|
|
|
248
265
|
config?: PaginationConfig,
|
|
249
266
|
hasGuardShape?: boolean,
|
|
250
267
|
): Record<string, unknown> {
|
|
251
|
-
if (!config
|
|
268
|
+
if (!config) return query
|
|
252
269
|
const result: Record<string, unknown> = { ...query }
|
|
253
|
-
if (result.take === undefined && config.defaultLimit !== undefined) {
|
|
270
|
+
if (!hasGuardShape && result.take === undefined && config.defaultLimit !== undefined) {
|
|
254
271
|
result.take = config.defaultLimit
|
|
255
272
|
}
|
|
256
273
|
if (config.maxLimit !== undefined && result.take !== undefined) {
|
|
257
274
|
const takeNum = Number(result.take)
|
|
258
|
-
if (
|
|
275
|
+
if (!Number.isFinite(takeNum)) {
|
|
276
|
+
result.take = config.maxLimit
|
|
277
|
+
} else if (Math.abs(takeNum) > config.maxLimit) {
|
|
259
278
|
result.take = takeNum < 0 ? -config.maxLimit : config.maxLimit
|
|
260
279
|
}
|
|
261
280
|
}
|
|
@@ -47,15 +47,15 @@ export type ReadOperationConfig<
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
type ReadOperationOverrides<TShape, TCtx, TPrisma> = {
|
|
50
|
-
findFirst?: ReadOperationConfig<TShape, TCtx, TPrisma>
|
|
51
|
-
findFirstOrThrow?: ReadOperationConfig<TShape, TCtx, TPrisma>
|
|
52
|
-
findUnique?: ReadOperationConfig<TShape, TCtx, TPrisma>
|
|
53
|
-
findUniqueOrThrow?: ReadOperationConfig<TShape, TCtx, TPrisma>
|
|
54
|
-
findMany?: ReadOperationConfig<TShape, TCtx, TPrisma>
|
|
55
|
-
findManyPaginated?: ReadOperationConfig<TShape, TCtx, TPrisma>
|
|
56
|
-
count?: ReadOperationConfig<TShape, TCtx, TPrisma>
|
|
57
|
-
aggregate?: ReadOperationConfig<TShape, TCtx, TPrisma>
|
|
58
|
-
groupBy?: ReadOperationConfig<TShape, TCtx, TPrisma>
|
|
50
|
+
findFirst?: ReadOperationConfig<TShape, TCtx, TPrisma> | false
|
|
51
|
+
findFirstOrThrow?: ReadOperationConfig<TShape, TCtx, TPrisma> | false
|
|
52
|
+
findUnique?: ReadOperationConfig<TShape, TCtx, TPrisma> | false
|
|
53
|
+
findUniqueOrThrow?: ReadOperationConfig<TShape, TCtx, TPrisma> | false
|
|
54
|
+
findMany?: ReadOperationConfig<TShape, TCtx, TPrisma> | false
|
|
55
|
+
findManyPaginated?: ReadOperationConfig<TShape, TCtx, TPrisma> | false
|
|
56
|
+
count?: ReadOperationConfig<TShape, TCtx, TPrisma> | false
|
|
57
|
+
aggregate?: ReadOperationConfig<TShape, TCtx, TPrisma> | false
|
|
58
|
+
groupBy?: ReadOperationConfig<TShape, TCtx, TPrisma> | false
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export type RouteConfig<
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Context
|
|
1
|
+
import type { Context } from 'hono'
|
|
2
2
|
import type {
|
|
3
3
|
BaseOperationConfig,
|
|
4
4
|
BaseRouteConfig,
|
|
@@ -44,18 +44,76 @@ export type GeneratedHonoEnv<TEnv extends HonoEnvBase = HonoEnvBase> = {
|
|
|
44
44
|
Bindings: TEnv['Bindings']
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export type
|
|
47
|
+
export type HonoBeforeHook<TEnv extends HonoEnvBase = HonoEnvBase> = (
|
|
48
48
|
c: Context<GeneratedHonoEnv<TEnv>>,
|
|
49
|
-
next: Next,
|
|
50
49
|
) => Promise<Response | void> | Response | void
|
|
51
50
|
|
|
51
|
+
export type HonoAfterHook<TEnv extends HonoEnvBase = HonoEnvBase> = (
|
|
52
|
+
c: Context<GeneratedHonoEnv<TEnv>>,
|
|
53
|
+
) => Promise<Response | void> | Response | void
|
|
54
|
+
|
|
55
|
+
/** @deprecated use HonoBeforeHook or HonoAfterHook */
|
|
56
|
+
export type HonoHookHandler<TEnv extends HonoEnvBase = HonoEnvBase> = HonoBeforeHook<TEnv>
|
|
57
|
+
|
|
52
58
|
export type OperationConfig<
|
|
53
59
|
TShape = Record<string, unknown>,
|
|
54
60
|
TEnv extends HonoEnvBase = HonoEnvBase,
|
|
55
|
-
> = BaseOperationConfig<
|
|
61
|
+
> = Omit<BaseOperationConfig<HonoBeforeHook<TEnv>, TShape>, 'before' | 'after'> & {
|
|
62
|
+
before?: HonoBeforeHook<TEnv>[]
|
|
63
|
+
after?: HonoAfterHook<TEnv>[]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type UpdateEachConfig<TEnv extends HonoEnvBase = HonoEnvBase> = {
|
|
67
|
+
before?: HonoBeforeHook<TEnv>[]
|
|
68
|
+
after?: HonoAfterHook<TEnv>[]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type HonoOpKeys =
|
|
72
|
+
| 'findUnique'
|
|
73
|
+
| 'findUniqueOrThrow'
|
|
74
|
+
| 'findFirst'
|
|
75
|
+
| 'findFirstOrThrow'
|
|
76
|
+
| 'findMany'
|
|
77
|
+
| 'findManyPaginated'
|
|
78
|
+
| 'create'
|
|
79
|
+
| 'createMany'
|
|
80
|
+
| 'createManyAndReturn'
|
|
81
|
+
| 'update'
|
|
82
|
+
| 'updateMany'
|
|
83
|
+
| 'updateManyAndReturn'
|
|
84
|
+
| 'upsert'
|
|
85
|
+
| 'delete'
|
|
86
|
+
| 'deleteMany'
|
|
87
|
+
| 'aggregate'
|
|
88
|
+
| 'count'
|
|
89
|
+
| 'groupBy'
|
|
90
|
+
| 'updateEach'
|
|
56
91
|
|
|
57
92
|
export type RouteConfig<
|
|
58
93
|
TShape = Record<string, unknown>,
|
|
59
94
|
TCtx = unknown,
|
|
60
95
|
TEnv extends HonoEnvBase = HonoEnvBase,
|
|
61
|
-
> =
|
|
96
|
+
> = Omit<
|
|
97
|
+
BaseRouteConfig<HonoBeforeHook<TEnv>, Context<GeneratedHonoEnv<TEnv>>, TShape, TCtx>,
|
|
98
|
+
HonoOpKeys
|
|
99
|
+
> & {
|
|
100
|
+
findUnique?: OperationConfig<TShape, TEnv> | false
|
|
101
|
+
findUniqueOrThrow?: OperationConfig<TShape, TEnv> | false
|
|
102
|
+
findFirst?: OperationConfig<TShape, TEnv> | false
|
|
103
|
+
findFirstOrThrow?: OperationConfig<TShape, TEnv> | false
|
|
104
|
+
findMany?: OperationConfig<TShape, TEnv> | false
|
|
105
|
+
findManyPaginated?: OperationConfig<TShape, TEnv> | false
|
|
106
|
+
create?: OperationConfig<TShape, TEnv> | false
|
|
107
|
+
createMany?: OperationConfig<TShape, TEnv> | false
|
|
108
|
+
createManyAndReturn?: OperationConfig<TShape, TEnv> | false
|
|
109
|
+
update?: OperationConfig<TShape, TEnv> | false
|
|
110
|
+
updateMany?: OperationConfig<TShape, TEnv> | false
|
|
111
|
+
updateManyAndReturn?: OperationConfig<TShape, TEnv> | false
|
|
112
|
+
upsert?: OperationConfig<TShape, TEnv> | false
|
|
113
|
+
delete?: OperationConfig<TShape, TEnv> | false
|
|
114
|
+
deleteMany?: OperationConfig<TShape, TEnv> | false
|
|
115
|
+
aggregate?: OperationConfig<TShape, TEnv> | false
|
|
116
|
+
count?: OperationConfig<TShape, TEnv> | false
|
|
117
|
+
groupBy?: OperationConfig<TShape, TEnv> | false
|
|
118
|
+
updateEach?: UpdateEachConfig<TEnv>
|
|
119
|
+
}
|
package/src/copy/routeConfig.ts
CHANGED
|
@@ -91,10 +91,14 @@ export interface BaseOperationConfig<HookHandler, TShape = Record<string, unknow
|
|
|
91
91
|
before?: HookHandler[]
|
|
92
92
|
after?: HookHandler[]
|
|
93
93
|
shape?: TShape
|
|
94
|
-
dropGuard?: boolean
|
|
95
94
|
pagination?: Partial<PaginationConfig>
|
|
96
95
|
}
|
|
97
96
|
|
|
97
|
+
export interface BaseUpdateEachConfig<HookHandler> {
|
|
98
|
+
before?: HookHandler[]
|
|
99
|
+
after?: HookHandler[]
|
|
100
|
+
}
|
|
101
|
+
|
|
98
102
|
export interface BaseRouteConfig<
|
|
99
103
|
HookHandler,
|
|
100
104
|
RequestType,
|
|
@@ -106,7 +110,6 @@ export interface BaseRouteConfig<
|
|
|
106
110
|
customUrlPrefix?: string
|
|
107
111
|
specBasePath?: string
|
|
108
112
|
disableOpenApi?: boolean
|
|
109
|
-
dropGuard?: boolean
|
|
110
113
|
disablePostReads?: boolean
|
|
111
114
|
scalarCdnUrl?: string
|
|
112
115
|
openApiTitle?: string
|
|
@@ -122,26 +125,45 @@ export interface BaseRouteConfig<
|
|
|
122
125
|
resolveContext?: (request: RequestType) => TCtx | Promise<TCtx>
|
|
123
126
|
queryBuilder?: QueryBuilderConfig | false
|
|
124
127
|
pagination?: PaginationConfig
|
|
125
|
-
findUnique?: BaseOperationConfig<HookHandler, TShape>
|
|
126
|
-
findUniqueOrThrow?: BaseOperationConfig<HookHandler, TShape>
|
|
127
|
-
findFirst?: BaseOperationConfig<HookHandler, TShape>
|
|
128
|
-
findFirstOrThrow?: BaseOperationConfig<HookHandler, TShape>
|
|
129
|
-
findMany?: BaseOperationConfig<HookHandler, TShape>
|
|
130
|
-
findManyPaginated?: BaseOperationConfig<HookHandler, TShape>
|
|
131
|
-
create?: BaseOperationConfig<HookHandler, TShape>
|
|
132
|
-
createMany?: BaseOperationConfig<HookHandler, TShape>
|
|
133
|
-
createManyAndReturn?: BaseOperationConfig<HookHandler, TShape>
|
|
134
|
-
update?: BaseOperationConfig<HookHandler, TShape>
|
|
135
|
-
updateMany?: BaseOperationConfig<HookHandler, TShape>
|
|
136
|
-
updateManyAndReturn?: BaseOperationConfig<HookHandler, TShape>
|
|
137
|
-
upsert?: BaseOperationConfig<HookHandler, TShape>
|
|
138
|
-
delete?: BaseOperationConfig<HookHandler, TShape>
|
|
139
|
-
deleteMany?: BaseOperationConfig<HookHandler, TShape>
|
|
140
|
-
updateEach?:
|
|
141
|
-
aggregate?: BaseOperationConfig<HookHandler, TShape>
|
|
142
|
-
count?: BaseOperationConfig<HookHandler, TShape>
|
|
143
|
-
groupBy?: BaseOperationConfig<HookHandler, TShape>
|
|
128
|
+
findUnique?: BaseOperationConfig<HookHandler, TShape> | false
|
|
129
|
+
findUniqueOrThrow?: BaseOperationConfig<HookHandler, TShape> | false
|
|
130
|
+
findFirst?: BaseOperationConfig<HookHandler, TShape> | false
|
|
131
|
+
findFirstOrThrow?: BaseOperationConfig<HookHandler, TShape> | false
|
|
132
|
+
findMany?: BaseOperationConfig<HookHandler, TShape> | false
|
|
133
|
+
findManyPaginated?: BaseOperationConfig<HookHandler, TShape> | false
|
|
134
|
+
create?: BaseOperationConfig<HookHandler, TShape> | false
|
|
135
|
+
createMany?: BaseOperationConfig<HookHandler, TShape> | false
|
|
136
|
+
createManyAndReturn?: BaseOperationConfig<HookHandler, TShape> | false
|
|
137
|
+
update?: BaseOperationConfig<HookHandler, TShape> | false
|
|
138
|
+
updateMany?: BaseOperationConfig<HookHandler, TShape> | false
|
|
139
|
+
updateManyAndReturn?: BaseOperationConfig<HookHandler, TShape> | false
|
|
140
|
+
upsert?: BaseOperationConfig<HookHandler, TShape> | false
|
|
141
|
+
delete?: BaseOperationConfig<HookHandler, TShape> | false
|
|
142
|
+
deleteMany?: BaseOperationConfig<HookHandler, TShape> | false
|
|
143
|
+
updateEach?: BaseUpdateEachConfig<HookHandler>
|
|
144
|
+
aggregate?: BaseOperationConfig<HookHandler, TShape> | false
|
|
145
|
+
count?: BaseOperationConfig<HookHandler, TShape> | false
|
|
146
|
+
groupBy?: BaseOperationConfig<HookHandler, TShape> | false
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
export type OperationConfig = BaseOperationConfig<unknown>
|
|
147
|
-
export type RouteConfig = BaseRouteConfig<unknown, unknown>
|
|
150
|
+
export type RouteConfig = BaseRouteConfig<unknown, unknown>
|
|
151
|
+
|
|
152
|
+
export function validateCountSourceWhere(
|
|
153
|
+
cs: PaginationCountSource | undefined,
|
|
154
|
+
location: string,
|
|
155
|
+
): void {
|
|
156
|
+
if (!cs) return
|
|
157
|
+
if ((cs as { type?: string }).type !== 'materializedView') return
|
|
158
|
+
const where = (cs as { where?: Record<string, unknown> }).where
|
|
159
|
+
if (!where) return
|
|
160
|
+
for (const [key, value] of Object.entries(where)) {
|
|
161
|
+
if (value === null) continue
|
|
162
|
+
if (typeof value === 'object') {
|
|
163
|
+
throw new Error(
|
|
164
|
+
location + ': countSource.where["' + key + '"] must be scalar or null; got ' +
|
|
165
|
+
(Array.isArray(value) ? 'array' : 'object'),
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -75,6 +75,17 @@ export async function ${exportName}(
|
|
|
75
75
|
}`
|
|
76
76
|
}).join('\n')
|
|
77
77
|
|
|
78
|
+
const updateEachExportName = `${modelName}UpdateEach`
|
|
79
|
+
const updateEachHandler = `
|
|
80
|
+
export async function ${updateEachExportName}(
|
|
81
|
+
request: FastifyRequest,
|
|
82
|
+
_reply: FastifyReply,
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
const atomic = request.headers['x-batch-atomic'] === 'true'
|
|
85
|
+
const data = await core.updateEach(buildContext(request), atomic)
|
|
86
|
+
;(request as FastifyExtended).resultData = data
|
|
87
|
+
}`
|
|
88
|
+
|
|
78
89
|
return `import type { FastifyRequest, FastifyReply } from 'fastify'
|
|
79
90
|
import * as core from './${modelName}Core${ext}'
|
|
80
91
|
import type { OperationContext, FindManyPaginatedMode } from '../operationRuntime${ext}'
|
|
@@ -108,5 +119,6 @@ function buildContext(request: FastifyRequest): OperationContext {
|
|
|
108
119
|
}
|
|
109
120
|
${readHandlers}
|
|
110
121
|
${writeHandlers}
|
|
122
|
+
${updateEachHandler}
|
|
111
123
|
`
|
|
112
124
|
}
|
|
@@ -50,7 +50,7 @@ export function generateHonoHandler(options: {
|
|
|
50
50
|
const readHandlers = READ_OPS.map((op) => {
|
|
51
51
|
const exportName = `${modelName}${op.charAt(0).toUpperCase() + op.slice(1)}`
|
|
52
52
|
return `
|
|
53
|
-
export async function ${exportName}(c:
|
|
53
|
+
export async function ${exportName}(c: HandlerContext): Promise<void> {
|
|
54
54
|
const data = await core.${coreFnName(op)}(buildContext(c))
|
|
55
55
|
c.set('resultData', data)
|
|
56
56
|
}`
|
|
@@ -61,34 +61,29 @@ export async function ${exportName}(c: Context<HonoEnv>): Promise<void> {
|
|
|
61
61
|
const statusCode = CREATED_OPS.has(op) ? 201 : 200
|
|
62
62
|
|
|
63
63
|
return `
|
|
64
|
-
export async function ${exportName}(c:
|
|
64
|
+
export async function ${exportName}(c: HandlerContext): Promise<void> {
|
|
65
65
|
const data = await core.${coreFnName(op)}(buildContext(c))
|
|
66
66
|
c.set('resultData', data)
|
|
67
67
|
c.set('resultStatus', ${statusCode})
|
|
68
68
|
}`
|
|
69
69
|
}).join('\n')
|
|
70
70
|
|
|
71
|
+
const updateEachExportName = `${modelName}UpdateEach`
|
|
72
|
+
const updateEachHandler = `
|
|
73
|
+
export async function ${updateEachExportName}(c: HandlerContext): Promise<void> {
|
|
74
|
+
const atomic = c.req.header('x-batch-atomic') === 'true'
|
|
75
|
+
const data = await core.updateEach(buildContext(c), atomic)
|
|
76
|
+
c.set('resultData', data)
|
|
77
|
+
}`
|
|
78
|
+
|
|
71
79
|
return `import type { Context } from 'hono'
|
|
72
80
|
import * as core from './${modelName}Core${ext}'
|
|
73
|
-
import type { OperationContext
|
|
74
|
-
|
|
75
|
-
type HonoVariables = {
|
|
76
|
-
prisma: unknown
|
|
77
|
-
postgres?: unknown
|
|
78
|
-
sqlite?: unknown
|
|
79
|
-
parsedQuery?: Record<string, unknown>
|
|
80
|
-
body?: unknown
|
|
81
|
-
routeConfig?: { pagination?: OperationContext['paginationConfig'] }
|
|
82
|
-
guardShape?: Record<string, unknown>
|
|
83
|
-
guardCaller?: string
|
|
84
|
-
findManyPaginatedMode?: FindManyPaginatedMode
|
|
85
|
-
resultData?: unknown
|
|
86
|
-
resultStatus?: number
|
|
87
|
-
}
|
|
81
|
+
import type { OperationContext } from '../operationRuntime${ext}'
|
|
82
|
+
import type { HonoInternalVariables } from '../routeConfig.target${ext}'
|
|
88
83
|
|
|
89
|
-
type
|
|
84
|
+
type HandlerContext = Context<{ Variables: HonoInternalVariables }>
|
|
90
85
|
|
|
91
|
-
function buildContext(c:
|
|
86
|
+
function buildContext(c: HandlerContext): OperationContext {
|
|
92
87
|
return {
|
|
93
88
|
prisma: c.get('prisma'),
|
|
94
89
|
postgres: c.get('postgres'),
|
|
@@ -98,10 +93,10 @@ function buildContext(c: Context<HonoEnv>): OperationContext {
|
|
|
98
93
|
guardShape: c.get('guardShape'),
|
|
99
94
|
guardCaller: c.get('guardCaller'),
|
|
100
95
|
paginationConfig: c.get('routeConfig')?.pagination,
|
|
101
|
-
findManyPaginatedMode: c.get('findManyPaginatedMode'),
|
|
102
96
|
}
|
|
103
97
|
}
|
|
104
98
|
${readHandlers}
|
|
105
99
|
${writeHandlers}
|
|
100
|
+
${updateEachHandler}
|
|
106
101
|
`
|
|
107
102
|
}
|
|
@@ -76,7 +76,16 @@ export function getGuardShapesImport(
|
|
|
76
76
|
if (!outputValue) return null
|
|
77
77
|
|
|
78
78
|
const shapesFilePath = path.join(guard.output.value, 'shapes.ts')
|
|
79
|
-
if (!fs.existsSync(shapesFilePath))
|
|
79
|
+
if (!fs.existsSync(shapesFilePath)) {
|
|
80
|
+
console.warn(
|
|
81
|
+
'[prisma-generator-express] prisma-guard generator detected but "' +
|
|
82
|
+
shapesFilePath +
|
|
83
|
+
'" was not found. Guard shapes will not be imported for model "' +
|
|
84
|
+
modelName +
|
|
85
|
+
'". Declare the "guard" generator before "express" in schema.prisma and re-run prisma generate.',
|
|
86
|
+
)
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
80
89
|
|
|
81
90
|
const fromDir = path.join(outputValue, modelName)
|
|
82
91
|
const shapesPath = path.join(guard.output.value, 'shapes')
|
|
@@ -184,7 +184,7 @@ ${paginatedBody}
|
|
|
184
184
|
const skip = (typeof query.skip === 'number' ? query.skip : 0)
|
|
185
185
|
const takeRaw = (typeof query.take === 'number' ? query.take : items.length)
|
|
186
186
|
const absTake = Math.abs(takeRaw)
|
|
187
|
-
const hasMore = items.length >= absTake && skip + items.length < total
|
|
187
|
+
const hasMore = absTake > 0 && items.length >= absTake && skip + items.length < total
|
|
188
188
|
|
|
189
189
|
return { data: items, total, hasMore }
|
|
190
190
|
}
|
|
@@ -193,35 +193,76 @@ export async function updateEach(
|
|
|
193
193
|
ctx: OperationContext,
|
|
194
194
|
atomic: boolean,
|
|
195
195
|
): Promise<unknown> {
|
|
196
|
-
const
|
|
197
|
-
if (!Array.isArray(
|
|
196
|
+
const rawBody = ctx.body
|
|
197
|
+
if (!Array.isArray(rawBody)) {
|
|
198
198
|
throw new HttpError(400, 'updateEach body must be an array of { where, data } items')
|
|
199
199
|
}
|
|
200
|
-
|
|
201
|
-
const
|
|
200
|
+
|
|
201
|
+
const MAX_ITEMS_NON_ATOMIC = 1000
|
|
202
|
+
const MAX_ITEMS_ATOMIC = 100
|
|
203
|
+
|
|
204
|
+
if (atomic && rawBody.length > MAX_ITEMS_ATOMIC) {
|
|
205
|
+
throw new HttpError(
|
|
206
|
+
400,
|
|
207
|
+
'atomic updateEach body exceeds max size of ' + MAX_ITEMS_ATOMIC + ' items',
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
if (!atomic && rawBody.length > MAX_ITEMS_NON_ATOMIC) {
|
|
211
|
+
throw new HttpError(
|
|
212
|
+
400,
|
|
213
|
+
'updateEach body exceeds max size of ' + MAX_ITEMS_NON_ATOMIC + ' items',
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const items = rawBody.map((item, index) => {
|
|
218
|
+
const sanitized = validateBody(item)
|
|
219
|
+
if (!('where' in sanitized) || sanitized.where === undefined) {
|
|
220
|
+
throw new HttpError(400, 'updateEach item at index ' + index + ' is missing "where"')
|
|
221
|
+
}
|
|
222
|
+
if (!('data' in sanitized) || sanitized.data === undefined) {
|
|
223
|
+
throw new HttpError(400, 'updateEach item at index ' + index + ' is missing "data"')
|
|
224
|
+
}
|
|
225
|
+
return sanitized
|
|
226
|
+
})
|
|
227
|
+
const extended = await getExtendedClient(ctx)
|
|
202
228
|
|
|
203
229
|
if (atomic) {
|
|
204
|
-
|
|
230
|
+
const txClient = extended as PrismaClientLike
|
|
231
|
+
if (typeof txClient.$transaction !== 'function') {
|
|
205
232
|
throw new HttpError(500, 'Atomic updateEach requires transaction support on the Prisma client')
|
|
206
233
|
}
|
|
207
|
-
const runInteractive =
|
|
234
|
+
const runInteractive = txClient.$transaction as unknown as <T>(
|
|
208
235
|
fn: (tx: unknown) => Promise<T>,
|
|
209
236
|
) => Promise<T>
|
|
210
237
|
return runInteractive(async (tx) => {
|
|
211
238
|
const txDelegate = getDelegate(tx, '${modelNameLower}')
|
|
212
|
-
|
|
239
|
+
const out: unknown[] = new Array(items.length)
|
|
240
|
+
for (let i = 0; i < items.length; i++) {
|
|
241
|
+
out[i] = await txDelegate.update(items[i])
|
|
242
|
+
}
|
|
243
|
+
return out
|
|
213
244
|
})
|
|
214
245
|
}
|
|
215
246
|
|
|
216
|
-
const delegate = getDelegate(
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
247
|
+
const delegate = getDelegate(extended, '${modelNameLower}')
|
|
248
|
+
const CONCURRENCY = 8
|
|
249
|
+
const results: Array<{ status: 'ok'; data: unknown } | { status: 'error'; error: string }> =
|
|
250
|
+
new Array(items.length)
|
|
251
|
+
let cursor = 0
|
|
252
|
+
const workerCount = Math.min(CONCURRENCY, items.length)
|
|
253
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
254
|
+
for (;;) {
|
|
255
|
+
const i = cursor++
|
|
256
|
+
if (i >= items.length) return
|
|
257
|
+
try {
|
|
258
|
+
results[i] = { status: 'ok', data: await delegate.update(items[i]) }
|
|
259
|
+
} catch (err) {
|
|
260
|
+
results[i] = { status: 'error', error: mapError(err).message }
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
await Promise.all(workers)
|
|
265
|
+
return results
|
|
225
266
|
}
|
|
226
267
|
`
|
|
227
268
|
}
|