@beignet/cli 0.0.37 → 0.0.39

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 (113) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +138 -101
  3. package/dist/choices.d.ts +17 -21
  4. package/dist/choices.d.ts.map +1 -1
  5. package/dist/choices.js +21 -67
  6. package/dist/choices.js.map +1 -1
  7. package/dist/config.d.ts +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +1 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/create-prompts.d.ts +5 -6
  12. package/dist/create-prompts.d.ts.map +1 -1
  13. package/dist/create-prompts.js +15 -15
  14. package/dist/create-prompts.js.map +1 -1
  15. package/dist/create.d.ts +11 -2
  16. package/dist/create.d.ts.map +1 -1
  17. package/dist/create.js +38 -28
  18. package/dist/create.js.map +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +149 -47
  21. package/dist/index.js.map +1 -1
  22. package/dist/inspect.js +69 -16
  23. package/dist/inspect.js.map +1 -1
  24. package/dist/lib.d.ts +1 -1
  25. package/dist/lib.d.ts.map +1 -1
  26. package/dist/lint.d.ts.map +1 -1
  27. package/dist/lint.js +27 -6
  28. package/dist/lint.js.map +1 -1
  29. package/dist/make/inbox.js +10 -10
  30. package/dist/make/inbox.js.map +1 -1
  31. package/dist/make/payments.d.ts +2 -1
  32. package/dist/make/payments.d.ts.map +1 -1
  33. package/dist/make/payments.js +18 -14
  34. package/dist/make/payments.js.map +1 -1
  35. package/dist/make/shared.d.ts +11 -4
  36. package/dist/make/shared.d.ts.map +1 -1
  37. package/dist/make/shared.js +56 -45
  38. package/dist/make/shared.js.map +1 -1
  39. package/dist/make/tenancy.js +8 -8
  40. package/dist/make/tenancy.js.map +1 -1
  41. package/dist/make.d.ts +3 -2
  42. package/dist/make.d.ts.map +1 -1
  43. package/dist/make.js +481 -200
  44. package/dist/make.js.map +1 -1
  45. package/dist/mcp.d.ts.map +1 -1
  46. package/dist/mcp.js +15 -11
  47. package/dist/mcp.js.map +1 -1
  48. package/dist/operational-error-reporting.d.ts +15 -0
  49. package/dist/operational-error-reporting.d.ts.map +1 -0
  50. package/dist/operational-error-reporting.js +42 -0
  51. package/dist/operational-error-reporting.js.map +1 -0
  52. package/dist/outbox.d.ts.map +1 -1
  53. package/dist/outbox.js +73 -0
  54. package/dist/outbox.js.map +1 -1
  55. package/dist/provider-add.d.ts +21 -3
  56. package/dist/provider-add.d.ts.map +1 -1
  57. package/dist/provider-add.js +117 -81
  58. package/dist/provider-add.js.map +1 -1
  59. package/dist/provider-audit.js +3 -3
  60. package/dist/provider-audit.js.map +1 -1
  61. package/dist/schedule.d.ts.map +1 -1
  62. package/dist/schedule.js +28 -1
  63. package/dist/schedule.js.map +1 -1
  64. package/dist/task.d.ts.map +1 -1
  65. package/dist/task.js +38 -5
  66. package/dist/task.js.map +1 -1
  67. package/dist/templates/base.d.ts +1 -1
  68. package/dist/templates/base.d.ts.map +1 -1
  69. package/dist/templates/base.js +15 -15
  70. package/dist/templates/base.js.map +1 -1
  71. package/dist/templates/index.d.ts +2 -2
  72. package/dist/templates/index.d.ts.map +1 -1
  73. package/dist/templates/index.js +7 -7
  74. package/dist/templates/index.js.map +1 -1
  75. package/dist/templates/server.d.ts +1 -1
  76. package/dist/templates/server.d.ts.map +1 -1
  77. package/dist/templates/server.js +22 -18
  78. package/dist/templates/server.js.map +1 -1
  79. package/dist/templates/shared.d.ts +4 -4
  80. package/dist/templates/shared.d.ts.map +1 -1
  81. package/dist/templates/shared.js +6 -6
  82. package/dist/templates/shared.js.map +1 -1
  83. package/dist/templates/testing.js +1 -1
  84. package/dist/templates/todos.js +6 -6
  85. package/package.json +4 -4
  86. package/skills/app-structure/SKILL.md +13 -8
  87. package/src/choices.ts +38 -88
  88. package/src/config.ts +2 -2
  89. package/src/create-prompts.ts +20 -21
  90. package/src/create.ts +54 -36
  91. package/src/index.ts +193 -65
  92. package/src/inspect.ts +116 -18
  93. package/src/lib.ts +1 -1
  94. package/src/lint.ts +35 -4
  95. package/src/make/inbox.ts +10 -10
  96. package/src/make/payments.ts +26 -18
  97. package/src/make/shared.ts +72 -50
  98. package/src/make/tenancy.ts +8 -8
  99. package/src/make.ts +607 -191
  100. package/src/mcp.ts +20 -13
  101. package/src/operational-error-reporting.ts +60 -0
  102. package/src/outbox.ts +77 -0
  103. package/src/provider-add.ts +179 -95
  104. package/src/provider-audit.ts +3 -3
  105. package/src/schedule.ts +32 -1
  106. package/src/task.ts +42 -5
  107. package/src/templates/base.ts +16 -16
  108. package/src/templates/index.ts +8 -8
  109. package/src/templates/server.ts +22 -18
  110. package/src/templates/shared.ts +10 -10
  111. package/src/templates/testing.ts +1 -1
  112. package/src/templates/todos.ts +6 -6
  113. package/src/test-helpers/generated-app.ts +1 -0
package/src/make.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import ts from "typescript";
4
5
  import type { MakeFeatureAddon, MakeFeatureRecipe } from "./choices.js";
5
6
  import {
6
7
  type BeignetConfig,
8
+ clientFormsPath,
7
9
  clientIndexPath,
8
10
  loadBeignetConfig,
9
11
  type ResolvedBeignetConfig,
@@ -146,6 +148,7 @@ import {
146
148
  identifiersFromArrayExpression,
147
149
  insertAfterImports,
148
150
  } from "./registry-edits.js";
151
+ import { externalVersions } from "./templates/shared.js";
149
152
  import { testSupportTemplateFiles } from "./templates/testing.js";
150
153
 
151
154
  export type { MakeFeatureAddon, MakeFeatureRecipe } from "./choices.js";
@@ -164,8 +167,8 @@ type MakeResourceOptions = {
164
167
  force?: boolean;
165
168
  dryRun?: boolean;
166
169
  config?: BeignetConfig;
167
- auth?: boolean;
168
- tenant?: boolean;
170
+ authorization?: boolean;
171
+ tenantScoped?: boolean;
169
172
  events?: boolean;
170
173
  softDelete?: boolean;
171
174
  };
@@ -313,6 +316,7 @@ type MakeUploadOptions = {
313
316
  force?: boolean;
314
317
  dryRun?: boolean;
315
318
  config?: BeignetConfig;
319
+ ui?: boolean;
316
320
  };
317
321
 
318
322
  type MakeOutboxOptions = {
@@ -428,7 +432,7 @@ export async function makeFeature(
428
432
  const addons = normalizeMakeFeatureAddons([
429
433
  ...makeFeatureRecipeAddons(options.recipe),
430
434
  ...(options.with ?? []),
431
- ...(options.events ? (["events"] as const) : []),
435
+ ...(options.events ? (["event"] as const) : []),
432
436
  ]);
433
437
  const featureOptions = {
434
438
  ...options,
@@ -580,20 +584,25 @@ function resourceGenerationOptions(
580
584
  ): ResourceGenerationOptions {
581
585
  if (mode === "feature") {
582
586
  return {
583
- auth: false,
584
- tenant: false,
587
+ authorization: false,
588
+ tenantScoped: false,
585
589
  events: Boolean(options.events),
586
590
  softDelete: false,
587
591
  };
588
592
  }
589
593
 
590
594
  if (mode !== "resource") {
591
- return { auth: false, tenant: false, events: false, softDelete: false };
595
+ return {
596
+ authorization: false,
597
+ tenantScoped: false,
598
+ events: false,
599
+ softDelete: false,
600
+ };
592
601
  }
593
602
 
594
603
  return {
595
- auth: Boolean(options.auth),
596
- tenant: Boolean(options.tenant),
604
+ authorization: Boolean(options.authorization),
605
+ tenantScoped: Boolean(options.tenantScoped),
597
606
  events: Boolean(options.events),
598
607
  softDelete: Boolean(options.softDelete),
599
608
  };
@@ -604,11 +613,12 @@ async function makeFeatureSlice(
604
613
  addons: readonly NormalizedMakeFeatureAddon[],
605
614
  ): Promise<MakeFeatureResult> {
606
615
  let result = await makeFeatureResource(options);
616
+ const includeUploadUi = addons.includes("ui") && addons.includes("upload");
607
617
 
608
618
  for (const addon of addons) {
609
619
  result = mergeMakeResults(
610
620
  result,
611
- await makeFeatureAddon(addon, result.name, options),
621
+ await makeFeatureAddon(addon, result.name, options, includeUploadUi),
612
622
  );
613
623
  }
614
624
 
@@ -619,6 +629,7 @@ async function makeFeatureAddon(
619
629
  addon: NormalizedMakeFeatureAddon,
620
630
  feature: string,
621
631
  options: MakeFeatureOptions,
632
+ includeUploadUi: boolean,
622
633
  ): Promise<MakeResult> {
623
634
  const shared = {
624
635
  cwd: options.cwd,
@@ -633,37 +644,37 @@ async function makeFeatureAddon(
633
644
  if (addon === "factory") {
634
645
  return makeFactory({
635
646
  ...shared,
636
- name: `${feature}/${singularize(feature)}`,
647
+ name: `${feature}.${singularize(feature)}`,
637
648
  skipPersistenceAssert: true,
638
649
  });
639
650
  }
640
651
  if (addon === "task") {
641
- return makeTask({ ...shared, name: `${feature}/backfill` });
652
+ return makeTask({ ...shared, name: `${feature}.backfill` });
642
653
  }
643
654
  if (addon === "event") {
644
- return makeEvent({ ...shared, name: `${feature}/created` });
655
+ return makeEvent({ ...shared, name: `${feature}.created` });
645
656
  }
646
657
  if (addon === "job") {
647
- return makeJob({ ...shared, name: `${feature}/process` });
658
+ return makeJob({ ...shared, name: `${feature}.process` });
648
659
  }
649
660
  if (addon === "listener") {
650
661
  return makeListener({
651
662
  ...shared,
652
- name: `${feature}/handle-created`,
653
- event: `${feature}/created`,
663
+ name: `${feature}.handle-created`,
664
+ event: `${feature}.created`,
654
665
  skipEventAssert: true,
655
666
  });
656
667
  }
657
668
  if (addon === "notification") {
658
- return makeNotification({ ...shared, name: `${feature}/created` });
669
+ return makeNotification({ ...shared, name: `${feature}.created` });
659
670
  }
660
671
  if (addon === "schedule") {
661
- return makeSchedule({ ...shared, name: `${feature}/daily-summary` });
672
+ return makeSchedule({ ...shared, name: `${feature}.daily-summary` });
662
673
  }
663
674
  if (addon === "seed") {
664
675
  return makeSeed({
665
676
  ...shared,
666
- name: `${feature}/demo-${feature}`,
677
+ name: `${feature}.demo-${feature}`,
667
678
  skipPersistenceAssert: true,
668
679
  });
669
680
  }
@@ -671,7 +682,11 @@ async function makeFeatureAddon(
671
682
  return makeFeatureUi({ ...shared, name: feature });
672
683
  }
673
684
 
674
- return makeUpload({ ...shared, name: `${feature}/attachment` });
685
+ return makeUpload({
686
+ ...shared,
687
+ name: `${feature}.attachment`,
688
+ ui: includeUploadUi,
689
+ });
675
690
  }
676
691
 
677
692
  function normalizeMakeFeatureAddons(
@@ -680,18 +695,7 @@ function normalizeMakeFeatureAddons(
680
695
  const normalized = new Set<NormalizedMakeFeatureAddon>();
681
696
 
682
697
  for (const addon of addons) {
683
- if (addon === "factories") normalized.add("factory");
684
- else if (addon === "events") normalized.add("event");
685
- else if (addon === "listeners") {
686
- normalized.add("event");
687
- normalized.add("listener");
688
- } else if (addon === "tasks") normalized.add("task");
689
- else if (addon === "jobs") normalized.add("job");
690
- else if (addon === "notifications") normalized.add("notification");
691
- else if (addon === "schedules") normalized.add("schedule");
692
- else if (addon === "seeds") normalized.add("seed");
693
- else if (addon === "uploads") normalized.add("upload");
694
- else if (addon === "listener") {
698
+ if (addon === "listener") {
695
699
  normalized.add("event");
696
700
  normalized.add("listener");
697
701
  } else normalized.add(addon);
@@ -706,16 +710,16 @@ function makeFeatureRecipeAddons(
706
710
  if (recipe === "full-slice") {
707
711
  return [
708
712
  "policy",
709
- "factories",
710
- "seeds",
711
- "tasks",
712
- "events",
713
- "listeners",
714
- "jobs",
715
- "notifications",
716
- "schedules",
713
+ "factory",
714
+ "seed",
715
+ "task",
716
+ "event",
717
+ "listener",
718
+ "job",
719
+ "notification",
720
+ "schedule",
717
721
  "ui",
718
- "uploads",
722
+ "upload",
719
723
  ];
720
724
  }
721
725
 
@@ -807,7 +811,7 @@ export async function makeUseCase(
807
811
 
808
812
  return {
809
813
  schemaVersion: 1,
810
- name: `${names.feature.kebab}/${names.action.kebab}`,
814
+ name: `${names.feature.kebab}.${names.action.kebab}`,
811
815
  targetDir,
812
816
  dryRun: Boolean(options.dryRun),
813
817
  files: [...generatedFiles.map((file) => file.path), indexFile.path],
@@ -853,7 +857,7 @@ export async function makeTest(
853
857
 
854
858
  return {
855
859
  schemaVersion: 1,
856
- name: `${names.feature.kebab}/${names.action.kebab}`,
860
+ name: `${names.feature.kebab}.${names.action.kebab}`,
857
861
  targetDir,
858
862
  dryRun: Boolean(options.dryRun),
859
863
  files: generatedFiles.map((file) => file.path),
@@ -911,7 +915,7 @@ export async function makePort(
911
915
  dryRun: Boolean(options.dryRun),
912
916
  })
913
917
  ) {
914
- updatedFiles.push(config.paths.infrastructurePorts);
918
+ updatedFiles.push(config.paths.portWiring);
915
919
  }
916
920
 
917
921
  return {
@@ -957,7 +961,7 @@ export async function makeAdapter(
957
961
  dryRun: Boolean(options.dryRun),
958
962
  })
959
963
  ) {
960
- updatedFiles.push(config.paths.infrastructurePorts);
964
+ updatedFiles.push(config.paths.portWiring);
961
965
  }
962
966
 
963
967
  return {
@@ -1034,13 +1038,16 @@ export async function makeEvent(
1034
1038
  const outboxDrainPlan = await planOutboxDrainWiring(targetDir, config, {
1035
1039
  force: Boolean(options.force),
1036
1040
  });
1041
+ await updateOutboxRegistry(targetDir, names, "events", config, {
1042
+ dryRun: true,
1043
+ });
1037
1044
 
1038
1045
  const result = await makeFeatureArtifact({
1039
1046
  targetDir,
1040
1047
  config,
1041
1048
  force: Boolean(options.force),
1042
1049
  dryRun: Boolean(options.dryRun),
1043
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1050
+ name: names.stableName,
1044
1051
  files: eventFiles(names, config),
1045
1052
  index: featureArtifactIndexFile("event", names, config),
1046
1053
  dependencies: {
@@ -1078,13 +1085,16 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
1078
1085
  const outboxDrainPlan = await planOutboxDrainWiring(targetDir, config, {
1079
1086
  force: Boolean(options.force),
1080
1087
  });
1088
+ await updateOutboxRegistry(targetDir, names, "jobs", config, {
1089
+ dryRun: true,
1090
+ });
1081
1091
 
1082
1092
  const result = await makeFeatureArtifact({
1083
1093
  targetDir,
1084
1094
  config,
1085
1095
  force: Boolean(options.force),
1086
1096
  dryRun: Boolean(options.dryRun),
1087
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1097
+ name: names.stableName,
1088
1098
  files: [
1089
1099
  ...(await missingCapabilityBuilderFiles(targetDir, "jobs", config)),
1090
1100
  ...jobFiles(names, config),
@@ -1124,7 +1134,7 @@ export async function makeTask(
1124
1134
  config,
1125
1135
  force: Boolean(options.force),
1126
1136
  dryRun: Boolean(options.dryRun),
1127
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1137
+ name: names.stableName,
1128
1138
  files: [
1129
1139
  ...(await missingCapabilityBuilderFiles(targetDir, "tasks", config)),
1130
1140
  ...taskFiles(names, config),
@@ -1169,7 +1179,7 @@ export async function makeFactory(
1169
1179
  config,
1170
1180
  force: Boolean(options.force),
1171
1181
  dryRun: Boolean(options.dryRun),
1172
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1182
+ name: names.stableName,
1173
1183
  files: factoryFiles(names, config),
1174
1184
  index: featureArtifactIndexFile("factory", names, config),
1175
1185
  dependencies: {
@@ -1196,7 +1206,7 @@ export async function makeSeed(
1196
1206
  config,
1197
1207
  force: Boolean(options.force),
1198
1208
  dryRun: Boolean(options.dryRun),
1199
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1209
+ name: names.stableName,
1200
1210
  files: seedFiles(names, config),
1201
1211
  index: featureArtifactIndexFile("seed", names, config),
1202
1212
  dependencies: {
@@ -1215,7 +1225,7 @@ export async function makeSeed(
1215
1225
  config,
1216
1226
  force: Boolean(options.force),
1217
1227
  dryRun: Boolean(options.dryRun),
1218
- name: `${factoryNamesForSeed.feature.kebab}/${factoryNamesForSeed.artifact.kebab}`,
1228
+ name: factoryNamesForSeed.stableName,
1219
1229
  files: factoryFiles(factoryNamesForSeed, config),
1220
1230
  index: featureArtifactIndexFile("factory", factoryNamesForSeed, config),
1221
1231
  dependencies: {
@@ -1271,7 +1281,7 @@ export async function makeNotification(
1271
1281
  // notifications uses ctx.ports.notifications, so the generator wires both
1272
1282
  // ports with dev-default providers. Each port is wired independently and
1273
1283
  // skipped when the app already has it, for example when the resend
1274
- // integration contributed the mailer. Plan the wiring before writing any
1284
+ // provider contributed the mailer. Plan the wiring before writing any
1275
1285
  // files so an anchor miss aborts the generator without leaving partial
1276
1286
  // output.
1277
1287
  const notificationWirings = notificationPortWirings("make notification");
@@ -1284,7 +1294,7 @@ export async function makeNotification(
1284
1294
  config,
1285
1295
  force: Boolean(options.force),
1286
1296
  dryRun: Boolean(options.dryRun),
1287
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1297
+ name: names.stableName,
1288
1298
  files: [
1289
1299
  ...(await missingCapabilityBuilderFiles(
1290
1300
  targetDir,
@@ -1335,7 +1345,7 @@ export async function makeListener(
1335
1345
  config,
1336
1346
  force: Boolean(options.force),
1337
1347
  dryRun: Boolean(options.dryRun),
1338
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1348
+ name: names.stableName,
1339
1349
  files: [
1340
1350
  ...(await missingCapabilityBuilderFiles(targetDir, "listeners", config)),
1341
1351
  ...listenerFiles(names, config),
@@ -1439,7 +1449,7 @@ async function assertListenerEventExists(
1439
1449
  await stat(path.join(targetDir, file));
1440
1450
  } catch {
1441
1451
  throw new Error(
1442
- `beignet make listener expected event ${names.eventName} at ${file}. Run beignet make event ${names.feature.kebab}/${names.artifact.kebab} first, or pass an event that exists.`,
1452
+ `beignet make listener expected event ${names.eventName} at ${file}. Run beignet make event ${names.eventName} first, or pass an event that exists.`,
1443
1453
  );
1444
1454
  }
1445
1455
  }
@@ -1509,7 +1519,7 @@ export async function makeSchedule(
1509
1519
  config,
1510
1520
  force: Boolean(options.force),
1511
1521
  dryRun: Boolean(options.dryRun),
1512
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1522
+ name: names.stableName,
1513
1523
  files,
1514
1524
  index: featureArtifactIndexFile("schedule", names, config),
1515
1525
  dependencies: {
@@ -1697,7 +1707,7 @@ async function updateOutboxPortWiring(
1697
1707
  changes.updatedFiles.push(config.paths.ports);
1698
1708
  }
1699
1709
  if (await updateOutboxInfrastructurePorts(targetDir, config, options)) {
1700
- changes.updatedFiles.push(config.paths.infrastructurePorts);
1710
+ changes.updatedFiles.push(config.paths.portWiring);
1701
1711
  }
1702
1712
  if (await updateOutboxDatabaseProvider(targetDir, config, options)) {
1703
1713
  changes.updatedFiles.push(
@@ -1805,7 +1815,7 @@ async function updateOutboxInfrastructurePorts(
1805
1815
  config: ResolvedBeignetConfig,
1806
1816
  options: { dryRun: boolean },
1807
1817
  ): Promise<boolean> {
1808
- const filePath = path.join(targetDir, config.paths.infrastructurePorts);
1818
+ const filePath = path.join(targetDir, config.paths.portWiring);
1809
1819
  const original = await readFile(filePath, "utf8");
1810
1820
  let next = appendDeferredPortKey(original, "outbox");
1811
1821
  next = appendDeferredPortKey(next, "outboxAdmin");
@@ -2059,6 +2069,9 @@ export async function makeUpload(
2059
2069
  : await loadBeignetConfig(targetDir);
2060
2070
 
2061
2071
  await assertFeatureArtifactApp(targetDir, config, "upload");
2072
+ if (options.ui) {
2073
+ await assertUploadUiApp(targetDir, config);
2074
+ }
2062
2075
 
2063
2076
  await wirePortProviders(
2064
2077
  targetDir,
@@ -2074,10 +2087,10 @@ export async function makeUpload(
2074
2087
  config,
2075
2088
  force: Boolean(options.force),
2076
2089
  dryRun: Boolean(options.dryRun),
2077
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
2090
+ name: names.stableName,
2078
2091
  files: [
2079
2092
  ...(await missingCapabilityBuilderFiles(targetDir, "uploads", config)),
2080
- ...uploadFiles(names, config),
2093
+ ...uploadFiles(names, config, { ui: Boolean(options.ui) }),
2081
2094
  ],
2082
2095
  index: featureArtifactIndexFile("upload", names, config),
2083
2096
  dependencies: {
@@ -2106,6 +2119,44 @@ export async function makeUpload(
2106
2119
  result.files.push(uploadRoutePath(config));
2107
2120
  }
2108
2121
 
2122
+ if (options.ui) {
2123
+ const componentIndex = uploadUiComponentIndexFile(names, config);
2124
+ const componentIndexResult = await updateFeatureUiIndex(
2125
+ targetDir,
2126
+ componentIndex,
2127
+ { dryRun: Boolean(options.dryRun) },
2128
+ );
2129
+ if (componentIndexResult === "created") {
2130
+ result.createdFiles.push(componentIndex.path);
2131
+ }
2132
+ if (componentIndexResult === "updated") {
2133
+ result.updatedFiles.push(componentIndex.path);
2134
+ }
2135
+ if (componentIndexResult === "skipped") {
2136
+ result.skippedFiles.push(componentIndex.path);
2137
+ }
2138
+ if (!result.files.includes(componentIndex.path)) {
2139
+ result.files.push(componentIndex.path);
2140
+ }
2141
+
2142
+ if (
2143
+ await updatePackageDependencies(
2144
+ targetDir,
2145
+ { "@beignet/react-uploads": beignetDependencyVersion },
2146
+ { dryRun: Boolean(options.dryRun) },
2147
+ )
2148
+ ) {
2149
+ result.updatedFiles.push("package.json");
2150
+ if (!result.files.includes("package.json")) {
2151
+ result.files.push("package.json");
2152
+ }
2153
+ }
2154
+ }
2155
+
2156
+ result.createdFiles = uniqueStrings(result.createdFiles);
2157
+ result.updatedFiles = uniqueStrings(result.updatedFiles);
2158
+ result.skippedFiles = uniqueStrings(result.skippedFiles);
2159
+
2109
2160
  return result;
2110
2161
  }
2111
2162
 
@@ -2166,7 +2217,7 @@ export async function makeOutbox(
2166
2217
  ...cronSecretPlan.files.map((file) => file.path),
2167
2218
  ...cronSecretPlan.skippedFiles,
2168
2219
  config.paths.ports,
2169
- config.paths.infrastructurePorts,
2220
+ config.paths.portWiring,
2170
2221
  path.join(infrastructureDir(config), "db/provider.ts"),
2171
2222
  drizzleRepositoriesPath(config),
2172
2223
  outboxDrizzleSchemaFilePath(config),
@@ -2189,7 +2240,10 @@ async function makeFeatureUi(
2189
2240
 
2190
2241
  await assertFeatureUiApp(targetDir, config);
2191
2242
 
2192
- const componentFiles = featureUiComponentFiles(names, config);
2243
+ const componentFiles = [
2244
+ ...(await missingFeatureUiFormsFiles(targetDir, config)),
2245
+ ...featureUiComponentFiles(names, config),
2246
+ ];
2193
2247
  const indexFile = featureUiIndexFile(names, config);
2194
2248
  const plannedFiles = await planGeneratedFiles(targetDir, componentFiles, {
2195
2249
  force: Boolean(options.force),
@@ -2221,8 +2275,15 @@ async function makeFeatureUi(
2221
2275
  await updatePackageDependencies(
2222
2276
  targetDir,
2223
2277
  {
2278
+ "@beignet/react-hook-form": beignetDependencyVersion,
2224
2279
  "@beignet/react-query": beignetDependencyVersion,
2280
+ "@hookform/resolvers": (packageJson) =>
2281
+ packageJson.dependencies?.["@hookform/resolvers"] ??
2282
+ externalVersions.hookformResolvers,
2225
2283
  "@tanstack/react-query": tanstackReactQueryDependencyVersion,
2284
+ "react-hook-form": (packageJson) =>
2285
+ packageJson.dependencies?.["react-hook-form"] ??
2286
+ externalVersions.reactHookForm,
2226
2287
  },
2227
2288
  { dryRun: Boolean(options.dryRun) },
2228
2289
  )
@@ -2361,19 +2422,19 @@ async function updateResourceWiring(
2361
2422
  persistence === "memory" &&
2362
2423
  (await updateInfrastructurePorts(targetDir, names, config, options))
2363
2424
  ) {
2364
- updated.add(config.paths.infrastructurePorts);
2425
+ updated.add(config.paths.portWiring);
2365
2426
  }
2366
2427
  if (
2367
- generationOptions.auth &&
2428
+ generationOptions.authorization &&
2368
2429
  (await updateInfrastructureGatePolicies(targetDir, names, config, options))
2369
2430
  ) {
2370
- updated.add(config.paths.infrastructurePorts);
2431
+ updated.add(config.paths.portWiring);
2371
2432
  }
2372
2433
  if (
2373
2434
  persistence === "drizzle" &&
2374
2435
  (await updateInfrastructureDeferredPorts(targetDir, names, config, options))
2375
2436
  ) {
2376
- updated.add(config.paths.infrastructurePorts);
2437
+ updated.add(config.paths.portWiring);
2377
2438
  }
2378
2439
  if (
2379
2440
  persistence === "drizzle" &&
@@ -2426,13 +2487,12 @@ function typeHasTopLevelProperty(
2426
2487
  function useCaseNames(input: string): UseCaseNames {
2427
2488
  const parts = input
2428
2489
  .trim()
2429
- .split("/")
2430
- .map((part) => part.trim())
2431
- .filter(Boolean);
2490
+ .split(".")
2491
+ .map((part) => part.trim());
2432
2492
 
2433
- if (parts.length !== 2) {
2493
+ if (parts.length !== 2 || parts.some((part) => part.length === 0)) {
2434
2494
  throw new Error(
2435
- "Use case name must use feature/action format, for example projects/archive-project.",
2495
+ "Use case name must use feature.action format, for example projects.archive-project.",
2436
2496
  );
2437
2497
  }
2438
2498
 
@@ -2472,7 +2532,7 @@ function policyNames(input: string): PolicyNames {
2472
2532
  }
2473
2533
 
2474
2534
  function eventNames(input: string): EventNames {
2475
- const names = featureArtifactNames(normalizeEventReference(input), "Event");
2535
+ const names = featureArtifactNames(input, "Event");
2476
2536
  const eventExportName = `${names.featureSingularPascal}${names.artifact.pascal}`;
2477
2537
 
2478
2538
  assertGeneratedIdentifiers(input, "Event name", [eventExportName]);
@@ -2530,7 +2590,7 @@ function factoryNames(input: string): FactoryNames {
2530
2590
  }
2531
2591
 
2532
2592
  function defaultFactoryNamesForSeed(names: SeedNames): FactoryNames {
2533
- return factoryNames(`${names.feature.kebab}/${names.featureSingularKebab}`);
2593
+ return factoryNames(`${names.feature.kebab}.${names.featureSingularKebab}`);
2534
2594
  }
2535
2595
 
2536
2596
  function notificationNames(input: string): NotificationNames {
@@ -2587,8 +2647,12 @@ function scheduleNames(
2587
2647
  function uploadNames(input: string): UploadNames {
2588
2648
  const names = featureArtifactNames(input, "Upload");
2589
2649
  const uploadExportName = `${names.artifact.pascal}Upload`;
2650
+ const componentExportName = `${names.artifact.pascal}Uploader`;
2590
2651
 
2591
- assertGeneratedIdentifiers(input, "Upload name", [uploadExportName]);
2652
+ assertGeneratedIdentifiers(input, "Upload name", [
2653
+ uploadExportName,
2654
+ componentExportName,
2655
+ ]);
2592
2656
 
2593
2657
  return {
2594
2658
  ...names,
@@ -2596,19 +2660,16 @@ function uploadNames(input: string): UploadNames {
2596
2660
  uploadExportName,
2597
2661
  metadataSchemaName: `${uploadExportName}MetadataSchema`,
2598
2662
  metadataTypeName: `${uploadExportName}Metadata`,
2663
+ fileConstraintsExportName: `${names.artifact.camel}UploadFile`,
2664
+ manifestExportName: `${names.artifact.camel}UploadManifest`,
2665
+ manifestFileName: `${names.artifact.kebab}-upload-manifest`,
2666
+ reactUploadsExportName: `${names.artifact.camel}ReactUploads`,
2667
+ clientFileName: `${names.artifact.kebab}-upload`,
2668
+ componentExportName,
2669
+ componentFileName: `${names.artifact.kebab}-uploader`,
2599
2670
  };
2600
2671
  }
2601
2672
 
2602
- function normalizeEventReference(input: string): string {
2603
- const trimmed = input.trim();
2604
- if (trimmed.includes("/")) return trimmed;
2605
-
2606
- const dotIndex = trimmed.indexOf(".");
2607
- if (dotIndex === -1) return trimmed;
2608
-
2609
- return `${trimmed.slice(0, dotIndex)}/${trimmed.slice(dotIndex + 1)}`;
2610
- }
2611
-
2612
2673
  function inferUseCaseKind(action: string): "command" | "query" {
2613
2674
  if (/^(count|find|get|list|search)-/.test(`${action}-`)) {
2614
2675
  return "query";
@@ -2622,8 +2683,8 @@ function resourceFiles(
2622
2683
  persistence: ResourcePersistence = "memory",
2623
2684
  mode: ResourceGenerationMode = "feature",
2624
2685
  options: ResourceGenerationOptions = {
2625
- auth: false,
2626
- tenant: false,
2686
+ authorization: false,
2687
+ tenantScoped: false,
2627
2688
  events: false,
2628
2689
  softDelete: false,
2629
2690
  },
@@ -2678,7 +2739,7 @@ function resourceFiles(
2678
2739
  },
2679
2740
  ]
2680
2741
  : []),
2681
- ...(mode === "resource" && options.auth
2742
+ ...(mode === "resource" && options.authorization
2682
2743
  ? [
2683
2744
  {
2684
2745
  path: path.join(featureDir, "tests/policy.test.ts"),
@@ -2748,6 +2809,99 @@ function featureUiComponentFiles(
2748
2809
  ];
2749
2810
  }
2750
2811
 
2812
+ async function missingFeatureUiFormsFiles(
2813
+ targetDir: string,
2814
+ config: ResolvedBeignetConfig,
2815
+ ): Promise<GeneratedFile[]> {
2816
+ const formsPath = clientFormsPath(config);
2817
+ const existing = await readOptionalFile(path.join(targetDir, formsPath));
2818
+
2819
+ if (existing === undefined) {
2820
+ return [
2821
+ {
2822
+ path: formsPath,
2823
+ content: `import { createReactHookForm } from "@beignet/react-hook-form";
2824
+
2825
+ export const rhf = createReactHookForm();
2826
+ `,
2827
+ },
2828
+ ];
2829
+ }
2830
+
2831
+ if (!sourceExportsValue(existing, "rhf")) {
2832
+ throw new Error(
2833
+ `beignet make feature --with ui expects ${formsPath} to export rhf. Add \`export const rhf = createReactHookForm();\` from @beignet/react-hook-form, or move the existing helper before generating UI.`,
2834
+ );
2835
+ }
2836
+
2837
+ return [];
2838
+ }
2839
+
2840
+ async function assertUploadUiApp(
2841
+ targetDir: string,
2842
+ config: ResolvedBeignetConfig,
2843
+ ): Promise<void> {
2844
+ const clientPath = clientIndexPath(config);
2845
+ const clientSource = await readOptionalFile(path.join(targetDir, clientPath));
2846
+
2847
+ if (!clientSource || !sourceExportsValue(clientSource, "rq")) {
2848
+ throw new Error(
2849
+ `beignet make upload --ui expects a full-stack Beignet app with ${clientPath} exporting rq. API-only apps stay backend-only; omit --ui or add the full-stack client setup first.`,
2850
+ );
2851
+ }
2852
+ }
2853
+
2854
+ function sourceExportsValue(source: string, name: string): boolean {
2855
+ const sourceFile = ts.createSourceFile(
2856
+ "client-forms.ts",
2857
+ source,
2858
+ ts.ScriptTarget.Latest,
2859
+ false,
2860
+ ts.ScriptKind.TS,
2861
+ );
2862
+
2863
+ for (const statement of sourceFile.statements) {
2864
+ const exported =
2865
+ ts.canHaveModifiers(statement) &&
2866
+ ts
2867
+ .getModifiers(statement)
2868
+ ?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
2869
+
2870
+ if (exported && ts.isVariableStatement(statement)) {
2871
+ if (
2872
+ statement.declarationList.declarations.some(
2873
+ (declaration) =>
2874
+ ts.isIdentifier(declaration.name) && declaration.name.text === name,
2875
+ )
2876
+ ) {
2877
+ return true;
2878
+ }
2879
+ }
2880
+
2881
+ if (
2882
+ exported &&
2883
+ ts.isFunctionDeclaration(statement) &&
2884
+ statement.name?.text === name
2885
+ ) {
2886
+ return true;
2887
+ }
2888
+
2889
+ if (
2890
+ ts.isExportDeclaration(statement) &&
2891
+ !statement.isTypeOnly &&
2892
+ statement.exportClause &&
2893
+ ts.isNamedExports(statement.exportClause) &&
2894
+ statement.exportClause.elements.some(
2895
+ (element) => !element.isTypeOnly && element.name.text === name,
2896
+ )
2897
+ ) {
2898
+ return true;
2899
+ }
2900
+ }
2901
+
2902
+ return false;
2903
+ }
2904
+
2751
2905
  function featureUiIndexFile(
2752
2906
  names: FeatureUiNames,
2753
2907
  config: ResolvedBeignetConfig,
@@ -2943,12 +3097,98 @@ function scheduleFiles(
2943
3097
  function uploadFiles(
2944
3098
  names: UploadNames,
2945
3099
  config: ResolvedBeignetConfig,
3100
+ options: { ui: boolean },
2946
3101
  ): GeneratedFile[] {
2947
3102
  return [
3103
+ {
3104
+ path: uploadManifestFilePath(names, config),
3105
+ content: uploadManifestFile(names),
3106
+ },
2948
3107
  {
2949
3108
  path: uploadFilePath(names, config),
2950
3109
  content: uploadFile(names, config),
2951
3110
  },
3111
+ ...(options.ui ? uploadUiFiles(names, config) : []),
3112
+ ];
3113
+ }
3114
+
3115
+ function uploadFeatureDir(
3116
+ names: UploadNames,
3117
+ config: ResolvedBeignetConfig,
3118
+ ): string {
3119
+ return path.join(config.paths.features, names.feature.kebab);
3120
+ }
3121
+
3122
+ function uploadManifestFilePath(
3123
+ names: UploadNames,
3124
+ config: ResolvedBeignetConfig,
3125
+ ): string {
3126
+ return path.join(
3127
+ uploadFeatureDir(names, config),
3128
+ `${names.manifestFileName}.ts`,
3129
+ );
3130
+ }
3131
+
3132
+ function uploadUiClientFilePath(
3133
+ names: UploadNames,
3134
+ config: ResolvedBeignetConfig,
3135
+ ): string {
3136
+ return path.join(
3137
+ uploadFeatureDir(names, config),
3138
+ "client",
3139
+ `${names.clientFileName}.ts`,
3140
+ );
3141
+ }
3142
+
3143
+ function uploadUiComponentFilePath(
3144
+ names: UploadNames,
3145
+ config: ResolvedBeignetConfig,
3146
+ ): string {
3147
+ return path.join(
3148
+ uploadFeatureDir(names, config),
3149
+ "components",
3150
+ `${names.componentFileName}.tsx`,
3151
+ );
3152
+ }
3153
+
3154
+ function uploadUiComponentTestFilePath(
3155
+ names: UploadNames,
3156
+ config: ResolvedBeignetConfig,
3157
+ ): string {
3158
+ return path.join(
3159
+ uploadFeatureDir(names, config),
3160
+ "tests",
3161
+ `${names.componentFileName}.test.tsx`,
3162
+ );
3163
+ }
3164
+
3165
+ function uploadUiComponentIndexFile(
3166
+ names: UploadNames,
3167
+ config: ResolvedBeignetConfig,
3168
+ ): GeneratedFile {
3169
+ return {
3170
+ path: path.join(uploadFeatureDir(names, config), "components", "index.ts"),
3171
+ content: `export { ${names.componentExportName} } from "./${names.componentFileName}";\n`,
3172
+ };
3173
+ }
3174
+
3175
+ function uploadUiFiles(
3176
+ names: UploadNames,
3177
+ config: ResolvedBeignetConfig,
3178
+ ): GeneratedFile[] {
3179
+ return [
3180
+ {
3181
+ path: uploadUiClientFilePath(names, config),
3182
+ content: uploadUiClientFile(names, config),
3183
+ },
3184
+ {
3185
+ path: uploadUiComponentFilePath(names, config),
3186
+ content: uploadUiComponentFile(names, config),
3187
+ },
3188
+ {
3189
+ path: uploadUiComponentTestFilePath(names, config),
3190
+ content: uploadUiComponentTestFile(names, config),
3191
+ },
2952
3192
  ];
2953
3193
  }
2954
3194
 
@@ -3381,7 +3621,13 @@ async function updateOutboxRegistry(
3381
3621
  importLine,
3382
3622
  );
3383
3623
 
3384
- if (appended.kind === "missing" || appended.kind === "unchanged") {
3624
+ if (appended.kind === "missing") {
3625
+ throw new Error(
3626
+ `Could not find the ${kind} array in ${config.paths.outbox}. Register ${registryName} manually, or restore the generated outbox file before running this command.`,
3627
+ );
3628
+ }
3629
+
3630
+ if (appended.kind === "unchanged") {
3385
3631
  return "skipped";
3386
3632
  }
3387
3633
 
@@ -3435,7 +3681,7 @@ function schemasFile(
3435
3681
 
3436
3682
  export const ${names.singularPascal}Schema = z.object({
3437
3683
  id: z.string().uuid(),
3438
- ${options.tenant ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
3684
+ ${options.tenantScoped ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
3439
3685
  version: z.number().int().min(1),
3440
3686
  createdAt: z.string().datetime(),
3441
3687
  updatedAt: z.string().datetime(),
@@ -3535,7 +3781,7 @@ export const List${names.pluralPascal}OutputSchema = z.object({
3535
3781
  });
3536
3782
 
3537
3783
  export const Create${names.singularPascal}InputSchema = z.object({
3538
- name: z.string().min(1).max(120),
3784
+ name: z.string().trim().min(1).max(120),
3539
3785
  });
3540
3786
  ${
3541
3787
  mode === "resource"
@@ -3545,7 +3791,7 @@ export const ${names.singularPascal}IdInputSchema = z.object({
3545
3791
  });
3546
3792
 
3547
3793
  export const Update${names.singularPascal}BodySchema = z.object({
3548
- name: z.string().min(1).max(120),
3794
+ name: z.string().trim().min(1).max(120),
3549
3795
  version: z.number().int().min(1),
3550
3796
  });
3551
3797
 
@@ -3591,7 +3837,7 @@ function listUseCaseFile(
3591
3837
 
3592
3838
  return `import "@beignet/core/server-only";
3593
3839
  import { normalizeCursorPage } from "@beignet/core/pagination";
3594
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
3840
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
3595
3841
  import {
3596
3842
  decode${names.singularPascal}Cursor,
3597
3843
  List${names.pluralPascal}InputSchema,
@@ -3603,7 +3849,7 @@ export const list${names.pluralPascal}UseCase = useCase
3603
3849
  .input(List${names.pluralPascal}InputSchema)
3604
3850
  .output(List${names.pluralPascal}OutputSchema)
3605
3851
  .run(async ({ ctx, input }) => {
3606
- ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const page = normalizeCursorPage(input, {
3852
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const page = normalizeCursorPage(input, {
3607
3853
  defaultLimit: 20,
3608
3854
  maxLimit: 100,
3609
3855
  });
@@ -3614,7 +3860,7 @@ ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const
3614
3860
  name: input.name,
3615
3861
  sortBy: input.sortBy,
3616
3862
  sortDirection: input.sortDirection,
3617
- }${options.tenant ? ", scope" : ""});
3863
+ }${options.tenantScoped ? ", scope" : ""});
3618
3864
  });
3619
3865
  `;
3620
3866
  }
@@ -3630,7 +3876,7 @@ function createUseCaseFile(
3630
3876
  );
3631
3877
 
3632
3878
  return `import "@beignet/core/server-only";
3633
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
3879
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
3634
3880
  import {
3635
3881
  Create${names.singularPascal}InputSchema,
3636
3882
  ${names.singularPascal}Schema,
@@ -3642,7 +3888,7 @@ export const create${names.singularPascal}UseCase = useCase
3642
3888
  .input(Create${names.singularPascal}InputSchema)
3643
3889
  .output(${names.singularPascal}Schema)
3644
3890
  .run(async ({ ctx, input }) => {
3645
- ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""}${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.create");\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.create(input${options.tenant ? ", scope" : ""});
3891
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""}${options.authorization ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.create");\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.create(input${options.tenantScoped ? ", scope" : ""});
3646
3892
  ${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Created, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
3647
3893
  return ${names.singularCamel};
3648
3894
  });
@@ -3660,7 +3906,7 @@ function getUseCaseFile(
3660
3906
  );
3661
3907
 
3662
3908
  return `import "@beignet/core/server-only";
3663
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
3909
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
3664
3910
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
3665
3911
  import {
3666
3912
  ${names.singularPascal}IdInputSchema,
@@ -3672,7 +3918,7 @@ export const get${names.singularPascal}UseCase = useCase
3672
3918
  .input(${names.singularPascal}IdInputSchema)
3673
3919
  .output(${names.singularPascal}Schema)
3674
3920
  .run(async ({ ctx, input }) => {
3675
- ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenant ? ", scope" : ""});
3921
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
3676
3922
  if (!${names.singularCamel}) {
3677
3923
  throw appError("${names.singularPascal}NotFound", {
3678
3924
  details: { id: input.id },
@@ -3695,7 +3941,7 @@ function updateUseCaseFile(
3695
3941
  );
3696
3942
 
3697
3943
  return `import "@beignet/core/server-only";
3698
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
3944
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
3699
3945
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
3700
3946
  ${options.events ? `import { ${names.singularPascal}Updated } from "../domain/events";\n` : ""}import {
3701
3947
  Update${names.singularPascal}InputSchema,
@@ -3707,13 +3953,13 @@ export const update${names.singularPascal}UseCase = useCase
3707
3953
  .input(Update${names.singularPascal}InputSchema)
3708
3954
  .output(${names.singularPascal}Schema)
3709
3955
  .run(async ({ ctx, input }) => {
3710
- ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenant ? ", scope" : ""});
3956
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
3711
3957
  if (!existing) {
3712
3958
  throw appError("${names.singularPascal}NotFound", {
3713
3959
  details: { id: input.id },
3714
3960
  });
3715
3961
  }
3716
- ${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
3962
+ ${options.authorization ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
3717
3963
  throw appError("${names.singularPascal}Conflict", {
3718
3964
  details: { id: input.id, expectedVersion: existing.version },
3719
3965
  });
@@ -3721,7 +3967,7 @@ ${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", ex
3721
3967
 
3722
3968
  const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.update({
3723
3969
  ...input,
3724
- }${options.tenant ? ", scope" : ""});
3970
+ }${options.tenantScoped ? ", scope" : ""});
3725
3971
  if (!${names.singularCamel}) {
3726
3972
  throw appError("${names.singularPascal}Conflict", {
3727
3973
  details: { id: input.id, expectedVersion: existing.version },
@@ -3745,7 +3991,7 @@ function deleteUseCaseFile(
3745
3991
 
3746
3992
  return `import "@beignet/core/server-only";
3747
3993
  import { z } from "zod";
3748
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
3994
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
3749
3995
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
3750
3996
  ${options.events ? `import { ${names.singularPascal}Deleted } from "../domain/events";\n` : ""}import { ${names.singularPascal}IdInputSchema } from "../schemas";
3751
3997
 
@@ -3754,7 +4000,7 @@ export const delete${names.singularPascal}UseCase = useCase
3754
4000
  .input(${names.singularPascal}IdInputSchema)
3755
4001
  .output(z.void())
3756
4002
  .run(async ({ ctx, input }) => {
3757
- ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""}${options.auth ? `\t\tconst existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenant ? ", scope" : ""});\n\t\tif (!existing) {\n\t\t\tthrow appError("${names.singularPascal}NotFound", {\n\t\t\t\tdetails: { id: input.id },\n\t\t\t});\n\t\t}\n\t\tawait ctx.gate.authorize("${names.pluralCamel}.delete", existing);\n\n` : ""} const deleted = await ctx.ports.${names.pluralCamel}.delete(input.id${options.tenant ? ", scope" : ""});
4003
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""}${options.authorization ? `\t\tconst existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});\n\t\tif (!existing) {\n\t\t\tthrow appError("${names.singularPascal}NotFound", {\n\t\t\t\tdetails: { id: input.id },\n\t\t\t});\n\t\t}\n\t\tawait ctx.gate.authorize("${names.pluralCamel}.delete", existing);\n\n` : ""} const deleted = await ctx.ports.${names.pluralCamel}.delete(input.id${options.tenantScoped ? ", scope" : ""});
3758
4004
  if (!deleted) {
3759
4005
  throw appError("${names.singularPascal}NotFound", {
3760
4006
  details: { id: input.id },
@@ -3794,7 +4040,7 @@ function repositoryPortFile(
3794
4040
  PageResult,
3795
4041
  SortDirection,
3796
4042
  } from "@beignet/core/pagination";
3797
- ${options.tenant ? `import type { TenantScope } from "@beignet/core/tenancy";\n` : ""}import type {
4043
+ ${options.tenantScoped ? `import type { TenantScope } from "@beignet/core/tenancy";\n` : ""}import type {
3798
4044
  Create${names.singularPascal}Input,
3799
4045
  ${names.singularPascal}Cursor,
3800
4046
  ${names.singularPascal}SortBy,
@@ -3811,9 +4057,9 @@ export type List${names.pluralPascal}Query = {
3811
4057
  };
3812
4058
 
3813
4059
  export interface ${names.singularPascal}Repository {
3814
- list(query: List${names.pluralPascal}Query${options.tenant ? ", scope: TenantScope" : ""}): Promise<List${names.pluralPascal}Result>;
3815
- create(input: Create${names.singularPascal}Input${options.tenant ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal}>;
3816
- ${mode === "resource" ? ` findById(id: string${options.tenant ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal} | null>;\n update(input: Update${names.singularPascal}Input${options.tenant ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal} | null>;\n delete(id: string${options.tenant ? ", scope: TenantScope" : ""}): Promise<boolean>;\n` : ""}}
4060
+ list(query: List${names.pluralPascal}Query${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<List${names.pluralPascal}Result>;
4061
+ create(input: Create${names.singularPascal}Input${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal}>;
4062
+ ${mode === "resource" ? ` findById(id: string${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal} | null>;\n update(input: Update${names.singularPascal}Input${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal} | null>;\n delete(id: string${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<boolean>;\n` : ""}}
3817
4063
  `;
3818
4064
  }
3819
4065
 
@@ -3847,14 +4093,14 @@ function inMemoryRepositoryFile(
3847
4093
  ? " || existing.deletedAt !== null"
3848
4094
  : "";
3849
4095
  const deleteImplementation = options.softDelete
3850
- ? `${options.tenant ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.tenantId !== tenantId || existing.deletedAt !== null) return false;\n` : `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.deletedAt !== null) return false;\n`} const now = new Date().toISOString();
4096
+ ? `${options.tenantScoped ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.tenantId !== tenantId || existing.deletedAt !== null) return false;\n` : `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.deletedAt !== null) return false;\n`} const now = new Date().toISOString();
3851
4097
  ${names.pluralCamel}.set(id, { ...existing, deletedAt: now, updatedAt: now });
3852
4098
  return true;`
3853
- : `${options.tenant ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (existing?.tenantId !== tenantId) return false;\n` : ""} return ${names.pluralCamel}.delete(id);`;
4099
+ : `${options.tenantScoped ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (existing?.tenantId !== tenantId) return false;\n` : ""} return ${names.pluralCamel}.delete(id);`;
3854
4100
 
3855
4101
  return `import "@beignet/core/server-only";
3856
4102
  import { cursorPageResult } from "@beignet/core/pagination";
3857
- ${options.tenant ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
4103
+ ${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
3858
4104
  import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
3859
4105
  import type {
3860
4106
  Create${names.singularPascal}Input,
@@ -3864,7 +4110,7 @@ ${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.s
3864
4110
  ${recordType}function to${names.singularPascal}(${names.singularCamel}: ${mapValueType}): ${names.singularPascal} {
3865
4111
  return {
3866
4112
  id: ${names.singularCamel}.id,
3867
- ${options.tenant ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
4113
+ ${options.tenantScoped ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
3868
4114
  version: ${names.singularCamel}.version,
3869
4115
  createdAt: ${names.singularCamel}.createdAt,
3870
4116
  updatedAt: ${names.singularCamel}.updatedAt,
@@ -3922,10 +4168,10 @@ export function createInMemory${names.singularPascal}Repository(
3922
4168
  );
3923
4169
 
3924
4170
  return {
3925
- async list(query${options.tenant ? ", scope" : ""}) {
4171
+ async list(query${options.tenantScoped ? ", scope" : ""}) {
3926
4172
  const name = query.name?.toLocaleLowerCase();
3927
- ${options.tenant ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values())
3928
- ${activeFilter}${options.tenant ? `\t\t\t\t.filter((${names.singularCamel}) => ${names.singularCamel}.tenantId === tenantId)\n` : ""} .filter((${names.singularCamel}) => !name || ${names.singularCamel}.name.toLocaleLowerCase().includes(name))
4173
+ ${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values())
4174
+ ${activeFilter}${options.tenantScoped ? `\t\t\t\t.filter((${names.singularCamel}) => ${names.singularCamel}.tenantId === tenantId)\n` : ""} .filter((${names.singularCamel}) => !name || ${names.singularCamel}.name.toLocaleLowerCase().includes(name))
3929
4175
  .sort((left, right) => compare${names.pluralPascal}(left, right, query))
3930
4176
  .filter((${names.singularCamel}) => isAfter${names.singularPascal}Cursor(${names.singularCamel}, query));
3931
4177
  const pageItems = all${names.pluralPascal}.slice(0, query.page.limit);
@@ -3940,11 +4186,11 @@ ${activeFilter}${options.tenant ? `\t\t\t\t.filter((${names.singularCamel}) => $
3940
4186
  nextCursor,
3941
4187
  );
3942
4188
  },
3943
- async create(input${options.tenant ? ", scope" : ""}) {
4189
+ async create(input${options.tenantScoped ? ", scope" : ""}) {
3944
4190
  const now = new Date().toISOString();
3945
- ${options.tenant ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel}: ${names.singularPascal} = {
4191
+ ${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel}: ${names.singularPascal} = {
3946
4192
  id: crypto.randomUUID(),
3947
- ${options.tenant ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
4193
+ ${options.tenantScoped ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
3948
4194
  version: 1,
3949
4195
  createdAt: now,
3950
4196
  updatedAt: now,
@@ -3954,7 +4200,7 @@ ${options.tenant ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
3954
4200
  ${newDeletedAtField} });
3955
4201
  return ${names.singularCamel};
3956
4202
  },
3957
- ${mode === "resource" ? ` async findById(id: string${options.tenant ? ", scope" : ""}) {\n${options.tenant ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel} = ${names.pluralCamel}.get(id) ?? null;\n${options.tenant ? `\t\t\tif (${names.singularCamel}?.tenantId !== tenantId) return null;\n` : ""}${findDeletedGuard} return ${names.singularCamel} ? to${names.singularPascal}(${names.singularCamel}) : null;\n },\n async update(input${options.tenant ? ", scope" : ""}) {\n${options.tenant ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const existing = ${names.pluralCamel}.get(input.id);\n if (!existing${options.tenant ? " || existing.tenantId !== tenantId" : ""}${updateDeletedGuard} || existing.version !== input.version) return null;\n\n const next: ${mapValueType} = {\n ...existing,\n name: input.name,\n version: existing.version + 1,\n updatedAt: new Date().toISOString(),\n };\n ${names.pluralCamel}.set(input.id, next);\n return to${names.singularPascal}(next);\n },\n async delete(id: string${options.tenant ? ", scope" : ""}) {\n${options.tenant ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""}${deleteImplementation}\n },\n` : ""}
4203
+ ${mode === "resource" ? ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {\n${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel} = ${names.pluralCamel}.get(id) ?? null;\n${options.tenantScoped ? `\t\t\tif (${names.singularCamel}?.tenantId !== tenantId) return null;\n` : ""}${findDeletedGuard} return ${names.singularCamel} ? to${names.singularPascal}(${names.singularCamel}) : null;\n },\n async update(input${options.tenantScoped ? ", scope" : ""}) {\n${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const existing = ${names.pluralCamel}.get(input.id);\n if (!existing${options.tenantScoped ? " || existing.tenantId !== tenantId" : ""}${updateDeletedGuard} || existing.version !== input.version) return null;\n\n const next: ${mapValueType} = {\n ...existing,\n name: input.name,\n version: existing.version + 1,\n updatedAt: new Date().toISOString(),\n };\n ${names.pluralCamel}.set(input.id, next);\n return to${names.singularPascal}(next);\n },\n async delete(id: string${options.tenantScoped ? ", scope" : ""}) {\n${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""}${deleteImplementation}\n },\n` : ""}
3958
4204
  };
3959
4205
  }
3960
4206
  `;
@@ -3969,7 +4215,7 @@ function drizzleSchemaFile(
3969
4215
 
3970
4216
  export const ${names.pluralCamel} = ${dialect.tableFunction}("${names.pluralKebab.replaceAll("-", "_")}", {
3971
4217
  id: ${dialect.idColumn("id")}.primaryKey(),
3972
- ${options.tenant ? `\ttenantId: ${dialect.idColumn("tenant_id")}.notNull(),\n` : ""} name: text("name").notNull(),
4218
+ ${options.tenantScoped ? `\ttenantId: ${dialect.idColumn("tenant_id")}.notNull(),\n` : ""} name: text("name").notNull(),
3973
4219
  version: ${dialect.integerColumn("version")}.notNull(),
3974
4220
  createdAt: ${dialect.timestampColumn("created_at")}.notNull(),
3975
4221
  updatedAt: ${dialect.timestampColumn("updated_at")}.notNull(),
@@ -3988,6 +4234,7 @@ export const outboxMessages = mysqlTable(
3988
4234
  \t\tkind: varchar("kind", { length: 32 }).notNull(),
3989
4235
  \t\tname: varchar("name", { length: 255 }).notNull(),
3990
4236
  \t\tpayloadJson: longtext("payload_json").notNull(),
4237
+ \t\ttraceContextJson: longtext("trace_context_json"),
3991
4238
  \t\tstatus: varchar("status", { length: 32 }).notNull(),
3992
4239
  \t\tattempts: int("attempts").notNull().default(0),
3993
4240
  \t\tmaxAttempts: int("max_attempts").notNull().default(3),
@@ -4023,6 +4270,7 @@ export const outboxMessages = ${dialect.tableFunction}(
4023
4270
  \t\tkind: text("kind").notNull(),
4024
4271
  \t\tname: text("name").notNull(),
4025
4272
  \t\tpayloadJson: text("payload_json").notNull(),
4273
+ \t\ttraceContextJson: text("trace_context_json"),
4026
4274
  \t\tstatus: text("status").notNull(),
4027
4275
  \t\tattempts: integer("attempts").notNull().default(0),
4028
4276
  \t\tmaxAttempts: integer("max_attempts").notNull().default(3),
@@ -4077,20 +4325,20 @@ function drizzleRepositoryFile(
4077
4325
  const schemaPath = drizzleSchemaIndexPath(config);
4078
4326
  const findWhere = drizzleResourceWhere(names, options, [
4079
4327
  `eq(schema.${names.pluralCamel}.id, id)`,
4080
- ...(options.tenant
4328
+ ...(options.tenantScoped
4081
4329
  ? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
4082
4330
  : []),
4083
4331
  ]);
4084
4332
  const updateWhere = drizzleResourceWhere(names, options, [
4085
4333
  `eq(schema.${names.pluralCamel}.id, input.id)`,
4086
4334
  `eq(schema.${names.pluralCamel}.version, input.version)`,
4087
- ...(options.tenant
4335
+ ...(options.tenantScoped
4088
4336
  ? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
4089
4337
  : []),
4090
4338
  ]);
4091
4339
  const deleteWhere = drizzleResourceWhere(names, options, [
4092
4340
  `eq(schema.${names.pluralCamel}.id, id)`,
4093
- ...(options.tenant
4341
+ ...(options.tenantScoped
4094
4342
  ? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
4095
4343
  : []),
4096
4344
  ]);
@@ -4108,7 +4356,7 @@ function drizzleRepositoryFile(
4108
4356
  ].filter((name): name is string => Boolean(name));
4109
4357
  // After a version-guarded update succeeds, re-select by identity only —
4110
4358
  // matching what `.returning()` yields on the dialects that support it.
4111
- const updateReselectWhere = options.tenant
4359
+ const updateReselectWhere = options.tenantScoped
4112
4360
  ? `and(eq(schema.${names.pluralCamel}.id, input.id), eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)))`
4113
4361
  : `eq(schema.${names.pluralCamel}.id, input.id)`;
4114
4362
  // drizzle-orm/mysql2 has no `.returning(...)`; mutations resolve to a
@@ -4131,13 +4379,13 @@ function affectedRows(result: unknown): number {
4131
4379
  `
4132
4380
  : "";
4133
4381
  const createMethod = dialect.supportsReturning
4134
- ? ` async create(input${options.tenant ? ", scope" : ""}) {
4382
+ ? ` async create(input${options.tenantScoped ? ", scope" : ""}) {
4135
4383
  const now = new Date().toISOString();
4136
4384
  const [row] = await db
4137
4385
  .insert(schema.${names.pluralCamel})
4138
4386
  .values({
4139
4387
  id: crypto.randomUUID(),
4140
- ${options.tenant ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
4388
+ ${options.tenantScoped ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
4141
4389
  version: 1,
4142
4390
  createdAt: now,
4143
4391
  updatedAt: now,
@@ -4151,11 +4399,11 @@ ${options.tenant ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name:
4151
4399
  return to${names.singularPascal}(row);
4152
4400
  },
4153
4401
  `
4154
- : ` async create(input${options.tenant ? ", scope" : ""}) {
4402
+ : ` async create(input${options.tenantScoped ? ", scope" : ""}) {
4155
4403
  const now = new Date().toISOString();
4156
4404
  const ${names.singularCamel}: ${names.singularPascal} = {
4157
4405
  id: crypto.randomUUID(),
4158
- ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
4406
+ ${options.tenantScoped ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
4159
4407
  version: 1,
4160
4408
  createdAt: now,
4161
4409
  updatedAt: now,
@@ -4169,7 +4417,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
4169
4417
  mode !== "resource"
4170
4418
  ? ""
4171
4419
  : dialect.supportsReturning
4172
- ? ` async findById(id: string${options.tenant ? ", scope" : ""}) {
4420
+ ? ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
4173
4421
  const [row] = await db
4174
4422
  .select()
4175
4423
  .from(schema.${names.pluralCamel})
@@ -4178,7 +4426,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
4178
4426
 
4179
4427
  return row ? to${names.singularPascal}(row) : null;
4180
4428
  },
4181
- async update(input${options.tenant ? ", scope" : ""}) {
4429
+ async update(input${options.tenantScoped ? ", scope" : ""}) {
4182
4430
  const [row] = await db
4183
4431
  .update(schema.${names.pluralCamel})
4184
4432
  .set({
@@ -4191,12 +4439,12 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
4191
4439
 
4192
4440
  return row ? to${names.singularPascal}(row) : null;
4193
4441
  },
4194
- async delete(id: string${options.tenant ? ", scope" : ""}) {
4442
+ async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
4195
4443
  ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst rows = await db\n\t\t\t\t.update(schema.${names.pluralCamel})\n\t\t\t\t.set({ deletedAt: now, updatedAt: now })\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n` : `\t\t\tconst rows = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n`}
4196
4444
  return rows.length > 0;
4197
4445
  },
4198
4446
  `
4199
- : ` async findById(id: string${options.tenant ? ", scope" : ""}) {
4447
+ : ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
4200
4448
  const [row] = await db
4201
4449
  .select()
4202
4450
  .from(schema.${names.pluralCamel})
@@ -4205,7 +4453,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
4205
4453
 
4206
4454
  return row ? to${names.singularPascal}(row) : null;
4207
4455
  },
4208
- async update(input${options.tenant ? ", scope" : ""}) {
4456
+ async update(input${options.tenantScoped ? ", scope" : ""}) {
4209
4457
  const result = await db
4210
4458
  .update(schema.${names.pluralCamel})
4211
4459
  .set({
@@ -4225,7 +4473,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
4225
4473
 
4226
4474
  return row ? to${names.singularPascal}(row) : null;
4227
4475
  },
4228
- async delete(id: string${options.tenant ? ", scope" : ""}) {
4476
+ async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
4229
4477
  ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst result = await db\n\t\t\t\t.update(schema.${names.pluralCamel})\n\t\t\t\t.set({ deletedAt: now, updatedAt: now })\n\t\t\t\t.where(${deleteWhere});\n` : `\t\t\tconst result = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere});\n`}
4230
4478
  return affectedRows(result) > 0;
4231
4479
  },
@@ -4233,7 +4481,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
4233
4481
 
4234
4482
  return `import "@beignet/core/server-only";
4235
4483
  import { cursorPageResult } from "@beignet/core/pagination";
4236
- ${options.tenant ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
4484
+ ${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
4237
4485
  import { ${drizzleImports.join(", ")} } from "drizzle-orm";
4238
4486
  import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
4239
4487
  import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
@@ -4248,7 +4496,7 @@ type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelec
4248
4496
  function to${names.singularPascal}(row: ${names.singularPascal}Row): ${names.singularPascal} {
4249
4497
  return {
4250
4498
  id: row.id,
4251
- ${options.tenant ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
4499
+ ${options.tenantScoped ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
4252
4500
  version: row.version,
4253
4501
  createdAt: row.createdAt,
4254
4502
  updatedAt: row.updatedAt,
@@ -4280,9 +4528,9 @@ function ${names.singularCamel}CursorFilter(
4280
4528
 
4281
4529
  function ${names.singularCamel}ListWhere(
4282
4530
  query: List${names.pluralPascal}Query,
4283
- ${options.tenant ? `\tscope: Parameters<${names.singularPascal}Repository["list"]>[1],\n` : ""}): SQL<unknown> | undefined {
4531
+ ${options.tenantScoped ? `\tscope: Parameters<${names.singularPascal}Repository["list"]>[1],\n` : ""}): SQL<unknown> | undefined {
4284
4532
  const filters: SQL<unknown>[] = [
4285
- ${options.tenant ? `\t\teq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
4533
+ ${options.tenantScoped ? `\t\teq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
4286
4534
  const cursor = ${names.singularCamel}CursorFilter(query);
4287
4535
 
4288
4536
  if (query.name) {
@@ -4316,8 +4564,8 @@ export function createDrizzle${names.singularPascal}Repository(
4316
4564
  db: ${dialect.dbTypeName}<typeof schema>,
4317
4565
  ): ${names.singularPascal}Repository {
4318
4566
  return {
4319
- async list(query${options.tenant ? ", scope" : ""}) {
4320
- const where = ${names.singularCamel}ListWhere(query${options.tenant ? ", scope" : ""});
4567
+ async list(query${options.tenantScoped ? ", scope" : ""}) {
4568
+ const where = ${names.singularCamel}ListWhere(query${options.tenantScoped ? ", scope" : ""});
4321
4569
  const rows = await db
4322
4570
  .select()
4323
4571
  .from(schema.${names.pluralCamel})
@@ -4379,7 +4627,7 @@ export const list${names.pluralPascal} = ${names.pluralCamel}
4379
4627
  export const create${names.singularPascal} = ${names.pluralCamel}
4380
4628
  .post("/api/${names.pluralKebab}")
4381
4629
  .body(Create${names.singularPascal}InputSchema)
4382
- ${options.auth ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
4630
+ ${options.authorization ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
4383
4631
  201: ${names.singularPascal}Schema,
4384
4632
  });
4385
4633
  ${
@@ -4397,7 +4645,7 @@ export const update${names.singularPascal} = ${names.pluralCamel}
4397
4645
  .patch("/api/${names.pluralKebab}/:id")
4398
4646
  .pathParams(${names.singularPascal}IdInputSchema)
4399
4647
  .body(Update${names.singularPascal}BodySchema)
4400
- ${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.update" },\n\t})\n` : ""} .errors({${options.auth ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound, ${names.singularPascal}Conflict: errors.${names.singularPascal}Conflict })
4648
+ ${options.authorization ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.update" },\n\t})\n` : ""} .errors({${options.authorization ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound, ${names.singularPascal}Conflict: errors.${names.singularPascal}Conflict })
4401
4649
  .responses({
4402
4650
  200: ${names.singularPascal}Schema,
4403
4651
  });
@@ -4405,7 +4653,7 @@ ${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability
4405
4653
  export const delete${names.singularPascal} = ${names.pluralCamel}
4406
4654
  .delete("/api/${names.pluralKebab}/:id")
4407
4655
  .pathParams(${names.singularPascal}IdInputSchema)
4408
- ${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.delete" },\n\t})\n` : ""} .errors({${options.auth ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound })
4656
+ ${options.authorization ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.delete" },\n\t})\n` : ""} .errors({${options.authorization ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound })
4409
4657
  .responses({
4410
4658
  204: null,
4411
4659
  });
@@ -4503,15 +4751,15 @@ import { createInMemoryDevtools } from "@beignet/devtools";
4503
4751
  import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
4504
4752
  import { createTestAnonymousActor } from "@beignet/core/testing";
4505
4753
  import type { AppContext } from "${aliasModule(config.paths.appContext)}";
4506
- import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
4754
+ import { initialPorts } from "${aliasModule(config.paths.portWiring)}";
4507
4755
  import { ${names.exportName} } from "${relativeModule(filePath, useCaseFilePath(names, config))}";
4508
4756
 
4509
4757
  describe("${names.exportName}", () => {
4510
4758
  it("runs ${names.action.camel}", async () => {
4511
4759
  const testFixture = createTestPorts<AppContext["ports"]>({
4512
- base: appPorts,
4760
+ base: initialPorts,
4513
4761
  overrides: {
4514
- gate: appPorts.gate,
4762
+ gate: initialPorts.gate,
4515
4763
  devtools: createInMemoryDevtools(),
4516
4764
  },
4517
4765
  });
@@ -4610,7 +4858,7 @@ function isAuthenticated(ctx: AuthorizationContext) {
4610
4858
 
4611
4859
  function canWrite(ctx: AuthorizationContext) {
4612
4860
  if (!isAuthenticated(ctx)) return deny("You must be signed in.");
4613
- ${options.tenant ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
4861
+ ${options.tenantScoped ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
4614
4862
  }
4615
4863
 
4616
4864
  export const ${names.singularCamel}Policy = definePolicy({
@@ -4620,13 +4868,13 @@ export const ${names.singularCamel}Policy = definePolicy({
4620
4868
  "${names.pluralCamel}.update": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
4621
4869
  const decision = canWrite(ctx);
4622
4870
  if (decision !== true) return decision;
4623
- ${options.tenant ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot update a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
4871
+ ${options.tenantScoped ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot update a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
4624
4872
  return true;
4625
4873
  },
4626
4874
  "${names.pluralCamel}.delete": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
4627
4875
  const decision = canWrite(ctx);
4628
4876
  if (decision !== true) return decision;
4629
- ${options.tenant ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot delete a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
4877
+ ${options.tenantScoped ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot delete a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
4630
4878
  return true;
4631
4879
  },
4632
4880
  "${names.pluralCamel}.manage": (_ctx: AuthorizationContext) =>
@@ -4678,7 +4926,7 @@ function create${names.singularPascal}(): ${names.singularPascal} {
4678
4926
  const now = new Date().toISOString();
4679
4927
  return {
4680
4928
  id: "00000000-0000-4000-8000-000000000001",
4681
- ${options.tenant ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
4929
+ ${options.tenantScoped ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
4682
4930
  version: 1,
4683
4931
  createdAt: now,
4684
4932
  updatedAt: now,
@@ -4693,7 +4941,7 @@ describe("${names.singularCamel}Policy", () => {
4693
4941
  const ${names.singularCamel} = create${names.singularPascal}();
4694
4942
  const ctx = {
4695
4943
  actor: { type: "user" as const, id: "user_test" },
4696
- ${options.tenant ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
4944
+ ${options.tenantScoped ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
4697
4945
 
4698
4946
  await expect(
4699
4947
  tester.assertMatrix([
@@ -4919,8 +5167,33 @@ ${names.timezone ? `\t\ttimezone: "${names.timezone}",\n` : ""}\t\tpayload: ${na
4919
5167
  `;
4920
5168
  }
4921
5169
 
5170
+ function uploadManifestFile(names: UploadNames): string {
5171
+ return `import type {
5172
+ \tUploadFileConstraints,
5173
+ \tUploadManifestEntry,
5174
+ } from "@beignet/core/uploads";
5175
+
5176
+ export const ${names.fileConstraintsExportName} = {
5177
+ \tcontentTypes: ["application/pdf", "text/plain"],
5178
+ \tmaxSizeBytes: 5 * 1024 * 1024,
5179
+ \tmaxFiles: 1,
5180
+ \tvisibility: "private",
5181
+ \tcacheControl: "private, max-age=0",
5182
+ } satisfies UploadFileConstraints;
5183
+
5184
+ export const ${names.manifestExportName} = [
5185
+ \t{
5186
+ \t\tname: "${names.uploadName}",
5187
+ \t\tdescription: "${names.artifact.pascal} upload",
5188
+ \t\tfile: ${names.fileConstraintsExportName},
5189
+ \t},
5190
+ ] satisfies readonly UploadManifestEntry[];
5191
+ `;
5192
+ }
5193
+
4922
5194
  function uploadFile(names: UploadNames, config: ResolvedBeignetConfig): string {
4923
5195
  return `import { z } from "zod";
5196
+ import { ${names.fileConstraintsExportName} } from "${relativeModule(uploadFilePath(names, config), uploadManifestFilePath(names, config))}";
4924
5197
  import { defineUpload } from "${aliasModule(config.paths.uploadsBuilder)}";
4925
5198
 
4926
5199
  export const ${names.metadataSchemaName} = z.object({
@@ -4931,13 +5204,7 @@ export type ${names.metadataTypeName} = z.infer<typeof ${names.metadataSchemaNam
4931
5204
 
4932
5205
  export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
4933
5206
  \tmetadata: ${names.metadataSchemaName},
4934
- \tfile: {
4935
- \t\tcontentTypes: ["application/pdf", "text/plain"],
4936
- \t\tmaxSizeBytes: 5 * 1024 * 1024,
4937
- \t\tmaxFiles: 1,
4938
- \t\tvisibility: "private",
4939
- \t\tcacheControl: "private, max-age=0",
4940
- \t},
5207
+ \tfile: ${names.fileConstraintsExportName},
4941
5208
  \tauthorize({ ctx }) {
4942
5209
  \t\treturn ctx.actor.type === "user";
4943
5210
  \t},
@@ -4968,6 +5235,148 @@ export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
4968
5235
  `;
4969
5236
  }
4970
5237
 
5238
+ function uploadUiClientFile(
5239
+ names: UploadNames,
5240
+ config: ResolvedBeignetConfig,
5241
+ ): string {
5242
+ const clientPath = uploadUiClientFilePath(names, config);
5243
+ const registryPath = path.join(
5244
+ featureArtifactDir(names, "uploads", config),
5245
+ "index.ts",
5246
+ );
5247
+
5248
+ return `import { createUploadClient } from "@beignet/core/uploads/client";
5249
+ import { createReactUploads } from "@beignet/react-uploads";
5250
+ import { ${names.manifestExportName} } from "${relativeModule(clientPath, uploadManifestFilePath(names, config))}";
5251
+ import type { ${names.featureSingularCamel}Uploads } from "${relativeModule(clientPath, registryPath)}";
5252
+
5253
+ const ${names.artifact.camel}UploadClient = createUploadClient<typeof ${names.featureSingularCamel}Uploads>({
5254
+ \tbaseUrl: "/api/uploads",
5255
+ \tmanifest: ${names.manifestExportName},
5256
+ });
5257
+
5258
+ export const ${names.reactUploadsExportName} = createReactUploads({
5259
+ \tuploads: ${names.artifact.camel}UploadClient,
5260
+ });
5261
+ `;
5262
+ }
5263
+
5264
+ function uploadUiComponentFile(
5265
+ names: UploadNames,
5266
+ config: ResolvedBeignetConfig,
5267
+ ): string {
5268
+ const componentPath = uploadUiComponentFilePath(names, config);
5269
+
5270
+ return `"use client";
5271
+
5272
+ import { useId } from "react";
5273
+ import { ${names.reactUploadsExportName} } from "${relativeModule(componentPath, uploadUiClientFilePath(names, config))}";
5274
+
5275
+ export type ${names.componentExportName}Props = {
5276
+ \tresourceId: string;
5277
+ \tdisabled?: boolean;
5278
+ \tonComplete?(objectKeys: readonly string[]): void | Promise<void>;
5279
+ };
5280
+
5281
+ export function ${names.componentExportName}({
5282
+ \tresourceId,
5283
+ \tdisabled = false,
5284
+ \tonComplete,
5285
+ }: ${names.componentExportName}Props) {
5286
+ \tconst inputId = useId();
5287
+ \tconst upload = ${names.reactUploadsExportName}.useUpload("${names.uploadName}", {
5288
+ \t\tonSuccess({ result }) {
5289
+ \t\t\treturn onComplete?.(result.objectKeys);
5290
+ \t\t},
5291
+ \t});
5292
+ \tconst maxSizeMb = upload.constraints?.maxSizeBytes
5293
+ \t\t? Math.round(upload.constraints.maxSizeBytes / 1024 / 1024)
5294
+ \t\t: undefined;
5295
+
5296
+ \treturn (
5297
+ \t\t<section className="${names.artifact.kebab}-uploader">
5298
+ \t\t\t<label htmlFor={inputId}>${names.artifact.pascal}</label>
5299
+ \t\t\t<input
5300
+ \t\t\t\tid={inputId}
5301
+ \t\t\t\ttype="file"
5302
+ \t\t\t\taccept={upload.accept}
5303
+ \t\t\t\tdisabled={disabled || upload.isUploading}
5304
+ \t\t\t\tonChange={(event) => {
5305
+ \t\t\t\t\tconst file = event.currentTarget.files?.[0];
5306
+ \t\t\t\t\tevent.currentTarget.value = "";
5307
+ \t\t\t\t\tif (!file) return;
5308
+
5309
+ \t\t\t\t\tupload.uploadFile(file, {
5310
+ \t\t\t\t\t\tmetadata: { resourceId },
5311
+ \t\t\t\t\t});
5312
+ \t\t\t\t}}
5313
+ \t\t\t/>
5314
+ \t\t\t<p>
5315
+ \t\t\t\tPDF or plain text{maxSizeMb ? <> up to {maxSizeMb} MB</> : null}.
5316
+ \t\t\t</p>
5317
+
5318
+ \t\t\t{upload.files[0] ? <p>{upload.files[0].fileName}</p> : null}
5319
+ \t\t\t{upload.isUploading ? (
5320
+ \t\t\t\t<div aria-live="polite">
5321
+ \t\t\t\t\t<progress
5322
+ \t\t\t\t\t\taria-label="Upload progress"
5323
+ \t\t\t\t\t\tmax={100}
5324
+ \t\t\t\t\t\tvalue={upload.progress}
5325
+ \t\t\t\t\t/>
5326
+ \t\t\t\t\t<span>{Math.round(upload.progress)}%</span>
5327
+ \t\t\t\t\t<button type="button" onClick={upload.abort}>
5328
+ \t\t\t\t\t\tCancel upload
5329
+ \t\t\t\t\t</button>
5330
+ \t\t\t\t</div>
5331
+ \t\t\t) : null}
5332
+ \t\t\t{upload.isError ? (
5333
+ \t\t\t\t<p role="alert">{uploadErrorMessage(upload.error)}</p>
5334
+ \t\t\t) : null}
5335
+ \t\t\t{upload.isSuccess ? <p role="status">Upload complete.</p> : null}
5336
+ \t\t\t{upload.isError || upload.isSuccess ? (
5337
+ \t\t\t\t<button type="button" onClick={upload.reset}>
5338
+ \t\t\t\t\tReset upload
5339
+ \t\t\t\t</button>
5340
+ \t\t\t) : null}
5341
+ \t\t</section>
5342
+ \t);
5343
+ }
5344
+
5345
+ function uploadErrorMessage(error: unknown): string {
5346
+ \treturn error instanceof Error ? error.message : "Upload failed.";
5347
+ }
5348
+ `;
5349
+ }
5350
+
5351
+ function uploadUiComponentTestFile(
5352
+ names: UploadNames,
5353
+ config: ResolvedBeignetConfig,
5354
+ ): string {
5355
+ const testPath = uploadUiComponentTestFilePath(names, config);
5356
+ const testHelperPath = path.join(
5357
+ path.dirname(config.paths.useCaseBuilder),
5358
+ "beignet-test.ts",
5359
+ );
5360
+
5361
+ return `import { renderToStaticMarkup } from "react-dom/server";
5362
+ import { describe, expect, test } from "${relativeModule(testPath, testHelperPath)}";
5363
+ import { ${names.manifestExportName} } from "${relativeModule(testPath, uploadManifestFilePath(names, config))}";
5364
+ import { ${names.componentExportName} } from "${relativeModule(testPath, uploadUiComponentFilePath(names, config))}";
5365
+
5366
+ describe("${names.componentExportName}", () => {
5367
+ \ttest("renders the generated upload constraints", () => {
5368
+ \t\tconst markup = renderToStaticMarkup(
5369
+ \t\t\t<${names.componentExportName} resourceId="resource-1" />,
5370
+ \t\t);
5371
+
5372
+ \t\texpect(${names.manifestExportName}[0]?.file.maxSizeBytes).toBe(5 * 1024 * 1024);
5373
+ \t\texpect(markup).toContain('accept="application/pdf,text/plain"');
5374
+ \t\texpect(markup).toContain("PDF or plain text");
5375
+ \t});
5376
+ });
5377
+ `;
5378
+ }
5379
+
4971
5380
  function uploadRouteFile(
4972
5381
  names: UploadNames,
4973
5382
  config: ResolvedBeignetConfig,
@@ -5087,63 +5496,70 @@ function featureUiComponentFile(
5087
5496
 
5088
5497
  return `"use client";
5089
5498
 
5090
- import { contractErrorMessage } from "@beignet/core/client";
5499
+ import { rootFormError } from "@beignet/react-hook-form";
5091
5500
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
5092
- import { useState } from "react";
5501
+ import { rhf } from "${aliasModule(clientFormsPath(config))}";
5502
+ import { create${names.singularPascal} } from "${relativeModule(componentPath, resourceContractFilePath(names, config))}";
5093
5503
  import {
5094
5504
  \tcreate${names.singularPascal}MutationOptions,
5095
5505
  \tinvalidate${names.pluralPascal},
5096
5506
  \tlist${names.pluralPascal}QueryOptions,
5097
5507
  } from "${relativeModule(componentPath, clientQueriesPath)}";
5098
5508
 
5509
+ const create${names.singularPascal}Form = rhf(create${names.singularPascal});
5510
+
5099
5511
  export function ${names.componentExportName}() {
5100
- \tconst [name, setName] = useState("");
5101
5512
  \tconst queryClient = useQueryClient();
5102
5513
  \tconst ${names.pluralCamel}Query = useQuery(list${names.pluralPascal}QueryOptions());
5514
+ \tconst form = create${names.singularPascal}Form.useForm({
5515
+ \t\tdefaultValues: { name: "" },
5516
+ \t});
5517
+ \tconst name = form.watch("name");
5103
5518
  \tconst create${names.singularPascal}Mutation = useMutation({
5104
5519
  \t\t...create${names.singularPascal}MutationOptions(),
5105
5520
  \t\tonSuccess: async () => {
5106
- \t\t\tsetName("");
5521
+ \t\t\tform.reset({ name: "" });
5107
5522
  \t\t\tawait invalidate${names.pluralPascal}(queryClient);
5108
5523
  \t\t},
5524
+ \t\tonError: (error) => {
5525
+ \t\t\tform.setError(
5526
+ \t\t\t\t"root",
5527
+ \t\t\t\trootFormError(error, "Could not create ${names.singularKebab}."),
5528
+ \t\t\t);
5529
+ \t\t},
5109
5530
  \t});
5110
5531
 
5111
5532
  \treturn (
5112
5533
  \t\t<section className="${names.pluralKebab}-panel">
5113
5534
  \t\t\t<form
5114
5535
  \t\t\t\tclassName="${names.pluralKebab}-composer"
5115
- \t\t\t\tonSubmit={(event) => {
5116
- \t\t\t\t\tevent.preventDefault();
5117
- \t\t\t\t\tconst trimmedName = name.trim();
5118
- \t\t\t\t\tif (!trimmedName) return;
5536
+ \t\t\t\tonSubmit={form.handleSubmit((values) => {
5537
+ \t\t\t\t\tform.clearErrors("root");
5119
5538
  \t\t\t\t\tcreate${names.singularPascal}Mutation.reset();
5120
5539
  \t\t\t\t\tcreate${names.singularPascal}Mutation.mutate({
5121
- \t\t\t\t\t\tbody: { name: trimmedName },
5540
+ \t\t\t\t\t\tbody: values,
5122
5541
  \t\t\t\t\t});
5123
- \t\t\t\t}}
5542
+ \t\t\t\t})}
5124
5543
  \t\t\t>
5125
5544
  \t\t\t\t<label htmlFor="${names.singularKebab}-name">New ${names.singularKebab}</label>
5126
5545
  \t\t\t\t<div className="${names.pluralKebab}-composer-row">
5127
5546
  \t\t\t\t\t<input
5128
5547
  \t\t\t\t\t\tid="${names.singularKebab}-name"
5129
- \t\t\t\t\t\tvalue={name}
5130
- \t\t\t\t\t\tonChange={(event) => setName(event.currentTarget.value)}
5548
+ \t\t\t\t\t\t{...form.register("name")}
5131
5549
  \t\t\t\t\t\tplaceholder="Name"
5132
5550
  \t\t\t\t\t/>
5133
5551
  \t\t\t\t\t<button
5134
5552
  \t\t\t\t\t\ttype="submit"
5135
- \t\t\t\t\t\tdisabled={!name.trim() || create${names.singularPascal}Mutation.isPending}
5553
+ \t\t\t\t\t\tdisabled={!name?.trim() || create${names.singularPascal}Mutation.isPending}
5136
5554
  \t\t\t\t\t>
5137
5555
  \t\t\t\t\t\t{create${names.singularPascal}Mutation.isPending ? "Creating" : "Create"}
5138
5556
  \t\t\t\t\t</button>
5139
5557
  \t\t\t\t</div>
5140
- \t\t\t\t{create${names.singularPascal}Mutation.isError ? (
5141
- \t\t\t\t\t<p role="alert">
5142
- \t\t\t\t\t\t{contractErrorMessage(
5143
- \t\t\t\t\t\t\tcreate${names.singularPascal}Mutation.error,
5144
- \t\t\t\t\t\t\t"Could not create ${names.singularKebab}.",
5145
- \t\t\t\t\t\t)}
5146
- \t\t\t\t\t</p>
5558
+ \t\t\t\t{form.formState.errors.name ? (
5559
+ \t\t\t\t\t<p role="alert">{form.formState.errors.name.message}</p>
5560
+ \t\t\t\t) : null}
5561
+ \t\t\t\t{form.formState.errors.root ? (
5562
+ \t\t\t\t\t<p role="alert">{form.formState.errors.root.message}</p>
5147
5563
  \t\t\t\t) : null}
5148
5564
  \t\t\t</form>
5149
5565
 
@@ -5276,7 +5692,7 @@ function testFile(
5276
5692
  options: ResourceGenerationOptions,
5277
5693
  ): string {
5278
5694
  const repositoryPath = path.join(
5279
- path.dirname(config.paths.infrastructurePorts),
5695
+ path.dirname(config.paths.portWiring),
5280
5696
  names.pluralKebab,
5281
5697
  `in-memory-${names.singularKebab}-repository.ts`,
5282
5698
  );
@@ -5292,9 +5708,9 @@ ${mode === "resource" ? `import { isAppError } from "@beignet/core/errors";\n` :
5292
5708
  import { createTestApp } from "@beignet/web/testing";
5293
5709
  import { createInMemoryDevtools } from "@beignet/devtools";
5294
5710
  import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
5295
- import { ${options.auth ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenant ? ", createTestTenant" : ""} } from "@beignet/core/testing";
5711
+ import { ${options.authorization ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenantScoped ? ", createTestTenant" : ""} } from "@beignet/core/testing";
5296
5712
  import type { AppContext } from "${aliasModule(config.paths.appContext)}";
5297
- import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
5713
+ import { initialPorts } from "${aliasModule(config.paths.portWiring)}";
5298
5714
  import { appContext } from "${aliasModule(serverContextPath)}";
5299
5715
  import {
5300
5716
  create${names.singularPascal},
@@ -5312,40 +5728,40 @@ describe("${names.pluralCamel} resource", () => {
5312
5728
  const seed${names.pluralPascal} = [
5313
5729
  {
5314
5730
  id: "00000000-0000-4000-8000-000000000101",
5315
- ${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
5731
+ ${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
5316
5732
  version: 1,
5317
5733
  createdAt: "2024-01-01T00:00:00.000Z",
5318
5734
  updatedAt: "2024-01-01T00:00:00.000Z",
5319
5735
  },
5320
5736
  {
5321
5737
  id: "00000000-0000-4000-8000-000000000102",
5322
- ${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
5738
+ ${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
5323
5739
  version: 1,
5324
5740
  createdAt: "2024-01-02T00:00:00.000Z",
5325
5741
  updatedAt: "2024-01-02T00:00:00.000Z",
5326
5742
  },
5327
5743
  {
5328
5744
  id: "00000000-0000-4000-8000-000000000103",
5329
- ${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
5745
+ ${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
5330
5746
  version: 1,
5331
5747
  createdAt: "2024-01-03T00:00:00.000Z",
5332
5748
  updatedAt: "2024-01-03T00:00:00.000Z",
5333
5749
  },
5334
- ${options.tenant ? `\t\t\t{\n\t\t\t\tid: "00000000-0000-4000-8000-000000000104",\n\t\t\t\ttenantId: "tenant_other",\n\t\t\t\tname: "Other Tenant ${names.singularPascal}",\n\t\t\t\tversion: 1,\n\t\t\t\tcreatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t\tupdatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t},\n` : ""} ];
5750
+ ${options.tenantScoped ? `\t\t\t{\n\t\t\t\tid: "00000000-0000-4000-8000-000000000104",\n\t\t\t\ttenantId: "tenant_other",\n\t\t\t\tname: "Other Tenant ${names.singularPascal}",\n\t\t\t\tversion: 1,\n\t\t\t\tcreatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t\tupdatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t},\n` : ""} ];
5335
5751
  const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository(seed${names.pluralPascal});
5336
5752
  const testFixture = createTestPorts<AppContext["ports"]>({
5337
- base: appPorts,
5753
+ base: initialPorts,
5338
5754
  overrides: {
5339
5755
  ${names.pluralCamel},
5340
5756
  ${
5341
- options.auth || options.tenant
5757
+ options.authorization || options.tenantScoped
5342
5758
  ? `\t\t\t\tauth: {
5343
5759
  getSession: async () => ({
5344
- user: { id: "user_test", name: "Test User"${options.tenant ? `, tenantId: "tenant_test"` : ""} },
5345
- ${options.tenant ? `\t\t\t\t\t\tsession: { tenantId: "tenant_test" },\n` : ""}
5760
+ user: { id: "user_test", name: "Test User"${options.tenantScoped ? `, tenantId: "tenant_test"` : ""} },
5761
+ ${options.tenantScoped ? `\t\t\t\t\t\tsession: { tenantId: "tenant_test" },\n` : ""}
5346
5762
  }),
5347
5763
  },
5348
- ${options.auth ? `\t\t\t\tgate: appPorts.gate,\n` : ""}
5764
+ ${options.authorization ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
5349
5765
  `
5350
5766
  : `\t\t\t\tauth: {
5351
5767
  getSession: async () => null,
@@ -5361,8 +5777,8 @@ ${options.auth ? `\t\t\t\tgate: appPorts.gate,\n` : ""}
5361
5777
  });
5362
5778
  const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
5363
5779
  ports: testFixture.ports,
5364
- actor: ${options.auth ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
5365
- tenant: ${options.tenant ? `createTestTenant("tenant_test")` : "null"},
5780
+ actor: ${options.authorization ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
5781
+ tenant: ${options.tenantScoped ? `createTestTenant("tenant_test")` : "null"},
5366
5782
  });
5367
5783
  const tester = createUseCaseTester<AppContext>(createTestContext);
5368
5784