prisma-generator-express 1.59.0 → 1.60.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/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/autoIncludeRuntime.ts +117 -228
- 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,20 @@
|
|
|
1
|
+
export async function mapLimited<T>(
|
|
2
|
+
items: T[],
|
|
3
|
+
limit: number,
|
|
4
|
+
fn: (item: T, index: number) => Promise<void>,
|
|
5
|
+
): Promise<void> {
|
|
6
|
+
if (items.length === 0) return
|
|
7
|
+
let index = 0
|
|
8
|
+
const workerCount = Math.min(limit, items.length)
|
|
9
|
+
const workers: Promise<void>[] = []
|
|
10
|
+
for (let w = 0; w < workerCount; w++) {
|
|
11
|
+
workers.push((async () => {
|
|
12
|
+
for (;;) {
|
|
13
|
+
const i = index++
|
|
14
|
+
if (i >= items.length) return
|
|
15
|
+
await fn(items[i], i)
|
|
16
|
+
}
|
|
17
|
+
})())
|
|
18
|
+
}
|
|
19
|
+
await Promise.all(workers)
|
|
20
|
+
}
|
package/src/copy/docsRenderer.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { RouteConfig, WriteStrategy } from './routeConfig'
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
OPERATION_METADATA,
|
|
4
|
+
OPERATION_BY_NAME,
|
|
4
5
|
isOperationEnabled,
|
|
5
6
|
READ_OPERATION_NAMES,
|
|
6
7
|
} from './operationDefinitions'
|
|
7
8
|
import { getEnv, normalizePrefix } from './misc'
|
|
9
|
+
import { NUMERIC_SCALAR_TYPES } from './scalarTypes'
|
|
8
10
|
|
|
9
11
|
const _env = getEnv()
|
|
10
12
|
|
|
@@ -44,10 +46,10 @@ export interface DocsModelContext {
|
|
|
44
46
|
|
|
45
47
|
interface OpDetail {
|
|
46
48
|
transport: string
|
|
47
|
-
required: string[]
|
|
49
|
+
required: readonly string[]
|
|
48
50
|
optional: string[]
|
|
49
51
|
responseDesc: string
|
|
50
|
-
errors: number[]
|
|
52
|
+
errors: readonly number[]
|
|
51
53
|
supportsSelect: boolean
|
|
52
54
|
supportsInclude: boolean
|
|
53
55
|
supportsOmit: boolean
|
|
@@ -60,295 +62,27 @@ const PRISM_JS = 'https://cdn.jsdelivr.net/npm/prismjs@1/prism.min.js'
|
|
|
60
62
|
const PRISM_JSON =
|
|
61
63
|
'https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-json.min.js'
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
notes: 'Pagination limits may apply when configured.',
|
|
84
|
-
},
|
|
85
|
-
findUnique: {
|
|
86
|
-
transport: 'GET query params',
|
|
87
|
-
required: ['where'],
|
|
88
|
-
optional: ['select', 'include', 'omit'],
|
|
89
|
-
responseDesc: 'Single record or null',
|
|
90
|
-
errors: [400, 403, 500, 501, 503],
|
|
91
|
-
supportsSelect: true,
|
|
92
|
-
supportsInclude: true,
|
|
93
|
-
supportsOmit: true,
|
|
94
|
-
notes: 'Returns null (not 404) when no record matches.',
|
|
95
|
-
},
|
|
96
|
-
findUniqueOrThrow: {
|
|
97
|
-
transport: 'GET query params',
|
|
98
|
-
required: ['where'],
|
|
99
|
-
optional: ['select', 'include', 'omit'],
|
|
100
|
-
responseDesc: 'Single record',
|
|
101
|
-
errors: [400, 403, 404, 500, 501, 503],
|
|
102
|
-
supportsSelect: true,
|
|
103
|
-
supportsInclude: true,
|
|
104
|
-
supportsOmit: true,
|
|
105
|
-
notes: 'Returns 404 when no record matches.',
|
|
106
|
-
},
|
|
107
|
-
findFirst: {
|
|
108
|
-
transport: 'GET query params',
|
|
109
|
-
required: [],
|
|
110
|
-
optional: [
|
|
111
|
-
'where',
|
|
112
|
-
'select',
|
|
113
|
-
'include',
|
|
114
|
-
'omit',
|
|
115
|
-
'orderBy',
|
|
116
|
-
'cursor',
|
|
117
|
-
'take',
|
|
118
|
-
'skip',
|
|
119
|
-
'distinct',
|
|
120
|
-
],
|
|
121
|
-
responseDesc: 'Single record or null',
|
|
122
|
-
errors: [400, 403, 500, 501, 503],
|
|
123
|
-
supportsSelect: true,
|
|
124
|
-
supportsInclude: true,
|
|
125
|
-
supportsOmit: true,
|
|
126
|
-
notes: 'Returns null (not 404) when no record matches.',
|
|
127
|
-
},
|
|
128
|
-
findFirstOrThrow: {
|
|
129
|
-
transport: 'GET query params',
|
|
130
|
-
required: [],
|
|
131
|
-
optional: [
|
|
132
|
-
'where',
|
|
133
|
-
'select',
|
|
134
|
-
'include',
|
|
135
|
-
'omit',
|
|
136
|
-
'orderBy',
|
|
137
|
-
'cursor',
|
|
138
|
-
'take',
|
|
139
|
-
'skip',
|
|
140
|
-
'distinct',
|
|
141
|
-
],
|
|
142
|
-
responseDesc: 'Single record',
|
|
143
|
-
errors: [400, 403, 404, 500, 501, 503],
|
|
144
|
-
supportsSelect: true,
|
|
145
|
-
supportsInclude: true,
|
|
146
|
-
supportsOmit: true,
|
|
147
|
-
notes: 'Returns 404 when no record matches.',
|
|
148
|
-
},
|
|
149
|
-
findManyPaginated: {
|
|
150
|
-
transport: 'GET query params',
|
|
151
|
-
required: [],
|
|
152
|
-
optional: [
|
|
153
|
-
'where',
|
|
154
|
-
'select',
|
|
155
|
-
'include',
|
|
156
|
-
'omit',
|
|
157
|
-
'orderBy',
|
|
158
|
-
'cursor',
|
|
159
|
-
'take',
|
|
160
|
-
'skip',
|
|
161
|
-
'distinct',
|
|
162
|
-
],
|
|
163
|
-
responseDesc: '{ data: Record[], total: number, hasMore: boolean }',
|
|
164
|
-
errors: [400, 403, 409, 500, 501, 503],
|
|
165
|
-
supportsSelect: true,
|
|
166
|
-
supportsInclude: true,
|
|
167
|
-
supportsOmit: true,
|
|
168
|
-
notes:
|
|
169
|
-
'Wraps findMany with total count. hasMore is reliable for forward offset pagination (skip + take) only. Distinct count over 100k falls back to approximate total. 409 possible on transaction conflict.',
|
|
170
|
-
},
|
|
171
|
-
create: {
|
|
172
|
-
transport: 'POST JSON body',
|
|
173
|
-
required: ['data'],
|
|
174
|
-
optional: ['select', 'include', 'omit'],
|
|
175
|
-
responseDesc: 'Created record (201)',
|
|
176
|
-
errors: [400, 403, 409, 500, 501, 503],
|
|
177
|
-
supportsSelect: true,
|
|
178
|
-
supportsInclude: true,
|
|
179
|
-
supportsOmit: true,
|
|
180
|
-
notes: '409 on unique constraint violation.',
|
|
181
|
-
},
|
|
182
|
-
createMany: {
|
|
183
|
-
transport: 'POST JSON body',
|
|
184
|
-
required: ['data'],
|
|
185
|
-
optional: ['skipDuplicates'],
|
|
186
|
-
responseDesc: '{ count: number } (201)',
|
|
187
|
-
errors: [400, 403, 409, 500, 501, 503],
|
|
188
|
-
supportsSelect: false,
|
|
189
|
-
supportsInclude: false,
|
|
190
|
-
supportsOmit: false,
|
|
191
|
-
notes:
|
|
192
|
-
'data is an array of scalar-only inputs. Nested relation writes are not supported. skipDuplicates silently ignores conflicts (not supported on all providers).',
|
|
193
|
-
},
|
|
194
|
-
createManyAndReturn: {
|
|
195
|
-
transport: 'POST JSON body',
|
|
196
|
-
required: ['data'],
|
|
197
|
-
optional: ['skipDuplicates', 'select', 'include', 'omit'],
|
|
198
|
-
responseDesc: 'Array of created records (201)',
|
|
199
|
-
errors: [400, 403, 409, 500, 501, 503],
|
|
200
|
-
supportsSelect: true,
|
|
201
|
-
supportsInclude: true,
|
|
202
|
-
supportsOmit: true,
|
|
203
|
-
notes:
|
|
204
|
-
'Like createMany but returns created records. data items are scalar-only. Requires Prisma 5.14.0+, PostgreSQL/CockroachDB/SQLite only. The order of returned records is not guaranteed.',
|
|
205
|
-
},
|
|
206
|
-
update: {
|
|
207
|
-
transport: 'PUT JSON body',
|
|
208
|
-
required: ['where', 'data'],
|
|
209
|
-
optional: ['select', 'include', 'omit'],
|
|
210
|
-
responseDesc: 'Updated record',
|
|
211
|
-
errors: [400, 403, 404, 409, 500, 501, 503],
|
|
212
|
-
supportsSelect: true,
|
|
213
|
-
supportsInclude: true,
|
|
214
|
-
supportsOmit: true,
|
|
215
|
-
notes:
|
|
216
|
-
'404 when the record to update is not found. 409 on unique constraint violation or transaction conflict.',
|
|
217
|
-
},
|
|
218
|
-
updateEach: {
|
|
219
|
-
transport: 'POST JSON body (array)',
|
|
220
|
-
required: ['array of { where, data }'],
|
|
221
|
-
optional: [],
|
|
222
|
-
responseDesc:
|
|
223
|
-
'Non-atomic: per-row { status, data } / { status, error } array. Atomic: array of records.',
|
|
224
|
-
errors: [400, 403, 409, 500, 501, 503],
|
|
225
|
-
supportsSelect: false,
|
|
226
|
-
supportsInclude: false,
|
|
227
|
-
supportsOmit: false,
|
|
228
|
-
notes:
|
|
229
|
-
'Internal batch endpoint. Bypasses guard shapes. Not enabled by enableAll. Non-atomic max 1000 items, atomic max 100 items. Header x-batch-atomic:true switches to transactional mode.',
|
|
230
|
-
},
|
|
231
|
-
updateMany: {
|
|
232
|
-
transport: 'PUT JSON body',
|
|
233
|
-
required: ['where', 'data'],
|
|
234
|
-
optional: [],
|
|
235
|
-
responseDesc: '{ count: number }',
|
|
236
|
-
errors: [400, 403, 409, 500, 501, 503],
|
|
237
|
-
supportsSelect: false,
|
|
238
|
-
supportsInclude: false,
|
|
239
|
-
supportsOmit: false,
|
|
240
|
-
notes:
|
|
241
|
-
'Updates all matching records with scalar-only data. Nested relation writes are not supported. Returns count, not records. 409 on unique constraint violation.',
|
|
242
|
-
},
|
|
243
|
-
updateManyAndReturn: {
|
|
244
|
-
transport: 'PUT JSON body',
|
|
245
|
-
required: ['where', 'data'],
|
|
246
|
-
optional: ['select', 'include', 'omit'],
|
|
247
|
-
responseDesc: 'Array of updated records',
|
|
248
|
-
errors: [400, 403, 409, 500, 501, 503],
|
|
249
|
-
supportsSelect: true,
|
|
250
|
-
supportsInclude: true,
|
|
251
|
-
supportsOmit: true,
|
|
252
|
-
notes:
|
|
253
|
-
'Like updateMany but returns updated records. data is scalar-only. Requires Prisma 6.2.0+, PostgreSQL/CockroachDB/SQLite only. 409 on unique constraint violation.',
|
|
254
|
-
},
|
|
255
|
-
upsert: {
|
|
256
|
-
transport: 'PATCH JSON body',
|
|
257
|
-
required: ['where', 'create', 'update'],
|
|
258
|
-
optional: ['select', 'include', 'omit'],
|
|
259
|
-
responseDesc: 'Created or updated record',
|
|
260
|
-
errors: [400, 403, 409, 500, 501, 503],
|
|
261
|
-
supportsSelect: true,
|
|
262
|
-
supportsInclude: true,
|
|
263
|
-
supportsOmit: true,
|
|
264
|
-
notes: 'Creates if not found, updates if found.',
|
|
265
|
-
},
|
|
266
|
-
delete: {
|
|
267
|
-
transport: 'DELETE JSON body',
|
|
268
|
-
required: ['where'],
|
|
269
|
-
optional: ['select', 'include', 'omit'],
|
|
270
|
-
responseDesc: 'Deleted record',
|
|
271
|
-
errors: [400, 403, 404, 500, 501, 503],
|
|
272
|
-
supportsSelect: true,
|
|
273
|
-
supportsInclude: true,
|
|
274
|
-
supportsOmit: true,
|
|
275
|
-
notes: '404 when the record to delete is not found.',
|
|
276
|
-
},
|
|
277
|
-
deleteMany: {
|
|
278
|
-
transport: 'DELETE JSON body',
|
|
279
|
-
required: ['where'],
|
|
280
|
-
optional: [],
|
|
281
|
-
responseDesc: '{ count: number }',
|
|
282
|
-
errors: [400, 403, 500, 501, 503],
|
|
283
|
-
supportsSelect: false,
|
|
284
|
-
supportsInclude: false,
|
|
285
|
-
supportsOmit: false,
|
|
286
|
-
notes: 'Deletes all matching records. Returns count, not records.',
|
|
287
|
-
},
|
|
288
|
-
count: {
|
|
289
|
-
transport: 'GET query params',
|
|
290
|
-
required: [],
|
|
291
|
-
optional: ['where', 'orderBy', 'cursor', 'take', 'skip', 'select'],
|
|
292
|
-
responseDesc: 'Integer, or per-field count object when select is provided',
|
|
293
|
-
errors: [400, 403, 500, 501, 503],
|
|
294
|
-
supportsSelect: false,
|
|
295
|
-
supportsInclude: false,
|
|
296
|
-
supportsOmit: false,
|
|
297
|
-
notes:
|
|
298
|
-
'select here means count-specific field selection, not record field selection.',
|
|
299
|
-
},
|
|
300
|
-
aggregate: {
|
|
301
|
-
transport: 'GET query params',
|
|
302
|
-
required: [],
|
|
303
|
-
optional: [
|
|
304
|
-
'where',
|
|
305
|
-
'orderBy',
|
|
306
|
-
'cursor',
|
|
307
|
-
'take',
|
|
308
|
-
'skip',
|
|
309
|
-
'_count',
|
|
310
|
-
'_avg',
|
|
311
|
-
'_sum',
|
|
312
|
-
'_min',
|
|
313
|
-
'_max',
|
|
314
|
-
],
|
|
315
|
-
responseDesc:
|
|
316
|
-
'Object with requested aggregate fields (_count, _avg, _sum, _min, _max)',
|
|
317
|
-
errors: [400, 403, 500, 501, 503],
|
|
318
|
-
supportsSelect: false,
|
|
319
|
-
supportsInclude: false,
|
|
320
|
-
supportsOmit: false,
|
|
321
|
-
notes: '_avg, _sum only apply to numeric fields.',
|
|
322
|
-
},
|
|
323
|
-
groupBy: {
|
|
324
|
-
transport: 'GET query params',
|
|
325
|
-
required: ['by'],
|
|
326
|
-
optional: [
|
|
327
|
-
'where',
|
|
328
|
-
'orderBy',
|
|
329
|
-
'having',
|
|
330
|
-
'take',
|
|
331
|
-
'skip',
|
|
332
|
-
'_count',
|
|
333
|
-
'_avg',
|
|
334
|
-
'_sum',
|
|
335
|
-
'_min',
|
|
336
|
-
'_max',
|
|
337
|
-
],
|
|
338
|
-
responseDesc:
|
|
339
|
-
'Array of objects, each with grouped field values and requested aggregates',
|
|
340
|
-
errors: [400, 403, 500, 501, 503],
|
|
341
|
-
supportsSelect: false,
|
|
342
|
-
supportsInclude: false,
|
|
343
|
-
supportsOmit: false,
|
|
344
|
-
notes:
|
|
345
|
-
'by is a JSON-encoded array of scalar field names. orderBy is required when using skip or take. Response contains only the by-fields plus requested aggregates.',
|
|
346
|
-
},
|
|
347
|
-
}
|
|
65
|
+
function detailForOp(
|
|
66
|
+
opName: string,
|
|
67
|
+
writeStrategy?: WriteStrategy,
|
|
68
|
+
): OpDetail | null {
|
|
69
|
+
const meta = OPERATION_BY_NAME[opName]
|
|
70
|
+
if (!meta) return null
|
|
71
|
+
|
|
72
|
+
const optional = meta.argsSchema.filter((a) => !meta.requiredArgs.includes(a))
|
|
73
|
+
|
|
74
|
+
const base: OpDetail = {
|
|
75
|
+
transport: meta.transport,
|
|
76
|
+
required: meta.requiredArgs,
|
|
77
|
+
optional,
|
|
78
|
+
responseDesc: meta.responseDesc,
|
|
79
|
+
errors: meta.errors,
|
|
80
|
+
supportsSelect: meta.supportsProjection,
|
|
81
|
+
supportsInclude: meta.supportsProjection,
|
|
82
|
+
supportsOmit: meta.supportsProjection,
|
|
83
|
+
notes: meta.notes,
|
|
84
|
+
}
|
|
348
85
|
|
|
349
|
-
function detailForOp(opName: string, writeStrategy?: WriteStrategy): OpDetail {
|
|
350
|
-
const base = OP_DETAIL_MAP[opName]
|
|
351
|
-
if (!base) return base
|
|
352
86
|
if (writeStrategy === 'forceReturn') {
|
|
353
87
|
if (opName === 'createMany') {
|
|
354
88
|
return {
|
|
@@ -375,6 +109,7 @@ function detailForOp(opName: string, writeStrategy?: WriteStrategy): OpDetail {
|
|
|
375
109
|
}
|
|
376
110
|
}
|
|
377
111
|
}
|
|
112
|
+
|
|
378
113
|
return base
|
|
379
114
|
}
|
|
380
115
|
|
|
@@ -570,14 +305,8 @@ function scalarFilterOperators(scalarType: string): string[] {
|
|
|
570
305
|
'not',
|
|
571
306
|
]
|
|
572
307
|
}
|
|
573
|
-
if (
|
|
574
|
-
scalarType === 'Int' ||
|
|
575
|
-
scalarType === 'BigInt' ||
|
|
576
|
-
scalarType === 'Float' ||
|
|
577
|
-
scalarType === 'Decimal'
|
|
578
|
-
) {
|
|
308
|
+
if (NUMERIC_SCALAR_TYPES.has(scalarType))
|
|
579
309
|
return ['equals', 'in', 'notIn', 'lt', 'lte', 'gt', 'gte', 'not']
|
|
580
|
-
}
|
|
581
310
|
if (scalarType === 'DateTime')
|
|
582
311
|
return ['equals', 'in', 'notIn', 'lt', 'lte', 'gt', 'gte', 'not']
|
|
583
312
|
if (scalarType === 'Boolean') return ['equals', 'not']
|
|
@@ -696,19 +425,19 @@ export function renderDocs(
|
|
|
696
425
|
const listRelations = relationFields.filter((f) => f.isList)
|
|
697
426
|
const singleRelations = relationFields.filter((f) => !f.isList)
|
|
698
427
|
|
|
699
|
-
const getOps =
|
|
700
|
-
isOperationEnabled(config as Record<string, any>,
|
|
428
|
+
const getOps = OPERATION_METADATA.filter((meta) =>
|
|
429
|
+
isOperationEnabled(config as Record<string, any>, meta),
|
|
701
430
|
)
|
|
702
|
-
.filter((
|
|
703
|
-
.map((
|
|
704
|
-
const detail = detailForOp(
|
|
431
|
+
.filter((meta) => !isOpHiddenByStrategy(meta.name, writeStrategy))
|
|
432
|
+
.map((meta) => {
|
|
433
|
+
const detail = detailForOp(meta.name, writeStrategy)
|
|
705
434
|
return {
|
|
706
|
-
op:
|
|
707
|
-
method:
|
|
708
|
-
path: buildFullPath(exampleBasePath,
|
|
435
|
+
op: meta.name,
|
|
436
|
+
method: meta.method.toUpperCase(),
|
|
437
|
+
path: buildFullPath(exampleBasePath, meta.pathSuffix),
|
|
709
438
|
transport: detail
|
|
710
439
|
? detail.transport
|
|
711
|
-
:
|
|
440
|
+
: meta.method === 'get'
|
|
712
441
|
? 'GET query params'
|
|
713
442
|
: 'JSON body',
|
|
714
443
|
responseDesc: detail ? detail.responseDesc : '',
|
|
@@ -723,32 +452,32 @@ export function renderDocs(
|
|
|
723
452
|
})
|
|
724
453
|
|
|
725
454
|
const postReadOps = postReadsEnabled
|
|
726
|
-
?
|
|
727
|
-
(
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
455
|
+
? OPERATION_METADATA.filter((meta) => READ_OPERATION_NAMES.has(meta.name))
|
|
456
|
+
.filter((meta) =>
|
|
457
|
+
isOperationEnabled(config as Record<string, any>, meta),
|
|
458
|
+
)
|
|
459
|
+
.map((meta) => {
|
|
460
|
+
const detail = detailForOp(meta.name)
|
|
461
|
+
const postPath =
|
|
462
|
+
meta.name === 'findMany'
|
|
463
|
+
? buildFullPath(exampleBasePath, '/read')
|
|
464
|
+
: buildFullPath(exampleBasePath, meta.pathSuffix)
|
|
465
|
+
return {
|
|
466
|
+
op: meta.name + ' (POST)',
|
|
467
|
+
method: 'POST',
|
|
468
|
+
path: postPath,
|
|
469
|
+
transport: 'POST JSON body',
|
|
470
|
+
responseDesc: detail ? detail.responseDesc : '',
|
|
471
|
+
errors: detail ? detail.errors.join(', ') : '',
|
|
472
|
+
required: detail ? detail.required : [],
|
|
473
|
+
optional: detail ? detail.optional : [],
|
|
474
|
+
supportsSelect: detail ? detail.supportsSelect : false,
|
|
475
|
+
supportsInclude: detail ? detail.supportsInclude : false,
|
|
476
|
+
supportsOmit: detail ? detail.supportsOmit : false,
|
|
477
|
+
notes:
|
|
478
|
+
'POST alternative for complex queries exceeding URL length limits. Same args as GET but in request body.',
|
|
479
|
+
}
|
|
480
|
+
})
|
|
752
481
|
: []
|
|
753
482
|
|
|
754
483
|
const ops = [...getOps, ...postReadOps]
|
|
@@ -1301,6 +1030,7 @@ export function renderDocs(
|
|
|
1301
1030
|
'hasMore is reliable for forward offset pagination (skip + take) only. With cursor pagination or negative take, hasMore may be inaccurate.',
|
|
1302
1031
|
'When the server config sets pagination.defaultLimit, take is automatically applied to findMany and findManyPaginated if omitted.',
|
|
1303
1032
|
'When the server config sets pagination.maxLimit, take is capped by absolute value to that limit for findMany and findManyPaginated. This applies to both positive and negative take values.',
|
|
1033
|
+
'When maxLimit is set and take is a non-finite or non-integer value, the request is rejected with 400.',
|
|
1304
1034
|
'Clients cannot detect these server-side limits from the API alone. Check with the API provider for configured limits.',
|
|
1305
1035
|
]
|
|
1306
1036
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export const LOG_PREFIX = '[auto-progressive]'
|
|
2
|
+
|
|
3
|
+
export class HttpError extends Error {
|
|
4
|
+
status: number
|
|
5
|
+
constructor(status: number, message: string) {
|
|
6
|
+
super(message)
|
|
7
|
+
this.name = 'HttpError'
|
|
8
|
+
this.status = status
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const PRISMA_ERROR_MAP: Record<string, { status: number; message: string }> = {
|
|
13
|
+
P2000: { status: 400, message: 'Value too long for column' },
|
|
14
|
+
P2001: { status: 404, message: 'Record not found' },
|
|
15
|
+
P2002: { status: 409, message: 'Unique constraint violation' },
|
|
16
|
+
P2003: { status: 400, message: 'Foreign key constraint failed' },
|
|
17
|
+
P2004: { status: 400, message: 'Constraint failed on the database' },
|
|
18
|
+
P2005: { status: 400, message: 'Invalid field value' },
|
|
19
|
+
P2006: { status: 400, message: 'Invalid value provided' },
|
|
20
|
+
P2007: { status: 400, message: 'Data validation error' },
|
|
21
|
+
P2008: { status: 400, message: 'Failed to parse the query' },
|
|
22
|
+
P2009: { status: 400, message: 'Failed to validate the query' },
|
|
23
|
+
P2010: { status: 500, message: 'Raw query failed' },
|
|
24
|
+
P2011: { status: 400, message: 'Null constraint violation' },
|
|
25
|
+
P2012: { status: 400, message: 'Missing required value' },
|
|
26
|
+
P2013: { status: 400, message: 'Missing required argument' },
|
|
27
|
+
P2014: { status: 400, message: 'Required relation violation' },
|
|
28
|
+
P2015: { status: 404, message: 'Related record not found' },
|
|
29
|
+
P2016: { status: 400, message: 'Query interpretation error' },
|
|
30
|
+
P2017: { status: 400, message: 'Records not connected' },
|
|
31
|
+
P2018: { status: 404, message: 'Required connected record not found' },
|
|
32
|
+
P2019: { status: 400, message: 'Input error' },
|
|
33
|
+
P2020: { status: 400, message: 'Value out of range for the field type' },
|
|
34
|
+
P2021: { status: 500, message: 'Table does not exist in the database' },
|
|
35
|
+
P2022: { status: 500, message: 'Column does not exist in the database' },
|
|
36
|
+
P2023: { status: 500, message: 'Inconsistent column data' },
|
|
37
|
+
P2024: { status: 503, message: 'Connection pool timeout' },
|
|
38
|
+
P2025: { status: 404, message: 'Record not found' },
|
|
39
|
+
P2026: { status: 501, message: 'Feature not supported by the current database provider' },
|
|
40
|
+
P2027: { status: 400, message: 'Multiple errors occurred during transaction execution' },
|
|
41
|
+
P2028: { status: 500, message: 'Transaction API error' },
|
|
42
|
+
P2030: { status: 400, message: 'Cannot find a fulltext index for the search' },
|
|
43
|
+
P2033: { status: 400, message: 'Number out of range for the field type' },
|
|
44
|
+
P2034: { status: 409, message: 'Transaction conflict, please retry' },
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type ErrorShape = {
|
|
48
|
+
name?: string
|
|
49
|
+
code?: string
|
|
50
|
+
message?: string
|
|
51
|
+
issues?: unknown
|
|
52
|
+
status?: unknown
|
|
53
|
+
statusCode?: unknown
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function asErrorShape(error: unknown): ErrorShape {
|
|
57
|
+
if (error && typeof error === 'object') return error as ErrorShape
|
|
58
|
+
return {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isProduction(): boolean {
|
|
62
|
+
return typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'production'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isHttpStatus(value: unknown): value is number {
|
|
66
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 400 && value <= 599
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function mapError(error: unknown): HttpError {
|
|
70
|
+
if (error instanceof HttpError) return error
|
|
71
|
+
const e = asErrorShape(error)
|
|
72
|
+
const isProd = isProduction()
|
|
73
|
+
|
|
74
|
+
const duckStatus = isHttpStatus(e.status)
|
|
75
|
+
? e.status
|
|
76
|
+
: isHttpStatus(e.statusCode)
|
|
77
|
+
? e.statusCode
|
|
78
|
+
: null
|
|
79
|
+
if (duckStatus !== null) {
|
|
80
|
+
const detail = typeof e.message === 'string' && e.message ? e.message : 'Internal server error'
|
|
81
|
+
return new HttpError(duckStatus, detail)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (e.name === 'ShapeError') return new HttpError(400, e.message || 'Shape validation failed')
|
|
85
|
+
if (e.name === 'CallerError') return new HttpError(400, e.message || 'Caller validation failed')
|
|
86
|
+
if (e.name === 'PolicyError') return new HttpError(403, e.message || 'Policy denied')
|
|
87
|
+
if (e.name === 'ZodError') {
|
|
88
|
+
const issues = e.issues
|
|
89
|
+
const message = Array.isArray(issues)
|
|
90
|
+
? (issues as Array<{ message?: string }>).map((i) => i.message ?? '').filter(Boolean).join('; ')
|
|
91
|
+
: (e.message || 'Validation failed')
|
|
92
|
+
return new HttpError(400, message)
|
|
93
|
+
}
|
|
94
|
+
if (typeof e.code === 'string') {
|
|
95
|
+
const mapped = PRISMA_ERROR_MAP[e.code]
|
|
96
|
+
if (mapped) {
|
|
97
|
+
const detail = e.message
|
|
98
|
+
const shouldStripDetail = isProd && mapped.status >= 500
|
|
99
|
+
return new HttpError(
|
|
100
|
+
mapped.status,
|
|
101
|
+
!shouldStripDetail && detail ? mapped.message + ': ' + detail : mapped.message,
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
if (e.code.startsWith('P')) {
|
|
105
|
+
const msg = e.message || 'Database operation failed'
|
|
106
|
+
console.warn(LOG_PREFIX, 'Unmapped Prisma error code:', e.code, msg)
|
|
107
|
+
return new HttpError(500, isProd ? 'Internal server error' : msg)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (typeof e.name === 'string') {
|
|
111
|
+
if (e.name === 'PrismaClientValidationError') return new HttpError(400, e.message || 'Invalid query parameters')
|
|
112
|
+
if (e.name === 'PrismaClientKnownRequestError') return new HttpError(400, e.message || 'Database request error')
|
|
113
|
+
if (e.name === 'PrismaClientInitializationError') {
|
|
114
|
+
return new HttpError(503, isProd ? 'Service unavailable' : (e.message || 'Database connection failed'))
|
|
115
|
+
}
|
|
116
|
+
if (e.name === 'PrismaClientRustPanicError') {
|
|
117
|
+
return new HttpError(500, isProd ? 'Internal server error' : (e.message || 'Internal database engine error'))
|
|
118
|
+
}
|
|
119
|
+
if (e.name === 'PrismaClientUnknownRequestError') {
|
|
120
|
+
return new HttpError(500, isProd ? 'Internal server error' : (e.message || 'Unknown database error'))
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
124
|
+
console.error(LOG_PREFIX, 'Unhandled error:', error)
|
|
125
|
+
return new HttpError(500, isProd ? 'Internal server error' : (msg || 'Internal server error'))
|
|
126
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { HttpError } from './errorMapper'
|
|
2
|
+
|
|
3
|
+
export type PrismaDelegateLike = {
|
|
4
|
+
count: (args?: unknown) => Promise<unknown>
|
|
5
|
+
findMany: (args?: unknown) => Promise<unknown>
|
|
6
|
+
guard?: (shape: Record<string, unknown>, caller?: string) => PrismaDelegateLike
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function assertGuard(
|
|
10
|
+
delegate: PrismaDelegateLike,
|
|
11
|
+
): asserts delegate is PrismaDelegateLike & { guard: NonNullable<PrismaDelegateLike['guard']> } {
|
|
12
|
+
if (typeof delegate.guard !== 'function') {
|
|
13
|
+
throw new HttpError(
|
|
14
|
+
500,
|
|
15
|
+
'Guard shapes require prisma-guard extension on PrismaClient. Install: npm install prisma-guard, then extend your client with guardExtension().',
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const GUARD_SHAPE_CONFIG_KEYS = new Set([
|
|
21
|
+
'data', 'create', 'update', 'where', 'include', 'select', 'orderBy',
|
|
22
|
+
'cursor', 'take', 'skip', 'distinct', 'having', '_count', '_avg',
|
|
23
|
+
'_sum', '_min', '_max', 'by',
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
function keepWhereOnly(obj: Record<string, unknown>): Record<string, unknown> {
|
|
27
|
+
const result: Record<string, unknown> = {}
|
|
28
|
+
if ('where' in obj) result.where = obj.where
|
|
29
|
+
return result
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ShapeFn = (...args: unknown[]) => Record<string, unknown>
|
|
33
|
+
|
|
34
|
+
export function buildCountShape(
|
|
35
|
+
shape: Record<string, unknown> | ShapeFn,
|
|
36
|
+
): Record<string, unknown> | ShapeFn {
|
|
37
|
+
if (typeof shape === 'function') {
|
|
38
|
+
const fn = shape as ShapeFn
|
|
39
|
+
return (...args: unknown[]) => keepWhereOnly(fn(...args))
|
|
40
|
+
}
|
|
41
|
+
const keys = Object.keys(shape)
|
|
42
|
+
const isSingleShape = keys.length === 0 || keys.every((k) => GUARD_SHAPE_CONFIG_KEYS.has(k))
|
|
43
|
+
if (isSingleShape) return keepWhereOnly(shape)
|
|
44
|
+
const result: Record<string, unknown> = {}
|
|
45
|
+
for (const [key, variant] of Object.entries(shape)) {
|
|
46
|
+
if (typeof variant === 'function') {
|
|
47
|
+
const vfn = variant as ShapeFn
|
|
48
|
+
result[key] = (...args: unknown[]) => keepWhereOnly(vfn(...args))
|
|
49
|
+
} else if (variant && typeof variant === 'object') {
|
|
50
|
+
result[key] = keepWhereOnly(variant as Record<string, unknown>)
|
|
51
|
+
} else {
|
|
52
|
+
result[key] = variant
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return result
|
|
56
|
+
}
|