@beignet/cli 0.0.38 → 0.0.40

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 (92) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +154 -102
  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 +4 -2
  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/framework.d.ts +3 -0
  20. package/dist/framework.d.ts.map +1 -0
  21. package/dist/framework.js +6 -0
  22. package/dist/framework.js.map +1 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +149 -47
  25. package/dist/index.js.map +1 -1
  26. package/dist/inspect.d.ts +4 -2
  27. package/dist/inspect.d.ts.map +1 -1
  28. package/dist/inspect.js +174 -34
  29. package/dist/inspect.js.map +1 -1
  30. package/dist/lib.d.ts +1 -1
  31. package/dist/lib.d.ts.map +1 -1
  32. package/dist/make/inbox.js +3 -3
  33. package/dist/make/inbox.js.map +1 -1
  34. package/dist/make/payments.d.ts.map +1 -1
  35. package/dist/make/payments.js +5 -3
  36. package/dist/make/payments.js.map +1 -1
  37. package/dist/make/shared.d.ts +10 -3
  38. package/dist/make/shared.d.ts.map +1 -1
  39. package/dist/make/shared.js +7 -8
  40. package/dist/make/shared.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 +549 -186
  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/provider-add.d.ts +21 -3
  49. package/dist/provider-add.d.ts.map +1 -1
  50. package/dist/provider-add.js +101 -65
  51. package/dist/provider-add.js.map +1 -1
  52. package/dist/task.js +1 -1
  53. package/dist/task.js.map +1 -1
  54. package/dist/templates/base.d.ts +1 -1
  55. package/dist/templates/base.d.ts.map +1 -1
  56. package/dist/templates/base.js +15 -15
  57. package/dist/templates/base.js.map +1 -1
  58. package/dist/templates/index.d.ts +2 -2
  59. package/dist/templates/index.d.ts.map +1 -1
  60. package/dist/templates/index.js +5 -5
  61. package/dist/templates/index.js.map +1 -1
  62. package/dist/templates/server.d.ts.map +1 -1
  63. package/dist/templates/server.js +17 -13
  64. package/dist/templates/server.js.map +1 -1
  65. package/dist/templates/shadcn.js +1 -1
  66. package/dist/templates/shared.d.ts +4 -4
  67. package/dist/templates/shared.d.ts.map +1 -1
  68. package/dist/templates/shared.js +6 -6
  69. package/dist/templates/shared.js.map +1 -1
  70. package/package.json +2 -2
  71. package/skills/app-structure/SKILL.md +29 -9
  72. package/src/choices.ts +38 -88
  73. package/src/config.ts +7 -3
  74. package/src/create-prompts.ts +20 -21
  75. package/src/create.ts +54 -36
  76. package/src/framework.ts +13 -0
  77. package/src/index.ts +193 -65
  78. package/src/inspect.ts +290 -39
  79. package/src/lib.ts +1 -1
  80. package/src/make/inbox.ts +3 -3
  81. package/src/make/payments.ts +10 -3
  82. package/src/make/shared.ts +16 -10
  83. package/src/make.ts +720 -176
  84. package/src/mcp.ts +20 -13
  85. package/src/provider-add.ts +159 -79
  86. package/src/task.ts +1 -1
  87. package/src/templates/base.ts +16 -16
  88. package/src/templates/index.ts +6 -6
  89. package/src/templates/server.ts +17 -13
  90. package/src/templates/shadcn.ts +1 -1
  91. package/src/templates/shared.ts +10 -10
  92. package/src/test-helpers/generated-app.ts +2 -1
package/src/make.ts CHANGED
@@ -1,14 +1,17 @@
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,
10
12
  resolveConfig,
11
13
  } from "./config.js";
14
+ import { assertNextFramework } from "./framework.js";
12
15
  import {
13
16
  adapterFilePath,
14
17
  addNamedImport,
@@ -141,11 +144,13 @@ import {
141
144
  } from "./make/shared.js";
142
145
  import {
143
146
  appendToArrayExpression,
147
+ appendToNamedArray,
144
148
  appendToOutboxRegistryArray,
145
149
  arrayInitializerInfo,
146
150
  identifiersFromArrayExpression,
147
151
  insertAfterImports,
148
152
  } from "./registry-edits.js";
153
+ import { externalVersions } from "./templates/shared.js";
149
154
  import { testSupportTemplateFiles } from "./templates/testing.js";
150
155
 
151
156
  export type { MakeFeatureAddon, MakeFeatureRecipe } from "./choices.js";
@@ -164,8 +169,8 @@ type MakeResourceOptions = {
164
169
  force?: boolean;
165
170
  dryRun?: boolean;
166
171
  config?: BeignetConfig;
167
- auth?: boolean;
168
- tenant?: boolean;
172
+ authorization?: boolean;
173
+ tenantScoped?: boolean;
169
174
  events?: boolean;
170
175
  softDelete?: boolean;
171
176
  };
@@ -313,6 +318,7 @@ type MakeUploadOptions = {
313
318
  force?: boolean;
314
319
  dryRun?: boolean;
315
320
  config?: BeignetConfig;
321
+ ui?: boolean;
316
322
  };
317
323
 
318
324
  type MakeOutboxOptions = {
@@ -428,7 +434,7 @@ export async function makeFeature(
428
434
  const addons = normalizeMakeFeatureAddons([
429
435
  ...makeFeatureRecipeAddons(options.recipe),
430
436
  ...(options.with ?? []),
431
- ...(options.events ? (["events"] as const) : []),
437
+ ...(options.events ? (["event"] as const) : []),
432
438
  ]);
433
439
  const featureOptions = {
434
440
  ...options,
@@ -580,20 +586,25 @@ function resourceGenerationOptions(
580
586
  ): ResourceGenerationOptions {
581
587
  if (mode === "feature") {
582
588
  return {
583
- auth: false,
584
- tenant: false,
589
+ authorization: false,
590
+ tenantScoped: false,
585
591
  events: Boolean(options.events),
586
592
  softDelete: false,
587
593
  };
588
594
  }
589
595
 
590
596
  if (mode !== "resource") {
591
- return { auth: false, tenant: false, events: false, softDelete: false };
597
+ return {
598
+ authorization: false,
599
+ tenantScoped: false,
600
+ events: false,
601
+ softDelete: false,
602
+ };
592
603
  }
593
604
 
594
605
  return {
595
- auth: Boolean(options.auth),
596
- tenant: Boolean(options.tenant),
606
+ authorization: Boolean(options.authorization),
607
+ tenantScoped: Boolean(options.tenantScoped),
597
608
  events: Boolean(options.events),
598
609
  softDelete: Boolean(options.softDelete),
599
610
  };
@@ -604,11 +615,12 @@ async function makeFeatureSlice(
604
615
  addons: readonly NormalizedMakeFeatureAddon[],
605
616
  ): Promise<MakeFeatureResult> {
606
617
  let result = await makeFeatureResource(options);
618
+ const includeUploadUi = addons.includes("ui") && addons.includes("upload");
607
619
 
608
620
  for (const addon of addons) {
609
621
  result = mergeMakeResults(
610
622
  result,
611
- await makeFeatureAddon(addon, result.name, options),
623
+ await makeFeatureAddon(addon, result.name, options, includeUploadUi),
612
624
  );
613
625
  }
614
626
 
@@ -619,6 +631,7 @@ async function makeFeatureAddon(
619
631
  addon: NormalizedMakeFeatureAddon,
620
632
  feature: string,
621
633
  options: MakeFeatureOptions,
634
+ includeUploadUi: boolean,
622
635
  ): Promise<MakeResult> {
623
636
  const shared = {
624
637
  cwd: options.cwd,
@@ -633,37 +646,37 @@ async function makeFeatureAddon(
633
646
  if (addon === "factory") {
634
647
  return makeFactory({
635
648
  ...shared,
636
- name: `${feature}/${singularize(feature)}`,
649
+ name: `${feature}.${singularize(feature)}`,
637
650
  skipPersistenceAssert: true,
638
651
  });
639
652
  }
640
653
  if (addon === "task") {
641
- return makeTask({ ...shared, name: `${feature}/backfill` });
654
+ return makeTask({ ...shared, name: `${feature}.backfill` });
642
655
  }
643
656
  if (addon === "event") {
644
- return makeEvent({ ...shared, name: `${feature}/created` });
657
+ return makeEvent({ ...shared, name: `${feature}.created` });
645
658
  }
646
659
  if (addon === "job") {
647
- return makeJob({ ...shared, name: `${feature}/process` });
660
+ return makeJob({ ...shared, name: `${feature}.process` });
648
661
  }
649
662
  if (addon === "listener") {
650
663
  return makeListener({
651
664
  ...shared,
652
- name: `${feature}/handle-created`,
653
- event: `${feature}/created`,
665
+ name: `${feature}.handle-created`,
666
+ event: `${feature}.created`,
654
667
  skipEventAssert: true,
655
668
  });
656
669
  }
657
670
  if (addon === "notification") {
658
- return makeNotification({ ...shared, name: `${feature}/created` });
671
+ return makeNotification({ ...shared, name: `${feature}.created` });
659
672
  }
660
673
  if (addon === "schedule") {
661
- return makeSchedule({ ...shared, name: `${feature}/daily-summary` });
674
+ return makeSchedule({ ...shared, name: `${feature}.daily-summary` });
662
675
  }
663
676
  if (addon === "seed") {
664
677
  return makeSeed({
665
678
  ...shared,
666
- name: `${feature}/demo-${feature}`,
679
+ name: `${feature}.demo-${feature}`,
667
680
  skipPersistenceAssert: true,
668
681
  });
669
682
  }
@@ -671,7 +684,11 @@ async function makeFeatureAddon(
671
684
  return makeFeatureUi({ ...shared, name: feature });
672
685
  }
673
686
 
674
- return makeUpload({ ...shared, name: `${feature}/attachment` });
687
+ return makeUpload({
688
+ ...shared,
689
+ name: `${feature}.attachment`,
690
+ ui: includeUploadUi,
691
+ });
675
692
  }
676
693
 
677
694
  function normalizeMakeFeatureAddons(
@@ -680,18 +697,7 @@ function normalizeMakeFeatureAddons(
680
697
  const normalized = new Set<NormalizedMakeFeatureAddon>();
681
698
 
682
699
  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") {
700
+ if (addon === "listener") {
695
701
  normalized.add("event");
696
702
  normalized.add("listener");
697
703
  } else normalized.add(addon);
@@ -706,16 +712,16 @@ function makeFeatureRecipeAddons(
706
712
  if (recipe === "full-slice") {
707
713
  return [
708
714
  "policy",
709
- "factories",
710
- "seeds",
711
- "tasks",
712
- "events",
713
- "listeners",
714
- "jobs",
715
- "notifications",
716
- "schedules",
715
+ "factory",
716
+ "seed",
717
+ "task",
718
+ "event",
719
+ "listener",
720
+ "job",
721
+ "notification",
722
+ "schedule",
717
723
  "ui",
718
- "uploads",
724
+ "upload",
719
725
  ];
720
726
  }
721
727
 
@@ -807,7 +813,7 @@ export async function makeUseCase(
807
813
 
808
814
  return {
809
815
  schemaVersion: 1,
810
- name: `${names.feature.kebab}/${names.action.kebab}`,
816
+ name: `${names.feature.kebab}.${names.action.kebab}`,
811
817
  targetDir,
812
818
  dryRun: Boolean(options.dryRun),
813
819
  files: [...generatedFiles.map((file) => file.path), indexFile.path],
@@ -853,7 +859,7 @@ export async function makeTest(
853
859
 
854
860
  return {
855
861
  schemaVersion: 1,
856
- name: `${names.feature.kebab}/${names.action.kebab}`,
862
+ name: `${names.feature.kebab}.${names.action.kebab}`,
857
863
  targetDir,
858
864
  dryRun: Boolean(options.dryRun),
859
865
  files: generatedFiles.map((file) => file.path),
@@ -1043,7 +1049,7 @@ export async function makeEvent(
1043
1049
  config,
1044
1050
  force: Boolean(options.force),
1045
1051
  dryRun: Boolean(options.dryRun),
1046
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1052
+ name: names.stableName,
1047
1053
  files: eventFiles(names, config),
1048
1054
  index: featureArtifactIndexFile("event", names, config),
1049
1055
  dependencies: {
@@ -1090,7 +1096,7 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
1090
1096
  config,
1091
1097
  force: Boolean(options.force),
1092
1098
  dryRun: Boolean(options.dryRun),
1093
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1099
+ name: names.stableName,
1094
1100
  files: [
1095
1101
  ...(await missingCapabilityBuilderFiles(targetDir, "jobs", config)),
1096
1102
  ...jobFiles(names, config),
@@ -1130,7 +1136,7 @@ export async function makeTask(
1130
1136
  config,
1131
1137
  force: Boolean(options.force),
1132
1138
  dryRun: Boolean(options.dryRun),
1133
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1139
+ name: names.stableName,
1134
1140
  files: [
1135
1141
  ...(await missingCapabilityBuilderFiles(targetDir, "tasks", config)),
1136
1142
  ...taskFiles(names, config),
@@ -1175,7 +1181,7 @@ export async function makeFactory(
1175
1181
  config,
1176
1182
  force: Boolean(options.force),
1177
1183
  dryRun: Boolean(options.dryRun),
1178
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1184
+ name: names.stableName,
1179
1185
  files: factoryFiles(names, config),
1180
1186
  index: featureArtifactIndexFile("factory", names, config),
1181
1187
  dependencies: {
@@ -1202,7 +1208,7 @@ export async function makeSeed(
1202
1208
  config,
1203
1209
  force: Boolean(options.force),
1204
1210
  dryRun: Boolean(options.dryRun),
1205
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1211
+ name: names.stableName,
1206
1212
  files: seedFiles(names, config),
1207
1213
  index: featureArtifactIndexFile("seed", names, config),
1208
1214
  dependencies: {
@@ -1221,7 +1227,7 @@ export async function makeSeed(
1221
1227
  config,
1222
1228
  force: Boolean(options.force),
1223
1229
  dryRun: Boolean(options.dryRun),
1224
- name: `${factoryNamesForSeed.feature.kebab}/${factoryNamesForSeed.artifact.kebab}`,
1230
+ name: factoryNamesForSeed.stableName,
1225
1231
  files: factoryFiles(factoryNamesForSeed, config),
1226
1232
  index: featureArtifactIndexFile("factory", factoryNamesForSeed, config),
1227
1233
  dependencies: {
@@ -1277,7 +1283,7 @@ export async function makeNotification(
1277
1283
  // notifications uses ctx.ports.notifications, so the generator wires both
1278
1284
  // ports with dev-default providers. Each port is wired independently and
1279
1285
  // skipped when the app already has it, for example when the resend
1280
- // integration contributed the mailer. Plan the wiring before writing any
1286
+ // provider contributed the mailer. Plan the wiring before writing any
1281
1287
  // files so an anchor miss aborts the generator without leaving partial
1282
1288
  // output.
1283
1289
  const notificationWirings = notificationPortWirings("make notification");
@@ -1290,7 +1296,7 @@ export async function makeNotification(
1290
1296
  config,
1291
1297
  force: Boolean(options.force),
1292
1298
  dryRun: Boolean(options.dryRun),
1293
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1299
+ name: names.stableName,
1294
1300
  files: [
1295
1301
  ...(await missingCapabilityBuilderFiles(
1296
1302
  targetDir,
@@ -1341,7 +1347,7 @@ export async function makeListener(
1341
1347
  config,
1342
1348
  force: Boolean(options.force),
1343
1349
  dryRun: Boolean(options.dryRun),
1344
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1350
+ name: names.stableName,
1345
1351
  files: [
1346
1352
  ...(await missingCapabilityBuilderFiles(targetDir, "listeners", config)),
1347
1353
  ...listenerFiles(names, config),
@@ -1445,7 +1451,7 @@ async function assertListenerEventExists(
1445
1451
  await stat(path.join(targetDir, file));
1446
1452
  } catch {
1447
1453
  throw new Error(
1448
- `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.`,
1454
+ `beignet make listener expected event ${names.eventName} at ${file}. Run beignet make event ${names.eventName} first, or pass an event that exists.`,
1449
1455
  );
1450
1456
  }
1451
1457
  }
@@ -1495,6 +1501,14 @@ export async function makeSchedule(
1495
1501
  ? resolveConfig(options.config)
1496
1502
  : await loadBeignetConfig(targetDir);
1497
1503
 
1504
+ if (options.route) {
1505
+ assertNextFramework(
1506
+ config,
1507
+ "beignet make schedule --route",
1508
+ "a Next.js App Router cron handler",
1509
+ );
1510
+ }
1511
+
1498
1512
  await assertFeatureArtifactApp(targetDir, config, "schedule", {
1499
1513
  requireServer: Boolean(options.route),
1500
1514
  });
@@ -1515,7 +1529,7 @@ export async function makeSchedule(
1515
1529
  config,
1516
1530
  force: Boolean(options.force),
1517
1531
  dryRun: Boolean(options.dryRun),
1518
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1532
+ name: names.stableName,
1519
1533
  files,
1520
1534
  index: featureArtifactIndexFile("schedule", names, config),
1521
1535
  dependencies: {
@@ -1710,6 +1724,9 @@ async function updateOutboxPortWiring(
1710
1724
  path.join(infrastructureDir(config), "db/provider.ts"),
1711
1725
  );
1712
1726
  }
1727
+ if (await updateOutboxDrainProvider(targetDir, config, options)) {
1728
+ changes.updatedFiles.push(providersFilePath(config));
1729
+ }
1713
1730
  if (await updateOutboxRepositoriesReturnType(targetDir, config, options)) {
1714
1731
  changes.updatedFiles.push(drizzleRepositoriesPath(config));
1715
1732
  }
@@ -1821,6 +1838,31 @@ async function updateOutboxInfrastructurePorts(
1821
1838
  return true;
1822
1839
  }
1823
1840
 
1841
+ async function projectSupportsNextAfter(targetDir: string): Promise<boolean> {
1842
+ const packageJson = JSON.parse(
1843
+ await readFile(path.join(targetDir, "package.json"), "utf8"),
1844
+ ) as PackageJsonLike;
1845
+ const version =
1846
+ packageJson.dependencies?.next ?? packageJson.devDependencies?.next;
1847
+ if (!version) return false;
1848
+
1849
+ return version.split("||").every((range) => {
1850
+ const lowerBound =
1851
+ /^\s*(\^|~|>=|>|=|<=|<)?\s*v?(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?([+-][0-9A-Za-z.-]+)?(?=\s|$)/.exec(
1852
+ range,
1853
+ );
1854
+ if (!lowerBound) return false;
1855
+
1856
+ const operator = lowerBound[1];
1857
+ if (operator === "<" || operator === "<=") return false;
1858
+ if (lowerBound[5]?.startsWith("-")) return false;
1859
+
1860
+ const major = Number(lowerBound[2]);
1861
+ const minor = /^\d+$/.test(lowerBound[3] ?? "") ? Number(lowerBound[3]) : 0;
1862
+ return major > 15 || (major === 15 && minor >= 1);
1863
+ });
1864
+ }
1865
+
1824
1866
  async function updateOutboxDatabaseProvider(
1825
1867
  targetDir: string,
1826
1868
  config: ResolvedBeignetConfig,
@@ -1919,6 +1961,95 @@ async function updateOutboxDatabaseProvider(
1919
1961
  return true;
1920
1962
  }
1921
1963
 
1964
+ async function updateOutboxDrainProvider(
1965
+ targetDir: string,
1966
+ config: ResolvedBeignetConfig,
1967
+ options: { dryRun: boolean },
1968
+ ): Promise<boolean> {
1969
+ if (!(await projectSupportsNextAfter(targetDir))) return false;
1970
+
1971
+ const file = providersFilePath(config);
1972
+ const filePath = path.join(targetDir, file);
1973
+ const original = await readOptionalFile(filePath);
1974
+ if (original === undefined) {
1975
+ throw new Error(
1976
+ `Could not find the generated providers file ${file}. Register the outbox drain trigger manually, or restore the generated providers file before running make outbox.`,
1977
+ );
1978
+ }
1979
+
1980
+ let next = addNamedImport(
1981
+ original,
1982
+ "createObservedUnitOfWork",
1983
+ "@beignet/core/ports",
1984
+ );
1985
+ next = addNamedImport(next, "createProvider", "@beignet/core/providers");
1986
+ next = addNamedImport(next, "createNextOutboxDrainTrigger", "@beignet/next");
1987
+ next = addNamedImport(next, "after", "next/server");
1988
+ next = addNamedTypeImport(
1989
+ next,
1990
+ "AppContext",
1991
+ aliasModule(config.paths.appContext),
1992
+ );
1993
+ next = addNamedTypeImport(next, "AppPorts", aliasModule(config.paths.ports));
1994
+ next = addNamedTypeImport(
1995
+ next,
1996
+ "AppServiceContextInput",
1997
+ aliasModule(path.join(path.dirname(config.paths.server), "context.ts")),
1998
+ );
1999
+
2000
+ const providerName = "outboxDrainProvider";
2001
+ if (!new RegExp(`\\bconst\\s+${providerName}\\b`).test(next)) {
2002
+ const outboxModule = aliasModule(config.paths.outbox);
2003
+ const providerDefinition = `const ${providerName} = createProvider<
2004
+ \tPick<AppPorts, "uow">,
2005
+ \tAppContext,
2006
+ \tAppServiceContextInput
2007
+ >()({
2008
+ \tname: "outbox-drain-trigger",
2009
+ \tsetup({ ports, createServiceContext }): { ports: Pick<AppPorts, "uow"> } {
2010
+ \t\tconst triggerOutboxDrain: () => void = createNextOutboxDrainTrigger({
2011
+ \t\t\tdefer: after,
2012
+ \t\t\tcreateContext: () => createServiceContext(undefined),
2013
+ \t\t\tregistry: async () => (await import("${outboxModule}")).outboxRegistry,
2014
+ \t\t\tbatchSize: 100,
2015
+ \t\t});
2016
+
2017
+ \t\treturn {
2018
+ \t\t\tports: {
2019
+ \t\t\t\tuow: createObservedUnitOfWork({
2020
+ \t\t\t\t\tunitOfWork: ports.uow,
2021
+ \t\t\t\t\tafterCommit: triggerOutboxDrain,
2022
+ \t\t\t\t}),
2023
+ \t\t\t},
2024
+ \t\t};
2025
+ \t},
2026
+ });
2027
+ `;
2028
+ const withProvider = next.replace(
2029
+ /\nexport const providers = \[/,
2030
+ `\n${providerDefinition}\nexport const providers = [`,
2031
+ );
2032
+ if (withProvider === next) {
2033
+ throw new Error(
2034
+ `Could not find the exported providers array in ${file}. Register outboxDrainProvider manually, or restore the generated providers file before running make outbox.`,
2035
+ );
2036
+ }
2037
+ next = withProvider;
2038
+ }
2039
+
2040
+ const appended = appendToNamedArray(next, "providers", providerName);
2041
+ if (appended.kind === "missing") {
2042
+ throw new Error(
2043
+ `Could not find the exported providers array in ${file}. Register outboxDrainProvider manually, or restore the generated providers file before running make outbox.`,
2044
+ );
2045
+ }
2046
+ if (appended.kind === "updated") next = appended.source;
2047
+
2048
+ if (next === original) return false;
2049
+ if (!options.dryRun) await writeFile(filePath, next);
2050
+ return true;
2051
+ }
2052
+
1922
2053
  async function updateOutboxRepositoriesReturnType(
1923
2054
  targetDir: string,
1924
2055
  config: ResolvedBeignetConfig,
@@ -2064,7 +2195,16 @@ export async function makeUpload(
2064
2195
  ? resolveConfig(options.config)
2065
2196
  : await loadBeignetConfig(targetDir);
2066
2197
 
2198
+ assertNextFramework(
2199
+ config,
2200
+ "beignet make upload",
2201
+ "a Next.js App Router upload handler",
2202
+ );
2203
+
2067
2204
  await assertFeatureArtifactApp(targetDir, config, "upload");
2205
+ if (options.ui) {
2206
+ await assertUploadUiApp(targetDir, config);
2207
+ }
2068
2208
 
2069
2209
  await wirePortProviders(
2070
2210
  targetDir,
@@ -2080,10 +2220,10 @@ export async function makeUpload(
2080
2220
  config,
2081
2221
  force: Boolean(options.force),
2082
2222
  dryRun: Boolean(options.dryRun),
2083
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
2223
+ name: names.stableName,
2084
2224
  files: [
2085
2225
  ...(await missingCapabilityBuilderFiles(targetDir, "uploads", config)),
2086
- ...uploadFiles(names, config),
2226
+ ...uploadFiles(names, config, { ui: Boolean(options.ui) }),
2087
2227
  ],
2088
2228
  index: featureArtifactIndexFile("upload", names, config),
2089
2229
  dependencies: {
@@ -2112,6 +2252,44 @@ export async function makeUpload(
2112
2252
  result.files.push(uploadRoutePath(config));
2113
2253
  }
2114
2254
 
2255
+ if (options.ui) {
2256
+ const componentIndex = uploadUiComponentIndexFile(names, config);
2257
+ const componentIndexResult = await updateFeatureUiIndex(
2258
+ targetDir,
2259
+ componentIndex,
2260
+ { dryRun: Boolean(options.dryRun) },
2261
+ );
2262
+ if (componentIndexResult === "created") {
2263
+ result.createdFiles.push(componentIndex.path);
2264
+ }
2265
+ if (componentIndexResult === "updated") {
2266
+ result.updatedFiles.push(componentIndex.path);
2267
+ }
2268
+ if (componentIndexResult === "skipped") {
2269
+ result.skippedFiles.push(componentIndex.path);
2270
+ }
2271
+ if (!result.files.includes(componentIndex.path)) {
2272
+ result.files.push(componentIndex.path);
2273
+ }
2274
+
2275
+ if (
2276
+ await updatePackageDependencies(
2277
+ targetDir,
2278
+ { "@beignet/react-uploads": beignetDependencyVersion },
2279
+ { dryRun: Boolean(options.dryRun) },
2280
+ )
2281
+ ) {
2282
+ result.updatedFiles.push("package.json");
2283
+ if (!result.files.includes("package.json")) {
2284
+ result.files.push("package.json");
2285
+ }
2286
+ }
2287
+ }
2288
+
2289
+ result.createdFiles = uniqueStrings(result.createdFiles);
2290
+ result.updatedFiles = uniqueStrings(result.updatedFiles);
2291
+ result.skippedFiles = uniqueStrings(result.skippedFiles);
2292
+
2115
2293
  return result;
2116
2294
  }
2117
2295
 
@@ -2123,6 +2301,12 @@ export async function makeOutbox(
2123
2301
  ? resolveConfig(options.config)
2124
2302
  : await loadBeignetConfig(targetDir);
2125
2303
 
2304
+ assertNextFramework(
2305
+ config,
2306
+ "beignet make outbox",
2307
+ "a Next.js App Router recovery drain handler",
2308
+ );
2309
+
2126
2310
  await assertServerRuntimeApp(targetDir, config, "outbox");
2127
2311
  await updateOutboxPortWiring(targetDir, config, { dryRun: true });
2128
2312
 
@@ -2174,6 +2358,9 @@ export async function makeOutbox(
2174
2358
  config.paths.ports,
2175
2359
  config.paths.portWiring,
2176
2360
  path.join(infrastructureDir(config), "db/provider.ts"),
2361
+ ...((await projectSupportsNextAfter(targetDir))
2362
+ ? [providersFilePath(config)]
2363
+ : []),
2177
2364
  drizzleRepositoriesPath(config),
2178
2365
  outboxDrizzleSchemaFilePath(config),
2179
2366
  drizzleSchemaIndexPath(config),
@@ -2195,7 +2382,10 @@ async function makeFeatureUi(
2195
2382
 
2196
2383
  await assertFeatureUiApp(targetDir, config);
2197
2384
 
2198
- const componentFiles = featureUiComponentFiles(names, config);
2385
+ const componentFiles = [
2386
+ ...(await missingFeatureUiFormsFiles(targetDir, config)),
2387
+ ...featureUiComponentFiles(names, config),
2388
+ ];
2199
2389
  const indexFile = featureUiIndexFile(names, config);
2200
2390
  const plannedFiles = await planGeneratedFiles(targetDir, componentFiles, {
2201
2391
  force: Boolean(options.force),
@@ -2227,8 +2417,15 @@ async function makeFeatureUi(
2227
2417
  await updatePackageDependencies(
2228
2418
  targetDir,
2229
2419
  {
2420
+ "@beignet/react-hook-form": beignetDependencyVersion,
2230
2421
  "@beignet/react-query": beignetDependencyVersion,
2422
+ "@hookform/resolvers": (packageJson) =>
2423
+ packageJson.dependencies?.["@hookform/resolvers"] ??
2424
+ externalVersions.hookformResolvers,
2231
2425
  "@tanstack/react-query": tanstackReactQueryDependencyVersion,
2426
+ "react-hook-form": (packageJson) =>
2427
+ packageJson.dependencies?.["react-hook-form"] ??
2428
+ externalVersions.reactHookForm,
2232
2429
  },
2233
2430
  { dryRun: Boolean(options.dryRun) },
2234
2431
  )
@@ -2370,7 +2567,7 @@ async function updateResourceWiring(
2370
2567
  updated.add(config.paths.portWiring);
2371
2568
  }
2372
2569
  if (
2373
- generationOptions.auth &&
2570
+ generationOptions.authorization &&
2374
2571
  (await updateInfrastructureGatePolicies(targetDir, names, config, options))
2375
2572
  ) {
2376
2573
  updated.add(config.paths.portWiring);
@@ -2432,13 +2629,12 @@ function typeHasTopLevelProperty(
2432
2629
  function useCaseNames(input: string): UseCaseNames {
2433
2630
  const parts = input
2434
2631
  .trim()
2435
- .split("/")
2436
- .map((part) => part.trim())
2437
- .filter(Boolean);
2632
+ .split(".")
2633
+ .map((part) => part.trim());
2438
2634
 
2439
- if (parts.length !== 2) {
2635
+ if (parts.length !== 2 || parts.some((part) => part.length === 0)) {
2440
2636
  throw new Error(
2441
- "Use case name must use feature/action format, for example projects/archive-project.",
2637
+ "Use case name must use feature.action format, for example projects.archive-project.",
2442
2638
  );
2443
2639
  }
2444
2640
 
@@ -2478,7 +2674,7 @@ function policyNames(input: string): PolicyNames {
2478
2674
  }
2479
2675
 
2480
2676
  function eventNames(input: string): EventNames {
2481
- const names = featureArtifactNames(normalizeEventReference(input), "Event");
2677
+ const names = featureArtifactNames(input, "Event");
2482
2678
  const eventExportName = `${names.featureSingularPascal}${names.artifact.pascal}`;
2483
2679
 
2484
2680
  assertGeneratedIdentifiers(input, "Event name", [eventExportName]);
@@ -2536,7 +2732,7 @@ function factoryNames(input: string): FactoryNames {
2536
2732
  }
2537
2733
 
2538
2734
  function defaultFactoryNamesForSeed(names: SeedNames): FactoryNames {
2539
- return factoryNames(`${names.feature.kebab}/${names.featureSingularKebab}`);
2735
+ return factoryNames(`${names.feature.kebab}.${names.featureSingularKebab}`);
2540
2736
  }
2541
2737
 
2542
2738
  function notificationNames(input: string): NotificationNames {
@@ -2593,8 +2789,12 @@ function scheduleNames(
2593
2789
  function uploadNames(input: string): UploadNames {
2594
2790
  const names = featureArtifactNames(input, "Upload");
2595
2791
  const uploadExportName = `${names.artifact.pascal}Upload`;
2792
+ const componentExportName = `${names.artifact.pascal}Uploader`;
2596
2793
 
2597
- assertGeneratedIdentifiers(input, "Upload name", [uploadExportName]);
2794
+ assertGeneratedIdentifiers(input, "Upload name", [
2795
+ uploadExportName,
2796
+ componentExportName,
2797
+ ]);
2598
2798
 
2599
2799
  return {
2600
2800
  ...names,
@@ -2602,19 +2802,16 @@ function uploadNames(input: string): UploadNames {
2602
2802
  uploadExportName,
2603
2803
  metadataSchemaName: `${uploadExportName}MetadataSchema`,
2604
2804
  metadataTypeName: `${uploadExportName}Metadata`,
2805
+ fileConstraintsExportName: `${names.artifact.camel}UploadFile`,
2806
+ manifestExportName: `${names.artifact.camel}UploadManifest`,
2807
+ manifestFileName: `${names.artifact.kebab}-upload-manifest`,
2808
+ reactUploadsExportName: `${names.artifact.camel}ReactUploads`,
2809
+ clientFileName: `${names.artifact.kebab}-upload`,
2810
+ componentExportName,
2811
+ componentFileName: `${names.artifact.kebab}-uploader`,
2605
2812
  };
2606
2813
  }
2607
2814
 
2608
- function normalizeEventReference(input: string): string {
2609
- const trimmed = input.trim();
2610
- if (trimmed.includes("/")) return trimmed;
2611
-
2612
- const dotIndex = trimmed.indexOf(".");
2613
- if (dotIndex === -1) return trimmed;
2614
-
2615
- return `${trimmed.slice(0, dotIndex)}/${trimmed.slice(dotIndex + 1)}`;
2616
- }
2617
-
2618
2815
  function inferUseCaseKind(action: string): "command" | "query" {
2619
2816
  if (/^(count|find|get|list|search)-/.test(`${action}-`)) {
2620
2817
  return "query";
@@ -2628,8 +2825,8 @@ function resourceFiles(
2628
2825
  persistence: ResourcePersistence = "memory",
2629
2826
  mode: ResourceGenerationMode = "feature",
2630
2827
  options: ResourceGenerationOptions = {
2631
- auth: false,
2632
- tenant: false,
2828
+ authorization: false,
2829
+ tenantScoped: false,
2633
2830
  events: false,
2634
2831
  softDelete: false,
2635
2832
  },
@@ -2684,7 +2881,7 @@ function resourceFiles(
2684
2881
  },
2685
2882
  ]
2686
2883
  : []),
2687
- ...(mode === "resource" && options.auth
2884
+ ...(mode === "resource" && options.authorization
2688
2885
  ? [
2689
2886
  {
2690
2887
  path: path.join(featureDir, "tests/policy.test.ts"),
@@ -2754,6 +2951,99 @@ function featureUiComponentFiles(
2754
2951
  ];
2755
2952
  }
2756
2953
 
2954
+ async function missingFeatureUiFormsFiles(
2955
+ targetDir: string,
2956
+ config: ResolvedBeignetConfig,
2957
+ ): Promise<GeneratedFile[]> {
2958
+ const formsPath = clientFormsPath(config);
2959
+ const existing = await readOptionalFile(path.join(targetDir, formsPath));
2960
+
2961
+ if (existing === undefined) {
2962
+ return [
2963
+ {
2964
+ path: formsPath,
2965
+ content: `import { createReactHookForm } from "@beignet/react-hook-form";
2966
+
2967
+ export const rhf = createReactHookForm();
2968
+ `,
2969
+ },
2970
+ ];
2971
+ }
2972
+
2973
+ if (!sourceExportsValue(existing, "rhf")) {
2974
+ throw new Error(
2975
+ `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.`,
2976
+ );
2977
+ }
2978
+
2979
+ return [];
2980
+ }
2981
+
2982
+ async function assertUploadUiApp(
2983
+ targetDir: string,
2984
+ config: ResolvedBeignetConfig,
2985
+ ): Promise<void> {
2986
+ const clientPath = clientIndexPath(config);
2987
+ const clientSource = await readOptionalFile(path.join(targetDir, clientPath));
2988
+
2989
+ if (!clientSource || !sourceExportsValue(clientSource, "rq")) {
2990
+ throw new Error(
2991
+ `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.`,
2992
+ );
2993
+ }
2994
+ }
2995
+
2996
+ function sourceExportsValue(source: string, name: string): boolean {
2997
+ const sourceFile = ts.createSourceFile(
2998
+ "client-forms.ts",
2999
+ source,
3000
+ ts.ScriptTarget.Latest,
3001
+ false,
3002
+ ts.ScriptKind.TS,
3003
+ );
3004
+
3005
+ for (const statement of sourceFile.statements) {
3006
+ const exported =
3007
+ ts.canHaveModifiers(statement) &&
3008
+ ts
3009
+ .getModifiers(statement)
3010
+ ?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
3011
+
3012
+ if (exported && ts.isVariableStatement(statement)) {
3013
+ if (
3014
+ statement.declarationList.declarations.some(
3015
+ (declaration) =>
3016
+ ts.isIdentifier(declaration.name) && declaration.name.text === name,
3017
+ )
3018
+ ) {
3019
+ return true;
3020
+ }
3021
+ }
3022
+
3023
+ if (
3024
+ exported &&
3025
+ ts.isFunctionDeclaration(statement) &&
3026
+ statement.name?.text === name
3027
+ ) {
3028
+ return true;
3029
+ }
3030
+
3031
+ if (
3032
+ ts.isExportDeclaration(statement) &&
3033
+ !statement.isTypeOnly &&
3034
+ statement.exportClause &&
3035
+ ts.isNamedExports(statement.exportClause) &&
3036
+ statement.exportClause.elements.some(
3037
+ (element) => !element.isTypeOnly && element.name.text === name,
3038
+ )
3039
+ ) {
3040
+ return true;
3041
+ }
3042
+ }
3043
+
3044
+ return false;
3045
+ }
3046
+
2757
3047
  function featureUiIndexFile(
2758
3048
  names: FeatureUiNames,
2759
3049
  config: ResolvedBeignetConfig,
@@ -2949,12 +3239,98 @@ function scheduleFiles(
2949
3239
  function uploadFiles(
2950
3240
  names: UploadNames,
2951
3241
  config: ResolvedBeignetConfig,
3242
+ options: { ui: boolean },
2952
3243
  ): GeneratedFile[] {
2953
3244
  return [
3245
+ {
3246
+ path: uploadManifestFilePath(names, config),
3247
+ content: uploadManifestFile(names),
3248
+ },
2954
3249
  {
2955
3250
  path: uploadFilePath(names, config),
2956
3251
  content: uploadFile(names, config),
2957
3252
  },
3253
+ ...(options.ui ? uploadUiFiles(names, config) : []),
3254
+ ];
3255
+ }
3256
+
3257
+ function uploadFeatureDir(
3258
+ names: UploadNames,
3259
+ config: ResolvedBeignetConfig,
3260
+ ): string {
3261
+ return path.join(config.paths.features, names.feature.kebab);
3262
+ }
3263
+
3264
+ function uploadManifestFilePath(
3265
+ names: UploadNames,
3266
+ config: ResolvedBeignetConfig,
3267
+ ): string {
3268
+ return path.join(
3269
+ uploadFeatureDir(names, config),
3270
+ `${names.manifestFileName}.ts`,
3271
+ );
3272
+ }
3273
+
3274
+ function uploadUiClientFilePath(
3275
+ names: UploadNames,
3276
+ config: ResolvedBeignetConfig,
3277
+ ): string {
3278
+ return path.join(
3279
+ uploadFeatureDir(names, config),
3280
+ "client",
3281
+ `${names.clientFileName}.ts`,
3282
+ );
3283
+ }
3284
+
3285
+ function uploadUiComponentFilePath(
3286
+ names: UploadNames,
3287
+ config: ResolvedBeignetConfig,
3288
+ ): string {
3289
+ return path.join(
3290
+ uploadFeatureDir(names, config),
3291
+ "components",
3292
+ `${names.componentFileName}.tsx`,
3293
+ );
3294
+ }
3295
+
3296
+ function uploadUiComponentTestFilePath(
3297
+ names: UploadNames,
3298
+ config: ResolvedBeignetConfig,
3299
+ ): string {
3300
+ return path.join(
3301
+ uploadFeatureDir(names, config),
3302
+ "tests",
3303
+ `${names.componentFileName}.test.tsx`,
3304
+ );
3305
+ }
3306
+
3307
+ function uploadUiComponentIndexFile(
3308
+ names: UploadNames,
3309
+ config: ResolvedBeignetConfig,
3310
+ ): GeneratedFile {
3311
+ return {
3312
+ path: path.join(uploadFeatureDir(names, config), "components", "index.ts"),
3313
+ content: `export { ${names.componentExportName} } from "./${names.componentFileName}";\n`,
3314
+ };
3315
+ }
3316
+
3317
+ function uploadUiFiles(
3318
+ names: UploadNames,
3319
+ config: ResolvedBeignetConfig,
3320
+ ): GeneratedFile[] {
3321
+ return [
3322
+ {
3323
+ path: uploadUiClientFilePath(names, config),
3324
+ content: uploadUiClientFile(names, config),
3325
+ },
3326
+ {
3327
+ path: uploadUiComponentFilePath(names, config),
3328
+ content: uploadUiComponentFile(names, config),
3329
+ },
3330
+ {
3331
+ path: uploadUiComponentTestFilePath(names, config),
3332
+ content: uploadUiComponentTestFile(names, config),
3333
+ },
2958
3334
  ];
2959
3335
  }
2960
3336
 
@@ -3447,7 +3823,7 @@ function schemasFile(
3447
3823
 
3448
3824
  export const ${names.singularPascal}Schema = z.object({
3449
3825
  id: z.string().uuid(),
3450
- ${options.tenant ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
3826
+ ${options.tenantScoped ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
3451
3827
  version: z.number().int().min(1),
3452
3828
  createdAt: z.string().datetime(),
3453
3829
  updatedAt: z.string().datetime(),
@@ -3547,7 +3923,7 @@ export const List${names.pluralPascal}OutputSchema = z.object({
3547
3923
  });
3548
3924
 
3549
3925
  export const Create${names.singularPascal}InputSchema = z.object({
3550
- name: z.string().min(1).max(120),
3926
+ name: z.string().trim().min(1).max(120),
3551
3927
  });
3552
3928
  ${
3553
3929
  mode === "resource"
@@ -3557,7 +3933,7 @@ export const ${names.singularPascal}IdInputSchema = z.object({
3557
3933
  });
3558
3934
 
3559
3935
  export const Update${names.singularPascal}BodySchema = z.object({
3560
- name: z.string().min(1).max(120),
3936
+ name: z.string().trim().min(1).max(120),
3561
3937
  version: z.number().int().min(1),
3562
3938
  });
3563
3939
 
@@ -3603,7 +3979,7 @@ function listUseCaseFile(
3603
3979
 
3604
3980
  return `import "@beignet/core/server-only";
3605
3981
  import { normalizeCursorPage } from "@beignet/core/pagination";
3606
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
3982
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
3607
3983
  import {
3608
3984
  decode${names.singularPascal}Cursor,
3609
3985
  List${names.pluralPascal}InputSchema,
@@ -3615,7 +3991,7 @@ export const list${names.pluralPascal}UseCase = useCase
3615
3991
  .input(List${names.pluralPascal}InputSchema)
3616
3992
  .output(List${names.pluralPascal}OutputSchema)
3617
3993
  .run(async ({ ctx, input }) => {
3618
- ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const page = normalizeCursorPage(input, {
3994
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const page = normalizeCursorPage(input, {
3619
3995
  defaultLimit: 20,
3620
3996
  maxLimit: 100,
3621
3997
  });
@@ -3626,7 +4002,7 @@ ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const
3626
4002
  name: input.name,
3627
4003
  sortBy: input.sortBy,
3628
4004
  sortDirection: input.sortDirection,
3629
- }${options.tenant ? ", scope" : ""});
4005
+ }${options.tenantScoped ? ", scope" : ""});
3630
4006
  });
3631
4007
  `;
3632
4008
  }
@@ -3642,7 +4018,7 @@ function createUseCaseFile(
3642
4018
  );
3643
4019
 
3644
4020
  return `import "@beignet/core/server-only";
3645
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
4021
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
3646
4022
  import {
3647
4023
  Create${names.singularPascal}InputSchema,
3648
4024
  ${names.singularPascal}Schema,
@@ -3654,7 +4030,7 @@ export const create${names.singularPascal}UseCase = useCase
3654
4030
  .input(Create${names.singularPascal}InputSchema)
3655
4031
  .output(${names.singularPascal}Schema)
3656
4032
  .run(async ({ ctx, input }) => {
3657
- ${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" : ""});
4033
+ ${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" : ""});
3658
4034
  ${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Created, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
3659
4035
  return ${names.singularCamel};
3660
4036
  });
@@ -3672,7 +4048,7 @@ function getUseCaseFile(
3672
4048
  );
3673
4049
 
3674
4050
  return `import "@beignet/core/server-only";
3675
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
4051
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
3676
4052
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
3677
4053
  import {
3678
4054
  ${names.singularPascal}IdInputSchema,
@@ -3684,7 +4060,7 @@ export const get${names.singularPascal}UseCase = useCase
3684
4060
  .input(${names.singularPascal}IdInputSchema)
3685
4061
  .output(${names.singularPascal}Schema)
3686
4062
  .run(async ({ ctx, input }) => {
3687
- ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenant ? ", scope" : ""});
4063
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
3688
4064
  if (!${names.singularCamel}) {
3689
4065
  throw appError("${names.singularPascal}NotFound", {
3690
4066
  details: { id: input.id },
@@ -3707,7 +4083,7 @@ function updateUseCaseFile(
3707
4083
  );
3708
4084
 
3709
4085
  return `import "@beignet/core/server-only";
3710
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
4086
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
3711
4087
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
3712
4088
  ${options.events ? `import { ${names.singularPascal}Updated } from "../domain/events";\n` : ""}import {
3713
4089
  Update${names.singularPascal}InputSchema,
@@ -3719,13 +4095,13 @@ export const update${names.singularPascal}UseCase = useCase
3719
4095
  .input(Update${names.singularPascal}InputSchema)
3720
4096
  .output(${names.singularPascal}Schema)
3721
4097
  .run(async ({ ctx, input }) => {
3722
- ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenant ? ", scope" : ""});
4098
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
3723
4099
  if (!existing) {
3724
4100
  throw appError("${names.singularPascal}NotFound", {
3725
4101
  details: { id: input.id },
3726
4102
  });
3727
4103
  }
3728
- ${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
4104
+ ${options.authorization ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
3729
4105
  throw appError("${names.singularPascal}Conflict", {
3730
4106
  details: { id: input.id, expectedVersion: existing.version },
3731
4107
  });
@@ -3733,7 +4109,7 @@ ${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", ex
3733
4109
 
3734
4110
  const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.update({
3735
4111
  ...input,
3736
- }${options.tenant ? ", scope" : ""});
4112
+ }${options.tenantScoped ? ", scope" : ""});
3737
4113
  if (!${names.singularCamel}) {
3738
4114
  throw appError("${names.singularPascal}Conflict", {
3739
4115
  details: { id: input.id, expectedVersion: existing.version },
@@ -3757,7 +4133,7 @@ function deleteUseCaseFile(
3757
4133
 
3758
4134
  return `import "@beignet/core/server-only";
3759
4135
  import { z } from "zod";
3760
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
4136
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
3761
4137
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
3762
4138
  ${options.events ? `import { ${names.singularPascal}Deleted } from "../domain/events";\n` : ""}import { ${names.singularPascal}IdInputSchema } from "../schemas";
3763
4139
 
@@ -3766,7 +4142,7 @@ export const delete${names.singularPascal}UseCase = useCase
3766
4142
  .input(${names.singularPascal}IdInputSchema)
3767
4143
  .output(z.void())
3768
4144
  .run(async ({ ctx, input }) => {
3769
- ${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" : ""});
4145
+ ${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" : ""});
3770
4146
  if (!deleted) {
3771
4147
  throw appError("${names.singularPascal}NotFound", {
3772
4148
  details: { id: input.id },
@@ -3806,7 +4182,7 @@ function repositoryPortFile(
3806
4182
  PageResult,
3807
4183
  SortDirection,
3808
4184
  } from "@beignet/core/pagination";
3809
- ${options.tenant ? `import type { TenantScope } from "@beignet/core/tenancy";\n` : ""}import type {
4185
+ ${options.tenantScoped ? `import type { TenantScope } from "@beignet/core/tenancy";\n` : ""}import type {
3810
4186
  Create${names.singularPascal}Input,
3811
4187
  ${names.singularPascal}Cursor,
3812
4188
  ${names.singularPascal}SortBy,
@@ -3823,9 +4199,9 @@ export type List${names.pluralPascal}Query = {
3823
4199
  };
3824
4200
 
3825
4201
  export interface ${names.singularPascal}Repository {
3826
- list(query: List${names.pluralPascal}Query${options.tenant ? ", scope: TenantScope" : ""}): Promise<List${names.pluralPascal}Result>;
3827
- create(input: Create${names.singularPascal}Input${options.tenant ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal}>;
3828
- ${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` : ""}}
4202
+ list(query: List${names.pluralPascal}Query${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<List${names.pluralPascal}Result>;
4203
+ create(input: Create${names.singularPascal}Input${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal}>;
4204
+ ${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` : ""}}
3829
4205
  `;
3830
4206
  }
3831
4207
 
@@ -3859,14 +4235,14 @@ function inMemoryRepositoryFile(
3859
4235
  ? " || existing.deletedAt !== null"
3860
4236
  : "";
3861
4237
  const deleteImplementation = options.softDelete
3862
- ? `${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();
4238
+ ? `${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();
3863
4239
  ${names.pluralCamel}.set(id, { ...existing, deletedAt: now, updatedAt: now });
3864
4240
  return true;`
3865
- : `${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);`;
4241
+ : `${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);`;
3866
4242
 
3867
4243
  return `import "@beignet/core/server-only";
3868
4244
  import { cursorPageResult } from "@beignet/core/pagination";
3869
- ${options.tenant ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
4245
+ ${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
3870
4246
  import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
3871
4247
  import type {
3872
4248
  Create${names.singularPascal}Input,
@@ -3876,7 +4252,7 @@ ${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.s
3876
4252
  ${recordType}function to${names.singularPascal}(${names.singularCamel}: ${mapValueType}): ${names.singularPascal} {
3877
4253
  return {
3878
4254
  id: ${names.singularCamel}.id,
3879
- ${options.tenant ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
4255
+ ${options.tenantScoped ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
3880
4256
  version: ${names.singularCamel}.version,
3881
4257
  createdAt: ${names.singularCamel}.createdAt,
3882
4258
  updatedAt: ${names.singularCamel}.updatedAt,
@@ -3934,10 +4310,10 @@ export function createInMemory${names.singularPascal}Repository(
3934
4310
  );
3935
4311
 
3936
4312
  return {
3937
- async list(query${options.tenant ? ", scope" : ""}) {
4313
+ async list(query${options.tenantScoped ? ", scope" : ""}) {
3938
4314
  const name = query.name?.toLocaleLowerCase();
3939
- ${options.tenant ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values())
3940
- ${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))
4315
+ ${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values())
4316
+ ${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))
3941
4317
  .sort((left, right) => compare${names.pluralPascal}(left, right, query))
3942
4318
  .filter((${names.singularCamel}) => isAfter${names.singularPascal}Cursor(${names.singularCamel}, query));
3943
4319
  const pageItems = all${names.pluralPascal}.slice(0, query.page.limit);
@@ -3952,11 +4328,11 @@ ${activeFilter}${options.tenant ? `\t\t\t\t.filter((${names.singularCamel}) => $
3952
4328
  nextCursor,
3953
4329
  );
3954
4330
  },
3955
- async create(input${options.tenant ? ", scope" : ""}) {
4331
+ async create(input${options.tenantScoped ? ", scope" : ""}) {
3956
4332
  const now = new Date().toISOString();
3957
- ${options.tenant ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel}: ${names.singularPascal} = {
4333
+ ${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel}: ${names.singularPascal} = {
3958
4334
  id: crypto.randomUUID(),
3959
- ${options.tenant ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
4335
+ ${options.tenantScoped ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
3960
4336
  version: 1,
3961
4337
  createdAt: now,
3962
4338
  updatedAt: now,
@@ -3966,7 +4342,7 @@ ${options.tenant ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
3966
4342
  ${newDeletedAtField} });
3967
4343
  return ${names.singularCamel};
3968
4344
  },
3969
- ${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` : ""}
4345
+ ${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` : ""}
3970
4346
  };
3971
4347
  }
3972
4348
  `;
@@ -3981,7 +4357,7 @@ function drizzleSchemaFile(
3981
4357
 
3982
4358
  export const ${names.pluralCamel} = ${dialect.tableFunction}("${names.pluralKebab.replaceAll("-", "_")}", {
3983
4359
  id: ${dialect.idColumn("id")}.primaryKey(),
3984
- ${options.tenant ? `\ttenantId: ${dialect.idColumn("tenant_id")}.notNull(),\n` : ""} name: text("name").notNull(),
4360
+ ${options.tenantScoped ? `\ttenantId: ${dialect.idColumn("tenant_id")}.notNull(),\n` : ""} name: text("name").notNull(),
3985
4361
  version: ${dialect.integerColumn("version")}.notNull(),
3986
4362
  createdAt: ${dialect.timestampColumn("created_at")}.notNull(),
3987
4363
  updatedAt: ${dialect.timestampColumn("updated_at")}.notNull(),
@@ -4091,20 +4467,20 @@ function drizzleRepositoryFile(
4091
4467
  const schemaPath = drizzleSchemaIndexPath(config);
4092
4468
  const findWhere = drizzleResourceWhere(names, options, [
4093
4469
  `eq(schema.${names.pluralCamel}.id, id)`,
4094
- ...(options.tenant
4470
+ ...(options.tenantScoped
4095
4471
  ? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
4096
4472
  : []),
4097
4473
  ]);
4098
4474
  const updateWhere = drizzleResourceWhere(names, options, [
4099
4475
  `eq(schema.${names.pluralCamel}.id, input.id)`,
4100
4476
  `eq(schema.${names.pluralCamel}.version, input.version)`,
4101
- ...(options.tenant
4477
+ ...(options.tenantScoped
4102
4478
  ? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
4103
4479
  : []),
4104
4480
  ]);
4105
4481
  const deleteWhere = drizzleResourceWhere(names, options, [
4106
4482
  `eq(schema.${names.pluralCamel}.id, id)`,
4107
- ...(options.tenant
4483
+ ...(options.tenantScoped
4108
4484
  ? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
4109
4485
  : []),
4110
4486
  ]);
@@ -4122,7 +4498,7 @@ function drizzleRepositoryFile(
4122
4498
  ].filter((name): name is string => Boolean(name));
4123
4499
  // After a version-guarded update succeeds, re-select by identity only —
4124
4500
  // matching what `.returning()` yields on the dialects that support it.
4125
- const updateReselectWhere = options.tenant
4501
+ const updateReselectWhere = options.tenantScoped
4126
4502
  ? `and(eq(schema.${names.pluralCamel}.id, input.id), eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)))`
4127
4503
  : `eq(schema.${names.pluralCamel}.id, input.id)`;
4128
4504
  // drizzle-orm/mysql2 has no `.returning(...)`; mutations resolve to a
@@ -4145,13 +4521,13 @@ function affectedRows(result: unknown): number {
4145
4521
  `
4146
4522
  : "";
4147
4523
  const createMethod = dialect.supportsReturning
4148
- ? ` async create(input${options.tenant ? ", scope" : ""}) {
4524
+ ? ` async create(input${options.tenantScoped ? ", scope" : ""}) {
4149
4525
  const now = new Date().toISOString();
4150
4526
  const [row] = await db
4151
4527
  .insert(schema.${names.pluralCamel})
4152
4528
  .values({
4153
4529
  id: crypto.randomUUID(),
4154
- ${options.tenant ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
4530
+ ${options.tenantScoped ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
4155
4531
  version: 1,
4156
4532
  createdAt: now,
4157
4533
  updatedAt: now,
@@ -4165,11 +4541,11 @@ ${options.tenant ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name:
4165
4541
  return to${names.singularPascal}(row);
4166
4542
  },
4167
4543
  `
4168
- : ` async create(input${options.tenant ? ", scope" : ""}) {
4544
+ : ` async create(input${options.tenantScoped ? ", scope" : ""}) {
4169
4545
  const now = new Date().toISOString();
4170
4546
  const ${names.singularCamel}: ${names.singularPascal} = {
4171
4547
  id: crypto.randomUUID(),
4172
- ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
4548
+ ${options.tenantScoped ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
4173
4549
  version: 1,
4174
4550
  createdAt: now,
4175
4551
  updatedAt: now,
@@ -4183,7 +4559,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
4183
4559
  mode !== "resource"
4184
4560
  ? ""
4185
4561
  : dialect.supportsReturning
4186
- ? ` async findById(id: string${options.tenant ? ", scope" : ""}) {
4562
+ ? ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
4187
4563
  const [row] = await db
4188
4564
  .select()
4189
4565
  .from(schema.${names.pluralCamel})
@@ -4192,7 +4568,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
4192
4568
 
4193
4569
  return row ? to${names.singularPascal}(row) : null;
4194
4570
  },
4195
- async update(input${options.tenant ? ", scope" : ""}) {
4571
+ async update(input${options.tenantScoped ? ", scope" : ""}) {
4196
4572
  const [row] = await db
4197
4573
  .update(schema.${names.pluralCamel})
4198
4574
  .set({
@@ -4205,12 +4581,12 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
4205
4581
 
4206
4582
  return row ? to${names.singularPascal}(row) : null;
4207
4583
  },
4208
- async delete(id: string${options.tenant ? ", scope" : ""}) {
4584
+ async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
4209
4585
  ${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`}
4210
4586
  return rows.length > 0;
4211
4587
  },
4212
4588
  `
4213
- : ` async findById(id: string${options.tenant ? ", scope" : ""}) {
4589
+ : ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
4214
4590
  const [row] = await db
4215
4591
  .select()
4216
4592
  .from(schema.${names.pluralCamel})
@@ -4219,7 +4595,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
4219
4595
 
4220
4596
  return row ? to${names.singularPascal}(row) : null;
4221
4597
  },
4222
- async update(input${options.tenant ? ", scope" : ""}) {
4598
+ async update(input${options.tenantScoped ? ", scope" : ""}) {
4223
4599
  const result = await db
4224
4600
  .update(schema.${names.pluralCamel})
4225
4601
  .set({
@@ -4239,7 +4615,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
4239
4615
 
4240
4616
  return row ? to${names.singularPascal}(row) : null;
4241
4617
  },
4242
- async delete(id: string${options.tenant ? ", scope" : ""}) {
4618
+ async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
4243
4619
  ${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`}
4244
4620
  return affectedRows(result) > 0;
4245
4621
  },
@@ -4247,7 +4623,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
4247
4623
 
4248
4624
  return `import "@beignet/core/server-only";
4249
4625
  import { cursorPageResult } from "@beignet/core/pagination";
4250
- ${options.tenant ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
4626
+ ${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
4251
4627
  import { ${drizzleImports.join(", ")} } from "drizzle-orm";
4252
4628
  import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
4253
4629
  import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
@@ -4262,7 +4638,7 @@ type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelec
4262
4638
  function to${names.singularPascal}(row: ${names.singularPascal}Row): ${names.singularPascal} {
4263
4639
  return {
4264
4640
  id: row.id,
4265
- ${options.tenant ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
4641
+ ${options.tenantScoped ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
4266
4642
  version: row.version,
4267
4643
  createdAt: row.createdAt,
4268
4644
  updatedAt: row.updatedAt,
@@ -4294,9 +4670,9 @@ function ${names.singularCamel}CursorFilter(
4294
4670
 
4295
4671
  function ${names.singularCamel}ListWhere(
4296
4672
  query: List${names.pluralPascal}Query,
4297
- ${options.tenant ? `\tscope: Parameters<${names.singularPascal}Repository["list"]>[1],\n` : ""}): SQL<unknown> | undefined {
4673
+ ${options.tenantScoped ? `\tscope: Parameters<${names.singularPascal}Repository["list"]>[1],\n` : ""}): SQL<unknown> | undefined {
4298
4674
  const filters: SQL<unknown>[] = [
4299
- ${options.tenant ? `\t\teq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
4675
+ ${options.tenantScoped ? `\t\teq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
4300
4676
  const cursor = ${names.singularCamel}CursorFilter(query);
4301
4677
 
4302
4678
  if (query.name) {
@@ -4330,8 +4706,8 @@ export function createDrizzle${names.singularPascal}Repository(
4330
4706
  db: ${dialect.dbTypeName}<typeof schema>,
4331
4707
  ): ${names.singularPascal}Repository {
4332
4708
  return {
4333
- async list(query${options.tenant ? ", scope" : ""}) {
4334
- const where = ${names.singularCamel}ListWhere(query${options.tenant ? ", scope" : ""});
4709
+ async list(query${options.tenantScoped ? ", scope" : ""}) {
4710
+ const where = ${names.singularCamel}ListWhere(query${options.tenantScoped ? ", scope" : ""});
4335
4711
  const rows = await db
4336
4712
  .select()
4337
4713
  .from(schema.${names.pluralCamel})
@@ -4393,7 +4769,7 @@ export const list${names.pluralPascal} = ${names.pluralCamel}
4393
4769
  export const create${names.singularPascal} = ${names.pluralCamel}
4394
4770
  .post("/api/${names.pluralKebab}")
4395
4771
  .body(Create${names.singularPascal}InputSchema)
4396
- ${options.auth ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
4772
+ ${options.authorization ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
4397
4773
  201: ${names.singularPascal}Schema,
4398
4774
  });
4399
4775
  ${
@@ -4411,7 +4787,7 @@ export const update${names.singularPascal} = ${names.pluralCamel}
4411
4787
  .patch("/api/${names.pluralKebab}/:id")
4412
4788
  .pathParams(${names.singularPascal}IdInputSchema)
4413
4789
  .body(Update${names.singularPascal}BodySchema)
4414
- ${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 })
4790
+ ${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 })
4415
4791
  .responses({
4416
4792
  200: ${names.singularPascal}Schema,
4417
4793
  });
@@ -4419,7 +4795,7 @@ ${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability
4419
4795
  export const delete${names.singularPascal} = ${names.pluralCamel}
4420
4796
  .delete("/api/${names.pluralKebab}/:id")
4421
4797
  .pathParams(${names.singularPascal}IdInputSchema)
4422
- ${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 })
4798
+ ${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 })
4423
4799
  .responses({
4424
4800
  204: null,
4425
4801
  });
@@ -4624,7 +5000,7 @@ function isAuthenticated(ctx: AuthorizationContext) {
4624
5000
 
4625
5001
  function canWrite(ctx: AuthorizationContext) {
4626
5002
  if (!isAuthenticated(ctx)) return deny("You must be signed in.");
4627
- ${options.tenant ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
5003
+ ${options.tenantScoped ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
4628
5004
  }
4629
5005
 
4630
5006
  export const ${names.singularCamel}Policy = definePolicy({
@@ -4634,13 +5010,13 @@ export const ${names.singularCamel}Policy = definePolicy({
4634
5010
  "${names.pluralCamel}.update": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
4635
5011
  const decision = canWrite(ctx);
4636
5012
  if (decision !== true) return decision;
4637
- ${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` : ""}
5013
+ ${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` : ""}
4638
5014
  return true;
4639
5015
  },
4640
5016
  "${names.pluralCamel}.delete": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
4641
5017
  const decision = canWrite(ctx);
4642
5018
  if (decision !== true) return decision;
4643
- ${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` : ""}
5019
+ ${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` : ""}
4644
5020
  return true;
4645
5021
  },
4646
5022
  "${names.pluralCamel}.manage": (_ctx: AuthorizationContext) =>
@@ -4692,7 +5068,7 @@ function create${names.singularPascal}(): ${names.singularPascal} {
4692
5068
  const now = new Date().toISOString();
4693
5069
  return {
4694
5070
  id: "00000000-0000-4000-8000-000000000001",
4695
- ${options.tenant ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
5071
+ ${options.tenantScoped ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
4696
5072
  version: 1,
4697
5073
  createdAt: now,
4698
5074
  updatedAt: now,
@@ -4707,7 +5083,7 @@ describe("${names.singularCamel}Policy", () => {
4707
5083
  const ${names.singularCamel} = create${names.singularPascal}();
4708
5084
  const ctx = {
4709
5085
  actor: { type: "user" as const, id: "user_test" },
4710
- ${options.tenant ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
5086
+ ${options.tenantScoped ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
4711
5087
 
4712
5088
  await expect(
4713
5089
  tester.assertMatrix([
@@ -4933,8 +5309,33 @@ ${names.timezone ? `\t\ttimezone: "${names.timezone}",\n` : ""}\t\tpayload: ${na
4933
5309
  `;
4934
5310
  }
4935
5311
 
5312
+ function uploadManifestFile(names: UploadNames): string {
5313
+ return `import type {
5314
+ \tUploadFileConstraints,
5315
+ \tUploadManifestEntry,
5316
+ } from "@beignet/core/uploads";
5317
+
5318
+ export const ${names.fileConstraintsExportName} = {
5319
+ \tcontentTypes: ["application/pdf", "text/plain"],
5320
+ \tmaxSizeBytes: 5 * 1024 * 1024,
5321
+ \tmaxFiles: 1,
5322
+ \tvisibility: "private",
5323
+ \tcacheControl: "private, max-age=0",
5324
+ } satisfies UploadFileConstraints;
5325
+
5326
+ export const ${names.manifestExportName} = [
5327
+ \t{
5328
+ \t\tname: "${names.uploadName}",
5329
+ \t\tdescription: "${names.artifact.pascal} upload",
5330
+ \t\tfile: ${names.fileConstraintsExportName},
5331
+ \t},
5332
+ ] satisfies readonly UploadManifestEntry[];
5333
+ `;
5334
+ }
5335
+
4936
5336
  function uploadFile(names: UploadNames, config: ResolvedBeignetConfig): string {
4937
5337
  return `import { z } from "zod";
5338
+ import { ${names.fileConstraintsExportName} } from "${relativeModule(uploadFilePath(names, config), uploadManifestFilePath(names, config))}";
4938
5339
  import { defineUpload } from "${aliasModule(config.paths.uploadsBuilder)}";
4939
5340
 
4940
5341
  export const ${names.metadataSchemaName} = z.object({
@@ -4945,13 +5346,7 @@ export type ${names.metadataTypeName} = z.infer<typeof ${names.metadataSchemaNam
4945
5346
 
4946
5347
  export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
4947
5348
  \tmetadata: ${names.metadataSchemaName},
4948
- \tfile: {
4949
- \t\tcontentTypes: ["application/pdf", "text/plain"],
4950
- \t\tmaxSizeBytes: 5 * 1024 * 1024,
4951
- \t\tmaxFiles: 1,
4952
- \t\tvisibility: "private",
4953
- \t\tcacheControl: "private, max-age=0",
4954
- \t},
5349
+ \tfile: ${names.fileConstraintsExportName},
4955
5350
  \tauthorize({ ctx }) {
4956
5351
  \t\treturn ctx.actor.type === "user";
4957
5352
  \t},
@@ -4982,6 +5377,148 @@ export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
4982
5377
  `;
4983
5378
  }
4984
5379
 
5380
+ function uploadUiClientFile(
5381
+ names: UploadNames,
5382
+ config: ResolvedBeignetConfig,
5383
+ ): string {
5384
+ const clientPath = uploadUiClientFilePath(names, config);
5385
+ const registryPath = path.join(
5386
+ featureArtifactDir(names, "uploads", config),
5387
+ "index.ts",
5388
+ );
5389
+
5390
+ return `import { createUploadClient } from "@beignet/core/uploads/client";
5391
+ import { createReactUploads } from "@beignet/react-uploads";
5392
+ import { ${names.manifestExportName} } from "${relativeModule(clientPath, uploadManifestFilePath(names, config))}";
5393
+ import type { ${names.featureSingularCamel}Uploads } from "${relativeModule(clientPath, registryPath)}";
5394
+
5395
+ const ${names.artifact.camel}UploadClient = createUploadClient<typeof ${names.featureSingularCamel}Uploads>({
5396
+ \tbaseUrl: "/api/uploads",
5397
+ \tmanifest: ${names.manifestExportName},
5398
+ });
5399
+
5400
+ export const ${names.reactUploadsExportName} = createReactUploads({
5401
+ \tuploads: ${names.artifact.camel}UploadClient,
5402
+ });
5403
+ `;
5404
+ }
5405
+
5406
+ function uploadUiComponentFile(
5407
+ names: UploadNames,
5408
+ config: ResolvedBeignetConfig,
5409
+ ): string {
5410
+ const componentPath = uploadUiComponentFilePath(names, config);
5411
+
5412
+ return `"use client";
5413
+
5414
+ import { useId } from "react";
5415
+ import { ${names.reactUploadsExportName} } from "${relativeModule(componentPath, uploadUiClientFilePath(names, config))}";
5416
+
5417
+ export type ${names.componentExportName}Props = {
5418
+ \tresourceId: string;
5419
+ \tdisabled?: boolean;
5420
+ \tonComplete?(objectKeys: readonly string[]): void | Promise<void>;
5421
+ };
5422
+
5423
+ export function ${names.componentExportName}({
5424
+ \tresourceId,
5425
+ \tdisabled = false,
5426
+ \tonComplete,
5427
+ }: ${names.componentExportName}Props) {
5428
+ \tconst inputId = useId();
5429
+ \tconst upload = ${names.reactUploadsExportName}.useUpload("${names.uploadName}", {
5430
+ \t\tonSuccess({ result }) {
5431
+ \t\t\treturn onComplete?.(result.objectKeys);
5432
+ \t\t},
5433
+ \t});
5434
+ \tconst maxSizeMb = upload.constraints?.maxSizeBytes
5435
+ \t\t? Math.round(upload.constraints.maxSizeBytes / 1024 / 1024)
5436
+ \t\t: undefined;
5437
+
5438
+ \treturn (
5439
+ \t\t<section className="${names.artifact.kebab}-uploader">
5440
+ \t\t\t<label htmlFor={inputId}>${names.artifact.pascal}</label>
5441
+ \t\t\t<input
5442
+ \t\t\t\tid={inputId}
5443
+ \t\t\t\ttype="file"
5444
+ \t\t\t\taccept={upload.accept}
5445
+ \t\t\t\tdisabled={disabled || upload.isUploading}
5446
+ \t\t\t\tonChange={(event) => {
5447
+ \t\t\t\t\tconst file = event.currentTarget.files?.[0];
5448
+ \t\t\t\t\tevent.currentTarget.value = "";
5449
+ \t\t\t\t\tif (!file) return;
5450
+
5451
+ \t\t\t\t\tupload.uploadFile(file, {
5452
+ \t\t\t\t\t\tmetadata: { resourceId },
5453
+ \t\t\t\t\t});
5454
+ \t\t\t\t}}
5455
+ \t\t\t/>
5456
+ \t\t\t<p>
5457
+ \t\t\t\tPDF or plain text{maxSizeMb ? <> up to {maxSizeMb} MB</> : null}.
5458
+ \t\t\t</p>
5459
+
5460
+ \t\t\t{upload.files[0] ? <p>{upload.files[0].fileName}</p> : null}
5461
+ \t\t\t{upload.isUploading ? (
5462
+ \t\t\t\t<div aria-live="polite">
5463
+ \t\t\t\t\t<progress
5464
+ \t\t\t\t\t\taria-label="Upload progress"
5465
+ \t\t\t\t\t\tmax={100}
5466
+ \t\t\t\t\t\tvalue={upload.progress}
5467
+ \t\t\t\t\t/>
5468
+ \t\t\t\t\t<span>{Math.round(upload.progress)}%</span>
5469
+ \t\t\t\t\t<button type="button" onClick={upload.abort}>
5470
+ \t\t\t\t\t\tCancel upload
5471
+ \t\t\t\t\t</button>
5472
+ \t\t\t\t</div>
5473
+ \t\t\t) : null}
5474
+ \t\t\t{upload.isError ? (
5475
+ \t\t\t\t<p role="alert">{uploadErrorMessage(upload.error)}</p>
5476
+ \t\t\t) : null}
5477
+ \t\t\t{upload.isSuccess ? <p role="status">Upload complete.</p> : null}
5478
+ \t\t\t{upload.isError || upload.isSuccess ? (
5479
+ \t\t\t\t<button type="button" onClick={upload.reset}>
5480
+ \t\t\t\t\tReset upload
5481
+ \t\t\t\t</button>
5482
+ \t\t\t) : null}
5483
+ \t\t</section>
5484
+ \t);
5485
+ }
5486
+
5487
+ function uploadErrorMessage(error: unknown): string {
5488
+ \treturn error instanceof Error ? error.message : "Upload failed.";
5489
+ }
5490
+ `;
5491
+ }
5492
+
5493
+ function uploadUiComponentTestFile(
5494
+ names: UploadNames,
5495
+ config: ResolvedBeignetConfig,
5496
+ ): string {
5497
+ const testPath = uploadUiComponentTestFilePath(names, config);
5498
+ const testHelperPath = path.join(
5499
+ path.dirname(config.paths.useCaseBuilder),
5500
+ "beignet-test.ts",
5501
+ );
5502
+
5503
+ return `import { renderToStaticMarkup } from "react-dom/server";
5504
+ import { describe, expect, test } from "${relativeModule(testPath, testHelperPath)}";
5505
+ import { ${names.manifestExportName} } from "${relativeModule(testPath, uploadManifestFilePath(names, config))}";
5506
+ import { ${names.componentExportName} } from "${relativeModule(testPath, uploadUiComponentFilePath(names, config))}";
5507
+
5508
+ describe("${names.componentExportName}", () => {
5509
+ \ttest("renders the generated upload constraints", () => {
5510
+ \t\tconst markup = renderToStaticMarkup(
5511
+ \t\t\t<${names.componentExportName} resourceId="resource-1" />,
5512
+ \t\t);
5513
+
5514
+ \t\texpect(${names.manifestExportName}[0]?.file.maxSizeBytes).toBe(5 * 1024 * 1024);
5515
+ \t\texpect(markup).toContain('accept="application/pdf,text/plain"');
5516
+ \t\texpect(markup).toContain("PDF or plain text");
5517
+ \t});
5518
+ });
5519
+ `;
5520
+ }
5521
+
4985
5522
  function uploadRouteFile(
4986
5523
  names: UploadNames,
4987
5524
  config: ResolvedBeignetConfig,
@@ -5101,63 +5638,70 @@ function featureUiComponentFile(
5101
5638
 
5102
5639
  return `"use client";
5103
5640
 
5104
- import { contractErrorMessage } from "@beignet/core/client";
5641
+ import { rootFormError } from "@beignet/react-hook-form";
5105
5642
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
5106
- import { useState } from "react";
5643
+ import { rhf } from "${aliasModule(clientFormsPath(config))}";
5644
+ import { create${names.singularPascal} } from "${relativeModule(componentPath, resourceContractFilePath(names, config))}";
5107
5645
  import {
5108
5646
  \tcreate${names.singularPascal}MutationOptions,
5109
5647
  \tinvalidate${names.pluralPascal},
5110
5648
  \tlist${names.pluralPascal}QueryOptions,
5111
5649
  } from "${relativeModule(componentPath, clientQueriesPath)}";
5112
5650
 
5651
+ const create${names.singularPascal}Form = rhf(create${names.singularPascal});
5652
+
5113
5653
  export function ${names.componentExportName}() {
5114
- \tconst [name, setName] = useState("");
5115
5654
  \tconst queryClient = useQueryClient();
5116
5655
  \tconst ${names.pluralCamel}Query = useQuery(list${names.pluralPascal}QueryOptions());
5656
+ \tconst form = create${names.singularPascal}Form.useForm({
5657
+ \t\tdefaultValues: { name: "" },
5658
+ \t});
5659
+ \tconst name = form.watch("name");
5117
5660
  \tconst create${names.singularPascal}Mutation = useMutation({
5118
5661
  \t\t...create${names.singularPascal}MutationOptions(),
5119
5662
  \t\tonSuccess: async () => {
5120
- \t\t\tsetName("");
5663
+ \t\t\tform.reset({ name: "" });
5121
5664
  \t\t\tawait invalidate${names.pluralPascal}(queryClient);
5122
5665
  \t\t},
5666
+ \t\tonError: (error) => {
5667
+ \t\t\tform.setError(
5668
+ \t\t\t\t"root",
5669
+ \t\t\t\trootFormError(error, "Could not create ${names.singularKebab}."),
5670
+ \t\t\t);
5671
+ \t\t},
5123
5672
  \t});
5124
5673
 
5125
5674
  \treturn (
5126
5675
  \t\t<section className="${names.pluralKebab}-panel">
5127
5676
  \t\t\t<form
5128
5677
  \t\t\t\tclassName="${names.pluralKebab}-composer"
5129
- \t\t\t\tonSubmit={(event) => {
5130
- \t\t\t\t\tevent.preventDefault();
5131
- \t\t\t\t\tconst trimmedName = name.trim();
5132
- \t\t\t\t\tif (!trimmedName) return;
5678
+ \t\t\t\tonSubmit={form.handleSubmit((values) => {
5679
+ \t\t\t\t\tform.clearErrors("root");
5133
5680
  \t\t\t\t\tcreate${names.singularPascal}Mutation.reset();
5134
5681
  \t\t\t\t\tcreate${names.singularPascal}Mutation.mutate({
5135
- \t\t\t\t\t\tbody: { name: trimmedName },
5682
+ \t\t\t\t\t\tbody: values,
5136
5683
  \t\t\t\t\t});
5137
- \t\t\t\t}}
5684
+ \t\t\t\t})}
5138
5685
  \t\t\t>
5139
5686
  \t\t\t\t<label htmlFor="${names.singularKebab}-name">New ${names.singularKebab}</label>
5140
5687
  \t\t\t\t<div className="${names.pluralKebab}-composer-row">
5141
5688
  \t\t\t\t\t<input
5142
5689
  \t\t\t\t\t\tid="${names.singularKebab}-name"
5143
- \t\t\t\t\t\tvalue={name}
5144
- \t\t\t\t\t\tonChange={(event) => setName(event.currentTarget.value)}
5690
+ \t\t\t\t\t\t{...form.register("name")}
5145
5691
  \t\t\t\t\t\tplaceholder="Name"
5146
5692
  \t\t\t\t\t/>
5147
5693
  \t\t\t\t\t<button
5148
5694
  \t\t\t\t\t\ttype="submit"
5149
- \t\t\t\t\t\tdisabled={!name.trim() || create${names.singularPascal}Mutation.isPending}
5695
+ \t\t\t\t\t\tdisabled={!name?.trim() || create${names.singularPascal}Mutation.isPending}
5150
5696
  \t\t\t\t\t>
5151
5697
  \t\t\t\t\t\t{create${names.singularPascal}Mutation.isPending ? "Creating" : "Create"}
5152
5698
  \t\t\t\t\t</button>
5153
5699
  \t\t\t\t</div>
5154
- \t\t\t\t{create${names.singularPascal}Mutation.isError ? (
5155
- \t\t\t\t\t<p role="alert">
5156
- \t\t\t\t\t\t{contractErrorMessage(
5157
- \t\t\t\t\t\t\tcreate${names.singularPascal}Mutation.error,
5158
- \t\t\t\t\t\t\t"Could not create ${names.singularKebab}.",
5159
- \t\t\t\t\t\t)}
5160
- \t\t\t\t\t</p>
5700
+ \t\t\t\t{form.formState.errors.name ? (
5701
+ \t\t\t\t\t<p role="alert">{form.formState.errors.name.message}</p>
5702
+ \t\t\t\t) : null}
5703
+ \t\t\t\t{form.formState.errors.root ? (
5704
+ \t\t\t\t\t<p role="alert">{form.formState.errors.root.message}</p>
5161
5705
  \t\t\t\t) : null}
5162
5706
  \t\t\t</form>
5163
5707
 
@@ -5306,7 +5850,7 @@ ${mode === "resource" ? `import { isAppError } from "@beignet/core/errors";\n` :
5306
5850
  import { createTestApp } from "@beignet/web/testing";
5307
5851
  import { createInMemoryDevtools } from "@beignet/devtools";
5308
5852
  import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
5309
- import { ${options.auth ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenant ? ", createTestTenant" : ""} } from "@beignet/core/testing";
5853
+ import { ${options.authorization ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenantScoped ? ", createTestTenant" : ""} } from "@beignet/core/testing";
5310
5854
  import type { AppContext } from "${aliasModule(config.paths.appContext)}";
5311
5855
  import { initialPorts } from "${aliasModule(config.paths.portWiring)}";
5312
5856
  import { appContext } from "${aliasModule(serverContextPath)}";
@@ -5326,40 +5870,40 @@ describe("${names.pluralCamel} resource", () => {
5326
5870
  const seed${names.pluralPascal} = [
5327
5871
  {
5328
5872
  id: "00000000-0000-4000-8000-000000000101",
5329
- ${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
5873
+ ${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
5330
5874
  version: 1,
5331
5875
  createdAt: "2024-01-01T00:00:00.000Z",
5332
5876
  updatedAt: "2024-01-01T00:00:00.000Z",
5333
5877
  },
5334
5878
  {
5335
5879
  id: "00000000-0000-4000-8000-000000000102",
5336
- ${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
5880
+ ${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
5337
5881
  version: 1,
5338
5882
  createdAt: "2024-01-02T00:00:00.000Z",
5339
5883
  updatedAt: "2024-01-02T00:00:00.000Z",
5340
5884
  },
5341
5885
  {
5342
5886
  id: "00000000-0000-4000-8000-000000000103",
5343
- ${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
5887
+ ${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
5344
5888
  version: 1,
5345
5889
  createdAt: "2024-01-03T00:00:00.000Z",
5346
5890
  updatedAt: "2024-01-03T00:00:00.000Z",
5347
5891
  },
5348
- ${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` : ""} ];
5892
+ ${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` : ""} ];
5349
5893
  const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository(seed${names.pluralPascal});
5350
5894
  const testFixture = createTestPorts<AppContext["ports"]>({
5351
5895
  base: initialPorts,
5352
5896
  overrides: {
5353
5897
  ${names.pluralCamel},
5354
5898
  ${
5355
- options.auth || options.tenant
5899
+ options.authorization || options.tenantScoped
5356
5900
  ? `\t\t\t\tauth: {
5357
5901
  getSession: async () => ({
5358
- user: { id: "user_test", name: "Test User"${options.tenant ? `, tenantId: "tenant_test"` : ""} },
5359
- ${options.tenant ? `\t\t\t\t\t\tsession: { tenantId: "tenant_test" },\n` : ""}
5902
+ user: { id: "user_test", name: "Test User"${options.tenantScoped ? `, tenantId: "tenant_test"` : ""} },
5903
+ ${options.tenantScoped ? `\t\t\t\t\t\tsession: { tenantId: "tenant_test" },\n` : ""}
5360
5904
  }),
5361
5905
  },
5362
- ${options.auth ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
5906
+ ${options.authorization ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
5363
5907
  `
5364
5908
  : `\t\t\t\tauth: {
5365
5909
  getSession: async () => null,
@@ -5375,8 +5919,8 @@ ${options.auth ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
5375
5919
  });
5376
5920
  const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
5377
5921
  ports: testFixture.ports,
5378
- actor: ${options.auth ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
5379
- tenant: ${options.tenant ? `createTestTenant("tenant_test")` : "null"},
5922
+ actor: ${options.authorization ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
5923
+ tenant: ${options.tenantScoped ? `createTestTenant("tenant_test")` : "null"},
5380
5924
  });
5381
5925
  const tester = createUseCaseTester<AppContext>(createTestContext);
5382
5926