@beignet/cli 0.0.38 → 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 (77) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +120 -96
  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/create-prompts.d.ts +5 -6
  8. package/dist/create-prompts.d.ts.map +1 -1
  9. package/dist/create-prompts.js +15 -15
  10. package/dist/create-prompts.js.map +1 -1
  11. package/dist/create.d.ts +11 -2
  12. package/dist/create.d.ts.map +1 -1
  13. package/dist/create.js +38 -28
  14. package/dist/create.js.map +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +149 -47
  17. package/dist/index.js.map +1 -1
  18. package/dist/inspect.js +54 -1
  19. package/dist/inspect.js.map +1 -1
  20. package/dist/lib.d.ts +1 -1
  21. package/dist/lib.d.ts.map +1 -1
  22. package/dist/make/inbox.js +3 -3
  23. package/dist/make/inbox.js.map +1 -1
  24. package/dist/make/payments.js +3 -3
  25. package/dist/make/payments.js.map +1 -1
  26. package/dist/make/shared.d.ts +10 -3
  27. package/dist/make/shared.d.ts.map +1 -1
  28. package/dist/make/shared.js +7 -8
  29. package/dist/make/shared.js.map +1 -1
  30. package/dist/make.d.ts +3 -2
  31. package/dist/make.d.ts.map +1 -1
  32. package/dist/make.js +455 -185
  33. package/dist/make.js.map +1 -1
  34. package/dist/mcp.d.ts.map +1 -1
  35. package/dist/mcp.js +15 -11
  36. package/dist/mcp.js.map +1 -1
  37. package/dist/provider-add.d.ts +21 -3
  38. package/dist/provider-add.d.ts.map +1 -1
  39. package/dist/provider-add.js +101 -65
  40. package/dist/provider-add.js.map +1 -1
  41. package/dist/task.js +1 -1
  42. package/dist/task.js.map +1 -1
  43. package/dist/templates/base.d.ts +1 -1
  44. package/dist/templates/base.d.ts.map +1 -1
  45. package/dist/templates/base.js +15 -15
  46. package/dist/templates/base.js.map +1 -1
  47. package/dist/templates/index.d.ts +2 -2
  48. package/dist/templates/index.d.ts.map +1 -1
  49. package/dist/templates/index.js +5 -5
  50. package/dist/templates/index.js.map +1 -1
  51. package/dist/templates/server.d.ts.map +1 -1
  52. package/dist/templates/server.js +17 -13
  53. package/dist/templates/server.js.map +1 -1
  54. package/dist/templates/shared.d.ts +4 -4
  55. package/dist/templates/shared.d.ts.map +1 -1
  56. package/dist/templates/shared.js +6 -6
  57. package/dist/templates/shared.js.map +1 -1
  58. package/package.json +2 -2
  59. package/skills/app-structure/SKILL.md +13 -8
  60. package/src/choices.ts +38 -88
  61. package/src/create-prompts.ts +20 -21
  62. package/src/create.ts +54 -36
  63. package/src/index.ts +193 -65
  64. package/src/inspect.ts +101 -1
  65. package/src/lib.ts +1 -1
  66. package/src/make/inbox.ts +3 -3
  67. package/src/make/payments.ts +3 -3
  68. package/src/make/shared.ts +16 -10
  69. package/src/make.ts +578 -176
  70. package/src/mcp.ts +20 -13
  71. package/src/provider-add.ts +159 -79
  72. package/src/task.ts +1 -1
  73. package/src/templates/base.ts +16 -16
  74. package/src/templates/index.ts +6 -6
  75. package/src/templates/server.ts +17 -13
  76. package/src/templates/shared.ts +10 -10
  77. 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),
@@ -1043,7 +1047,7 @@ export async function makeEvent(
1043
1047
  config,
1044
1048
  force: Boolean(options.force),
1045
1049
  dryRun: Boolean(options.dryRun),
1046
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1050
+ name: names.stableName,
1047
1051
  files: eventFiles(names, config),
1048
1052
  index: featureArtifactIndexFile("event", names, config),
1049
1053
  dependencies: {
@@ -1090,7 +1094,7 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
1090
1094
  config,
1091
1095
  force: Boolean(options.force),
1092
1096
  dryRun: Boolean(options.dryRun),
1093
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1097
+ name: names.stableName,
1094
1098
  files: [
1095
1099
  ...(await missingCapabilityBuilderFiles(targetDir, "jobs", config)),
1096
1100
  ...jobFiles(names, config),
@@ -1130,7 +1134,7 @@ export async function makeTask(
1130
1134
  config,
1131
1135
  force: Boolean(options.force),
1132
1136
  dryRun: Boolean(options.dryRun),
1133
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1137
+ name: names.stableName,
1134
1138
  files: [
1135
1139
  ...(await missingCapabilityBuilderFiles(targetDir, "tasks", config)),
1136
1140
  ...taskFiles(names, config),
@@ -1175,7 +1179,7 @@ export async function makeFactory(
1175
1179
  config,
1176
1180
  force: Boolean(options.force),
1177
1181
  dryRun: Boolean(options.dryRun),
1178
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1182
+ name: names.stableName,
1179
1183
  files: factoryFiles(names, config),
1180
1184
  index: featureArtifactIndexFile("factory", names, config),
1181
1185
  dependencies: {
@@ -1202,7 +1206,7 @@ export async function makeSeed(
1202
1206
  config,
1203
1207
  force: Boolean(options.force),
1204
1208
  dryRun: Boolean(options.dryRun),
1205
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1209
+ name: names.stableName,
1206
1210
  files: seedFiles(names, config),
1207
1211
  index: featureArtifactIndexFile("seed", names, config),
1208
1212
  dependencies: {
@@ -1221,7 +1225,7 @@ export async function makeSeed(
1221
1225
  config,
1222
1226
  force: Boolean(options.force),
1223
1227
  dryRun: Boolean(options.dryRun),
1224
- name: `${factoryNamesForSeed.feature.kebab}/${factoryNamesForSeed.artifact.kebab}`,
1228
+ name: factoryNamesForSeed.stableName,
1225
1229
  files: factoryFiles(factoryNamesForSeed, config),
1226
1230
  index: featureArtifactIndexFile("factory", factoryNamesForSeed, config),
1227
1231
  dependencies: {
@@ -1277,7 +1281,7 @@ export async function makeNotification(
1277
1281
  // notifications uses ctx.ports.notifications, so the generator wires both
1278
1282
  // ports with dev-default providers. Each port is wired independently and
1279
1283
  // skipped when the app already has it, for example when the resend
1280
- // integration contributed the mailer. Plan the wiring before writing any
1284
+ // provider contributed the mailer. Plan the wiring before writing any
1281
1285
  // files so an anchor miss aborts the generator without leaving partial
1282
1286
  // output.
1283
1287
  const notificationWirings = notificationPortWirings("make notification");
@@ -1290,7 +1294,7 @@ export async function makeNotification(
1290
1294
  config,
1291
1295
  force: Boolean(options.force),
1292
1296
  dryRun: Boolean(options.dryRun),
1293
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1297
+ name: names.stableName,
1294
1298
  files: [
1295
1299
  ...(await missingCapabilityBuilderFiles(
1296
1300
  targetDir,
@@ -1341,7 +1345,7 @@ export async function makeListener(
1341
1345
  config,
1342
1346
  force: Boolean(options.force),
1343
1347
  dryRun: Boolean(options.dryRun),
1344
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1348
+ name: names.stableName,
1345
1349
  files: [
1346
1350
  ...(await missingCapabilityBuilderFiles(targetDir, "listeners", config)),
1347
1351
  ...listenerFiles(names, config),
@@ -1445,7 +1449,7 @@ async function assertListenerEventExists(
1445
1449
  await stat(path.join(targetDir, file));
1446
1450
  } catch {
1447
1451
  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.`,
1452
+ `beignet make listener expected event ${names.eventName} at ${file}. Run beignet make event ${names.eventName} first, or pass an event that exists.`,
1449
1453
  );
1450
1454
  }
1451
1455
  }
@@ -1515,7 +1519,7 @@ export async function makeSchedule(
1515
1519
  config,
1516
1520
  force: Boolean(options.force),
1517
1521
  dryRun: Boolean(options.dryRun),
1518
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1522
+ name: names.stableName,
1519
1523
  files,
1520
1524
  index: featureArtifactIndexFile("schedule", names, config),
1521
1525
  dependencies: {
@@ -2065,6 +2069,9 @@ export async function makeUpload(
2065
2069
  : await loadBeignetConfig(targetDir);
2066
2070
 
2067
2071
  await assertFeatureArtifactApp(targetDir, config, "upload");
2072
+ if (options.ui) {
2073
+ await assertUploadUiApp(targetDir, config);
2074
+ }
2068
2075
 
2069
2076
  await wirePortProviders(
2070
2077
  targetDir,
@@ -2080,10 +2087,10 @@ export async function makeUpload(
2080
2087
  config,
2081
2088
  force: Boolean(options.force),
2082
2089
  dryRun: Boolean(options.dryRun),
2083
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
2090
+ name: names.stableName,
2084
2091
  files: [
2085
2092
  ...(await missingCapabilityBuilderFiles(targetDir, "uploads", config)),
2086
- ...uploadFiles(names, config),
2093
+ ...uploadFiles(names, config, { ui: Boolean(options.ui) }),
2087
2094
  ],
2088
2095
  index: featureArtifactIndexFile("upload", names, config),
2089
2096
  dependencies: {
@@ -2112,6 +2119,44 @@ export async function makeUpload(
2112
2119
  result.files.push(uploadRoutePath(config));
2113
2120
  }
2114
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
+
2115
2160
  return result;
2116
2161
  }
2117
2162
 
@@ -2195,7 +2240,10 @@ async function makeFeatureUi(
2195
2240
 
2196
2241
  await assertFeatureUiApp(targetDir, config);
2197
2242
 
2198
- const componentFiles = featureUiComponentFiles(names, config);
2243
+ const componentFiles = [
2244
+ ...(await missingFeatureUiFormsFiles(targetDir, config)),
2245
+ ...featureUiComponentFiles(names, config),
2246
+ ];
2199
2247
  const indexFile = featureUiIndexFile(names, config);
2200
2248
  const plannedFiles = await planGeneratedFiles(targetDir, componentFiles, {
2201
2249
  force: Boolean(options.force),
@@ -2227,8 +2275,15 @@ async function makeFeatureUi(
2227
2275
  await updatePackageDependencies(
2228
2276
  targetDir,
2229
2277
  {
2278
+ "@beignet/react-hook-form": beignetDependencyVersion,
2230
2279
  "@beignet/react-query": beignetDependencyVersion,
2280
+ "@hookform/resolvers": (packageJson) =>
2281
+ packageJson.dependencies?.["@hookform/resolvers"] ??
2282
+ externalVersions.hookformResolvers,
2231
2283
  "@tanstack/react-query": tanstackReactQueryDependencyVersion,
2284
+ "react-hook-form": (packageJson) =>
2285
+ packageJson.dependencies?.["react-hook-form"] ??
2286
+ externalVersions.reactHookForm,
2232
2287
  },
2233
2288
  { dryRun: Boolean(options.dryRun) },
2234
2289
  )
@@ -2370,7 +2425,7 @@ async function updateResourceWiring(
2370
2425
  updated.add(config.paths.portWiring);
2371
2426
  }
2372
2427
  if (
2373
- generationOptions.auth &&
2428
+ generationOptions.authorization &&
2374
2429
  (await updateInfrastructureGatePolicies(targetDir, names, config, options))
2375
2430
  ) {
2376
2431
  updated.add(config.paths.portWiring);
@@ -2432,13 +2487,12 @@ function typeHasTopLevelProperty(
2432
2487
  function useCaseNames(input: string): UseCaseNames {
2433
2488
  const parts = input
2434
2489
  .trim()
2435
- .split("/")
2436
- .map((part) => part.trim())
2437
- .filter(Boolean);
2490
+ .split(".")
2491
+ .map((part) => part.trim());
2438
2492
 
2439
- if (parts.length !== 2) {
2493
+ if (parts.length !== 2 || parts.some((part) => part.length === 0)) {
2440
2494
  throw new Error(
2441
- "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.",
2442
2496
  );
2443
2497
  }
2444
2498
 
@@ -2478,7 +2532,7 @@ function policyNames(input: string): PolicyNames {
2478
2532
  }
2479
2533
 
2480
2534
  function eventNames(input: string): EventNames {
2481
- const names = featureArtifactNames(normalizeEventReference(input), "Event");
2535
+ const names = featureArtifactNames(input, "Event");
2482
2536
  const eventExportName = `${names.featureSingularPascal}${names.artifact.pascal}`;
2483
2537
 
2484
2538
  assertGeneratedIdentifiers(input, "Event name", [eventExportName]);
@@ -2536,7 +2590,7 @@ function factoryNames(input: string): FactoryNames {
2536
2590
  }
2537
2591
 
2538
2592
  function defaultFactoryNamesForSeed(names: SeedNames): FactoryNames {
2539
- return factoryNames(`${names.feature.kebab}/${names.featureSingularKebab}`);
2593
+ return factoryNames(`${names.feature.kebab}.${names.featureSingularKebab}`);
2540
2594
  }
2541
2595
 
2542
2596
  function notificationNames(input: string): NotificationNames {
@@ -2593,8 +2647,12 @@ function scheduleNames(
2593
2647
  function uploadNames(input: string): UploadNames {
2594
2648
  const names = featureArtifactNames(input, "Upload");
2595
2649
  const uploadExportName = `${names.artifact.pascal}Upload`;
2650
+ const componentExportName = `${names.artifact.pascal}Uploader`;
2596
2651
 
2597
- assertGeneratedIdentifiers(input, "Upload name", [uploadExportName]);
2652
+ assertGeneratedIdentifiers(input, "Upload name", [
2653
+ uploadExportName,
2654
+ componentExportName,
2655
+ ]);
2598
2656
 
2599
2657
  return {
2600
2658
  ...names,
@@ -2602,19 +2660,16 @@ function uploadNames(input: string): UploadNames {
2602
2660
  uploadExportName,
2603
2661
  metadataSchemaName: `${uploadExportName}MetadataSchema`,
2604
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`,
2605
2670
  };
2606
2671
  }
2607
2672
 
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
2673
  function inferUseCaseKind(action: string): "command" | "query" {
2619
2674
  if (/^(count|find|get|list|search)-/.test(`${action}-`)) {
2620
2675
  return "query";
@@ -2628,8 +2683,8 @@ function resourceFiles(
2628
2683
  persistence: ResourcePersistence = "memory",
2629
2684
  mode: ResourceGenerationMode = "feature",
2630
2685
  options: ResourceGenerationOptions = {
2631
- auth: false,
2632
- tenant: false,
2686
+ authorization: false,
2687
+ tenantScoped: false,
2633
2688
  events: false,
2634
2689
  softDelete: false,
2635
2690
  },
@@ -2684,7 +2739,7 @@ function resourceFiles(
2684
2739
  },
2685
2740
  ]
2686
2741
  : []),
2687
- ...(mode === "resource" && options.auth
2742
+ ...(mode === "resource" && options.authorization
2688
2743
  ? [
2689
2744
  {
2690
2745
  path: path.join(featureDir, "tests/policy.test.ts"),
@@ -2754,6 +2809,99 @@ function featureUiComponentFiles(
2754
2809
  ];
2755
2810
  }
2756
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
+
2757
2905
  function featureUiIndexFile(
2758
2906
  names: FeatureUiNames,
2759
2907
  config: ResolvedBeignetConfig,
@@ -2949,12 +3097,98 @@ function scheduleFiles(
2949
3097
  function uploadFiles(
2950
3098
  names: UploadNames,
2951
3099
  config: ResolvedBeignetConfig,
3100
+ options: { ui: boolean },
2952
3101
  ): GeneratedFile[] {
2953
3102
  return [
3103
+ {
3104
+ path: uploadManifestFilePath(names, config),
3105
+ content: uploadManifestFile(names),
3106
+ },
2954
3107
  {
2955
3108
  path: uploadFilePath(names, config),
2956
3109
  content: uploadFile(names, config),
2957
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
+ },
2958
3192
  ];
2959
3193
  }
2960
3194
 
@@ -3447,7 +3681,7 @@ function schemasFile(
3447
3681
 
3448
3682
  export const ${names.singularPascal}Schema = z.object({
3449
3683
  id: z.string().uuid(),
3450
- ${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),
3451
3685
  version: z.number().int().min(1),
3452
3686
  createdAt: z.string().datetime(),
3453
3687
  updatedAt: z.string().datetime(),
@@ -3547,7 +3781,7 @@ export const List${names.pluralPascal}OutputSchema = z.object({
3547
3781
  });
3548
3782
 
3549
3783
  export const Create${names.singularPascal}InputSchema = z.object({
3550
- name: z.string().min(1).max(120),
3784
+ name: z.string().trim().min(1).max(120),
3551
3785
  });
3552
3786
  ${
3553
3787
  mode === "resource"
@@ -3557,7 +3791,7 @@ export const ${names.singularPascal}IdInputSchema = z.object({
3557
3791
  });
3558
3792
 
3559
3793
  export const Update${names.singularPascal}BodySchema = z.object({
3560
- name: z.string().min(1).max(120),
3794
+ name: z.string().trim().min(1).max(120),
3561
3795
  version: z.number().int().min(1),
3562
3796
  });
3563
3797
 
@@ -3603,7 +3837,7 @@ function listUseCaseFile(
3603
3837
 
3604
3838
  return `import "@beignet/core/server-only";
3605
3839
  import { normalizeCursorPage } from "@beignet/core/pagination";
3606
- ${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)}";
3607
3841
  import {
3608
3842
  decode${names.singularPascal}Cursor,
3609
3843
  List${names.pluralPascal}InputSchema,
@@ -3615,7 +3849,7 @@ export const list${names.pluralPascal}UseCase = useCase
3615
3849
  .input(List${names.pluralPascal}InputSchema)
3616
3850
  .output(List${names.pluralPascal}OutputSchema)
3617
3851
  .run(async ({ ctx, input }) => {
3618
- ${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, {
3619
3853
  defaultLimit: 20,
3620
3854
  maxLimit: 100,
3621
3855
  });
@@ -3626,7 +3860,7 @@ ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const
3626
3860
  name: input.name,
3627
3861
  sortBy: input.sortBy,
3628
3862
  sortDirection: input.sortDirection,
3629
- }${options.tenant ? ", scope" : ""});
3863
+ }${options.tenantScoped ? ", scope" : ""});
3630
3864
  });
3631
3865
  `;
3632
3866
  }
@@ -3642,7 +3876,7 @@ function createUseCaseFile(
3642
3876
  );
3643
3877
 
3644
3878
  return `import "@beignet/core/server-only";
3645
- ${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)}";
3646
3880
  import {
3647
3881
  Create${names.singularPascal}InputSchema,
3648
3882
  ${names.singularPascal}Schema,
@@ -3654,7 +3888,7 @@ export const create${names.singularPascal}UseCase = useCase
3654
3888
  .input(Create${names.singularPascal}InputSchema)
3655
3889
  .output(${names.singularPascal}Schema)
3656
3890
  .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" : ""});
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" : ""});
3658
3892
  ${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Created, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
3659
3893
  return ${names.singularCamel};
3660
3894
  });
@@ -3672,7 +3906,7 @@ function getUseCaseFile(
3672
3906
  );
3673
3907
 
3674
3908
  return `import "@beignet/core/server-only";
3675
- ${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))}";
3676
3910
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
3677
3911
  import {
3678
3912
  ${names.singularPascal}IdInputSchema,
@@ -3684,7 +3918,7 @@ export const get${names.singularPascal}UseCase = useCase
3684
3918
  .input(${names.singularPascal}IdInputSchema)
3685
3919
  .output(${names.singularPascal}Schema)
3686
3920
  .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" : ""});
3921
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
3688
3922
  if (!${names.singularCamel}) {
3689
3923
  throw appError("${names.singularPascal}NotFound", {
3690
3924
  details: { id: input.id },
@@ -3707,7 +3941,7 @@ function updateUseCaseFile(
3707
3941
  );
3708
3942
 
3709
3943
  return `import "@beignet/core/server-only";
3710
- ${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))}";
3711
3945
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
3712
3946
  ${options.events ? `import { ${names.singularPascal}Updated } from "../domain/events";\n` : ""}import {
3713
3947
  Update${names.singularPascal}InputSchema,
@@ -3719,13 +3953,13 @@ export const update${names.singularPascal}UseCase = useCase
3719
3953
  .input(Update${names.singularPascal}InputSchema)
3720
3954
  .output(${names.singularPascal}Schema)
3721
3955
  .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" : ""});
3956
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
3723
3957
  if (!existing) {
3724
3958
  throw appError("${names.singularPascal}NotFound", {
3725
3959
  details: { id: input.id },
3726
3960
  });
3727
3961
  }
3728
- ${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) {
3729
3963
  throw appError("${names.singularPascal}Conflict", {
3730
3964
  details: { id: input.id, expectedVersion: existing.version },
3731
3965
  });
@@ -3733,7 +3967,7 @@ ${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", ex
3733
3967
 
3734
3968
  const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.update({
3735
3969
  ...input,
3736
- }${options.tenant ? ", scope" : ""});
3970
+ }${options.tenantScoped ? ", scope" : ""});
3737
3971
  if (!${names.singularCamel}) {
3738
3972
  throw appError("${names.singularPascal}Conflict", {
3739
3973
  details: { id: input.id, expectedVersion: existing.version },
@@ -3757,7 +3991,7 @@ function deleteUseCaseFile(
3757
3991
 
3758
3992
  return `import "@beignet/core/server-only";
3759
3993
  import { z } from "zod";
3760
- ${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))}";
3761
3995
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
3762
3996
  ${options.events ? `import { ${names.singularPascal}Deleted } from "../domain/events";\n` : ""}import { ${names.singularPascal}IdInputSchema } from "../schemas";
3763
3997
 
@@ -3766,7 +4000,7 @@ export const delete${names.singularPascal}UseCase = useCase
3766
4000
  .input(${names.singularPascal}IdInputSchema)
3767
4001
  .output(z.void())
3768
4002
  .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" : ""});
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" : ""});
3770
4004
  if (!deleted) {
3771
4005
  throw appError("${names.singularPascal}NotFound", {
3772
4006
  details: { id: input.id },
@@ -3806,7 +4040,7 @@ function repositoryPortFile(
3806
4040
  PageResult,
3807
4041
  SortDirection,
3808
4042
  } from "@beignet/core/pagination";
3809
- ${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 {
3810
4044
  Create${names.singularPascal}Input,
3811
4045
  ${names.singularPascal}Cursor,
3812
4046
  ${names.singularPascal}SortBy,
@@ -3823,9 +4057,9 @@ export type List${names.pluralPascal}Query = {
3823
4057
  };
3824
4058
 
3825
4059
  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` : ""}}
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` : ""}}
3829
4063
  `;
3830
4064
  }
3831
4065
 
@@ -3859,14 +4093,14 @@ function inMemoryRepositoryFile(
3859
4093
  ? " || existing.deletedAt !== null"
3860
4094
  : "";
3861
4095
  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();
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();
3863
4097
  ${names.pluralCamel}.set(id, { ...existing, deletedAt: now, updatedAt: now });
3864
4098
  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);`;
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);`;
3866
4100
 
3867
4101
  return `import "@beignet/core/server-only";
3868
4102
  import { cursorPageResult } from "@beignet/core/pagination";
3869
- ${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)}";
3870
4104
  import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
3871
4105
  import type {
3872
4106
  Create${names.singularPascal}Input,
@@ -3876,7 +4110,7 @@ ${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.s
3876
4110
  ${recordType}function to${names.singularPascal}(${names.singularCamel}: ${mapValueType}): ${names.singularPascal} {
3877
4111
  return {
3878
4112
  id: ${names.singularCamel}.id,
3879
- ${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,
3880
4114
  version: ${names.singularCamel}.version,
3881
4115
  createdAt: ${names.singularCamel}.createdAt,
3882
4116
  updatedAt: ${names.singularCamel}.updatedAt,
@@ -3934,10 +4168,10 @@ export function createInMemory${names.singularPascal}Repository(
3934
4168
  );
3935
4169
 
3936
4170
  return {
3937
- async list(query${options.tenant ? ", scope" : ""}) {
4171
+ async list(query${options.tenantScoped ? ", scope" : ""}) {
3938
4172
  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))
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))
3941
4175
  .sort((left, right) => compare${names.pluralPascal}(left, right, query))
3942
4176
  .filter((${names.singularCamel}) => isAfter${names.singularPascal}Cursor(${names.singularCamel}, query));
3943
4177
  const pageItems = all${names.pluralPascal}.slice(0, query.page.limit);
@@ -3952,11 +4186,11 @@ ${activeFilter}${options.tenant ? `\t\t\t\t.filter((${names.singularCamel}) => $
3952
4186
  nextCursor,
3953
4187
  );
3954
4188
  },
3955
- async create(input${options.tenant ? ", scope" : ""}) {
4189
+ async create(input${options.tenantScoped ? ", scope" : ""}) {
3956
4190
  const now = new Date().toISOString();
3957
- ${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} = {
3958
4192
  id: crypto.randomUUID(),
3959
- ${options.tenant ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
4193
+ ${options.tenantScoped ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
3960
4194
  version: 1,
3961
4195
  createdAt: now,
3962
4196
  updatedAt: now,
@@ -3966,7 +4200,7 @@ ${options.tenant ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
3966
4200
  ${newDeletedAtField} });
3967
4201
  return ${names.singularCamel};
3968
4202
  },
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` : ""}
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` : ""}
3970
4204
  };
3971
4205
  }
3972
4206
  `;
@@ -3981,7 +4215,7 @@ function drizzleSchemaFile(
3981
4215
 
3982
4216
  export const ${names.pluralCamel} = ${dialect.tableFunction}("${names.pluralKebab.replaceAll("-", "_")}", {
3983
4217
  id: ${dialect.idColumn("id")}.primaryKey(),
3984
- ${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(),
3985
4219
  version: ${dialect.integerColumn("version")}.notNull(),
3986
4220
  createdAt: ${dialect.timestampColumn("created_at")}.notNull(),
3987
4221
  updatedAt: ${dialect.timestampColumn("updated_at")}.notNull(),
@@ -4091,20 +4325,20 @@ function drizzleRepositoryFile(
4091
4325
  const schemaPath = drizzleSchemaIndexPath(config);
4092
4326
  const findWhere = drizzleResourceWhere(names, options, [
4093
4327
  `eq(schema.${names.pluralCamel}.id, id)`,
4094
- ...(options.tenant
4328
+ ...(options.tenantScoped
4095
4329
  ? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
4096
4330
  : []),
4097
4331
  ]);
4098
4332
  const updateWhere = drizzleResourceWhere(names, options, [
4099
4333
  `eq(schema.${names.pluralCamel}.id, input.id)`,
4100
4334
  `eq(schema.${names.pluralCamel}.version, input.version)`,
4101
- ...(options.tenant
4335
+ ...(options.tenantScoped
4102
4336
  ? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
4103
4337
  : []),
4104
4338
  ]);
4105
4339
  const deleteWhere = drizzleResourceWhere(names, options, [
4106
4340
  `eq(schema.${names.pluralCamel}.id, id)`,
4107
- ...(options.tenant
4341
+ ...(options.tenantScoped
4108
4342
  ? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
4109
4343
  : []),
4110
4344
  ]);
@@ -4122,7 +4356,7 @@ function drizzleRepositoryFile(
4122
4356
  ].filter((name): name is string => Boolean(name));
4123
4357
  // After a version-guarded update succeeds, re-select by identity only —
4124
4358
  // matching what `.returning()` yields on the dialects that support it.
4125
- const updateReselectWhere = options.tenant
4359
+ const updateReselectWhere = options.tenantScoped
4126
4360
  ? `and(eq(schema.${names.pluralCamel}.id, input.id), eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)))`
4127
4361
  : `eq(schema.${names.pluralCamel}.id, input.id)`;
4128
4362
  // drizzle-orm/mysql2 has no `.returning(...)`; mutations resolve to a
@@ -4145,13 +4379,13 @@ function affectedRows(result: unknown): number {
4145
4379
  `
4146
4380
  : "";
4147
4381
  const createMethod = dialect.supportsReturning
4148
- ? ` async create(input${options.tenant ? ", scope" : ""}) {
4382
+ ? ` async create(input${options.tenantScoped ? ", scope" : ""}) {
4149
4383
  const now = new Date().toISOString();
4150
4384
  const [row] = await db
4151
4385
  .insert(schema.${names.pluralCamel})
4152
4386
  .values({
4153
4387
  id: crypto.randomUUID(),
4154
- ${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,
4155
4389
  version: 1,
4156
4390
  createdAt: now,
4157
4391
  updatedAt: now,
@@ -4165,11 +4399,11 @@ ${options.tenant ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name:
4165
4399
  return to${names.singularPascal}(row);
4166
4400
  },
4167
4401
  `
4168
- : ` async create(input${options.tenant ? ", scope" : ""}) {
4402
+ : ` async create(input${options.tenantScoped ? ", scope" : ""}) {
4169
4403
  const now = new Date().toISOString();
4170
4404
  const ${names.singularCamel}: ${names.singularPascal} = {
4171
4405
  id: crypto.randomUUID(),
4172
- ${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,
4173
4407
  version: 1,
4174
4408
  createdAt: now,
4175
4409
  updatedAt: now,
@@ -4183,7 +4417,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
4183
4417
  mode !== "resource"
4184
4418
  ? ""
4185
4419
  : dialect.supportsReturning
4186
- ? ` async findById(id: string${options.tenant ? ", scope" : ""}) {
4420
+ ? ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
4187
4421
  const [row] = await db
4188
4422
  .select()
4189
4423
  .from(schema.${names.pluralCamel})
@@ -4192,7 +4426,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
4192
4426
 
4193
4427
  return row ? to${names.singularPascal}(row) : null;
4194
4428
  },
4195
- async update(input${options.tenant ? ", scope" : ""}) {
4429
+ async update(input${options.tenantScoped ? ", scope" : ""}) {
4196
4430
  const [row] = await db
4197
4431
  .update(schema.${names.pluralCamel})
4198
4432
  .set({
@@ -4205,12 +4439,12 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
4205
4439
 
4206
4440
  return row ? to${names.singularPascal}(row) : null;
4207
4441
  },
4208
- async delete(id: string${options.tenant ? ", scope" : ""}) {
4442
+ async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
4209
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`}
4210
4444
  return rows.length > 0;
4211
4445
  },
4212
4446
  `
4213
- : ` async findById(id: string${options.tenant ? ", scope" : ""}) {
4447
+ : ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
4214
4448
  const [row] = await db
4215
4449
  .select()
4216
4450
  .from(schema.${names.pluralCamel})
@@ -4219,7 +4453,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
4219
4453
 
4220
4454
  return row ? to${names.singularPascal}(row) : null;
4221
4455
  },
4222
- async update(input${options.tenant ? ", scope" : ""}) {
4456
+ async update(input${options.tenantScoped ? ", scope" : ""}) {
4223
4457
  const result = await db
4224
4458
  .update(schema.${names.pluralCamel})
4225
4459
  .set({
@@ -4239,7 +4473,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
4239
4473
 
4240
4474
  return row ? to${names.singularPascal}(row) : null;
4241
4475
  },
4242
- async delete(id: string${options.tenant ? ", scope" : ""}) {
4476
+ async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
4243
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`}
4244
4478
  return affectedRows(result) > 0;
4245
4479
  },
@@ -4247,7 +4481,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
4247
4481
 
4248
4482
  return `import "@beignet/core/server-only";
4249
4483
  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}";
4484
+ ${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
4251
4485
  import { ${drizzleImports.join(", ")} } from "drizzle-orm";
4252
4486
  import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
4253
4487
  import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
@@ -4262,7 +4496,7 @@ type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelec
4262
4496
  function to${names.singularPascal}(row: ${names.singularPascal}Row): ${names.singularPascal} {
4263
4497
  return {
4264
4498
  id: row.id,
4265
- ${options.tenant ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
4499
+ ${options.tenantScoped ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
4266
4500
  version: row.version,
4267
4501
  createdAt: row.createdAt,
4268
4502
  updatedAt: row.updatedAt,
@@ -4294,9 +4528,9 @@ function ${names.singularCamel}CursorFilter(
4294
4528
 
4295
4529
  function ${names.singularCamel}ListWhere(
4296
4530
  query: List${names.pluralPascal}Query,
4297
- ${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 {
4298
4532
  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` : ""} ];
4533
+ ${options.tenantScoped ? `\t\teq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
4300
4534
  const cursor = ${names.singularCamel}CursorFilter(query);
4301
4535
 
4302
4536
  if (query.name) {
@@ -4330,8 +4564,8 @@ export function createDrizzle${names.singularPascal}Repository(
4330
4564
  db: ${dialect.dbTypeName}<typeof schema>,
4331
4565
  ): ${names.singularPascal}Repository {
4332
4566
  return {
4333
- async list(query${options.tenant ? ", scope" : ""}) {
4334
- 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" : ""});
4335
4569
  const rows = await db
4336
4570
  .select()
4337
4571
  .from(schema.${names.pluralCamel})
@@ -4393,7 +4627,7 @@ export const list${names.pluralPascal} = ${names.pluralCamel}
4393
4627
  export const create${names.singularPascal} = ${names.pluralCamel}
4394
4628
  .post("/api/${names.pluralKebab}")
4395
4629
  .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({
4630
+ ${options.authorization ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
4397
4631
  201: ${names.singularPascal}Schema,
4398
4632
  });
4399
4633
  ${
@@ -4411,7 +4645,7 @@ export const update${names.singularPascal} = ${names.pluralCamel}
4411
4645
  .patch("/api/${names.pluralKebab}/:id")
4412
4646
  .pathParams(${names.singularPascal}IdInputSchema)
4413
4647
  .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 })
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 })
4415
4649
  .responses({
4416
4650
  200: ${names.singularPascal}Schema,
4417
4651
  });
@@ -4419,7 +4653,7 @@ ${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability
4419
4653
  export const delete${names.singularPascal} = ${names.pluralCamel}
4420
4654
  .delete("/api/${names.pluralKebab}/:id")
4421
4655
  .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 })
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 })
4423
4657
  .responses({
4424
4658
  204: null,
4425
4659
  });
@@ -4624,7 +4858,7 @@ function isAuthenticated(ctx: AuthorizationContext) {
4624
4858
 
4625
4859
  function canWrite(ctx: AuthorizationContext) {
4626
4860
  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;
4861
+ ${options.tenantScoped ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
4628
4862
  }
4629
4863
 
4630
4864
  export const ${names.singularCamel}Policy = definePolicy({
@@ -4634,13 +4868,13 @@ export const ${names.singularCamel}Policy = definePolicy({
4634
4868
  "${names.pluralCamel}.update": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
4635
4869
  const decision = canWrite(ctx);
4636
4870
  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` : ""}
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` : ""}
4638
4872
  return true;
4639
4873
  },
4640
4874
  "${names.pluralCamel}.delete": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
4641
4875
  const decision = canWrite(ctx);
4642
4876
  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` : ""}
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` : ""}
4644
4878
  return true;
4645
4879
  },
4646
4880
  "${names.pluralCamel}.manage": (_ctx: AuthorizationContext) =>
@@ -4692,7 +4926,7 @@ function create${names.singularPascal}(): ${names.singularPascal} {
4692
4926
  const now = new Date().toISOString();
4693
4927
  return {
4694
4928
  id: "00000000-0000-4000-8000-000000000001",
4695
- ${options.tenant ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
4929
+ ${options.tenantScoped ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
4696
4930
  version: 1,
4697
4931
  createdAt: now,
4698
4932
  updatedAt: now,
@@ -4707,7 +4941,7 @@ describe("${names.singularCamel}Policy", () => {
4707
4941
  const ${names.singularCamel} = create${names.singularPascal}();
4708
4942
  const ctx = {
4709
4943
  actor: { type: "user" as const, id: "user_test" },
4710
- ${options.tenant ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
4944
+ ${options.tenantScoped ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
4711
4945
 
4712
4946
  await expect(
4713
4947
  tester.assertMatrix([
@@ -4933,8 +5167,33 @@ ${names.timezone ? `\t\ttimezone: "${names.timezone}",\n` : ""}\t\tpayload: ${na
4933
5167
  `;
4934
5168
  }
4935
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
+
4936
5194
  function uploadFile(names: UploadNames, config: ResolvedBeignetConfig): string {
4937
5195
  return `import { z } from "zod";
5196
+ import { ${names.fileConstraintsExportName} } from "${relativeModule(uploadFilePath(names, config), uploadManifestFilePath(names, config))}";
4938
5197
  import { defineUpload } from "${aliasModule(config.paths.uploadsBuilder)}";
4939
5198
 
4940
5199
  export const ${names.metadataSchemaName} = z.object({
@@ -4945,13 +5204,7 @@ export type ${names.metadataTypeName} = z.infer<typeof ${names.metadataSchemaNam
4945
5204
 
4946
5205
  export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
4947
5206
  \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},
5207
+ \tfile: ${names.fileConstraintsExportName},
4955
5208
  \tauthorize({ ctx }) {
4956
5209
  \t\treturn ctx.actor.type === "user";
4957
5210
  \t},
@@ -4982,6 +5235,148 @@ export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
4982
5235
  `;
4983
5236
  }
4984
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
+
4985
5380
  function uploadRouteFile(
4986
5381
  names: UploadNames,
4987
5382
  config: ResolvedBeignetConfig,
@@ -5101,63 +5496,70 @@ function featureUiComponentFile(
5101
5496
 
5102
5497
  return `"use client";
5103
5498
 
5104
- import { contractErrorMessage } from "@beignet/core/client";
5499
+ import { rootFormError } from "@beignet/react-hook-form";
5105
5500
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
5106
- import { useState } from "react";
5501
+ import { rhf } from "${aliasModule(clientFormsPath(config))}";
5502
+ import { create${names.singularPascal} } from "${relativeModule(componentPath, resourceContractFilePath(names, config))}";
5107
5503
  import {
5108
5504
  \tcreate${names.singularPascal}MutationOptions,
5109
5505
  \tinvalidate${names.pluralPascal},
5110
5506
  \tlist${names.pluralPascal}QueryOptions,
5111
5507
  } from "${relativeModule(componentPath, clientQueriesPath)}";
5112
5508
 
5509
+ const create${names.singularPascal}Form = rhf(create${names.singularPascal});
5510
+
5113
5511
  export function ${names.componentExportName}() {
5114
- \tconst [name, setName] = useState("");
5115
5512
  \tconst queryClient = useQueryClient();
5116
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");
5117
5518
  \tconst create${names.singularPascal}Mutation = useMutation({
5118
5519
  \t\t...create${names.singularPascal}MutationOptions(),
5119
5520
  \t\tonSuccess: async () => {
5120
- \t\t\tsetName("");
5521
+ \t\t\tform.reset({ name: "" });
5121
5522
  \t\t\tawait invalidate${names.pluralPascal}(queryClient);
5122
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},
5123
5530
  \t});
5124
5531
 
5125
5532
  \treturn (
5126
5533
  \t\t<section className="${names.pluralKebab}-panel">
5127
5534
  \t\t\t<form
5128
5535
  \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;
5536
+ \t\t\t\tonSubmit={form.handleSubmit((values) => {
5537
+ \t\t\t\t\tform.clearErrors("root");
5133
5538
  \t\t\t\t\tcreate${names.singularPascal}Mutation.reset();
5134
5539
  \t\t\t\t\tcreate${names.singularPascal}Mutation.mutate({
5135
- \t\t\t\t\t\tbody: { name: trimmedName },
5540
+ \t\t\t\t\t\tbody: values,
5136
5541
  \t\t\t\t\t});
5137
- \t\t\t\t}}
5542
+ \t\t\t\t})}
5138
5543
  \t\t\t>
5139
5544
  \t\t\t\t<label htmlFor="${names.singularKebab}-name">New ${names.singularKebab}</label>
5140
5545
  \t\t\t\t<div className="${names.pluralKebab}-composer-row">
5141
5546
  \t\t\t\t\t<input
5142
5547
  \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)}
5548
+ \t\t\t\t\t\t{...form.register("name")}
5145
5549
  \t\t\t\t\t\tplaceholder="Name"
5146
5550
  \t\t\t\t\t/>
5147
5551
  \t\t\t\t\t<button
5148
5552
  \t\t\t\t\t\ttype="submit"
5149
- \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}
5150
5554
  \t\t\t\t\t>
5151
5555
  \t\t\t\t\t\t{create${names.singularPascal}Mutation.isPending ? "Creating" : "Create"}
5152
5556
  \t\t\t\t\t</button>
5153
5557
  \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>
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>
5161
5563
  \t\t\t\t) : null}
5162
5564
  \t\t\t</form>
5163
5565
 
@@ -5306,7 +5708,7 @@ ${mode === "resource" ? `import { isAppError } from "@beignet/core/errors";\n` :
5306
5708
  import { createTestApp } from "@beignet/web/testing";
5307
5709
  import { createInMemoryDevtools } from "@beignet/devtools";
5308
5710
  import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
5309
- 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";
5310
5712
  import type { AppContext } from "${aliasModule(config.paths.appContext)}";
5311
5713
  import { initialPorts } from "${aliasModule(config.paths.portWiring)}";
5312
5714
  import { appContext } from "${aliasModule(serverContextPath)}";
@@ -5326,40 +5728,40 @@ describe("${names.pluralCamel} resource", () => {
5326
5728
  const seed${names.pluralPascal} = [
5327
5729
  {
5328
5730
  id: "00000000-0000-4000-8000-000000000101",
5329
- ${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}",
5330
5732
  version: 1,
5331
5733
  createdAt: "2024-01-01T00:00:00.000Z",
5332
5734
  updatedAt: "2024-01-01T00:00:00.000Z",
5333
5735
  },
5334
5736
  {
5335
5737
  id: "00000000-0000-4000-8000-000000000102",
5336
- ${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}",
5337
5739
  version: 1,
5338
5740
  createdAt: "2024-01-02T00:00:00.000Z",
5339
5741
  updatedAt: "2024-01-02T00:00:00.000Z",
5340
5742
  },
5341
5743
  {
5342
5744
  id: "00000000-0000-4000-8000-000000000103",
5343
- ${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}",
5344
5746
  version: 1,
5345
5747
  createdAt: "2024-01-03T00:00:00.000Z",
5346
5748
  updatedAt: "2024-01-03T00:00:00.000Z",
5347
5749
  },
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` : ""} ];
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` : ""} ];
5349
5751
  const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository(seed${names.pluralPascal});
5350
5752
  const testFixture = createTestPorts<AppContext["ports"]>({
5351
5753
  base: initialPorts,
5352
5754
  overrides: {
5353
5755
  ${names.pluralCamel},
5354
5756
  ${
5355
- options.auth || options.tenant
5757
+ options.authorization || options.tenantScoped
5356
5758
  ? `\t\t\t\tauth: {
5357
5759
  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` : ""}
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` : ""}
5360
5762
  }),
5361
5763
  },
5362
- ${options.auth ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
5764
+ ${options.authorization ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
5363
5765
  `
5364
5766
  : `\t\t\t\tauth: {
5365
5767
  getSession: async () => null,
@@ -5375,8 +5777,8 @@ ${options.auth ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
5375
5777
  });
5376
5778
  const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
5377
5779
  ports: testFixture.ports,
5378
- actor: ${options.auth ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
5379
- tenant: ${options.tenant ? `createTestTenant("tenant_test")` : "null"},
5780
+ actor: ${options.authorization ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
5781
+ tenant: ${options.tenantScoped ? `createTestTenant("tenant_test")` : "null"},
5380
5782
  });
5381
5783
  const tester = createUseCaseTester<AppContext>(createTestContext);
5382
5784