prisma-generator-express 1.35.0 → 1.37.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 +117 -28
- package/dist/generators/generateRouter.js +39 -0
- package/dist/generators/generateRouter.js.map +1 -1
- package/dist/generators/generateRouterFastify.js +139 -1
- package/dist/generators/generateRouterFastify.js.map +1 -1
- package/package.json +1 -1
- package/src/copy/buildModelOpenApi.ts +262 -0
- package/src/copy/docsRenderer.ts +50 -7
- package/src/copy/operationDefinitions.ts +19 -1
- package/src/copy/parseQueryParams.ts +7 -3
- package/src/copy/routeConfig.ts +1 -0
- package/src/generators/generateRouter.ts +39 -0
- package/src/generators/generateRouterFastify.ts +139 -1
|
@@ -69,6 +69,11 @@ function opPath(basePath: string, name: string): string {
|
|
|
69
69
|
return `${basePath}${def.pathSuffix}`
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
function postReadPath(basePath: string, name: string): string {
|
|
73
|
+
if (name === 'findMany') return `${basePath}/read`
|
|
74
|
+
return opPath(basePath, name)
|
|
75
|
+
}
|
|
76
|
+
|
|
72
77
|
function errorRef(): RefObject {
|
|
73
78
|
return { $ref: '#/components/schemas/ErrorResponse' }
|
|
74
79
|
}
|
|
@@ -162,6 +167,109 @@ function listScalarUpdateOperations(
|
|
|
162
167
|
}
|
|
163
168
|
}
|
|
164
169
|
|
|
170
|
+
function findManyBodySchema(): SchemaObject {
|
|
171
|
+
return {
|
|
172
|
+
type: 'object',
|
|
173
|
+
properties: {
|
|
174
|
+
where: { type: 'object', description: 'Filter conditions' },
|
|
175
|
+
orderBy: { description: 'Sort order (object or array of objects)' },
|
|
176
|
+
take: { type: 'integer', description: 'Limit results' },
|
|
177
|
+
skip: { type: 'integer', description: 'Skip results' },
|
|
178
|
+
select: { type: 'object', description: 'Select fields' },
|
|
179
|
+
include: { type: 'object', description: 'Include relations' },
|
|
180
|
+
omit: { type: 'object', description: 'Omit fields from response' },
|
|
181
|
+
cursor: { type: 'object', description: 'Cursor for pagination' },
|
|
182
|
+
distinct: { description: 'Distinct fields (string or array of strings)' },
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function findUniqueBodySchema(): SchemaObject {
|
|
188
|
+
return {
|
|
189
|
+
type: 'object',
|
|
190
|
+
properties: {
|
|
191
|
+
where: { type: 'object', description: 'Unique selector' },
|
|
192
|
+
select: { type: 'object', description: 'Select fields' },
|
|
193
|
+
include: { type: 'object', description: 'Include relations' },
|
|
194
|
+
omit: { type: 'object', description: 'Omit fields from response' },
|
|
195
|
+
},
|
|
196
|
+
required: ['where'],
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function countBodySchema(): SchemaObject {
|
|
201
|
+
return {
|
|
202
|
+
type: 'object',
|
|
203
|
+
properties: {
|
|
204
|
+
where: { type: 'object', description: 'Filter conditions' },
|
|
205
|
+
orderBy: { description: 'Sort order' },
|
|
206
|
+
take: { type: 'integer', description: 'Limit results' },
|
|
207
|
+
skip: { type: 'integer', description: 'Skip results' },
|
|
208
|
+
cursor: { type: 'object', description: 'Cursor for pagination' },
|
|
209
|
+
select: { description: 'Count specific fields. When provided, returns per-field counts as an object instead of a single integer.' },
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function aggregateBodySchema(): SchemaObject {
|
|
215
|
+
return {
|
|
216
|
+
type: 'object',
|
|
217
|
+
properties: {
|
|
218
|
+
where: { type: 'object', description: 'Filter conditions' },
|
|
219
|
+
orderBy: { description: 'Sort order' },
|
|
220
|
+
cursor: { type: 'object', description: 'Cursor for pagination' },
|
|
221
|
+
take: { type: 'integer', description: 'Limit results' },
|
|
222
|
+
skip: { type: 'integer', description: 'Skip results' },
|
|
223
|
+
_count: { description: 'Count aggregate (true or field selection object)' },
|
|
224
|
+
_avg: { type: 'object', description: 'Average aggregate (field selection object)' },
|
|
225
|
+
_sum: { type: 'object', description: 'Sum aggregate (field selection object)' },
|
|
226
|
+
_min: { type: 'object', description: 'Min aggregate (field selection object)' },
|
|
227
|
+
_max: { type: 'object', description: 'Max aggregate (field selection object)' },
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function groupByBodySchema(): SchemaObject {
|
|
233
|
+
return {
|
|
234
|
+
type: 'object',
|
|
235
|
+
properties: {
|
|
236
|
+
by: { type: 'array', items: { type: 'string' }, description: 'Fields to group by' },
|
|
237
|
+
where: { type: 'object', description: 'Filter conditions' },
|
|
238
|
+
orderBy: { description: 'Sort order. Required when using skip or take.' },
|
|
239
|
+
having: { type: 'object', description: 'Having conditions (filter object)' },
|
|
240
|
+
take: { type: 'integer', description: 'Limit results' },
|
|
241
|
+
skip: { type: 'integer', description: 'Skip results' },
|
|
242
|
+
_count: { description: 'Count aggregate (true or field selection object)' },
|
|
243
|
+
_avg: { type: 'object', description: 'Average aggregate (field selection object)' },
|
|
244
|
+
_sum: { type: 'object', description: 'Sum aggregate (field selection object)' },
|
|
245
|
+
_min: { type: 'object', description: 'Min aggregate (field selection object)' },
|
|
246
|
+
_max: { type: 'object', description: 'Max aggregate (field selection object)' },
|
|
247
|
+
},
|
|
248
|
+
required: ['by'],
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function getPostReadBodySchema(opName: string): SchemaObject {
|
|
253
|
+
switch (opName) {
|
|
254
|
+
case 'findMany':
|
|
255
|
+
case 'findFirst':
|
|
256
|
+
case 'findFirstOrThrow':
|
|
257
|
+
case 'findManyPaginated':
|
|
258
|
+
return findManyBodySchema()
|
|
259
|
+
case 'findUnique':
|
|
260
|
+
case 'findUniqueOrThrow':
|
|
261
|
+
return findUniqueBodySchema()
|
|
262
|
+
case 'count':
|
|
263
|
+
return countBodySchema()
|
|
264
|
+
case 'aggregate':
|
|
265
|
+
return aggregateBodySchema()
|
|
266
|
+
case 'groupBy':
|
|
267
|
+
return groupByBodySchema()
|
|
268
|
+
default:
|
|
269
|
+
return findManyBodySchema()
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
165
273
|
export function buildModelOpenApi(
|
|
166
274
|
modelName: string,
|
|
167
275
|
modelFields: ModelField[],
|
|
@@ -535,6 +643,41 @@ function getGroupByParams() {
|
|
|
535
643
|
]
|
|
536
644
|
}
|
|
537
645
|
|
|
646
|
+
function addPostReadOperation(
|
|
647
|
+
spec: OpenApiSpec,
|
|
648
|
+
path: string,
|
|
649
|
+
modelName: string,
|
|
650
|
+
opName: string,
|
|
651
|
+
summary: string,
|
|
652
|
+
responseSchema: any,
|
|
653
|
+
errorCodes: number[],
|
|
654
|
+
description?: string,
|
|
655
|
+
) {
|
|
656
|
+
const op: any = {
|
|
657
|
+
tags: [modelName],
|
|
658
|
+
summary: summary + ' (POST)',
|
|
659
|
+
operationId: `${modelName}${opName.charAt(0).toUpperCase() + opName.slice(1)}Post`,
|
|
660
|
+
description: (description ? description + ' ' : '') +
|
|
661
|
+
'POST alternative for requests with complex query parameters that may exceed URL length limits. Accepts the same arguments as the GET endpoint but as a JSON request body instead of query parameters.',
|
|
662
|
+
requestBody: {
|
|
663
|
+
required: true,
|
|
664
|
+
content: {
|
|
665
|
+
'application/json': {
|
|
666
|
+
schema: getPostReadBodySchema(opName),
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
},
|
|
670
|
+
responses: {
|
|
671
|
+
'200': {
|
|
672
|
+
description: 'Success',
|
|
673
|
+
content: { 'application/json': { schema: responseSchema } },
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
}
|
|
677
|
+
addErrorResponses(op, errorCodes)
|
|
678
|
+
addPath(spec, path, 'post', op)
|
|
679
|
+
}
|
|
680
|
+
|
|
538
681
|
function generatePaths(
|
|
539
682
|
spec: OpenApiSpec,
|
|
540
683
|
modelName: string,
|
|
@@ -542,6 +685,8 @@ function generatePaths(
|
|
|
542
685
|
config: RouteConfig,
|
|
543
686
|
fields: ModelField[],
|
|
544
687
|
) {
|
|
688
|
+
const postReads = !config.disablePostReads
|
|
689
|
+
|
|
545
690
|
const createInputRef = {
|
|
546
691
|
$ref: `#/components/schemas/${modelName}CreateInput`,
|
|
547
692
|
}
|
|
@@ -588,6 +733,18 @@ function generatePaths(
|
|
|
588
733
|
}
|
|
589
734
|
addErrorResponses(op, [400, 403, 500, 501, 503])
|
|
590
735
|
addPath(spec, opPath(basePath, 'findMany'), 'get', op)
|
|
736
|
+
|
|
737
|
+
if (postReads) {
|
|
738
|
+
addPostReadOperation(
|
|
739
|
+
spec,
|
|
740
|
+
postReadPath(basePath, 'findMany'),
|
|
741
|
+
modelName,
|
|
742
|
+
'findMany',
|
|
743
|
+
`List ${modelName}`,
|
|
744
|
+
{ type: 'array', items: responseRef },
|
|
745
|
+
[400, 403, 500, 501, 503],
|
|
746
|
+
)
|
|
747
|
+
}
|
|
591
748
|
}
|
|
592
749
|
|
|
593
750
|
if (opEnabled(config, 'findUnique')) {
|
|
@@ -607,6 +764,19 @@ function generatePaths(
|
|
|
607
764
|
}
|
|
608
765
|
addErrorResponses(op, [400, 403, 500, 501, 503])
|
|
609
766
|
addPath(spec, opPath(basePath, 'findUnique'), 'get', op)
|
|
767
|
+
|
|
768
|
+
if (postReads) {
|
|
769
|
+
addPostReadOperation(
|
|
770
|
+
spec,
|
|
771
|
+
postReadPath(basePath, 'findUnique'),
|
|
772
|
+
modelName,
|
|
773
|
+
'findUnique',
|
|
774
|
+
`Get ${modelName} by unique constraint`,
|
|
775
|
+
nullableResponseSchema,
|
|
776
|
+
[400, 403, 500, 501, 503],
|
|
777
|
+
'Returns null with status 200 when no record matches the unique constraint.',
|
|
778
|
+
)
|
|
779
|
+
}
|
|
610
780
|
}
|
|
611
781
|
|
|
612
782
|
if (opEnabled(config, 'findUniqueOrThrow')) {
|
|
@@ -624,6 +794,18 @@ function generatePaths(
|
|
|
624
794
|
}
|
|
625
795
|
addErrorResponses(op, [400, 403, 404, 500, 501, 503])
|
|
626
796
|
addPath(spec, opPath(basePath, 'findUniqueOrThrow'), 'get', op)
|
|
797
|
+
|
|
798
|
+
if (postReads) {
|
|
799
|
+
addPostReadOperation(
|
|
800
|
+
spec,
|
|
801
|
+
postReadPath(basePath, 'findUniqueOrThrow'),
|
|
802
|
+
modelName,
|
|
803
|
+
'findUniqueOrThrow',
|
|
804
|
+
`Get ${modelName} by unique constraint (throws if not found)`,
|
|
805
|
+
responseRef,
|
|
806
|
+
[400, 403, 404, 500, 501, 503],
|
|
807
|
+
)
|
|
808
|
+
}
|
|
627
809
|
}
|
|
628
810
|
|
|
629
811
|
if (opEnabled(config, 'findFirst')) {
|
|
@@ -642,6 +824,19 @@ function generatePaths(
|
|
|
642
824
|
}
|
|
643
825
|
addErrorResponses(op, [400, 403, 500, 501, 503])
|
|
644
826
|
addPath(spec, opPath(basePath, 'findFirst'), 'get', op)
|
|
827
|
+
|
|
828
|
+
if (postReads) {
|
|
829
|
+
addPostReadOperation(
|
|
830
|
+
spec,
|
|
831
|
+
postReadPath(basePath, 'findFirst'),
|
|
832
|
+
modelName,
|
|
833
|
+
'findFirst',
|
|
834
|
+
`Get first ${modelName}`,
|
|
835
|
+
nullableResponseSchema,
|
|
836
|
+
[400, 403, 500, 501, 503],
|
|
837
|
+
'Returns null with status 200 when no record matches.',
|
|
838
|
+
)
|
|
839
|
+
}
|
|
645
840
|
}
|
|
646
841
|
|
|
647
842
|
if (opEnabled(config, 'findFirstOrThrow')) {
|
|
@@ -659,6 +854,18 @@ function generatePaths(
|
|
|
659
854
|
}
|
|
660
855
|
addErrorResponses(op, [400, 403, 404, 500, 501, 503])
|
|
661
856
|
addPath(spec, opPath(basePath, 'findFirstOrThrow'), 'get', op)
|
|
857
|
+
|
|
858
|
+
if (postReads) {
|
|
859
|
+
addPostReadOperation(
|
|
860
|
+
spec,
|
|
861
|
+
postReadPath(basePath, 'findFirstOrThrow'),
|
|
862
|
+
modelName,
|
|
863
|
+
'findFirstOrThrow',
|
|
864
|
+
`Get first ${modelName} (throws if not found)`,
|
|
865
|
+
responseRef,
|
|
866
|
+
[400, 403, 404, 500, 501, 503],
|
|
867
|
+
)
|
|
868
|
+
}
|
|
662
869
|
}
|
|
663
870
|
|
|
664
871
|
if (opEnabled(config, 'findManyPaginated')) {
|
|
@@ -678,6 +885,19 @@ function generatePaths(
|
|
|
678
885
|
}
|
|
679
886
|
addErrorResponses(op, [400, 403, 409, 500, 501, 503])
|
|
680
887
|
addPath(spec, opPath(basePath, 'findManyPaginated'), 'get', op)
|
|
888
|
+
|
|
889
|
+
if (postReads) {
|
|
890
|
+
addPostReadOperation(
|
|
891
|
+
spec,
|
|
892
|
+
postReadPath(basePath, 'findManyPaginated'),
|
|
893
|
+
modelName,
|
|
894
|
+
'findManyPaginated',
|
|
895
|
+
`List ${modelName} with pagination`,
|
|
896
|
+
listRef,
|
|
897
|
+
[400, 403, 409, 500, 501, 503],
|
|
898
|
+
'Returns paginated results with total count.',
|
|
899
|
+
)
|
|
900
|
+
}
|
|
681
901
|
}
|
|
682
902
|
|
|
683
903
|
if (opEnabled(config, 'create')) {
|
|
@@ -1074,6 +1294,23 @@ function generatePaths(
|
|
|
1074
1294
|
}
|
|
1075
1295
|
addErrorResponses(op, [400, 403, 500, 501, 503])
|
|
1076
1296
|
addPath(spec, opPath(basePath, 'count'), 'get', op)
|
|
1297
|
+
|
|
1298
|
+
if (postReads) {
|
|
1299
|
+
addPostReadOperation(
|
|
1300
|
+
spec,
|
|
1301
|
+
postReadPath(basePath, 'count'),
|
|
1302
|
+
modelName,
|
|
1303
|
+
'count',
|
|
1304
|
+
`Count ${modelName}`,
|
|
1305
|
+
{
|
|
1306
|
+
oneOf: [
|
|
1307
|
+
{ type: 'integer', description: 'Total count when select is not provided' },
|
|
1308
|
+
{ type: 'object', description: 'Per-field count object when select is provided' },
|
|
1309
|
+
],
|
|
1310
|
+
},
|
|
1311
|
+
[400, 403, 500, 501, 503],
|
|
1312
|
+
)
|
|
1313
|
+
}
|
|
1077
1314
|
}
|
|
1078
1315
|
|
|
1079
1316
|
if (opEnabled(config, 'aggregate')) {
|
|
@@ -1091,6 +1328,18 @@ function generatePaths(
|
|
|
1091
1328
|
}
|
|
1092
1329
|
addErrorResponses(op, [400, 403, 500, 501, 503])
|
|
1093
1330
|
addPath(spec, opPath(basePath, 'aggregate'), 'get', op)
|
|
1331
|
+
|
|
1332
|
+
if (postReads) {
|
|
1333
|
+
addPostReadOperation(
|
|
1334
|
+
spec,
|
|
1335
|
+
postReadPath(basePath, 'aggregate'),
|
|
1336
|
+
modelName,
|
|
1337
|
+
'aggregate',
|
|
1338
|
+
`Aggregate ${modelName}`,
|
|
1339
|
+
aggregateRef,
|
|
1340
|
+
[400, 403, 500, 501, 503],
|
|
1341
|
+
)
|
|
1342
|
+
}
|
|
1094
1343
|
}
|
|
1095
1344
|
|
|
1096
1345
|
if (opEnabled(config, 'groupBy')) {
|
|
@@ -1114,6 +1363,19 @@ function generatePaths(
|
|
|
1114
1363
|
}
|
|
1115
1364
|
addErrorResponses(op, [400, 403, 500, 501, 503])
|
|
1116
1365
|
addPath(spec, opPath(basePath, 'groupBy'), 'get', op)
|
|
1366
|
+
|
|
1367
|
+
if (postReads) {
|
|
1368
|
+
addPostReadOperation(
|
|
1369
|
+
spec,
|
|
1370
|
+
postReadPath(basePath, 'groupBy'),
|
|
1371
|
+
modelName,
|
|
1372
|
+
'groupBy',
|
|
1373
|
+
`Group ${modelName}`,
|
|
1374
|
+
{ type: 'array', items: groupByItemRef },
|
|
1375
|
+
[400, 403, 500, 501, 503],
|
|
1376
|
+
'Groups records by the specified fields and returns aggregates.',
|
|
1377
|
+
)
|
|
1378
|
+
}
|
|
1117
1379
|
}
|
|
1118
1380
|
}
|
|
1119
1381
|
|
package/src/copy/docsRenderer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RouteConfig } from './routeConfig'
|
|
2
|
-
import { OPERATION_DEFS, isOperationEnabled } from './operationDefinitions'
|
|
2
|
+
import { OPERATION_DEFS, isOperationEnabled, READ_OPERATION_NAMES } from './operationDefinitions'
|
|
3
3
|
|
|
4
4
|
const _env = typeof process !== 'undefined' && process.env ? process.env : {} as Record<string, string | undefined>
|
|
5
5
|
|
|
@@ -553,6 +553,8 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
553
553
|
const modelLower = modelName.charAt(0).toLowerCase() + modelName.slice(1)
|
|
554
554
|
const exampleBasePath = buildExampleBasePath(modelName, config)
|
|
555
555
|
|
|
556
|
+
const postReadsEnabled = !config.disablePostReads
|
|
557
|
+
|
|
556
558
|
const scalarFields = ctx.fields.filter((f) => isScalarField(f) || isEnumField(f))
|
|
557
559
|
const relationFields = ctx.fields.filter((f) => isRelationField(f))
|
|
558
560
|
|
|
@@ -561,7 +563,7 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
561
563
|
const listRelations = relationFields.filter((f) => f.isList)
|
|
562
564
|
const singleRelations = relationFields.filter((f) => !f.isList)
|
|
563
565
|
|
|
564
|
-
const
|
|
566
|
+
const getOps = OPERATION_DEFS
|
|
565
567
|
.filter((d) => isOperationEnabled(config as Record<string, any>, d))
|
|
566
568
|
.map((d) => {
|
|
567
569
|
const detail = OP_DETAIL_MAP[d.name]
|
|
@@ -581,6 +583,33 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
581
583
|
}
|
|
582
584
|
})
|
|
583
585
|
|
|
586
|
+
const postReadOps = postReadsEnabled
|
|
587
|
+
? OPERATION_DEFS
|
|
588
|
+
.filter((d) => READ_OPERATION_NAMES.has(d.name) && isOperationEnabled(config as Record<string, any>, d))
|
|
589
|
+
.map((d) => {
|
|
590
|
+
const detail = OP_DETAIL_MAP[d.name]
|
|
591
|
+
const postPath = d.name === 'findMany'
|
|
592
|
+
? buildFullPath(exampleBasePath, '/read')
|
|
593
|
+
: buildFullPath(exampleBasePath, d.pathSuffix)
|
|
594
|
+
return {
|
|
595
|
+
op: d.name + ' (POST)',
|
|
596
|
+
method: 'POST',
|
|
597
|
+
path: postPath,
|
|
598
|
+
transport: 'POST JSON body',
|
|
599
|
+
responseDesc: detail ? detail.responseDesc : '',
|
|
600
|
+
errors: detail ? detail.errors.join(', ') : '',
|
|
601
|
+
required: detail ? detail.required : [],
|
|
602
|
+
optional: detail ? detail.optional : [],
|
|
603
|
+
supportsSelect: detail ? detail.supportsSelect : false,
|
|
604
|
+
supportsInclude: detail ? detail.supportsInclude : false,
|
|
605
|
+
supportsOmit: detail ? detail.supportsOmit : false,
|
|
606
|
+
notes: 'POST alternative for complex queries exceeding URL length limits. Same args as GET but in request body.',
|
|
607
|
+
}
|
|
608
|
+
})
|
|
609
|
+
: []
|
|
610
|
+
|
|
611
|
+
const ops = [...getOps, ...postReadOps]
|
|
612
|
+
|
|
584
613
|
const firstUnique = uniqueFields[0]
|
|
585
614
|
const firstUniqueExample = firstUnique ? exampleValue(ctx, firstUnique.name) : null
|
|
586
615
|
const compoundWhere = !firstUnique ? compoundWhereExample(ctx) : null
|
|
@@ -640,6 +669,14 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
640
669
|
'const res = await fetch(BASE_URL + "' + exampleBasePath + '?" + params)\n' +
|
|
641
670
|
'const data = await res.json()'
|
|
642
671
|
|
|
672
|
+
const findManyPostFetchExample =
|
|
673
|
+
'const res = await fetch(BASE_URL + "' + exampleBasePath + '/read", {\n' +
|
|
674
|
+
' method: "POST",\n' +
|
|
675
|
+
' headers: { "Content-Type": "application/json" },\n' +
|
|
676
|
+
' body: JSON.stringify(' + JSON.stringify(findManyQueryArgs, null, 2) + ')\n' +
|
|
677
|
+
'})\n' +
|
|
678
|
+
'const data = await res.json()'
|
|
679
|
+
|
|
643
680
|
const findUniqueFetchExample = uniqueWhereExample
|
|
644
681
|
? 'const params = encodeQueryParams({\n' +
|
|
645
682
|
' where: ' + JSON.stringify(uniqueWhereExample) + ',\n' +
|
|
@@ -844,6 +881,7 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
844
881
|
const transportNotes = [
|
|
845
882
|
'GET endpoints: Prisma args as JSON-encoded query parameter strings via encodeQueryParams.',
|
|
846
883
|
'POST/PUT/DELETE/PATCH endpoints: Prisma args as JSON request body. Body must be a JSON object.',
|
|
884
|
+
'POST read endpoints: All read operations also accept POST with the same args as JSON body instead of query params. Use when query parameters exceed URL length limits. findMany uses POST /read, all others use the same path as GET.',
|
|
847
885
|
'findManyPaginated returns { data, total, hasMore }. hasMore is reliable for forward offset pagination only.',
|
|
848
886
|
'Batch mutations (createMany, updateMany, deleteMany) return { count }. Batch data inputs are scalar-only — nested relation writes are not supported.',
|
|
849
887
|
'findUnique and findFirst return null (not 404) when no record matches. Use the OrThrow variants for 404 behavior.',
|
|
@@ -967,6 +1005,7 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
967
1005
|
|
|
968
1006
|
const runtimeNotes = [
|
|
969
1007
|
'<strong>Query parameter parsing:</strong> GET query values are parsed server-side. Strings starting with <span class="font-mono">{</span>, <span class="font-mono">[</span>, or <span class="font-mono">"</span> are JSON-parsed. The strings <span class="font-mono">true</span>, <span class="font-mono">false</span>, <span class="font-mono">null</span> are converted to their JS equivalents. Numeric conversion only applies to <span class="font-mono">take</span> and <span class="font-mono">skip</span>. Use <span class="font-mono">encodeQueryParams</span> to avoid encoding issues.',
|
|
1008
|
+
'<strong>POST read endpoints:</strong> All read operations accept POST as an alternative transport. The request body is a plain JSON object with the same argument structure as the GET query params — no JSON-string encoding needed. POST reads use native JSON types (numbers, booleans, objects) directly. findMany POST read is at <span class="font-mono">/read</span>; all other read operations use the same path as their GET counterpart. Disable with <span class="font-mono">disablePostReads: true</span> in route config.',
|
|
970
1009
|
'<strong>Request body validation:</strong> All write endpoints require a JSON object body. Sending <span class="font-mono">null</span>, arrays, or non-object JSON values returns 400.',
|
|
971
1010
|
'<strong>Documentation in production:</strong> Docs endpoints are disabled by default when <span class="font-mono">NODE_ENV=production</span> or <span class="font-mono">DISABLE_OPENAPI=true</span>. To enable in production, set <span class="font-mono">disableOpenApi: false</span> in the route config.',
|
|
972
1011
|
'<strong>Paginated query atomicity:</strong> findManyPaginated wraps data + count in a database transaction when available. If interactive transactions are not supported (e.g. some edge adapters), the queries run separately and data/total may be slightly inconsistent under concurrent writes.',
|
|
@@ -1236,29 +1275,33 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
1236
1275
|
${codeBlock(findManyFetchExample)}
|
|
1237
1276
|
</div>
|
|
1238
1277
|
<div>
|
|
1239
|
-
<h3 class="mt-3.5 mb-2 text-sm">11.2
|
|
1278
|
+
<h3 class="mt-3.5 mb-2 text-sm">11.2 POST — findMany (read)</h3>
|
|
1279
|
+
${codeBlock(findManyPostFetchExample)}
|
|
1280
|
+
</div>
|
|
1281
|
+
<div>
|
|
1282
|
+
<h3 class="mt-3.5 mb-2 text-sm">11.3 GET — findUnique</h3>
|
|
1240
1283
|
${findUniqueFetchExample
|
|
1241
1284
|
? codeBlock(findUniqueFetchExample)
|
|
1242
1285
|
: noUniqueFieldNote}
|
|
1243
1286
|
</div>
|
|
1244
1287
|
<div>
|
|
1245
|
-
<h3 class="mt-3.5 mb-2 text-sm">11.
|
|
1288
|
+
<h3 class="mt-3.5 mb-2 text-sm">11.4 POST — create</h3>
|
|
1246
1289
|
${codeBlock(createFetchExample)}
|
|
1247
1290
|
</div>
|
|
1248
1291
|
<div>
|
|
1249
|
-
<h3 class="mt-3.5 mb-2 text-sm">11.
|
|
1292
|
+
<h3 class="mt-3.5 mb-2 text-sm">11.5 PUT — update</h3>
|
|
1250
1293
|
${updateFetchExample
|
|
1251
1294
|
? codeBlock(updateFetchExample)
|
|
1252
1295
|
: noUniqueFieldNote}
|
|
1253
1296
|
</div>
|
|
1254
1297
|
<div>
|
|
1255
|
-
<h3 class="mt-3.5 mb-2 text-sm">11.
|
|
1298
|
+
<h3 class="mt-3.5 mb-2 text-sm">11.6 DELETE — delete</h3>
|
|
1256
1299
|
${deleteFetchExample
|
|
1257
1300
|
? codeBlock(deleteFetchExample)
|
|
1258
1301
|
: noUniqueFieldNote}
|
|
1259
1302
|
</div>
|
|
1260
1303
|
<div>
|
|
1261
|
-
<h3 class="mt-3.5 mb-2 text-sm">11.
|
|
1304
|
+
<h3 class="mt-3.5 mb-2 text-sm">11.7 Guard variant header</h3>
|
|
1262
1305
|
${guardFetchExample
|
|
1263
1306
|
? codeBlock(guardFetchExample)
|
|
1264
1307
|
: noUniqueFieldNote}
|
|
@@ -88,9 +88,27 @@ export const OPERATION_DEFS: OperationDef[] = [
|
|
|
88
88
|
},
|
|
89
89
|
]
|
|
90
90
|
|
|
91
|
+
export const READ_OPERATION_NAMES = new Set([
|
|
92
|
+
'findMany',
|
|
93
|
+
'findUnique',
|
|
94
|
+
'findUniqueOrThrow',
|
|
95
|
+
'findFirst',
|
|
96
|
+
'findFirstOrThrow',
|
|
97
|
+
'findManyPaginated',
|
|
98
|
+
'count',
|
|
99
|
+
'aggregate',
|
|
100
|
+
'groupBy',
|
|
101
|
+
])
|
|
102
|
+
|
|
103
|
+
export function getPostReadPathSuffix(opName: string): string {
|
|
104
|
+
if (opName === 'findMany') return '/read'
|
|
105
|
+
const def = OPERATION_DEFS.find((d) => d.name === opName)
|
|
106
|
+
return def ? def.pathSuffix : ''
|
|
107
|
+
}
|
|
108
|
+
|
|
91
109
|
export function isOperationEnabled(
|
|
92
110
|
config: Record<string, any>,
|
|
93
111
|
def: OperationDef,
|
|
94
112
|
): boolean {
|
|
95
113
|
return !!(config.enableAll || config[def.configKey])
|
|
96
|
-
}
|
|
114
|
+
}
|
|
@@ -15,7 +15,7 @@ const parseQueryValue = (value: string, key?: string): unknown => {
|
|
|
15
15
|
if (value.startsWith('{') || value.startsWith('[') || value.startsWith('"')) {
|
|
16
16
|
try {
|
|
17
17
|
const parsed = JSON.parse(value)
|
|
18
|
-
return sanitizeKeys(parsed)
|
|
18
|
+
return parseQueryParams(sanitizeKeys(parsed) as QueryParams)
|
|
19
19
|
} catch {
|
|
20
20
|
// fall through
|
|
21
21
|
}
|
|
@@ -34,7 +34,11 @@ export const parseQueryParams = (params: QueryParams): unknown => {
|
|
|
34
34
|
return parseQueryValue(params)
|
|
35
35
|
}
|
|
36
36
|
if (Array.isArray(params)) {
|
|
37
|
-
return params.map(
|
|
37
|
+
return params.map((item) =>
|
|
38
|
+
typeof item === 'string'
|
|
39
|
+
? parseQueryValue(item)
|
|
40
|
+
: parseQueryParams(item as QueryParams),
|
|
41
|
+
)
|
|
38
42
|
}
|
|
39
43
|
if (isObject(params)) {
|
|
40
44
|
const parsedParams: Record<string, unknown> = {}
|
|
@@ -44,7 +48,7 @@ export const parseQueryParams = (params: QueryParams): unknown => {
|
|
|
44
48
|
if (typeof raw === 'string') {
|
|
45
49
|
parsedParams[key] = parseQueryValue(raw, key)
|
|
46
50
|
} else {
|
|
47
|
-
parsedParams[key] =
|
|
51
|
+
parsedParams[key] = parseQueryParams(raw as QueryParams)
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
return parsedParams
|
package/src/copy/routeConfig.ts
CHANGED
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
} from './${modelName}Handlers'
|
|
63
63
|
import type { RouteConfig } from '../routeConfig.target'
|
|
64
64
|
import { parseQueryParams } from '../parseQueryParams'
|
|
65
|
+
import { sanitizeKeys } from '../misc'
|
|
65
66
|
import { buildModelOpenApi } from '../buildModelOpenApi'
|
|
66
67
|
import { transformResult } from '../operationRuntime'
|
|
67
68
|
|
|
@@ -113,6 +114,8 @@ export function ${routerFunctionName}(config: RouteConfig = {}) {
|
|
|
113
114
|
|| _env.NODE_ENV === 'production'
|
|
114
115
|
))
|
|
115
116
|
|
|
117
|
+
const postReadsEnabled = !config.disablePostReads
|
|
118
|
+
|
|
116
119
|
const qbEnabled = isQueryBuilderEnabled(config)
|
|
117
120
|
|
|
118
121
|
if (qbEnabled) {
|
|
@@ -130,6 +133,14 @@ export function ${routerFunctionName}(config: RouteConfig = {}) {
|
|
|
130
133
|
next()
|
|
131
134
|
}
|
|
132
135
|
|
|
136
|
+
const parseBodyAsQuery: RequestHandler = (req, res, next) => {
|
|
137
|
+
if (!req.body || typeof req.body !== 'object' || Array.isArray(req.body)) {
|
|
138
|
+
return next({ status: 400, message: 'Request body must be a JSON object' })
|
|
139
|
+
}
|
|
140
|
+
res.locals.parsedQuery = sanitizeKeys(req.body)
|
|
141
|
+
next()
|
|
142
|
+
}
|
|
143
|
+
|
|
133
144
|
const setShape = (opConfig: any): RequestHandler => {
|
|
134
145
|
return (req, res, next) => {
|
|
135
146
|
res.locals.routeConfig = config
|
|
@@ -194,6 +205,9 @@ export function ${routerFunctionName}(config: RouteConfig = {}) {
|
|
|
194
205
|
const { before = [], after = [] } = opConfig
|
|
195
206
|
const path = basePath ? \`\${basePath}/first\` : '/first'
|
|
196
207
|
router.get(path, parseQuery, setShape(opConfig), ...before, ${prefix}FindFirst as RequestHandler, ...after, respond)
|
|
208
|
+
if (postReadsEnabled) {
|
|
209
|
+
router.post(path, parseBodyAsQuery, setShape(opConfig), ...before, ${prefix}FindFirst as RequestHandler, ...after, respond)
|
|
210
|
+
}
|
|
197
211
|
}
|
|
198
212
|
|
|
199
213
|
if (config.enableAll || config.findFirstOrThrow) {
|
|
@@ -201,6 +215,9 @@ export function ${routerFunctionName}(config: RouteConfig = {}) {
|
|
|
201
215
|
const { before = [], after = [] } = opConfig
|
|
202
216
|
const path = basePath ? \`\${basePath}/first/strict\` : '/first/strict'
|
|
203
217
|
router.get(path, parseQuery, setShape(opConfig), ...before, ${prefix}FindFirstOrThrow as RequestHandler, ...after, respond)
|
|
218
|
+
if (postReadsEnabled) {
|
|
219
|
+
router.post(path, parseBodyAsQuery, setShape(opConfig), ...before, ${prefix}FindFirstOrThrow as RequestHandler, ...after, respond)
|
|
220
|
+
}
|
|
204
221
|
}
|
|
205
222
|
|
|
206
223
|
if (config.enableAll || config.findManyPaginated) {
|
|
@@ -208,6 +225,9 @@ export function ${routerFunctionName}(config: RouteConfig = {}) {
|
|
|
208
225
|
const { before = [], after = [] } = opConfig
|
|
209
226
|
const path = basePath ? \`\${basePath}/paginated\` : '/paginated'
|
|
210
227
|
router.get(path, parseQuery, setShape(opConfig), ...before, ${prefix}FindManyPaginated as RequestHandler, ...after, respond)
|
|
228
|
+
if (postReadsEnabled) {
|
|
229
|
+
router.post(path, parseBodyAsQuery, setShape(opConfig), ...before, ${prefix}FindManyPaginated as RequestHandler, ...after, respond)
|
|
230
|
+
}
|
|
211
231
|
}
|
|
212
232
|
|
|
213
233
|
if (config.enableAll || config.aggregate) {
|
|
@@ -215,6 +235,9 @@ export function ${routerFunctionName}(config: RouteConfig = {}) {
|
|
|
215
235
|
const { before = [], after = [] } = opConfig
|
|
216
236
|
const path = basePath ? \`\${basePath}/aggregate\` : '/aggregate'
|
|
217
237
|
router.get(path, parseQuery, setShape(opConfig), ...before, ${prefix}Aggregate as RequestHandler, ...after, respond)
|
|
238
|
+
if (postReadsEnabled) {
|
|
239
|
+
router.post(path, parseBodyAsQuery, setShape(opConfig), ...before, ${prefix}Aggregate as RequestHandler, ...after, respond)
|
|
240
|
+
}
|
|
218
241
|
}
|
|
219
242
|
|
|
220
243
|
if (config.enableAll || config.count) {
|
|
@@ -222,6 +245,9 @@ export function ${routerFunctionName}(config: RouteConfig = {}) {
|
|
|
222
245
|
const { before = [], after = [] } = opConfig
|
|
223
246
|
const path = basePath ? \`\${basePath}/count\` : '/count'
|
|
224
247
|
router.get(path, parseQuery, setShape(opConfig), ...before, ${prefix}Count as RequestHandler, ...after, respond)
|
|
248
|
+
if (postReadsEnabled) {
|
|
249
|
+
router.post(path, parseBodyAsQuery, setShape(opConfig), ...before, ${prefix}Count as RequestHandler, ...after, respond)
|
|
250
|
+
}
|
|
225
251
|
}
|
|
226
252
|
|
|
227
253
|
if (config.enableAll || config.groupBy) {
|
|
@@ -229,6 +255,9 @@ export function ${routerFunctionName}(config: RouteConfig = {}) {
|
|
|
229
255
|
const { before = [], after = [] } = opConfig
|
|
230
256
|
const path = basePath ? \`\${basePath}/groupby\` : '/groupby'
|
|
231
257
|
router.get(path, parseQuery, setShape(opConfig), ...before, ${prefix}GroupBy as RequestHandler, ...after, respond)
|
|
258
|
+
if (postReadsEnabled) {
|
|
259
|
+
router.post(path, parseBodyAsQuery, setShape(opConfig), ...before, ${prefix}GroupBy as RequestHandler, ...after, respond)
|
|
260
|
+
}
|
|
232
261
|
}
|
|
233
262
|
|
|
234
263
|
if (config.enableAll || config.findUniqueOrThrow) {
|
|
@@ -236,6 +265,9 @@ export function ${routerFunctionName}(config: RouteConfig = {}) {
|
|
|
236
265
|
const { before = [], after = [] } = opConfig
|
|
237
266
|
const path = basePath ? \`\${basePath}/unique/strict\` : '/unique/strict'
|
|
238
267
|
router.get(path, parseQuery, setShape(opConfig), ...before, ${prefix}FindUniqueOrThrow as RequestHandler, ...after, respond)
|
|
268
|
+
if (postReadsEnabled) {
|
|
269
|
+
router.post(path, parseBodyAsQuery, setShape(opConfig), ...before, ${prefix}FindUniqueOrThrow as RequestHandler, ...after, respond)
|
|
270
|
+
}
|
|
239
271
|
}
|
|
240
272
|
|
|
241
273
|
if (config.enableAll || config.findUnique) {
|
|
@@ -243,6 +275,9 @@ export function ${routerFunctionName}(config: RouteConfig = {}) {
|
|
|
243
275
|
const { before = [], after = [] } = opConfig
|
|
244
276
|
const path = basePath ? \`\${basePath}/unique\` : '/unique'
|
|
245
277
|
router.get(path, parseQuery, setShape(opConfig), ...before, ${prefix}FindUnique as RequestHandler, ...after, respond)
|
|
278
|
+
if (postReadsEnabled) {
|
|
279
|
+
router.post(path, parseBodyAsQuery, setShape(opConfig), ...before, ${prefix}FindUnique as RequestHandler, ...after, respond)
|
|
280
|
+
}
|
|
246
281
|
}
|
|
247
282
|
|
|
248
283
|
if (config.enableAll || config.findMany) {
|
|
@@ -250,6 +285,10 @@ export function ${routerFunctionName}(config: RouteConfig = {}) {
|
|
|
250
285
|
const { before = [], after = [] } = opConfig
|
|
251
286
|
const path = basePath || '/'
|
|
252
287
|
router.get(path, parseQuery, setShape(opConfig), ...before, ${prefix}FindMany as RequestHandler, ...after, respond)
|
|
288
|
+
if (postReadsEnabled) {
|
|
289
|
+
const postPath = basePath ? \`\${basePath}/read\` : '/read'
|
|
290
|
+
router.post(postPath, parseBodyAsQuery, setShape(opConfig), ...before, ${prefix}FindMany as RequestHandler, ...after, respond)
|
|
291
|
+
}
|
|
253
292
|
}
|
|
254
293
|
|
|
255
294
|
if (config.enableAll || config.createManyAndReturn) {
|