prisma-generator-express 1.59.0 → 1.61.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 +150 -14
- package/dist/client/encodeQueryParams.js +19 -23
- package/dist/client/encodeQueryParams.js.map +1 -1
- package/dist/copy/operationDefinitions.d.ts +37 -0
- package/dist/copy/operationDefinitions.js +412 -0
- package/dist/copy/operationDefinitions.js.map +1 -0
- package/dist/generators/generateFastifyHandler.js +10 -41
- package/dist/generators/generateFastifyHandler.js.map +1 -1
- package/dist/generators/generateHonoHandler.js +10 -41
- package/dist/generators/generateHonoHandler.js.map +1 -1
- package/dist/generators/generateModelMetadata.d.ts +8 -0
- package/dist/generators/generateModelMetadata.js +77 -0
- package/dist/generators/generateModelMetadata.js.map +1 -0
- package/dist/generators/generateOperationCore.js +20 -30
- package/dist/generators/generateOperationCore.js.map +1 -1
- package/dist/generators/generateQueryBuilderHelper.js +1 -1
- package/dist/generators/generateQueryBuilderHelper.js.map +1 -1
- package/dist/generators/generateRouteConfigType.js +11 -57
- package/dist/generators/generateRouteConfigType.js.map +1 -1
- package/dist/generators/generateRouter.js +64 -177
- package/dist/generators/generateRouter.js.map +1 -1
- package/dist/generators/generateRouterFastify.js +59 -169
- package/dist/generators/generateRouterFastify.js.map +1 -1
- package/dist/generators/generateRouterHono.js +52 -152
- package/dist/generators/generateRouterHono.js.map +1 -1
- package/dist/generators/generateUnifiedHandler.js +7 -30
- package/dist/generators/generateUnifiedHandler.js.map +1 -1
- package/dist/generators/generateUnifiedScalarUI.js +9 -74
- package/dist/generators/generateUnifiedScalarUI.js.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/copyFiles.js +7 -0
- package/dist/utils/copyFiles.js.map +1 -1
- package/dist/utils/writeFileSafely.js +5 -12
- package/dist/utils/writeFileSafely.js.map +1 -1
- package/package.json +12 -2
- package/src/client/encodeQueryParams.ts +23 -36
- package/src/copy/autoIncludePlanner.ts +14 -23
- package/src/copy/autoIncludePlannerGuarded.ts +477 -0
- package/src/copy/autoIncludeRuntime.ts +269 -261
- package/src/copy/autoIncludeRuntimeGuarded.ts +379 -0
- package/src/copy/buildModelOpenApi.ts +248 -628
- package/src/copy/concurrency.ts +20 -0
- package/src/copy/docsRenderer.ts +63 -333
- package/src/copy/errorMapper.ts +126 -0
- package/src/copy/guardHelpers.ts +56 -0
- package/src/copy/materializedCount.ts +68 -0
- package/src/copy/materializedRouter.ts +33 -29
- package/src/copy/operationDefinitions.ts +359 -35
- package/src/copy/operationRuntime.ts +11 -605
- package/src/copy/pagination.ts +151 -0
- package/src/copy/scalarTypes.ts +2 -0
- package/src/copy/sse.ts +296 -0
- package/src/generators/generateFastifyHandler.ts +13 -47
- package/src/generators/generateHonoHandler.ts +13 -47
- package/src/generators/generateModelMetadata.ts +92 -0
- package/src/generators/generateOperationCore.ts +19 -32
- package/src/generators/generateQueryBuilderHelper.ts +1 -1
- package/src/generators/generateRouteConfigType.ts +9 -60
- package/src/generators/generateRouter.ts +88 -180
- package/src/generators/generateRouterFastify.ts +65 -172
- package/src/generators/generateRouterHono.ts +58 -155
- package/src/generators/generateUnifiedHandler.ts +8 -33
- package/src/generators/generateUnifiedScalarUI.ts +9 -91
- package/src/index.ts +13 -1
- package/src/utils/copyFiles.ts +7 -0
- package/src/utils/writeFileSafely.ts +5 -11
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { HttpError, LOG_PREFIX } from './errorMapper'
|
|
2
|
+
import { assertGuard, buildCountShape, type PrismaDelegateLike } from './guardHelpers'
|
|
3
|
+
import { countFromMaterializedView } from './materializedCount'
|
|
4
|
+
import { isPlainObject } from './misc'
|
|
5
|
+
import type { PaginationConfig, PaginationCountSource } from './routeConfig'
|
|
6
|
+
|
|
7
|
+
export const DISTINCT_COUNT_LIMIT = 100000
|
|
8
|
+
|
|
9
|
+
export function applyPaginationLimits(
|
|
10
|
+
query: Record<string, unknown>,
|
|
11
|
+
config?: PaginationConfig,
|
|
12
|
+
hasGuardShape?: boolean,
|
|
13
|
+
): Record<string, unknown> {
|
|
14
|
+
if (!config) return query
|
|
15
|
+
const result: Record<string, unknown> = { ...query }
|
|
16
|
+
if (!hasGuardShape && result.take === undefined && config.defaultLimit !== undefined) {
|
|
17
|
+
result.take = config.defaultLimit
|
|
18
|
+
}
|
|
19
|
+
if (config.maxLimit !== undefined && result.take !== undefined) {
|
|
20
|
+
const takeNum = typeof result.take === 'number' ? result.take : Number(result.take)
|
|
21
|
+
if (!Number.isFinite(takeNum)) {
|
|
22
|
+
throw new HttpError(400, 'Invalid take: must be a finite number')
|
|
23
|
+
}
|
|
24
|
+
if (!Number.isInteger(takeNum)) {
|
|
25
|
+
throw new HttpError(400, 'Invalid take: must be an integer')
|
|
26
|
+
}
|
|
27
|
+
if (Math.abs(takeNum) > config.maxLimit) {
|
|
28
|
+
result.take = takeNum < 0 ? -config.maxLimit : config.maxLimit
|
|
29
|
+
} else {
|
|
30
|
+
result.take = takeNum
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function mergeCountSource(
|
|
37
|
+
base: PaginationCountSource | undefined,
|
|
38
|
+
override: PaginationCountSource | undefined,
|
|
39
|
+
): PaginationCountSource | undefined {
|
|
40
|
+
if (!base && !override) return undefined
|
|
41
|
+
if (!base) return override
|
|
42
|
+
if (!override) return base
|
|
43
|
+
|
|
44
|
+
const overrideType = (override as { type?: string }).type
|
|
45
|
+
const baseType = (base as { type?: string }).type
|
|
46
|
+
const effectiveType = overrideType ?? baseType
|
|
47
|
+
|
|
48
|
+
if (effectiveType === 'delegate' || effectiveType === undefined) {
|
|
49
|
+
return { type: 'delegate' }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...(base as Record<string, unknown>),
|
|
54
|
+
...(override as Record<string, unknown>),
|
|
55
|
+
type: effectiveType,
|
|
56
|
+
} as PaginationCountSource
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function mergePaginationConfig(
|
|
60
|
+
base: PaginationConfig | undefined,
|
|
61
|
+
override: Partial<PaginationConfig> | undefined,
|
|
62
|
+
): PaginationConfig | undefined {
|
|
63
|
+
if (!base && !override) return undefined
|
|
64
|
+
const merged: PaginationConfig = {
|
|
65
|
+
...(base ?? {}),
|
|
66
|
+
...(override ?? {}),
|
|
67
|
+
}
|
|
68
|
+
const mergedCountSource = mergeCountSource(base?.countSource, override?.countSource)
|
|
69
|
+
if (mergedCountSource) {
|
|
70
|
+
merged.countSource = mergedCountSource
|
|
71
|
+
} else {
|
|
72
|
+
delete merged.countSource
|
|
73
|
+
}
|
|
74
|
+
return merged
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function normalizeDistinct(value: unknown): string[] {
|
|
78
|
+
if (typeof value === 'string') return [value]
|
|
79
|
+
if (Array.isArray(value)) return value.filter((v): v is string => typeof v === 'string')
|
|
80
|
+
return []
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isEffectivelyEmpty(where: unknown): boolean {
|
|
84
|
+
if (where === undefined || where === null) return true
|
|
85
|
+
if (!isPlainObject(where)) return false
|
|
86
|
+
return Object.keys(where).length === 0
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function countForPagination(
|
|
90
|
+
delegate: PrismaDelegateLike,
|
|
91
|
+
query: Record<string, unknown>,
|
|
92
|
+
shape: Record<string, unknown> | undefined,
|
|
93
|
+
caller: string | undefined,
|
|
94
|
+
distinctCountLimit?: number,
|
|
95
|
+
countSource?: PaginationCountSource,
|
|
96
|
+
rawClient?: unknown,
|
|
97
|
+
): Promise<number> {
|
|
98
|
+
const normalizedDistinct = normalizeDistinct(query.distinct)
|
|
99
|
+
const whereIsEmpty = isEffectivelyEmpty(query.where)
|
|
100
|
+
const distinctIsEmpty = normalizedDistinct.length === 0
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
countSource &&
|
|
104
|
+
countSource.type === 'materializedView' &&
|
|
105
|
+
!shape &&
|
|
106
|
+
whereIsEmpty &&
|
|
107
|
+
distinctIsEmpty
|
|
108
|
+
) {
|
|
109
|
+
return countFromMaterializedView(rawClient ?? delegate, countSource)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const hasDistinct = normalizedDistinct.length > 0
|
|
113
|
+
const effectiveLimit = distinctCountLimit ?? DISTINCT_COUNT_LIMIT
|
|
114
|
+
const countShape = shape ? buildCountShape(shape) : undefined
|
|
115
|
+
|
|
116
|
+
const runCount = async (): Promise<number> => {
|
|
117
|
+
const countArgs: Record<string, unknown> = {}
|
|
118
|
+
if (query.where) countArgs.where = query.where
|
|
119
|
+
if (countShape) {
|
|
120
|
+
assertGuard(delegate)
|
|
121
|
+
return (await delegate.guard(
|
|
122
|
+
countShape as Record<string, unknown>,
|
|
123
|
+
caller,
|
|
124
|
+
).count(countArgs)) as number
|
|
125
|
+
}
|
|
126
|
+
return (await delegate.count(countArgs)) as number
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (hasDistinct && shape) return runCount()
|
|
130
|
+
|
|
131
|
+
if (hasDistinct) {
|
|
132
|
+
const selectField = normalizedDistinct[0]
|
|
133
|
+
const distinctArgs: Record<string, unknown> = {
|
|
134
|
+
where: query.where,
|
|
135
|
+
distinct: normalizedDistinct,
|
|
136
|
+
select: { [selectField]: true },
|
|
137
|
+
take: effectiveLimit + 1,
|
|
138
|
+
}
|
|
139
|
+
const results = (await delegate.findMany(distinctArgs)) as unknown[]
|
|
140
|
+
if (results.length > effectiveLimit) {
|
|
141
|
+
console.warn(
|
|
142
|
+
LOG_PREFIX,
|
|
143
|
+
'Distinct count exceeds ' + effectiveLimit + ', falling back to approximate total',
|
|
144
|
+
)
|
|
145
|
+
return runCount()
|
|
146
|
+
}
|
|
147
|
+
return results.length
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return runCount()
|
|
151
|
+
}
|
package/src/copy/sse.ts
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { HttpError, LOG_PREFIX, mapError } from './errorMapper'
|
|
2
|
+
import { isPlainObject } from './misc'
|
|
3
|
+
import { transformResult } from './operationRuntime'
|
|
4
|
+
import type {
|
|
5
|
+
ProgressiveStage,
|
|
6
|
+
ProgressiveStopResult,
|
|
7
|
+
ProgressivePatch,
|
|
8
|
+
} from './routeConfig'
|
|
9
|
+
|
|
10
|
+
export function acceptsEventStream(accept: string | undefined): boolean {
|
|
11
|
+
if (!accept) return false
|
|
12
|
+
return accept
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.split(',')
|
|
15
|
+
.some((entry) => entry.split(';')[0].trim() === 'text/event-stream')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const UNSAFE_PATH_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype'])
|
|
19
|
+
|
|
20
|
+
export function setByPath(target: Record<string, unknown>, path: string, value: unknown): boolean {
|
|
21
|
+
const parts = path.split('.')
|
|
22
|
+
if (parts.length === 0) return false
|
|
23
|
+
for (const p of parts) {
|
|
24
|
+
if (p === '' || UNSAFE_PATH_SEGMENTS.has(p)) return false
|
|
25
|
+
}
|
|
26
|
+
let cursor: Record<string, unknown> = target
|
|
27
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
28
|
+
const part = parts[i]
|
|
29
|
+
const next = cursor[part]
|
|
30
|
+
if (!isPlainObject(next)) {
|
|
31
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
32
|
+
console.warn(
|
|
33
|
+
LOG_PREFIX,
|
|
34
|
+
'Dropping patch for "' + path +
|
|
35
|
+
'": cannot traverse non-plain-object at segment "' + part + '"',
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
cursor = next
|
|
41
|
+
}
|
|
42
|
+
cursor[parts[parts.length - 1]] = value
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type EventEmitterLike = {
|
|
47
|
+
on?: (event: string, listener: () => void) => unknown
|
|
48
|
+
off?: (event: string, listener: () => void) => unknown
|
|
49
|
+
removeListener?: (event: string, listener: () => void) => unknown
|
|
50
|
+
destroyed?: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type SseWritable = {
|
|
54
|
+
statusCode: number
|
|
55
|
+
setHeader: (name: string, value: string) => unknown
|
|
56
|
+
flushHeaders?: () => unknown
|
|
57
|
+
flush?: () => unknown
|
|
58
|
+
write: (chunk: string) => unknown
|
|
59
|
+
end: () => unknown
|
|
60
|
+
writableEnded: boolean
|
|
61
|
+
destroyed: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function removeReqCloseListener(req: EventEmitterLike, listener: () => void): void {
|
|
65
|
+
if (typeof req.off === 'function') {
|
|
66
|
+
req.off('close', listener)
|
|
67
|
+
} else if (typeof req.removeListener === 'function') {
|
|
68
|
+
req.removeListener('close', listener)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function initSSE(res: SseWritable): void {
|
|
73
|
+
res.statusCode = 200
|
|
74
|
+
res.setHeader('Content-Type', 'text/event-stream')
|
|
75
|
+
res.setHeader('Cache-Control', 'no-cache, no-transform')
|
|
76
|
+
res.setHeader('Connection', 'keep-alive')
|
|
77
|
+
res.setHeader('X-Accel-Buffering', 'no')
|
|
78
|
+
if (typeof res.flushHeaders === 'function') res.flushHeaders()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function flushSSE(res: SseWritable): void {
|
|
82
|
+
if (typeof res.flush === 'function') {
|
|
83
|
+
try { res.flush() } catch {}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function sendSSE(res: SseWritable, payload: unknown): boolean {
|
|
88
|
+
if (res.writableEnded || res.destroyed) return false
|
|
89
|
+
try {
|
|
90
|
+
res.write('data: ' + JSON.stringify(transformResult(payload)) + '\n\n')
|
|
91
|
+
flushSSE(res)
|
|
92
|
+
return true
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error(LOG_PREFIX, 'failed to send SSE event:', err)
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function sendSSEProgress(res: SseWritable, stage: string, completed: number, total: number): boolean {
|
|
100
|
+
return sendSSE(res, { type: 'progress', stage, completed, total })
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function sendSSEField(res: SseWritable, key: string, value: unknown): boolean {
|
|
104
|
+
return sendSSE(res, { type: 'field', key, value })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function sendSSEResult(res: SseWritable, data: unknown): boolean {
|
|
108
|
+
return sendSSE(res, { type: 'result', data })
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function sendSSERootArray(res: SseWritable, rows: unknown[]): boolean {
|
|
112
|
+
return sendSSE(res, { type: 'rootArray', data: rows })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function sendSSERelationBatch(
|
|
116
|
+
res: SseWritable,
|
|
117
|
+
relationPath: string,
|
|
118
|
+
values: unknown[],
|
|
119
|
+
): boolean {
|
|
120
|
+
return sendSSE(res, { type: 'relationBatch', relationPath, values })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function sendSSENestedRelationBatch(
|
|
124
|
+
res: SseWritable,
|
|
125
|
+
relationPath: string,
|
|
126
|
+
depth: number,
|
|
127
|
+
attachments: Array<{ locator: Array<number | string>; value: unknown }>,
|
|
128
|
+
): boolean {
|
|
129
|
+
return sendSSE(res, { type: 'nestedRelationBatch', relationPath, depth, attachments })
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function sendSSEPageMeta(
|
|
133
|
+
res: SseWritable,
|
|
134
|
+
total: number,
|
|
135
|
+
hasMore: boolean,
|
|
136
|
+
): boolean {
|
|
137
|
+
return sendSSE(res, { type: 'pageMeta', total, hasMore })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function sendSSEError(res: SseWritable, message: string): boolean {
|
|
141
|
+
if (res.writableEnded || res.destroyed) return false
|
|
142
|
+
try {
|
|
143
|
+
res.write('data: ' + JSON.stringify({ type: 'error', message }) + '\n\n')
|
|
144
|
+
flushSSE(res)
|
|
145
|
+
return true
|
|
146
|
+
} catch (err) {
|
|
147
|
+
console.error(LOG_PREFIX, 'failed to send SSE error event:', err)
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function safeSendError(res: SseWritable, message: string): void {
|
|
153
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
154
|
+
sendSSEError(res, message)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
type IntervalHandle = ReturnType<typeof setInterval>
|
|
159
|
+
|
|
160
|
+
export function startSSEKeepalive(res: SseWritable, intervalMs: number = 15000): IntervalHandle {
|
|
161
|
+
const handle = setInterval(() => {
|
|
162
|
+
if (res.writableEnded || res.destroyed) return
|
|
163
|
+
try {
|
|
164
|
+
res.write(': keepalive\n\n')
|
|
165
|
+
flushSSE(res)
|
|
166
|
+
} catch {}
|
|
167
|
+
}, intervalMs)
|
|
168
|
+
const maybeUnref = (handle as unknown as { unref?: () => void }).unref
|
|
169
|
+
if (typeof maybeUnref === 'function') maybeUnref.call(handle)
|
|
170
|
+
return handle
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function endSSE(res: SseWritable, keepaliveHandle: IntervalHandle | null): void {
|
|
174
|
+
if (keepaliveHandle) {
|
|
175
|
+
clearInterval(keepaliveHandle)
|
|
176
|
+
}
|
|
177
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
178
|
+
try { res.end() } catch {}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function emitTerminalSSEError(res: SseWritable, message: string): void {
|
|
183
|
+
let keepalive: IntervalHandle | null = null
|
|
184
|
+
try {
|
|
185
|
+
initSSE(res)
|
|
186
|
+
keepalive = startSSEKeepalive(res)
|
|
187
|
+
sendSSEError(res, message)
|
|
188
|
+
} finally {
|
|
189
|
+
endSSE(res, keepalive)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface WithSSEOptions {
|
|
194
|
+
res: SseWritable
|
|
195
|
+
signal?: AbortSignal
|
|
196
|
+
label: string
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function withSSE(
|
|
200
|
+
options: WithSSEOptions,
|
|
201
|
+
fn: () => Promise<void>,
|
|
202
|
+
): Promise<void> {
|
|
203
|
+
const { res, signal, label } = options
|
|
204
|
+
let keepalive: IntervalHandle | null = null
|
|
205
|
+
const isClientGone = () =>
|
|
206
|
+
signal?.aborted === true || res.writableEnded || res.destroyed
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
initSSE(res)
|
|
210
|
+
keepalive = startSSEKeepalive(res)
|
|
211
|
+
if (isClientGone()) return
|
|
212
|
+
await fn()
|
|
213
|
+
} catch (err) {
|
|
214
|
+
if (isClientGone()) return
|
|
215
|
+
console.error(LOG_PREFIX, label + ' dispatch error:', err)
|
|
216
|
+
safeSendError(res, mapError(err).message)
|
|
217
|
+
} finally {
|
|
218
|
+
endSSE(res, keepalive)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface RunSingleResultSSEOptions {
|
|
223
|
+
req: EventEmitterLike
|
|
224
|
+
res: SseWritable
|
|
225
|
+
coreQueryFn: () => Promise<unknown>
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function runSingleResultSSE(options: RunSingleResultSSEOptions): Promise<void> {
|
|
229
|
+
const { req, res, coreQueryFn } = options
|
|
230
|
+
await withSSE({ res, label: 'single-result' }, async () => {
|
|
231
|
+
if (req.destroyed) return
|
|
232
|
+
const data = await coreQueryFn()
|
|
233
|
+
if (res.writableEnded || res.destroyed) return
|
|
234
|
+
sendSSEResult(res, data)
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isStopResult(value: unknown): value is ProgressiveStopResult<unknown> {
|
|
239
|
+
return typeof value === 'object' && value !== null && (value as { stop?: unknown }).stop === true
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface RunProgressiveOptions {
|
|
243
|
+
req: EventEmitterLike
|
|
244
|
+
res: SseWritable
|
|
245
|
+
ctx: unknown
|
|
246
|
+
prisma: unknown
|
|
247
|
+
variant: string
|
|
248
|
+
stages: string[]
|
|
249
|
+
stageRegistry: Record<string, ProgressiveStage>
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function runProgressiveEndpoint(options: RunProgressiveOptions): Promise<void> {
|
|
253
|
+
const { req, res, ctx, prisma, variant, stages, stageRegistry } = options
|
|
254
|
+
const controller = new AbortController()
|
|
255
|
+
const onClose = () => controller.abort()
|
|
256
|
+
if (typeof req.on === 'function') req.on('close', onClose)
|
|
257
|
+
|
|
258
|
+
const accumulated: Record<string, unknown> = {}
|
|
259
|
+
const signal = controller.signal
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
await withSSE({ res, signal, label: 'stage' }, async () => {
|
|
263
|
+
sendSSEProgress(res, 'start', 0, stages.length)
|
|
264
|
+
|
|
265
|
+
for (let i = 0; i < stages.length; i++) {
|
|
266
|
+
if (res.writableEnded || res.destroyed || signal.aborted) return
|
|
267
|
+
const stageName = stages[i]
|
|
268
|
+
const stage = stageRegistry[stageName]
|
|
269
|
+
if (!stage) throw new HttpError(500, 'Missing progressive stage: ' + stageName)
|
|
270
|
+
|
|
271
|
+
const result = await stage({ ctx, req, res, prisma, variant, accumulated, signal })
|
|
272
|
+
if (res.writableEnded || res.destroyed) return
|
|
273
|
+
|
|
274
|
+
if (isStopResult(result)) {
|
|
275
|
+
sendSSEResult(res, result.data)
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const patches = Array.isArray(result) ? result : result ? [result] : []
|
|
280
|
+
for (const patch of patches) {
|
|
281
|
+
if (!patch || typeof patch !== 'object') continue
|
|
282
|
+
const p = patch as ProgressivePatch
|
|
283
|
+
if (typeof p.key !== 'string') continue
|
|
284
|
+
if (!('value' in p)) continue
|
|
285
|
+
const applied = setByPath(accumulated, p.key, p.value)
|
|
286
|
+
if (applied) sendSSEField(res, p.key, p.value)
|
|
287
|
+
}
|
|
288
|
+
sendSSEProgress(res, stageName, i + 1, stages.length)
|
|
289
|
+
}
|
|
290
|
+
if (res.writableEnded || res.destroyed) return
|
|
291
|
+
sendSSEResult(res, accumulated)
|
|
292
|
+
})
|
|
293
|
+
} finally {
|
|
294
|
+
removeReqCloseListener(req, onClose)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -1,44 +1,7 @@
|
|
|
1
1
|
import { DMMF } from '@prisma/generator-helper'
|
|
2
2
|
import { ImportStyle } from '../utils/resolveImportStyle'
|
|
3
3
|
import { importExt } from '../utils/importExt'
|
|
4
|
-
|
|
5
|
-
const CORE_NAME_MAP: Record<string, string> = {
|
|
6
|
-
delete: 'deleteUnique',
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function coreFnName(op: string): string {
|
|
10
|
-
return CORE_NAME_MAP[op] || op
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const READ_OPS = [
|
|
14
|
-
'findMany',
|
|
15
|
-
'findFirst',
|
|
16
|
-
'findFirstOrThrow',
|
|
17
|
-
'findUnique',
|
|
18
|
-
'findUniqueOrThrow',
|
|
19
|
-
'findManyPaginated',
|
|
20
|
-
'aggregate',
|
|
21
|
-
'count',
|
|
22
|
-
'groupBy',
|
|
23
|
-
]
|
|
24
|
-
|
|
25
|
-
const WRITE_OPS = [
|
|
26
|
-
'create',
|
|
27
|
-
'createMany',
|
|
28
|
-
'createManyAndReturn',
|
|
29
|
-
'update',
|
|
30
|
-
'updateMany',
|
|
31
|
-
'updateManyAndReturn',
|
|
32
|
-
'upsert',
|
|
33
|
-
'delete',
|
|
34
|
-
'deleteMany',
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
const CREATED_OPS = new Set([
|
|
38
|
-
'create',
|
|
39
|
-
'createMany',
|
|
40
|
-
'createManyAndReturn',
|
|
41
|
-
])
|
|
4
|
+
import { OPERATION_METADATA } from '../copy/operationDefinitions'
|
|
42
5
|
|
|
43
6
|
export function generateFastifyHandler(options: {
|
|
44
7
|
model: DMMF.Model
|
|
@@ -47,31 +10,34 @@ export function generateFastifyHandler(options: {
|
|
|
47
10
|
const ext = importExt(options.importStyle)
|
|
48
11
|
const modelName = options.model.name
|
|
49
12
|
|
|
50
|
-
const
|
|
51
|
-
|
|
13
|
+
const readOps = OPERATION_METADATA.filter((m) => m.kind === 'read')
|
|
14
|
+
const writeOps = OPERATION_METADATA.filter(
|
|
15
|
+
(m) => (m.kind === 'write' || m.kind === 'batch') && m.name !== 'updateEach',
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const readHandlers = readOps.map((meta) => {
|
|
19
|
+
const exportName = `${modelName}${meta.name.charAt(0).toUpperCase() + meta.name.slice(1)}`
|
|
52
20
|
return `
|
|
53
21
|
export async function ${exportName}(
|
|
54
22
|
request: FastifyRequest,
|
|
55
23
|
_reply: FastifyReply,
|
|
56
24
|
): Promise<void> {
|
|
57
|
-
const data = await core.${
|
|
25
|
+
const data = await core.${meta.coreName}(buildContext(request))
|
|
58
26
|
;(request as FastifyExtended).resultData = data
|
|
59
27
|
}`
|
|
60
28
|
}).join('\n')
|
|
61
29
|
|
|
62
|
-
const writeHandlers =
|
|
63
|
-
const exportName = `${modelName}${
|
|
64
|
-
const statusCode = CREATED_OPS.has(op) ? 201 : 200
|
|
65
|
-
|
|
30
|
+
const writeHandlers = writeOps.map((meta) => {
|
|
31
|
+
const exportName = `${modelName}${meta.name.charAt(0).toUpperCase() + meta.name.slice(1)}`
|
|
66
32
|
return `
|
|
67
33
|
export async function ${exportName}(
|
|
68
34
|
request: FastifyRequest,
|
|
69
35
|
_reply: FastifyReply,
|
|
70
36
|
): Promise<void> {
|
|
71
|
-
const data = await core.${
|
|
37
|
+
const data = await core.${meta.coreName}(buildContext(request))
|
|
72
38
|
const ext = request as FastifyExtended
|
|
73
39
|
ext.resultData = data
|
|
74
|
-
ext.resultStatus = ${
|
|
40
|
+
ext.resultStatus = ${meta.successStatus}
|
|
75
41
|
}`
|
|
76
42
|
}).join('\n')
|
|
77
43
|
|
|
@@ -1,44 +1,7 @@
|
|
|
1
1
|
import { DMMF } from '@prisma/generator-helper'
|
|
2
2
|
import { ImportStyle } from '../utils/resolveImportStyle'
|
|
3
3
|
import { importExt } from '../utils/importExt'
|
|
4
|
-
|
|
5
|
-
const CORE_NAME_MAP: Record<string, string> = {
|
|
6
|
-
delete: 'deleteUnique',
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function coreFnName(op: string): string {
|
|
10
|
-
return CORE_NAME_MAP[op] || op
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const READ_OPS = [
|
|
14
|
-
'findMany',
|
|
15
|
-
'findFirst',
|
|
16
|
-
'findFirstOrThrow',
|
|
17
|
-
'findUnique',
|
|
18
|
-
'findUniqueOrThrow',
|
|
19
|
-
'findManyPaginated',
|
|
20
|
-
'aggregate',
|
|
21
|
-
'count',
|
|
22
|
-
'groupBy',
|
|
23
|
-
]
|
|
24
|
-
|
|
25
|
-
const WRITE_OPS = [
|
|
26
|
-
'create',
|
|
27
|
-
'createMany',
|
|
28
|
-
'createManyAndReturn',
|
|
29
|
-
'update',
|
|
30
|
-
'updateMany',
|
|
31
|
-
'updateManyAndReturn',
|
|
32
|
-
'upsert',
|
|
33
|
-
'delete',
|
|
34
|
-
'deleteMany',
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
const CREATED_OPS = new Set([
|
|
38
|
-
'create',
|
|
39
|
-
'createMany',
|
|
40
|
-
'createManyAndReturn',
|
|
41
|
-
])
|
|
4
|
+
import { OPERATION_METADATA } from '../copy/operationDefinitions'
|
|
42
5
|
|
|
43
6
|
export function generateHonoHandler(options: {
|
|
44
7
|
model: DMMF.Model
|
|
@@ -47,24 +10,27 @@ export function generateHonoHandler(options: {
|
|
|
47
10
|
const ext = importExt(options.importStyle)
|
|
48
11
|
const modelName = options.model.name
|
|
49
12
|
|
|
50
|
-
const
|
|
51
|
-
|
|
13
|
+
const readOps = OPERATION_METADATA.filter((m) => m.kind === 'read')
|
|
14
|
+
const writeOps = OPERATION_METADATA.filter(
|
|
15
|
+
(m) => (m.kind === 'write' || m.kind === 'batch') && m.name !== 'updateEach',
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const readHandlers = readOps.map((meta) => {
|
|
19
|
+
const exportName = `${modelName}${meta.name.charAt(0).toUpperCase() + meta.name.slice(1)}`
|
|
52
20
|
return `
|
|
53
21
|
export async function ${exportName}(c: HandlerContext): Promise<void> {
|
|
54
|
-
const data = await core.${
|
|
22
|
+
const data = await core.${meta.coreName}(buildContext(c))
|
|
55
23
|
c.set('resultData', data)
|
|
56
24
|
}`
|
|
57
25
|
}).join('\n')
|
|
58
26
|
|
|
59
|
-
const writeHandlers =
|
|
60
|
-
const exportName = `${modelName}${
|
|
61
|
-
const statusCode = CREATED_OPS.has(op) ? 201 : 200
|
|
62
|
-
|
|
27
|
+
const writeHandlers = writeOps.map((meta) => {
|
|
28
|
+
const exportName = `${modelName}${meta.name.charAt(0).toUpperCase() + meta.name.slice(1)}`
|
|
63
29
|
return `
|
|
64
30
|
export async function ${exportName}(c: HandlerContext): Promise<void> {
|
|
65
|
-
const data = await core.${
|
|
31
|
+
const data = await core.${meta.coreName}(buildContext(c))
|
|
66
32
|
c.set('resultData', data)
|
|
67
|
-
c.set('resultStatus', ${
|
|
33
|
+
c.set('resultStatus', ${meta.successStatus})
|
|
68
34
|
}`
|
|
69
35
|
}).join('\n')
|
|
70
36
|
|