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.
Files changed (33) hide show
  1. package/README.md +59 -3
  2. package/dist/constants.d.ts +1 -0
  3. package/dist/generators/generateOperationCore.d.ts +2 -0
  4. package/dist/generators/generateOperationCore.js +30 -3
  5. package/dist/generators/generateOperationCore.js.map +1 -1
  6. package/dist/generators/generateRouter.d.ts +3 -1
  7. package/dist/generators/generateRouter.js +6 -4
  8. package/dist/generators/generateRouter.js.map +1 -1
  9. package/dist/generators/generateRouterFastify.d.ts +3 -1
  10. package/dist/generators/generateRouterFastify.js +6 -4
  11. package/dist/generators/generateRouterFastify.js.map +1 -1
  12. package/dist/generators/generateRouterHono.d.ts +3 -1
  13. package/dist/generators/generateRouterHono.js +6 -3
  14. package/dist/generators/generateRouterHono.js.map +1 -1
  15. package/dist/generators/generateUnifiedScalarUI.d.ts +2 -1
  16. package/dist/generators/generateUnifiedScalarUI.js +13 -10
  17. package/dist/generators/generateUnifiedScalarUI.js.map +1 -1
  18. package/dist/index.js +23 -3
  19. package/dist/index.js.map +1 -1
  20. package/package.json +1 -1
  21. package/src/constants.ts +3 -1
  22. package/src/copy/buildModelOpenApi.ts +144 -23
  23. package/src/copy/docsRenderer.ts +121 -95
  24. package/src/copy/routeConfig.express.ts +2 -0
  25. package/src/copy/routeConfig.fastify.ts +2 -0
  26. package/src/copy/routeConfig.hono.ts +2 -0
  27. package/src/copy/routeConfig.ts +2 -0
  28. package/src/generators/generateOperationCore.ts +43 -3
  29. package/src/generators/generateRouter.ts +8 -3
  30. package/src/generators/generateRouterFastify.ts +8 -3
  31. package/src/generators/generateRouterHono.ts +8 -2
  32. package/src/generators/generateUnifiedScalarUI.ts +15 -11
  33. package/src/index.ts +27 -7
@@ -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
- 'in',
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 === 'DateTime') {
437
- return ['equals', 'in', 'notIn', 'lt', 'lte', 'gt', 'gte', 'not']
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-list') {
470
- return '{ some?: RelatedWhere, every?: RelatedWhere, none?: RelatedWhere }'
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(modelName: string, config: DocsConfig, ctx: DocsModelContext): string {
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 = OP_DETAIL_MAP[d.name]
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
- whereExample.AND = andClauses
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 argsReferenceWrite = {
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
- 'Batch mutations (createMany, updateMany, deleteMany) return { count }. Batch data inputs are scalar-only — nested relation writes are not supported.',
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 = {
@@ -20,6 +20,8 @@ export interface OpenApiSecuritySchemeConfig {
20
20
  description?: string
21
21
  }
22
22
 
23
+ export type WriteStrategy = 'regular' | 'throwOnNonReturning' | 'forceReturn'
24
+
23
25
  export type ProgressivePatch = {
24
26
  key: string
25
27
  value: unknown
@@ -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 validationLines = op.requiredFields.map((field) => ` requireBodyField(body, '${field}')`).join('\n')
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).${op.method}(body)
96
+ return delegate.guard(ctx.guardShape, ctx.guardCaller).${method}(body)
57
97
  }
58
- return delegate.${op.method}(body)
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)) {