prisma-generator-express 1.54.0 → 1.55.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 +59 -3
- package/dist/constants.d.ts +1 -0
- package/dist/generators/generateOperationCore.d.ts +2 -0
- package/dist/generators/generateOperationCore.js +30 -3
- package/dist/generators/generateOperationCore.js.map +1 -1
- package/dist/generators/generateRouter.d.ts +3 -1
- package/dist/generators/generateRouter.js +6 -4
- package/dist/generators/generateRouter.js.map +1 -1
- package/dist/generators/generateRouterFastify.d.ts +3 -1
- package/dist/generators/generateRouterFastify.js +6 -4
- package/dist/generators/generateRouterFastify.js.map +1 -1
- package/dist/generators/generateRouterHono.d.ts +3 -1
- package/dist/generators/generateRouterHono.js +6 -3
- package/dist/generators/generateRouterHono.js.map +1 -1
- package/dist/generators/generateUnifiedScalarUI.d.ts +2 -1
- package/dist/generators/generateUnifiedScalarUI.js +13 -10
- package/dist/generators/generateUnifiedScalarUI.js.map +1 -1
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +3 -1
- package/src/copy/buildModelOpenApi.ts +144 -23
- package/src/copy/docsRenderer.ts +121 -95
- package/src/copy/routeConfig.express.ts +2 -0
- package/src/copy/routeConfig.fastify.ts +2 -0
- package/src/copy/routeConfig.hono.ts +2 -0
- package/src/copy/routeConfig.ts +2 -0
- package/src/generators/generateOperationCore.ts +43 -3
- package/src/generators/generateRouter.ts +8 -3
- package/src/generators/generateRouterFastify.ts +8 -3
- package/src/generators/generateRouterHono.ts +8 -2
- package/src/generators/generateUnifiedScalarUI.ts +15 -11
- package/src/index.ts +27 -7
package/src/copy/docsRenderer.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RouteConfig } from './routeConfig'
|
|
1
|
+
import type { RouteConfig, WriteStrategy } from './routeConfig'
|
|
2
2
|
import { OPERATION_DEFS, isOperationEnabled, READ_OPERATION_NAMES } from './operationDefinitions'
|
|
3
3
|
import { getEnv, normalizePrefix } from './misc'
|
|
4
4
|
|
|
@@ -256,6 +256,41 @@ const OP_DETAIL_MAP: Record<string, OpDetail> = {
|
|
|
256
256
|
},
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
function detailForOp(opName: string, writeStrategy?: WriteStrategy): OpDetail {
|
|
260
|
+
const base = OP_DETAIL_MAP[opName]
|
|
261
|
+
if (!base) return base
|
|
262
|
+
if (writeStrategy === 'forceReturn') {
|
|
263
|
+
if (opName === 'createMany') {
|
|
264
|
+
return {
|
|
265
|
+
...base,
|
|
266
|
+
optional: ['skipDuplicates', 'select', 'include', 'omit'],
|
|
267
|
+
responseDesc: 'Array of created records (201)',
|
|
268
|
+
supportsSelect: true,
|
|
269
|
+
supportsInclude: true,
|
|
270
|
+
supportsOmit: true,
|
|
271
|
+
notes: 'writeStrategy="forceReturn": silently invokes createManyAndReturn and returns the created records instead of { count }.',
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (opName === 'updateMany') {
|
|
275
|
+
return {
|
|
276
|
+
...base,
|
|
277
|
+
optional: ['select', 'include', 'omit'],
|
|
278
|
+
responseDesc: 'Array of updated records',
|
|
279
|
+
supportsSelect: true,
|
|
280
|
+
supportsInclude: true,
|
|
281
|
+
supportsOmit: true,
|
|
282
|
+
notes: 'writeStrategy="forceReturn": silently invokes updateManyAndReturn and returns the updated records instead of { count }.',
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return base
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function isOpHiddenByStrategy(opName: string, writeStrategy?: WriteStrategy): boolean {
|
|
290
|
+
if (writeStrategy !== 'throwOnNonReturning') return false
|
|
291
|
+
return opName === 'createMany' || opName === 'updateMany'
|
|
292
|
+
}
|
|
293
|
+
|
|
259
294
|
function exampleValue(ctx: DocsModelContext, fieldName: string): unknown {
|
|
260
295
|
return ctx.exampleValues[fieldName] ?? 'example'
|
|
261
296
|
}
|
|
@@ -409,46 +444,17 @@ function isEnumField(f: FieldMeta): boolean {
|
|
|
409
444
|
function scalarFilterOperators(scalarType: string): string[] {
|
|
410
445
|
if (scalarType === 'String') {
|
|
411
446
|
return [
|
|
412
|
-
'equals',
|
|
413
|
-
'
|
|
414
|
-
'notIn',
|
|
415
|
-
'lt',
|
|
416
|
-
'lte',
|
|
417
|
-
'gt',
|
|
418
|
-
'gte',
|
|
419
|
-
'contains',
|
|
420
|
-
'startsWith',
|
|
421
|
-
'endsWith',
|
|
422
|
-
'mode',
|
|
423
|
-
'not',
|
|
447
|
+
'equals', 'in', 'notIn', 'lt', 'lte', 'gt', 'gte',
|
|
448
|
+
'contains', 'startsWith', 'endsWith', 'mode', 'not',
|
|
424
449
|
]
|
|
425
450
|
}
|
|
426
|
-
|
|
427
|
-
if (
|
|
428
|
-
scalarType === 'Int' ||
|
|
429
|
-
scalarType === 'BigInt' ||
|
|
430
|
-
scalarType === 'Float' ||
|
|
431
|
-
scalarType === 'Decimal'
|
|
432
|
-
) {
|
|
451
|
+
if (scalarType === 'Int' || scalarType === 'BigInt' || scalarType === 'Float' || scalarType === 'Decimal') {
|
|
433
452
|
return ['equals', 'in', 'notIn', 'lt', 'lte', 'gt', 'gte', 'not']
|
|
434
453
|
}
|
|
435
|
-
|
|
436
|
-
if (scalarType === '
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (scalarType === 'Boolean') {
|
|
441
|
-
return ['equals', 'not']
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (scalarType === 'Json') {
|
|
445
|
-
return ['equals', 'path', 'string_contains', 'array_contains', 'not']
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
if (scalarType === 'Bytes') {
|
|
449
|
-
return ['equals', 'in', 'notIn', 'not']
|
|
450
|
-
}
|
|
451
|
-
|
|
454
|
+
if (scalarType === 'DateTime') return ['equals', 'in', 'notIn', 'lt', 'lte', 'gt', 'gte', 'not']
|
|
455
|
+
if (scalarType === 'Boolean') return ['equals', 'not']
|
|
456
|
+
if (scalarType === 'Json') return ['equals', 'path', 'string_contains', 'array_contains', 'not']
|
|
457
|
+
if (scalarType === 'Bytes') return ['equals', 'in', 'notIn', 'not']
|
|
452
458
|
return ['equals', 'in', 'notIn', 'not']
|
|
453
459
|
}
|
|
454
460
|
|
|
@@ -465,27 +471,11 @@ function whereFieldKind(f: FieldMeta): string {
|
|
|
465
471
|
|
|
466
472
|
function whereFieldShape(f: FieldMeta): string {
|
|
467
473
|
const kind = whereFieldKind(f)
|
|
468
|
-
|
|
469
|
-
if (kind === 'relation-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if (kind === 'relation-single') {
|
|
474
|
-
return '{ is?: RelatedWhere, isNot?: RelatedWhere }'
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (kind === 'scalar-list') {
|
|
478
|
-
return '{ has?, hasEvery?, hasSome?, isEmpty? }'
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
if (kind === 'scalar') {
|
|
482
|
-
return 'scalar value OR filter object'
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (kind === 'enum') {
|
|
486
|
-
return 'enum value OR enum filter'
|
|
487
|
-
}
|
|
488
|
-
|
|
474
|
+
if (kind === 'relation-list') return '{ some?: RelatedWhere, every?: RelatedWhere, none?: RelatedWhere }'
|
|
475
|
+
if (kind === 'relation-single') return '{ is?: RelatedWhere, isNot?: RelatedWhere }'
|
|
476
|
+
if (kind === 'scalar-list') return '{ has?, hasEvery?, hasSome?, isEmpty? }'
|
|
477
|
+
if (kind === 'scalar') return 'scalar value OR filter object'
|
|
478
|
+
if (kind === 'enum') return 'enum value OR enum filter'
|
|
489
479
|
return 'n/a'
|
|
490
480
|
}
|
|
491
481
|
|
|
@@ -540,7 +530,12 @@ function buildFullPath(basePath: string, suffix: string): string {
|
|
|
540
530
|
return basePath ? basePath + suffix : suffix
|
|
541
531
|
}
|
|
542
532
|
|
|
543
|
-
export function renderDocs(
|
|
533
|
+
export function renderDocs(
|
|
534
|
+
modelName: string,
|
|
535
|
+
config: DocsConfig,
|
|
536
|
+
ctx: DocsModelContext,
|
|
537
|
+
writeStrategy?: WriteStrategy,
|
|
538
|
+
): string {
|
|
544
539
|
const title = config.docsTitle || modelName + ' API'
|
|
545
540
|
const generatedAt = new Date().toISOString()
|
|
546
541
|
|
|
@@ -559,8 +554,9 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
559
554
|
|
|
560
555
|
const getOps = OPERATION_DEFS
|
|
561
556
|
.filter((d) => isOperationEnabled(config as Record<string, any>, d))
|
|
557
|
+
.filter((d) => !isOpHiddenByStrategy(d.name, writeStrategy))
|
|
562
558
|
.map((d) => {
|
|
563
|
-
const detail =
|
|
559
|
+
const detail = detailForOp(d.name, writeStrategy)
|
|
564
560
|
return {
|
|
565
561
|
op: d.name,
|
|
566
562
|
method: d.method.toUpperCase(),
|
|
@@ -627,12 +623,8 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
627
623
|
if (firstStringField) {
|
|
628
624
|
andClauses.push({ [firstStringField.name]: { contains: 'example', mode: 'insensitive' } })
|
|
629
625
|
}
|
|
630
|
-
if (andClauses.length > 0)
|
|
631
|
-
|
|
632
|
-
}
|
|
633
|
-
if (firstBooleanField) {
|
|
634
|
-
whereExample.OR = [{ [firstBooleanField.name]: { equals: true } }]
|
|
635
|
-
}
|
|
626
|
+
if (andClauses.length > 0) whereExample.AND = andClauses
|
|
627
|
+
if (firstBooleanField) whereExample.OR = [{ [firstBooleanField.name]: { equals: true } }]
|
|
636
628
|
|
|
637
629
|
const selectExample: any = {}
|
|
638
630
|
for (const f of scalarFields.slice(0, 10)) selectExample[f.name] = true
|
|
@@ -644,9 +636,7 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
644
636
|
const omitCandidates = scalarFields.filter((f) => !f.isId && !f.isUnique)
|
|
645
637
|
for (const f of omitCandidates.slice(0, 3)) omitExample[f.name] = true
|
|
646
638
|
|
|
647
|
-
const orderByField = firstUnique
|
|
648
|
-
? firstUnique.name
|
|
649
|
-
: firstFilterFieldName
|
|
639
|
+
const orderByField = firstUnique ? firstUnique.name : firstFilterFieldName
|
|
650
640
|
|
|
651
641
|
const findManyQueryArgs: any = {
|
|
652
642
|
where: whereExample,
|
|
@@ -698,9 +688,7 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
698
688
|
: null
|
|
699
689
|
if (updateBodyExample) {
|
|
700
690
|
const firstEditableString = scalarFields.find((sf) => sf.type === 'String' && !sf.isId)
|
|
701
|
-
if (firstEditableString)
|
|
702
|
-
updateBodyExample.data[firstEditableString.name] = 'updated'
|
|
703
|
-
}
|
|
691
|
+
if (firstEditableString) updateBodyExample.data[firstEditableString.name] = 'updated'
|
|
704
692
|
}
|
|
705
693
|
|
|
706
694
|
const updateFetchExample = updateBodyExample
|
|
@@ -746,11 +734,7 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
746
734
|
} else {
|
|
747
735
|
writeContract = 'Optional (nullable)'
|
|
748
736
|
}
|
|
749
|
-
return {
|
|
750
|
-
name: f.name,
|
|
751
|
-
type: describeFieldType(f),
|
|
752
|
-
writeContract,
|
|
753
|
-
}
|
|
737
|
+
return { name: f.name, type: describeFieldType(f), writeContract }
|
|
754
738
|
})
|
|
755
739
|
|
|
756
740
|
const argsReferenceRead = {
|
|
@@ -817,7 +801,7 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
817
801
|
},
|
|
818
802
|
}
|
|
819
803
|
|
|
820
|
-
const
|
|
804
|
+
const argsReferenceWriteBase: Record<string, Record<string, string>> = {
|
|
821
805
|
create: {
|
|
822
806
|
data: modelName + 'CreateInput (required)',
|
|
823
807
|
select: 'Select',
|
|
@@ -867,17 +851,49 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
867
851
|
include: 'Include',
|
|
868
852
|
omit: 'Omit',
|
|
869
853
|
},
|
|
870
|
-
deleteMany: {
|
|
871
|
-
where: 'WhereInput (required)',
|
|
872
|
-
},
|
|
854
|
+
deleteMany: { where: 'WhereInput (required)' },
|
|
873
855
|
}
|
|
874
856
|
|
|
857
|
+
const argsReferenceWrite: Record<string, Record<string, string>> = (() => {
|
|
858
|
+
if (writeStrategy === 'throwOnNonReturning') {
|
|
859
|
+
const { createMany, updateMany, ...rest } = argsReferenceWriteBase
|
|
860
|
+
return rest
|
|
861
|
+
}
|
|
862
|
+
if (writeStrategy === 'forceReturn') {
|
|
863
|
+
return {
|
|
864
|
+
...argsReferenceWriteBase,
|
|
865
|
+
createMany: {
|
|
866
|
+
data: modelName + 'CreateManyInput[] (required, scalar-only)',
|
|
867
|
+
skipDuplicates: 'boolean',
|
|
868
|
+
select: 'Select',
|
|
869
|
+
include: 'Include',
|
|
870
|
+
omit: 'Omit',
|
|
871
|
+
},
|
|
872
|
+
updateMany: {
|
|
873
|
+
where: 'WhereInput (required)',
|
|
874
|
+
data: modelName + 'UpdateManyMutationInput (required, scalar-only)',
|
|
875
|
+
select: 'Select',
|
|
876
|
+
include: 'Include',
|
|
877
|
+
omit: 'Omit',
|
|
878
|
+
},
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return argsReferenceWriteBase
|
|
882
|
+
})()
|
|
883
|
+
|
|
884
|
+
const batchMutationNote =
|
|
885
|
+
writeStrategy === 'forceReturn'
|
|
886
|
+
? 'Batch mutations: deleteMany returns { count }. createMany and updateMany silently invoke their returning counterparts and return arrays of records. Batch data inputs are scalar-only — nested relation writes are not supported.'
|
|
887
|
+
: writeStrategy === 'throwOnNonReturning'
|
|
888
|
+
? 'Batch mutations: deleteMany returns { count }. createMany and updateMany are disabled and return 501 if called directly; use the /many/return variants. Batch data inputs are scalar-only — nested relation writes are not supported.'
|
|
889
|
+
: 'Batch mutations (createMany, updateMany, deleteMany) return { count }. Batch data inputs are scalar-only — nested relation writes are not supported.'
|
|
890
|
+
|
|
875
891
|
const transportNotes = [
|
|
876
892
|
'GET endpoints: Prisma args as JSON-encoded query parameter strings via encodeQueryParams.',
|
|
877
893
|
'POST/PUT/DELETE/PATCH endpoints: Prisma args as JSON request body. Body must be a JSON object.',
|
|
878
894
|
'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.',
|
|
879
895
|
'findManyPaginated returns { data, total, hasMore }. hasMore is reliable for forward offset pagination only.',
|
|
880
|
-
|
|
896
|
+
batchMutationNote,
|
|
881
897
|
'findUnique and findFirst return null (not 404) when no record matches. Use the OrThrow variants for 404 behavior.',
|
|
882
898
|
'createManyAndReturn requires Prisma 5.14.0+, updateManyAndReturn requires Prisma 6.2.0+. Both are limited to PostgreSQL, CockroachDB, and SQLite.',
|
|
883
899
|
]
|
|
@@ -888,7 +904,7 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
888
904
|
{ status: '404', description: 'Not found', causes: 'Record not found. Only from OrThrow operations, update, and delete.' },
|
|
889
905
|
{ status: '409', description: 'Conflict', causes: 'Unique constraint violation on create/update/upsert, or transaction conflict (e.g. in findManyPaginated).' },
|
|
890
906
|
{ status: '500', description: 'Internal server error', causes: 'Database error, table/column missing, raw query failure, or unhandled error.' },
|
|
891
|
-
{ status: '501', description: 'Not implemented', causes: 'Database provider does not support the requested feature.' },
|
|
907
|
+
{ status: '501', description: 'Not implemented', causes: 'Database provider does not support the requested feature, or writeStrategy disabled the endpoint.' },
|
|
892
908
|
{ status: '503', description: 'Service unavailable', causes: 'Database connection pool timeout.' },
|
|
893
909
|
]
|
|
894
910
|
|
|
@@ -903,6 +919,15 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
903
919
|
'<a class="' + chipClass + '" href="?ui=yaml">YAML</a>' +
|
|
904
920
|
(hasPlayground ? '<a class="' + chipClass + '" href="?ui=playground">Playground</a>' : '')
|
|
905
921
|
|
|
922
|
+
const writeStrategyBanner = !writeStrategy || writeStrategy === 'regular'
|
|
923
|
+
? ''
|
|
924
|
+
: '<div class="mt-3 p-3 rounded-xl border border-amber-300 bg-amber-50 text-xs">' +
|
|
925
|
+
'<strong>writeStrategy = ' + escapeHtml(writeStrategy) + '.</strong> ' +
|
|
926
|
+
(writeStrategy === 'throwOnNonReturning'
|
|
927
|
+
? 'POST /many (createMany) and PUT /many (updateMany) are disabled and respond with 501. Use the /many/return variants.'
|
|
928
|
+
: 'POST /many silently invokes createManyAndReturn; PUT /many silently invokes updateManyAndReturn. Both return arrays of records instead of { count } and accept select/include/omit.') +
|
|
929
|
+
'</div>'
|
|
930
|
+
|
|
906
931
|
const tocHtml = '<ol class="m-0 pl-[18px]">' + anchors().map((a) => '<li class="my-1.5"><a href="#' + escapeHtml(a.id) + '" class="text-inherit">' + escapeHtml(a.label) + '</a></li>').join('') + '</ol>'
|
|
907
932
|
|
|
908
933
|
const whereRows = ctx.fields.map((f) => {
|
|
@@ -997,6 +1022,12 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
997
1022
|
'Forced values (literals instead of true) are injected server-side and cannot be overridden by the client.',
|
|
998
1023
|
]
|
|
999
1024
|
|
|
1025
|
+
const writeStrategyNotes = !writeStrategy || writeStrategy === 'regular'
|
|
1026
|
+
? []
|
|
1027
|
+
: writeStrategy === 'throwOnNonReturning'
|
|
1028
|
+
? ['<strong>writeStrategy="throwOnNonReturning":</strong> the createMany (POST /many) and updateMany (PUT /many) endpoints are disabled and return 501. Use the corresponding <span class="font-mono">/many/return</span> endpoints instead.']
|
|
1029
|
+
: ['<strong>writeStrategy="forceReturn":</strong> the createMany (POST /many) and updateMany (PUT /many) endpoints silently invoke createManyAndReturn and updateManyAndReturn respectively. They return arrays of records instead of <span class="font-mono">{ count }</span>, and their request bodies accept <span class="font-mono">select</span>, <span class="font-mono">include</span>, and <span class="font-mono">omit</span>.']
|
|
1030
|
+
|
|
1000
1031
|
const runtimeNotes = [
|
|
1001
1032
|
'<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.',
|
|
1002
1033
|
'<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.',
|
|
@@ -1011,6 +1042,7 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
1011
1042
|
'<strong>Bulk write constraints:</strong> createMany, createManyAndReturn, updateMany, and updateManyAndReturn accept scalar-only data inputs. Nested relation writes are not supported in these operations.',
|
|
1012
1043
|
'<strong>Provider compatibility:</strong> createManyAndReturn requires Prisma 5.14.0+ and is limited to PostgreSQL, CockroachDB, and SQLite. updateManyAndReturn requires Prisma 6.2.0+ with the same provider restrictions. skipDuplicates is not supported on all database providers.',
|
|
1013
1044
|
'<strong>omit compatibility:</strong> The omit parameter requires Prisma 6.2.0+. On versions 6.0.x–6.1.x, requests using omit return 400.',
|
|
1045
|
+
...writeStrategyNotes,
|
|
1014
1046
|
]
|
|
1015
1047
|
|
|
1016
1048
|
const noUniqueFieldNote = '<div class="bg-gray-50 border border-gray-200 rounded-xl p-3 text-xs text-gray-500">This model has no unique or id fields suitable for a generated example. Use the unique constraint from your schema.</div>'
|
|
@@ -1048,6 +1080,8 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
1048
1080
|
<div class="flex gap-2 flex-wrap items-center pt-0.5">${openApiLinks}</div>
|
|
1049
1081
|
</div>
|
|
1050
1082
|
|
|
1083
|
+
${writeStrategyBanner}
|
|
1084
|
+
|
|
1051
1085
|
<div class="${calloutClass}">${tocHtml}</div>
|
|
1052
1086
|
|
|
1053
1087
|
<h2 class="mt-[18px] mb-2 text-base border-t border-gray-200 pt-3.5" id="ops">1. Operations</h2>
|
|
@@ -1274,9 +1308,7 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
1274
1308
|
</div>
|
|
1275
1309
|
<div>
|
|
1276
1310
|
<h3 class="mt-3.5 mb-2 text-sm">11.3 GET — findUnique</h3>
|
|
1277
|
-
${findUniqueFetchExample
|
|
1278
|
-
? codeBlock(findUniqueFetchExample)
|
|
1279
|
-
: noUniqueFieldNote}
|
|
1311
|
+
${findUniqueFetchExample ? codeBlock(findUniqueFetchExample) : noUniqueFieldNote}
|
|
1280
1312
|
</div>
|
|
1281
1313
|
<div>
|
|
1282
1314
|
<h3 class="mt-3.5 mb-2 text-sm">11.4 POST — create</h3>
|
|
@@ -1284,21 +1316,15 @@ export function renderDocs(modelName: string, config: DocsConfig, ctx: DocsModel
|
|
|
1284
1316
|
</div>
|
|
1285
1317
|
<div>
|
|
1286
1318
|
<h3 class="mt-3.5 mb-2 text-sm">11.5 PUT — update</h3>
|
|
1287
|
-
${updateFetchExample
|
|
1288
|
-
? codeBlock(updateFetchExample)
|
|
1289
|
-
: noUniqueFieldNote}
|
|
1319
|
+
${updateFetchExample ? codeBlock(updateFetchExample) : noUniqueFieldNote}
|
|
1290
1320
|
</div>
|
|
1291
1321
|
<div>
|
|
1292
1322
|
<h3 class="mt-3.5 mb-2 text-sm">11.6 DELETE — delete</h3>
|
|
1293
|
-
${deleteFetchExample
|
|
1294
|
-
? codeBlock(deleteFetchExample)
|
|
1295
|
-
: noUniqueFieldNote}
|
|
1323
|
+
${deleteFetchExample ? codeBlock(deleteFetchExample) : noUniqueFieldNote}
|
|
1296
1324
|
</div>
|
|
1297
1325
|
<div>
|
|
1298
1326
|
<h3 class="mt-3.5 mb-2 text-sm">11.7 Guard variant header</h3>
|
|
1299
|
-
${guardFetchExample
|
|
1300
|
-
? codeBlock(guardFetchExample)
|
|
1301
|
-
: noUniqueFieldNote}
|
|
1327
|
+
${guardFetchExample ? codeBlock(guardFetchExample) : noUniqueFieldNote}
|
|
1302
1328
|
</div>
|
|
1303
1329
|
</div>
|
|
1304
1330
|
|
|
@@ -7,12 +7,14 @@ import type {
|
|
|
7
7
|
QueryBuilderConfig,
|
|
8
8
|
OpenApiServerConfig,
|
|
9
9
|
OpenApiSecuritySchemeConfig,
|
|
10
|
+
WriteStrategy,
|
|
10
11
|
} from './routeConfig'
|
|
11
12
|
|
|
12
13
|
export type {
|
|
13
14
|
QueryBuilderConfig,
|
|
14
15
|
OpenApiServerConfig,
|
|
15
16
|
OpenApiSecuritySchemeConfig,
|
|
17
|
+
WriteStrategy,
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export type {
|
|
@@ -5,12 +5,14 @@ import type {
|
|
|
5
5
|
QueryBuilderConfig,
|
|
6
6
|
OpenApiServerConfig,
|
|
7
7
|
OpenApiSecuritySchemeConfig,
|
|
8
|
+
WriteStrategy,
|
|
8
9
|
} from './routeConfig'
|
|
9
10
|
|
|
10
11
|
export type {
|
|
11
12
|
QueryBuilderConfig,
|
|
12
13
|
OpenApiServerConfig,
|
|
13
14
|
OpenApiSecuritySchemeConfig,
|
|
15
|
+
WriteStrategy
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export type FastifyHookHandler = (
|
|
@@ -5,12 +5,14 @@ import type {
|
|
|
5
5
|
QueryBuilderConfig,
|
|
6
6
|
OpenApiServerConfig,
|
|
7
7
|
OpenApiSecuritySchemeConfig,
|
|
8
|
+
WriteStrategy,
|
|
8
9
|
} from './routeConfig'
|
|
9
10
|
|
|
10
11
|
export type {
|
|
11
12
|
QueryBuilderConfig,
|
|
12
13
|
OpenApiServerConfig,
|
|
13
14
|
OpenApiSecuritySchemeConfig,
|
|
15
|
+
WriteStrategy
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export type HonoEnvBase = {
|
package/src/copy/routeConfig.ts
CHANGED
|
@@ -1,16 +1,43 @@
|
|
|
1
1
|
import { DMMF } from '@prisma/generator-helper'
|
|
2
2
|
import { ImportStyle } from '../utils/resolveImportStyle'
|
|
3
3
|
import { importExt } from '../utils/importExt'
|
|
4
|
+
import { WriteStrategy } from '../constants'
|
|
4
5
|
|
|
5
6
|
export interface ModelCoreOptions {
|
|
6
7
|
model: DMMF.Model
|
|
7
8
|
importStyle: ImportStyle
|
|
9
|
+
writeStrategy: WriteStrategy
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type WriteOpDecision =
|
|
13
|
+
| { mode: 'normal'; method: string }
|
|
14
|
+
| { mode: 'redirect'; method: string }
|
|
15
|
+
| { mode: 'throw' }
|
|
16
|
+
|
|
17
|
+
function decideWriteOp(
|
|
18
|
+
name: string,
|
|
19
|
+
defaultMethod: string,
|
|
20
|
+
strategy: WriteStrategy,
|
|
21
|
+
): WriteOpDecision {
|
|
22
|
+
if (strategy === 'regular') {
|
|
23
|
+
return { mode: 'normal', method: defaultMethod }
|
|
24
|
+
}
|
|
25
|
+
if (strategy === 'throwOnNonReturning') {
|
|
26
|
+
if (name === 'createMany' || name === 'updateMany') {
|
|
27
|
+
return { mode: 'throw' }
|
|
28
|
+
}
|
|
29
|
+
return { mode: 'normal', method: defaultMethod }
|
|
30
|
+
}
|
|
31
|
+
if (name === 'createMany') return { mode: 'redirect', method: 'createManyAndReturn' }
|
|
32
|
+
if (name === 'updateMany') return { mode: 'redirect', method: 'updateManyAndReturn' }
|
|
33
|
+
return { mode: 'normal', method: defaultMethod }
|
|
8
34
|
}
|
|
9
35
|
|
|
10
36
|
export function generateModelCore(options: ModelCoreOptions): string {
|
|
11
37
|
const ext = importExt(options.importStyle)
|
|
12
38
|
const modelName = options.model.name
|
|
13
39
|
const modelNameLower = modelName.charAt(0).toLowerCase() + modelName.slice(1)
|
|
40
|
+
const writeStrategy = options.writeStrategy
|
|
14
41
|
|
|
15
42
|
const standardReadOps = [
|
|
16
43
|
'findFirst', 'findUnique', 'findUniqueOrThrow', 'findFirstOrThrow',
|
|
@@ -44,7 +71,20 @@ export async function ${op}(ctx: OperationContext): Promise<unknown> {
|
|
|
44
71
|
]
|
|
45
72
|
|
|
46
73
|
const writeHandlers = writeOps.map((op) => {
|
|
47
|
-
const
|
|
74
|
+
const decision = decideWriteOp(op.name, op.method, writeStrategy)
|
|
75
|
+
|
|
76
|
+
if (decision.mode === 'throw') {
|
|
77
|
+
return `
|
|
78
|
+
export async function ${op.name}(_ctx: OperationContext): Promise<unknown> {
|
|
79
|
+
throw new HttpError(501, '${op.name} is disabled by writeStrategy="${writeStrategy}"')
|
|
80
|
+
}`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const method = decision.method
|
|
84
|
+
const validationLines = op.requiredFields
|
|
85
|
+
.map((field) => ` requireBodyField(body, '${field}')`)
|
|
86
|
+
.join('\n')
|
|
87
|
+
|
|
48
88
|
return `
|
|
49
89
|
export async function ${op.name}(ctx: OperationContext): Promise<unknown> {
|
|
50
90
|
const body = validateBody(ctx.body)
|
|
@@ -53,9 +93,9 @@ ${validationLines}
|
|
|
53
93
|
const delegate = getDelegate(extended, '${modelNameLower}')
|
|
54
94
|
if (ctx.guardShape) {
|
|
55
95
|
assertGuard(delegate)
|
|
56
|
-
return delegate.guard(ctx.guardShape, ctx.guardCaller).${
|
|
96
|
+
return delegate.guard(ctx.guardShape, ctx.guardCaller).${method}(body)
|
|
57
97
|
}
|
|
58
|
-
return delegate.${
|
|
98
|
+
return delegate.${method}(body)
|
|
59
99
|
}`
|
|
60
100
|
}).join('\n')
|
|
61
101
|
|
|
@@ -2,17 +2,20 @@ import { DMMF } from '@prisma/generator-helper'
|
|
|
2
2
|
import { generateRouteConfigType } from './generateRouteConfigType'
|
|
3
3
|
import { ImportStyle } from '../utils/resolveImportStyle'
|
|
4
4
|
import { importExt } from '../utils/importExt'
|
|
5
|
+
import { WriteStrategy } from '../constants'
|
|
5
6
|
|
|
6
7
|
export function generateRouterFunction({
|
|
7
8
|
model,
|
|
8
9
|
enums,
|
|
9
10
|
guardShapesImport,
|
|
10
11
|
importStyle,
|
|
12
|
+
writeStrategy,
|
|
11
13
|
}: {
|
|
12
14
|
model: DMMF.Model
|
|
13
15
|
enums: DMMF.DatamodelEnum[]
|
|
14
16
|
guardShapesImport: string | null
|
|
15
17
|
importStyle: ImportStyle
|
|
18
|
+
writeStrategy: WriteStrategy
|
|
16
19
|
}): string {
|
|
17
20
|
const ext = importExt(importStyle)
|
|
18
21
|
const modelName = model.name
|
|
@@ -67,7 +70,7 @@ import {
|
|
|
67
70
|
${modelName}GroupBy,
|
|
68
71
|
} from './${modelName}Handlers${ext}'
|
|
69
72
|
import * as core from './${modelName}Core${ext}'
|
|
70
|
-
import type { RouteConfig, QueryBuilderConfig } from '../routeConfig.target${ext}'
|
|
73
|
+
import type { RouteConfig, QueryBuilderConfig, WriteStrategy } from '../routeConfig.target${ext}'
|
|
71
74
|
import { parseQueryParams } from '../parseQueryParams${ext}'
|
|
72
75
|
import { sanitizeKeys, normalizePrefix, getEnv } from '../misc${ext}'
|
|
73
76
|
import { buildModelOpenApi } from '../buildModelOpenApi${ext}'
|
|
@@ -88,6 +91,8 @@ import { runAutoIncludeProgressive } from '../autoIncludeRuntime${ext}'
|
|
|
88
91
|
${generateRouteConfigType(modelName, 'RequestHandler', guardShapesImport, importStyle, 'express')}
|
|
89
92
|
const _env = getEnv()
|
|
90
93
|
|
|
94
|
+
const WRITE_STRATEGY: WriteStrategy = '${writeStrategy}'
|
|
95
|
+
|
|
91
96
|
const MODEL_FIELDS = ${JSON.stringify(fieldsMeta, null, 2)} as const
|
|
92
97
|
const MODEL_ENUMS = ${JSON.stringify(enumsMeta, null, 2)} as const
|
|
93
98
|
|
|
@@ -154,7 +159,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
154
159
|
MODEL_FIELDS as unknown as Parameters<typeof buildModelOpenApi>[1],
|
|
155
160
|
MODEL_ENUMS as unknown as Parameters<typeof buildModelOpenApi>[2],
|
|
156
161
|
config as unknown as Parameters<typeof buildModelOpenApi>[3],
|
|
157
|
-
{ format: 'json' },
|
|
162
|
+
{ format: 'json', writeStrategy: WRITE_STRATEGY },
|
|
158
163
|
)
|
|
159
164
|
const openApiYamlSpec = openApiDisabled
|
|
160
165
|
? null
|
|
@@ -163,7 +168,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(config: ${m
|
|
|
163
168
|
MODEL_FIELDS as unknown as Parameters<typeof buildModelOpenApi>[1],
|
|
164
169
|
MODEL_ENUMS as unknown as Parameters<typeof buildModelOpenApi>[2],
|
|
165
170
|
config as unknown as Parameters<typeof buildModelOpenApi>[3],
|
|
166
|
-
{ format: 'yaml' },
|
|
171
|
+
{ format: 'yaml', writeStrategy: WRITE_STRATEGY },
|
|
167
172
|
)
|
|
168
173
|
|
|
169
174
|
const qbEnabled = isQueryBuilderEnabled(config)
|
|
@@ -2,17 +2,20 @@ import { DMMF } from '@prisma/generator-helper'
|
|
|
2
2
|
import { generateRouteConfigType } from './generateRouteConfigType'
|
|
3
3
|
import { ImportStyle } from '../utils/resolveImportStyle'
|
|
4
4
|
import { importExt } from '../utils/importExt'
|
|
5
|
+
import { WriteStrategy } from '../constants'
|
|
5
6
|
|
|
6
7
|
export function generateFastifyRouterFunction({
|
|
7
8
|
model,
|
|
8
9
|
enums,
|
|
9
10
|
guardShapesImport,
|
|
10
11
|
importStyle,
|
|
12
|
+
writeStrategy,
|
|
11
13
|
}: {
|
|
12
14
|
model: DMMF.Model
|
|
13
15
|
enums: DMMF.DatamodelEnum[]
|
|
14
16
|
guardShapesImport: string | null
|
|
15
17
|
importStyle: ImportStyle
|
|
18
|
+
writeStrategy: WriteStrategy
|
|
16
19
|
}): string {
|
|
17
20
|
const ext = importExt(importStyle)
|
|
18
21
|
const modelName = model.name
|
|
@@ -64,7 +67,7 @@ import {
|
|
|
64
67
|
${modelName}Count,
|
|
65
68
|
${modelName}GroupBy,
|
|
66
69
|
} from './${modelName}Handlers${ext}'
|
|
67
|
-
import type { RouteConfig, FastifyHookHandler } from '../routeConfig.target${ext}'
|
|
70
|
+
import type { RouteConfig, FastifyHookHandler, WriteStrategy } from '../routeConfig.target${ext}'
|
|
68
71
|
import { parseQueryParams } from '../parseQueryParams${ext}'
|
|
69
72
|
import { sanitizeKeys, normalizePrefix, getEnv } from '../misc${ext}'
|
|
70
73
|
import { buildModelOpenApi } from '../buildModelOpenApi${ext}'
|
|
@@ -73,6 +76,8 @@ import { mapError, transformResult, HttpError, type OperationContext } from '../
|
|
|
73
76
|
${generateRouteConfigType(modelName, 'FastifyHookHandler', guardShapesImport, importStyle, 'fastify')}
|
|
74
77
|
const _env = getEnv()
|
|
75
78
|
|
|
79
|
+
const WRITE_STRATEGY: WriteStrategy = '${writeStrategy}'
|
|
80
|
+
|
|
76
81
|
const MODEL_FIELDS = ${JSON.stringify(fieldsMeta, null, 2)} as const
|
|
77
82
|
|
|
78
83
|
const MODEL_ENUMS = ${JSON.stringify(enumsMeta, null, 2)} as const
|
|
@@ -203,7 +208,7 @@ export async function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(
|
|
|
203
208
|
MODEL_FIELDS as unknown as Parameters<typeof buildModelOpenApi>[1],
|
|
204
209
|
MODEL_ENUMS as unknown as Parameters<typeof buildModelOpenApi>[2],
|
|
205
210
|
config,
|
|
206
|
-
{ format: 'json' },
|
|
211
|
+
{ format: 'json', writeStrategy: WRITE_STRATEGY },
|
|
207
212
|
)
|
|
208
213
|
const openApiYamlSpec = openApiDisabled
|
|
209
214
|
? null
|
|
@@ -212,7 +217,7 @@ export async function ${routerFunctionName}<TCtx = unknown, TPrisma = any>(
|
|
|
212
217
|
MODEL_FIELDS as unknown as Parameters<typeof buildModelOpenApi>[1],
|
|
213
218
|
MODEL_ENUMS as unknown as Parameters<typeof buildModelOpenApi>[2],
|
|
214
219
|
config,
|
|
215
|
-
{ format: 'yaml' },
|
|
220
|
+
{ format: 'yaml', writeStrategy: WRITE_STRATEGY },
|
|
216
221
|
)
|
|
217
222
|
|
|
218
223
|
const qbEnabled = isQueryBuilderEnabled(config)
|
|
@@ -2,17 +2,20 @@ import { DMMF } from '@prisma/generator-helper'
|
|
|
2
2
|
import { generateRouteConfigType } from './generateRouteConfigType'
|
|
3
3
|
import { ImportStyle } from '../utils/resolveImportStyle'
|
|
4
4
|
import { importExt } from '../utils/importExt'
|
|
5
|
+
import { WriteStrategy } from '../constants'
|
|
5
6
|
|
|
6
7
|
export function generateHonoRouterFunction({
|
|
7
8
|
model,
|
|
8
9
|
enums,
|
|
9
10
|
guardShapesImport,
|
|
10
11
|
importStyle,
|
|
12
|
+
writeStrategy,
|
|
11
13
|
}: {
|
|
12
14
|
model: DMMF.Model
|
|
13
15
|
enums: DMMF.DatamodelEnum[]
|
|
14
16
|
guardShapesImport: string | null
|
|
15
17
|
importStyle: ImportStyle
|
|
18
|
+
writeStrategy: WriteStrategy
|
|
16
19
|
}): string {
|
|
17
20
|
const ext = importExt(importStyle)
|
|
18
21
|
const modelName = model.name
|
|
@@ -73,6 +76,7 @@ import type {
|
|
|
73
76
|
HonoEnvBase,
|
|
74
77
|
HonoInternalVariables,
|
|
75
78
|
GeneratedHonoEnv,
|
|
79
|
+
WriteStrategy,
|
|
76
80
|
} from '../routeConfig.target${ext}'
|
|
77
81
|
import { parseQueryParams } from '../parseQueryParams${ext}'
|
|
78
82
|
import { sanitizeKeys, normalizePrefix, getEnv } from '../misc${ext}'
|
|
@@ -82,6 +86,8 @@ import { mapError, transformResult, type OperationContext } from '../operationRu
|
|
|
82
86
|
${generateRouteConfigType(modelName, 'HonoHookHandler', guardShapesImport, importStyle, 'hono')}
|
|
83
87
|
const _env = getEnv()
|
|
84
88
|
|
|
89
|
+
const WRITE_STRATEGY: WriteStrategy = '${writeStrategy}'
|
|
90
|
+
|
|
85
91
|
const MODEL_FIELDS = ${JSON.stringify(fieldsMeta, null, 2)} as const
|
|
86
92
|
|
|
87
93
|
const MODEL_ENUMS = ${JSON.stringify(enumsMeta, null, 2)} as const
|
|
@@ -224,7 +230,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
|
|
|
224
230
|
MODEL_FIELDS as unknown as Parameters<typeof buildModelOpenApi>[1],
|
|
225
231
|
MODEL_ENUMS as unknown as Parameters<typeof buildModelOpenApi>[2],
|
|
226
232
|
config as RouteConfig,
|
|
227
|
-
{ format: 'json' },
|
|
233
|
+
{ format: 'json', writeStrategy: WRITE_STRATEGY },
|
|
228
234
|
)
|
|
229
235
|
const openApiYamlSpec = openApiDisabled
|
|
230
236
|
? null
|
|
@@ -233,7 +239,7 @@ export function ${routerFunctionName}<TCtx = unknown, TPrisma = any, TEnv extend
|
|
|
233
239
|
MODEL_FIELDS as unknown as Parameters<typeof buildModelOpenApi>[1],
|
|
234
240
|
MODEL_ENUMS as unknown as Parameters<typeof buildModelOpenApi>[2],
|
|
235
241
|
config as RouteConfig,
|
|
236
|
-
{ format: 'yaml' },
|
|
242
|
+
{ format: 'yaml', writeStrategy: WRITE_STRATEGY },
|
|
237
243
|
)
|
|
238
244
|
|
|
239
245
|
if (isQueryBuilderEnabled(config as RouteConfig)) {
|