@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/dist/make.js 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 { clientIndexPath, loadBeignetConfig, resolveConfig, } from "./config.js";
4
+ import ts from "typescript";
5
+ import { clientFormsPath, clientIndexPath, loadBeignetConfig, resolveConfig, } from "./config.js";
5
6
  import { adapterFilePath, addNamedImport, addNamedTypeImport, aliasModule, appendDeferredPortKey, appendPickStringLiteralMember, applyPortProviderWiring, assertAdapterApp, assertFeatureArtifactApp, assertFeatureUiApp, assertGeneratedIdentifiers, assertPersistenceArtifactApp, assertPolicyApp, assertPortApp, assertServerRuntimeApp, assertStandardApp, assertTestApp, assertUseCaseApp, beignetDependencyVersion, constantCase, createListenersRegistrySource, defineUploadsInitializerInfo, detectResourceDatabase, detectResourcePersistence, drizzleDialects, drizzleRepositoriesPath, drizzleResourceRepositoryFilePath, drizzleSchemaIndexPath, eventBusPortWiring, eventFilePath, factoryFilePath, featureArtifactDir, featureArtifactIndexFile, featureArtifactNames, featureResourceNames, featureUiClientQueriesFilePath, featureUiComponentFilePath, featureUiIndexFilePath, featureUiNames, fileExists, hasTopLevelObjectProperty, identifierNames, infrastructureDir, insertTypeProperty, jobFilePath, listenerFilePath, listenersFilePath, mergeMakeResultInto, mergeMakeResults, missingCapabilityBuilderFiles, notificationFilePath, notificationPortWirings, planGeneratedFile, planGeneratedFiles, portFilePath, portNames, providersFilePath, readOptionalFile, relativeModule, resourceContractFilePath, resourceFeatureDir, resourceNames, resourcePortFilePath, resourceSharedErrorsPath, scheduleFilePath, seedEntrypointPath, seedFilePath, seedNames, singularize, storagePortWiring, tanstackReactQueryDependencyVersion, taskFilePath, typeBodySource, uniqueStrings, updateDrizzleRepositories, updateDrizzleSchemaIndex, updateFeatureArtifactIndex, updateInfrastructureAdapterWiring, updateInfrastructureDeferredPorts, updateInfrastructureGatePolicies, updateInfrastructurePortStub, updateInfrastructurePorts, updateListenersRegistrySource, updateOpenApiRoute, updatePackageDependencies, updatePackageJson, updatePackageScripts, updatePortIndex, updatePortsIndex, updateSeedEntrypoint, updateServerRoutes, updateServerTransactionPorts, updateSharedErrors, uploadFilePath, useCaseFeatureDir, useCaseFilePath, usesFeatureOwnedPolicies, usesFeatureOwnedTests, wireListenersProviderSource, wirePortProviders, writeGeneratedFile, writePlannedGeneratedFile, } from "./make/shared.js";
6
7
  import { appendToArrayExpression, appendToOutboxRegistryArray, arrayInitializerInfo, identifiersFromArrayExpression, insertAfterImports, } from "./registry-edits.js";
8
+ import { externalVersions } from "./templates/shared.js";
7
9
  import { testSupportTemplateFiles } from "./templates/testing.js";
8
10
  export { makeFeatureAddonChoices, makeFeatureRecipeChoices, } from "./choices.js";
9
11
  export * from "./make/inbox.js";
@@ -17,7 +19,7 @@ export async function makeFeature(options) {
17
19
  const addons = normalizeMakeFeatureAddons([
18
20
  ...makeFeatureRecipeAddons(options.recipe),
19
21
  ...(options.with ?? []),
20
- ...(options.events ? ["events"] : []),
22
+ ...(options.events ? ["event"] : []),
21
23
  ]);
22
24
  const featureOptions = {
23
25
  ...options,
@@ -122,30 +124,36 @@ async function makeResourceSlice(options, mode) {
122
124
  function resourceGenerationOptions(options, mode) {
123
125
  if (mode === "feature") {
124
126
  return {
125
- auth: false,
126
- tenant: false,
127
+ authorization: false,
128
+ tenantScoped: false,
127
129
  events: Boolean(options.events),
128
130
  softDelete: false,
129
131
  };
130
132
  }
131
133
  if (mode !== "resource") {
132
- return { auth: false, tenant: false, events: false, softDelete: false };
134
+ return {
135
+ authorization: false,
136
+ tenantScoped: false,
137
+ events: false,
138
+ softDelete: false,
139
+ };
133
140
  }
134
141
  return {
135
- auth: Boolean(options.auth),
136
- tenant: Boolean(options.tenant),
142
+ authorization: Boolean(options.authorization),
143
+ tenantScoped: Boolean(options.tenantScoped),
137
144
  events: Boolean(options.events),
138
145
  softDelete: Boolean(options.softDelete),
139
146
  };
140
147
  }
141
148
  async function makeFeatureSlice(options, addons) {
142
149
  let result = await makeFeatureResource(options);
150
+ const includeUploadUi = addons.includes("ui") && addons.includes("upload");
143
151
  for (const addon of addons) {
144
- result = mergeMakeResults(result, await makeFeatureAddon(addon, result.name, options));
152
+ result = mergeMakeResults(result, await makeFeatureAddon(addon, result.name, options, includeUploadUi));
145
153
  }
146
154
  return result;
147
155
  }
148
- async function makeFeatureAddon(addon, feature, options) {
156
+ async function makeFeatureAddon(addon, feature, options, includeUploadUi) {
149
157
  const shared = {
150
158
  cwd: options.cwd,
151
159
  force: options.force,
@@ -158,69 +166,53 @@ async function makeFeatureAddon(addon, feature, options) {
158
166
  if (addon === "factory") {
159
167
  return makeFactory({
160
168
  ...shared,
161
- name: `${feature}/${singularize(feature)}`,
169
+ name: `${feature}.${singularize(feature)}`,
162
170
  skipPersistenceAssert: true,
163
171
  });
164
172
  }
165
173
  if (addon === "task") {
166
- return makeTask({ ...shared, name: `${feature}/backfill` });
174
+ return makeTask({ ...shared, name: `${feature}.backfill` });
167
175
  }
168
176
  if (addon === "event") {
169
- return makeEvent({ ...shared, name: `${feature}/created` });
177
+ return makeEvent({ ...shared, name: `${feature}.created` });
170
178
  }
171
179
  if (addon === "job") {
172
- return makeJob({ ...shared, name: `${feature}/process` });
180
+ return makeJob({ ...shared, name: `${feature}.process` });
173
181
  }
174
182
  if (addon === "listener") {
175
183
  return makeListener({
176
184
  ...shared,
177
- name: `${feature}/handle-created`,
178
- event: `${feature}/created`,
185
+ name: `${feature}.handle-created`,
186
+ event: `${feature}.created`,
179
187
  skipEventAssert: true,
180
188
  });
181
189
  }
182
190
  if (addon === "notification") {
183
- return makeNotification({ ...shared, name: `${feature}/created` });
191
+ return makeNotification({ ...shared, name: `${feature}.created` });
184
192
  }
185
193
  if (addon === "schedule") {
186
- return makeSchedule({ ...shared, name: `${feature}/daily-summary` });
194
+ return makeSchedule({ ...shared, name: `${feature}.daily-summary` });
187
195
  }
188
196
  if (addon === "seed") {
189
197
  return makeSeed({
190
198
  ...shared,
191
- name: `${feature}/demo-${feature}`,
199
+ name: `${feature}.demo-${feature}`,
192
200
  skipPersistenceAssert: true,
193
201
  });
194
202
  }
195
203
  if (addon === "ui") {
196
204
  return makeFeatureUi({ ...shared, name: feature });
197
205
  }
198
- return makeUpload({ ...shared, name: `${feature}/attachment` });
206
+ return makeUpload({
207
+ ...shared,
208
+ name: `${feature}.attachment`,
209
+ ui: includeUploadUi,
210
+ });
199
211
  }
200
212
  function normalizeMakeFeatureAddons(addons) {
201
213
  const normalized = new Set();
202
214
  for (const addon of addons) {
203
- if (addon === "factories")
204
- normalized.add("factory");
205
- else if (addon === "events")
206
- normalized.add("event");
207
- else if (addon === "listeners") {
208
- normalized.add("event");
209
- normalized.add("listener");
210
- }
211
- else if (addon === "tasks")
212
- normalized.add("task");
213
- else if (addon === "jobs")
214
- normalized.add("job");
215
- else if (addon === "notifications")
216
- normalized.add("notification");
217
- else if (addon === "schedules")
218
- normalized.add("schedule");
219
- else if (addon === "seeds")
220
- normalized.add("seed");
221
- else if (addon === "uploads")
222
- normalized.add("upload");
223
- else if (addon === "listener") {
215
+ if (addon === "listener") {
224
216
  normalized.add("event");
225
217
  normalized.add("listener");
226
218
  }
@@ -233,16 +225,16 @@ function makeFeatureRecipeAddons(recipe) {
233
225
  if (recipe === "full-slice") {
234
226
  return [
235
227
  "policy",
236
- "factories",
237
- "seeds",
238
- "tasks",
239
- "events",
240
- "listeners",
241
- "jobs",
242
- "notifications",
243
- "schedules",
228
+ "factory",
229
+ "seed",
230
+ "task",
231
+ "event",
232
+ "listener",
233
+ "job",
234
+ "notification",
235
+ "schedule",
244
236
  "ui",
245
- "uploads",
237
+ "upload",
246
238
  ];
247
239
  }
248
240
  return [];
@@ -328,7 +320,7 @@ export async function makeUseCase(options) {
328
320
  skippedFiles.push(indexFile.path);
329
321
  return {
330
322
  schemaVersion: 1,
331
- name: `${names.feature.kebab}/${names.action.kebab}`,
323
+ name: `${names.feature.kebab}.${names.action.kebab}`,
332
324
  targetDir,
333
325
  dryRun: Boolean(options.dryRun),
334
326
  files: [...generatedFiles.map((file) => file.path), indexFile.path],
@@ -367,7 +359,7 @@ export async function makeTest(options) {
367
359
  }
368
360
  return {
369
361
  schemaVersion: 1,
370
- name: `${names.feature.kebab}/${names.action.kebab}`,
362
+ name: `${names.feature.kebab}.${names.action.kebab}`,
371
363
  targetDir,
372
364
  dryRun: Boolean(options.dryRun),
373
365
  files: generatedFiles.map((file) => file.path),
@@ -529,7 +521,7 @@ export async function makeEvent(options) {
529
521
  config,
530
522
  force: Boolean(options.force),
531
523
  dryRun: Boolean(options.dryRun),
532
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
524
+ name: names.stableName,
533
525
  files: eventFiles(names, config),
534
526
  index: featureArtifactIndexFile("event", names, config),
535
527
  dependencies: {
@@ -570,7 +562,7 @@ export async function makeJob(options) {
570
562
  config,
571
563
  force: Boolean(options.force),
572
564
  dryRun: Boolean(options.dryRun),
573
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
565
+ name: names.stableName,
574
566
  files: [
575
567
  ...(await missingCapabilityBuilderFiles(targetDir, "jobs", config)),
576
568
  ...jobFiles(names, config),
@@ -603,7 +595,7 @@ export async function makeTask(options) {
603
595
  config,
604
596
  force: Boolean(options.force),
605
597
  dryRun: Boolean(options.dryRun),
606
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
598
+ name: names.stableName,
607
599
  files: [
608
600
  ...(await missingCapabilityBuilderFiles(targetDir, "tasks", config)),
609
601
  ...taskFiles(names, config),
@@ -641,7 +633,7 @@ export async function makeFactory(options) {
641
633
  config,
642
634
  force: Boolean(options.force),
643
635
  dryRun: Boolean(options.dryRun),
644
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
636
+ name: names.stableName,
645
637
  files: factoryFiles(names, config),
646
638
  index: featureArtifactIndexFile("factory", names, config),
647
639
  dependencies: {
@@ -663,7 +655,7 @@ export async function makeSeed(options) {
663
655
  config,
664
656
  force: Boolean(options.force),
665
657
  dryRun: Boolean(options.dryRun),
666
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
658
+ name: names.stableName,
667
659
  files: seedFiles(names, config),
668
660
  index: featureArtifactIndexFile("seed", names, config),
669
661
  dependencies: {
@@ -677,7 +669,7 @@ export async function makeSeed(options) {
677
669
  config,
678
670
  force: Boolean(options.force),
679
671
  dryRun: Boolean(options.dryRun),
680
- name: `${factoryNamesForSeed.feature.kebab}/${factoryNamesForSeed.artifact.kebab}`,
672
+ name: factoryNamesForSeed.stableName,
681
673
  files: factoryFiles(factoryNamesForSeed, config),
682
674
  index: featureArtifactIndexFile("factory", factoryNamesForSeed, config),
683
675
  dependencies: {
@@ -714,7 +706,7 @@ export async function makeNotification(options) {
714
706
  // notifications uses ctx.ports.notifications, so the generator wires both
715
707
  // ports with dev-default providers. Each port is wired independently and
716
708
  // skipped when the app already has it, for example when the resend
717
- // integration contributed the mailer. Plan the wiring before writing any
709
+ // provider contributed the mailer. Plan the wiring before writing any
718
710
  // files so an anchor miss aborts the generator without leaving partial
719
711
  // output.
720
712
  const notificationWirings = notificationPortWirings("make notification");
@@ -726,7 +718,7 @@ export async function makeNotification(options) {
726
718
  config,
727
719
  force: Boolean(options.force),
728
720
  dryRun: Boolean(options.dryRun),
729
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
721
+ name: names.stableName,
730
722
  files: [
731
723
  ...(await missingCapabilityBuilderFiles(targetDir, "notifications", config)),
732
724
  ...notificationFiles(names, config),
@@ -760,7 +752,7 @@ export async function makeListener(options) {
760
752
  config,
761
753
  force: Boolean(options.force),
762
754
  dryRun: Boolean(options.dryRun),
763
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
755
+ name: names.stableName,
764
756
  files: [
765
757
  ...(await missingCapabilityBuilderFiles(targetDir, "listeners", config)),
766
758
  ...listenerFiles(names, config),
@@ -832,7 +824,7 @@ async function assertListenerEventExists(targetDir, names, config) {
832
824
  await stat(path.join(targetDir, file));
833
825
  }
834
826
  catch {
835
- throw new Error(`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.`);
827
+ throw new Error(`beignet make listener expected event ${names.eventName} at ${file}. Run beignet make event ${names.eventName} first, or pass an event that exists.`);
836
828
  }
837
829
  }
838
830
  async function updateListenerProviderWiring(targetDir, names, config, options) {
@@ -884,7 +876,7 @@ export async function makeSchedule(options) {
884
876
  config,
885
877
  force: Boolean(options.force),
886
878
  dryRun: Boolean(options.dryRun),
887
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
879
+ name: names.stableName,
888
880
  files,
889
881
  index: featureArtifactIndexFile("schedule", names, config),
890
882
  dependencies: {
@@ -1252,6 +1244,9 @@ export async function makeUpload(options) {
1252
1244
  ? resolveConfig(options.config)
1253
1245
  : await loadBeignetConfig(targetDir);
1254
1246
  await assertFeatureArtifactApp(targetDir, config, "upload");
1247
+ if (options.ui) {
1248
+ await assertUploadUiApp(targetDir, config);
1249
+ }
1255
1250
  await wirePortProviders(targetDir, config, [storagePortWiring("make upload")], {
1256
1251
  dryRun: true,
1257
1252
  });
@@ -1260,10 +1255,10 @@ export async function makeUpload(options) {
1260
1255
  config,
1261
1256
  force: Boolean(options.force),
1262
1257
  dryRun: Boolean(options.dryRun),
1263
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1258
+ name: names.stableName,
1264
1259
  files: [
1265
1260
  ...(await missingCapabilityBuilderFiles(targetDir, "uploads", config)),
1266
- ...uploadFiles(names, config),
1261
+ ...uploadFiles(names, config, { ui: Boolean(options.ui) }),
1267
1262
  ],
1268
1263
  index: featureArtifactIndexFile("upload", names, config),
1269
1264
  dependencies: {
@@ -1283,6 +1278,31 @@ export async function makeUpload(options) {
1283
1278
  if (!result.files.includes(uploadRoutePath(config))) {
1284
1279
  result.files.push(uploadRoutePath(config));
1285
1280
  }
1281
+ if (options.ui) {
1282
+ const componentIndex = uploadUiComponentIndexFile(names, config);
1283
+ const componentIndexResult = await updateFeatureUiIndex(targetDir, componentIndex, { dryRun: Boolean(options.dryRun) });
1284
+ if (componentIndexResult === "created") {
1285
+ result.createdFiles.push(componentIndex.path);
1286
+ }
1287
+ if (componentIndexResult === "updated") {
1288
+ result.updatedFiles.push(componentIndex.path);
1289
+ }
1290
+ if (componentIndexResult === "skipped") {
1291
+ result.skippedFiles.push(componentIndex.path);
1292
+ }
1293
+ if (!result.files.includes(componentIndex.path)) {
1294
+ result.files.push(componentIndex.path);
1295
+ }
1296
+ if (await updatePackageDependencies(targetDir, { "@beignet/react-uploads": beignetDependencyVersion }, { dryRun: Boolean(options.dryRun) })) {
1297
+ result.updatedFiles.push("package.json");
1298
+ if (!result.files.includes("package.json")) {
1299
+ result.files.push("package.json");
1300
+ }
1301
+ }
1302
+ }
1303
+ result.createdFiles = uniqueStrings(result.createdFiles);
1304
+ result.updatedFiles = uniqueStrings(result.updatedFiles);
1305
+ result.skippedFiles = uniqueStrings(result.skippedFiles);
1286
1306
  return result;
1287
1307
  }
1288
1308
  export async function makeOutbox(options = {}) {
@@ -1349,7 +1369,10 @@ async function makeFeatureUi(options) {
1349
1369
  ? resolveConfig(options.config)
1350
1370
  : await loadBeignetConfig(targetDir);
1351
1371
  await assertFeatureUiApp(targetDir, config);
1352
- const componentFiles = featureUiComponentFiles(names, config);
1372
+ const componentFiles = [
1373
+ ...(await missingFeatureUiFormsFiles(targetDir, config)),
1374
+ ...featureUiComponentFiles(names, config),
1375
+ ];
1353
1376
  const indexFile = featureUiIndexFile(names, config);
1354
1377
  const plannedFiles = await planGeneratedFiles(targetDir, componentFiles, {
1355
1378
  force: Boolean(options.force),
@@ -1378,8 +1401,13 @@ async function makeFeatureUi(options) {
1378
1401
  if (indexResult === "skipped")
1379
1402
  skippedFiles.push(indexFile.path);
1380
1403
  if (await updatePackageDependencies(targetDir, {
1404
+ "@beignet/react-hook-form": beignetDependencyVersion,
1381
1405
  "@beignet/react-query": beignetDependencyVersion,
1406
+ "@hookform/resolvers": (packageJson) => packageJson.dependencies?.["@hookform/resolvers"] ??
1407
+ externalVersions.hookformResolvers,
1382
1408
  "@tanstack/react-query": tanstackReactQueryDependencyVersion,
1409
+ "react-hook-form": (packageJson) => packageJson.dependencies?.["react-hook-form"] ??
1410
+ externalVersions.reactHookForm,
1383
1411
  }, { dryRun: Boolean(options.dryRun) })) {
1384
1412
  updatedFiles.push("package.json");
1385
1413
  }
@@ -1467,7 +1495,7 @@ async function updateResourceWiring(targetDir, names, config, persistenceOrOptio
1467
1495
  (await updateInfrastructurePorts(targetDir, names, config, options))) {
1468
1496
  updated.add(config.paths.portWiring);
1469
1497
  }
1470
- if (generationOptions.auth &&
1498
+ if (generationOptions.authorization &&
1471
1499
  (await updateInfrastructureGatePolicies(targetDir, names, config, options))) {
1472
1500
  updated.add(config.paths.portWiring);
1473
1501
  }
@@ -1505,11 +1533,10 @@ function typeHasTopLevelProperty(source, typeName, propertyName) {
1505
1533
  function useCaseNames(input) {
1506
1534
  const parts = input
1507
1535
  .trim()
1508
- .split("/")
1509
- .map((part) => part.trim())
1510
- .filter(Boolean);
1511
- if (parts.length !== 2) {
1512
- throw new Error("Use case name must use feature/action format, for example projects/archive-project.");
1536
+ .split(".")
1537
+ .map((part) => part.trim());
1538
+ if (parts.length !== 2 || parts.some((part) => part.length === 0)) {
1539
+ throw new Error("Use case name must use feature.action format, for example projects.archive-project.");
1513
1540
  }
1514
1541
  const feature = identifierNames(parts[0], "Use case feature");
1515
1542
  const action = identifierNames(parts[1], "Use case action");
@@ -1541,7 +1568,7 @@ function policyNames(input) {
1541
1568
  return resolvedNames;
1542
1569
  }
1543
1570
  function eventNames(input) {
1544
- const names = featureArtifactNames(normalizeEventReference(input), "Event");
1571
+ const names = featureArtifactNames(input, "Event");
1545
1572
  const eventExportName = `${names.featureSingularPascal}${names.artifact.pascal}`;
1546
1573
  assertGeneratedIdentifiers(input, "Event name", [eventExportName]);
1547
1574
  return {
@@ -1587,7 +1614,7 @@ function factoryNames(input) {
1587
1614
  };
1588
1615
  }
1589
1616
  function defaultFactoryNamesForSeed(names) {
1590
- return factoryNames(`${names.feature.kebab}/${names.featureSingularKebab}`);
1617
+ return factoryNames(`${names.feature.kebab}.${names.featureSingularKebab}`);
1591
1618
  }
1592
1619
  function notificationNames(input) {
1593
1620
  const names = featureArtifactNames(input, "Notification");
@@ -1631,24 +1658,26 @@ function scheduleNames(input, options) {
1631
1658
  function uploadNames(input) {
1632
1659
  const names = featureArtifactNames(input, "Upload");
1633
1660
  const uploadExportName = `${names.artifact.pascal}Upload`;
1634
- assertGeneratedIdentifiers(input, "Upload name", [uploadExportName]);
1661
+ const componentExportName = `${names.artifact.pascal}Uploader`;
1662
+ assertGeneratedIdentifiers(input, "Upload name", [
1663
+ uploadExportName,
1664
+ componentExportName,
1665
+ ]);
1635
1666
  return {
1636
1667
  ...names,
1637
1668
  uploadName: names.stableName,
1638
1669
  uploadExportName,
1639
1670
  metadataSchemaName: `${uploadExportName}MetadataSchema`,
1640
1671
  metadataTypeName: `${uploadExportName}Metadata`,
1672
+ fileConstraintsExportName: `${names.artifact.camel}UploadFile`,
1673
+ manifestExportName: `${names.artifact.camel}UploadManifest`,
1674
+ manifestFileName: `${names.artifact.kebab}-upload-manifest`,
1675
+ reactUploadsExportName: `${names.artifact.camel}ReactUploads`,
1676
+ clientFileName: `${names.artifact.kebab}-upload`,
1677
+ componentExportName,
1678
+ componentFileName: `${names.artifact.kebab}-uploader`,
1641
1679
  };
1642
1680
  }
1643
- function normalizeEventReference(input) {
1644
- const trimmed = input.trim();
1645
- if (trimmed.includes("/"))
1646
- return trimmed;
1647
- const dotIndex = trimmed.indexOf(".");
1648
- if (dotIndex === -1)
1649
- return trimmed;
1650
- return `${trimmed.slice(0, dotIndex)}/${trimmed.slice(dotIndex + 1)}`;
1651
- }
1652
1681
  function inferUseCaseKind(action) {
1653
1682
  if (/^(count|find|get|list|search)-/.test(`${action}-`)) {
1654
1683
  return "query";
@@ -1656,8 +1685,8 @@ function inferUseCaseKind(action) {
1656
1685
  return "command";
1657
1686
  }
1658
1687
  function resourceFiles(names, config, persistence = "memory", mode = "feature", options = {
1659
- auth: false,
1660
- tenant: false,
1688
+ authorization: false,
1689
+ tenantScoped: false,
1661
1690
  events: false,
1662
1691
  softDelete: false,
1663
1692
  }, database = "sqlite") {
@@ -1710,7 +1739,7 @@ function resourceFiles(names, config, persistence = "memory", mode = "feature",
1710
1739
  },
1711
1740
  ]
1712
1741
  : []),
1713
- ...(mode === "resource" && options.auth
1742
+ ...(mode === "resource" && options.authorization
1714
1743
  ? [
1715
1744
  {
1716
1745
  path: path.join(featureDir, "tests/policy.test.ts"),
@@ -1765,6 +1794,59 @@ function featureUiComponentFiles(names, config) {
1765
1794
  },
1766
1795
  ];
1767
1796
  }
1797
+ async function missingFeatureUiFormsFiles(targetDir, config) {
1798
+ const formsPath = clientFormsPath(config);
1799
+ const existing = await readOptionalFile(path.join(targetDir, formsPath));
1800
+ if (existing === undefined) {
1801
+ return [
1802
+ {
1803
+ path: formsPath,
1804
+ content: `import { createReactHookForm } from "@beignet/react-hook-form";
1805
+
1806
+ export const rhf = createReactHookForm();
1807
+ `,
1808
+ },
1809
+ ];
1810
+ }
1811
+ if (!sourceExportsValue(existing, "rhf")) {
1812
+ throw new Error(`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.`);
1813
+ }
1814
+ return [];
1815
+ }
1816
+ async function assertUploadUiApp(targetDir, config) {
1817
+ const clientPath = clientIndexPath(config);
1818
+ const clientSource = await readOptionalFile(path.join(targetDir, clientPath));
1819
+ if (!clientSource || !sourceExportsValue(clientSource, "rq")) {
1820
+ throw new Error(`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.`);
1821
+ }
1822
+ }
1823
+ function sourceExportsValue(source, name) {
1824
+ const sourceFile = ts.createSourceFile("client-forms.ts", source, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
1825
+ for (const statement of sourceFile.statements) {
1826
+ const exported = ts.canHaveModifiers(statement) &&
1827
+ ts
1828
+ .getModifiers(statement)
1829
+ ?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
1830
+ if (exported && ts.isVariableStatement(statement)) {
1831
+ if (statement.declarationList.declarations.some((declaration) => ts.isIdentifier(declaration.name) && declaration.name.text === name)) {
1832
+ return true;
1833
+ }
1834
+ }
1835
+ if (exported &&
1836
+ ts.isFunctionDeclaration(statement) &&
1837
+ statement.name?.text === name) {
1838
+ return true;
1839
+ }
1840
+ if (ts.isExportDeclaration(statement) &&
1841
+ !statement.isTypeOnly &&
1842
+ statement.exportClause &&
1843
+ ts.isNamedExports(statement.exportClause) &&
1844
+ statement.exportClause.elements.some((element) => !element.isTypeOnly && element.name.text === name)) {
1845
+ return true;
1846
+ }
1847
+ }
1848
+ return false;
1849
+ }
1768
1850
  function featureUiIndexFile(names, config) {
1769
1851
  return {
1770
1852
  path: featureUiIndexFilePath(names, config),
@@ -1893,12 +1975,54 @@ function scheduleFiles(names, config, options) {
1893
1975
  }
1894
1976
  return files;
1895
1977
  }
1896
- function uploadFiles(names, config) {
1978
+ function uploadFiles(names, config, options) {
1897
1979
  return [
1980
+ {
1981
+ path: uploadManifestFilePath(names, config),
1982
+ content: uploadManifestFile(names),
1983
+ },
1898
1984
  {
1899
1985
  path: uploadFilePath(names, config),
1900
1986
  content: uploadFile(names, config),
1901
1987
  },
1988
+ ...(options.ui ? uploadUiFiles(names, config) : []),
1989
+ ];
1990
+ }
1991
+ function uploadFeatureDir(names, config) {
1992
+ return path.join(config.paths.features, names.feature.kebab);
1993
+ }
1994
+ function uploadManifestFilePath(names, config) {
1995
+ return path.join(uploadFeatureDir(names, config), `${names.manifestFileName}.ts`);
1996
+ }
1997
+ function uploadUiClientFilePath(names, config) {
1998
+ return path.join(uploadFeatureDir(names, config), "client", `${names.clientFileName}.ts`);
1999
+ }
2000
+ function uploadUiComponentFilePath(names, config) {
2001
+ return path.join(uploadFeatureDir(names, config), "components", `${names.componentFileName}.tsx`);
2002
+ }
2003
+ function uploadUiComponentTestFilePath(names, config) {
2004
+ return path.join(uploadFeatureDir(names, config), "tests", `${names.componentFileName}.test.tsx`);
2005
+ }
2006
+ function uploadUiComponentIndexFile(names, config) {
2007
+ return {
2008
+ path: path.join(uploadFeatureDir(names, config), "components", "index.ts"),
2009
+ content: `export { ${names.componentExportName} } from "./${names.componentFileName}";\n`,
2010
+ };
2011
+ }
2012
+ function uploadUiFiles(names, config) {
2013
+ return [
2014
+ {
2015
+ path: uploadUiClientFilePath(names, config),
2016
+ content: uploadUiClientFile(names, config),
2017
+ },
2018
+ {
2019
+ path: uploadUiComponentFilePath(names, config),
2020
+ content: uploadUiComponentFile(names, config),
2021
+ },
2022
+ {
2023
+ path: uploadUiComponentTestFilePath(names, config),
2024
+ content: uploadUiComponentTestFile(names, config),
2025
+ },
1902
2026
  ];
1903
2027
  }
1904
2028
  function outboxFiles(config) {
@@ -2204,7 +2328,7 @@ function schemasFile(names, mode, options) {
2204
2328
 
2205
2329
  export const ${names.singularPascal}Schema = z.object({
2206
2330
  id: z.string().uuid(),
2207
- ${options.tenant ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
2331
+ ${options.tenantScoped ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
2208
2332
  version: z.number().int().min(1),
2209
2333
  createdAt: z.string().datetime(),
2210
2334
  updatedAt: z.string().datetime(),
@@ -2304,7 +2428,7 @@ export const List${names.pluralPascal}OutputSchema = z.object({
2304
2428
  });
2305
2429
 
2306
2430
  export const Create${names.singularPascal}InputSchema = z.object({
2307
- name: z.string().min(1).max(120),
2431
+ name: z.string().trim().min(1).max(120),
2308
2432
  });
2309
2433
  ${mode === "resource"
2310
2434
  ? `
@@ -2313,7 +2437,7 @@ export const ${names.singularPascal}IdInputSchema = z.object({
2313
2437
  });
2314
2438
 
2315
2439
  export const Update${names.singularPascal}BodySchema = z.object({
2316
- name: z.string().min(1).max(120),
2440
+ name: z.string().trim().min(1).max(120),
2317
2441
  version: z.number().int().min(1),
2318
2442
  });
2319
2443
 
@@ -2347,7 +2471,7 @@ function listUseCaseFile(names, config, options) {
2347
2471
  const filePath = path.join(resourceUseCaseDir(names, config), `list-${names.pluralKebab}.ts`);
2348
2472
  return `import "@beignet/core/server-only";
2349
2473
  import { normalizeCursorPage } from "@beignet/core/pagination";
2350
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2474
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2351
2475
  import {
2352
2476
  decode${names.singularPascal}Cursor,
2353
2477
  List${names.pluralPascal}InputSchema,
@@ -2359,7 +2483,7 @@ export const list${names.pluralPascal}UseCase = useCase
2359
2483
  .input(List${names.pluralPascal}InputSchema)
2360
2484
  .output(List${names.pluralPascal}OutputSchema)
2361
2485
  .run(async ({ ctx, input }) => {
2362
- ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const page = normalizeCursorPage(input, {
2486
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const page = normalizeCursorPage(input, {
2363
2487
  defaultLimit: 20,
2364
2488
  maxLimit: 100,
2365
2489
  });
@@ -2370,14 +2494,14 @@ ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const
2370
2494
  name: input.name,
2371
2495
  sortBy: input.sortBy,
2372
2496
  sortDirection: input.sortDirection,
2373
- }${options.tenant ? ", scope" : ""});
2497
+ }${options.tenantScoped ? ", scope" : ""});
2374
2498
  });
2375
2499
  `;
2376
2500
  }
2377
2501
  function createUseCaseFile(names, config, options) {
2378
2502
  const filePath = path.join(resourceUseCaseDir(names, config), `create-${names.singularKebab}.ts`);
2379
2503
  return `import "@beignet/core/server-only";
2380
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2504
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2381
2505
  import {
2382
2506
  Create${names.singularPascal}InputSchema,
2383
2507
  ${names.singularPascal}Schema,
@@ -2389,7 +2513,7 @@ export const create${names.singularPascal}UseCase = useCase
2389
2513
  .input(Create${names.singularPascal}InputSchema)
2390
2514
  .output(${names.singularPascal}Schema)
2391
2515
  .run(async ({ ctx, input }) => {
2392
- ${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" : ""});
2516
+ ${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" : ""});
2393
2517
  ${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Created, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
2394
2518
  return ${names.singularCamel};
2395
2519
  });
@@ -2398,7 +2522,7 @@ ${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPasca
2398
2522
  function getUseCaseFile(names, config, options) {
2399
2523
  const filePath = path.join(resourceUseCaseDir(names, config), `get-${names.singularKebab}.ts`);
2400
2524
  return `import "@beignet/core/server-only";
2401
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
2525
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
2402
2526
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2403
2527
  import {
2404
2528
  ${names.singularPascal}IdInputSchema,
@@ -2410,7 +2534,7 @@ export const get${names.singularPascal}UseCase = useCase
2410
2534
  .input(${names.singularPascal}IdInputSchema)
2411
2535
  .output(${names.singularPascal}Schema)
2412
2536
  .run(async ({ ctx, input }) => {
2413
- ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenant ? ", scope" : ""});
2537
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
2414
2538
  if (!${names.singularCamel}) {
2415
2539
  throw appError("${names.singularPascal}NotFound", {
2416
2540
  details: { id: input.id },
@@ -2424,7 +2548,7 @@ ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const
2424
2548
  function updateUseCaseFile(names, config, options) {
2425
2549
  const filePath = path.join(resourceUseCaseDir(names, config), `update-${names.singularKebab}.ts`);
2426
2550
  return `import "@beignet/core/server-only";
2427
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
2551
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
2428
2552
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2429
2553
  ${options.events ? `import { ${names.singularPascal}Updated } from "../domain/events";\n` : ""}import {
2430
2554
  Update${names.singularPascal}InputSchema,
@@ -2436,13 +2560,13 @@ export const update${names.singularPascal}UseCase = useCase
2436
2560
  .input(Update${names.singularPascal}InputSchema)
2437
2561
  .output(${names.singularPascal}Schema)
2438
2562
  .run(async ({ ctx, input }) => {
2439
- ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenant ? ", scope" : ""});
2563
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
2440
2564
  if (!existing) {
2441
2565
  throw appError("${names.singularPascal}NotFound", {
2442
2566
  details: { id: input.id },
2443
2567
  });
2444
2568
  }
2445
- ${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
2569
+ ${options.authorization ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
2446
2570
  throw appError("${names.singularPascal}Conflict", {
2447
2571
  details: { id: input.id, expectedVersion: existing.version },
2448
2572
  });
@@ -2450,7 +2574,7 @@ ${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", ex
2450
2574
 
2451
2575
  const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.update({
2452
2576
  ...input,
2453
- }${options.tenant ? ", scope" : ""});
2577
+ }${options.tenantScoped ? ", scope" : ""});
2454
2578
  if (!${names.singularCamel}) {
2455
2579
  throw appError("${names.singularPascal}Conflict", {
2456
2580
  details: { id: input.id, expectedVersion: existing.version },
@@ -2465,7 +2589,7 @@ function deleteUseCaseFile(names, config, options) {
2465
2589
  const filePath = path.join(resourceUseCaseDir(names, config), `delete-${names.singularKebab}.ts`);
2466
2590
  return `import "@beignet/core/server-only";
2467
2591
  import { z } from "zod";
2468
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
2592
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
2469
2593
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2470
2594
  ${options.events ? `import { ${names.singularPascal}Deleted } from "../domain/events";\n` : ""}import { ${names.singularPascal}IdInputSchema } from "../schemas";
2471
2595
 
@@ -2474,7 +2598,7 @@ export const delete${names.singularPascal}UseCase = useCase
2474
2598
  .input(${names.singularPascal}IdInputSchema)
2475
2599
  .output(z.void())
2476
2600
  .run(async ({ ctx, input }) => {
2477
- ${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" : ""});
2601
+ ${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" : ""});
2478
2602
  if (!deleted) {
2479
2603
  throw appError("${names.singularPascal}NotFound", {
2480
2604
  details: { id: input.id },
@@ -2504,7 +2628,7 @@ function repositoryPortFile(names, config, mode, options) {
2504
2628
  PageResult,
2505
2629
  SortDirection,
2506
2630
  } from "@beignet/core/pagination";
2507
- ${options.tenant ? `import type { TenantScope } from "@beignet/core/tenancy";\n` : ""}import type {
2631
+ ${options.tenantScoped ? `import type { TenantScope } from "@beignet/core/tenancy";\n` : ""}import type {
2508
2632
  Create${names.singularPascal}Input,
2509
2633
  ${names.singularPascal}Cursor,
2510
2634
  ${names.singularPascal}SortBy,
@@ -2521,9 +2645,9 @@ export type List${names.pluralPascal}Query = {
2521
2645
  };
2522
2646
 
2523
2647
  export interface ${names.singularPascal}Repository {
2524
- list(query: List${names.pluralPascal}Query${options.tenant ? ", scope: TenantScope" : ""}): Promise<List${names.pluralPascal}Result>;
2525
- create(input: Create${names.singularPascal}Input${options.tenant ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal}>;
2526
- ${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` : ""}}
2648
+ list(query: List${names.pluralPascal}Query${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<List${names.pluralPascal}Result>;
2649
+ create(input: Create${names.singularPascal}Input${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal}>;
2650
+ ${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` : ""}}
2527
2651
  `;
2528
2652
  }
2529
2653
  function inMemoryRepositoryFile(names, config, mode, options) {
@@ -2551,13 +2675,13 @@ function inMemoryRepositoryFile(names, config, mode, options) {
2551
2675
  ? " || existing.deletedAt !== null"
2552
2676
  : "";
2553
2677
  const deleteImplementation = options.softDelete
2554
- ? `${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();
2678
+ ? `${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();
2555
2679
  ${names.pluralCamel}.set(id, { ...existing, deletedAt: now, updatedAt: now });
2556
2680
  return true;`
2557
- : `${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);`;
2681
+ : `${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);`;
2558
2682
  return `import "@beignet/core/server-only";
2559
2683
  import { cursorPageResult } from "@beignet/core/pagination";
2560
- ${options.tenant ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
2684
+ ${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
2561
2685
  import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
2562
2686
  import type {
2563
2687
  Create${names.singularPascal}Input,
@@ -2567,7 +2691,7 @@ ${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.s
2567
2691
  ${recordType}function to${names.singularPascal}(${names.singularCamel}: ${mapValueType}): ${names.singularPascal} {
2568
2692
  return {
2569
2693
  id: ${names.singularCamel}.id,
2570
- ${options.tenant ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
2694
+ ${options.tenantScoped ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
2571
2695
  version: ${names.singularCamel}.version,
2572
2696
  createdAt: ${names.singularCamel}.createdAt,
2573
2697
  updatedAt: ${names.singularCamel}.updatedAt,
@@ -2625,10 +2749,10 @@ export function createInMemory${names.singularPascal}Repository(
2625
2749
  );
2626
2750
 
2627
2751
  return {
2628
- async list(query${options.tenant ? ", scope" : ""}) {
2752
+ async list(query${options.tenantScoped ? ", scope" : ""}) {
2629
2753
  const name = query.name?.toLocaleLowerCase();
2630
- ${options.tenant ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values())
2631
- ${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))
2754
+ ${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values())
2755
+ ${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))
2632
2756
  .sort((left, right) => compare${names.pluralPascal}(left, right, query))
2633
2757
  .filter((${names.singularCamel}) => isAfter${names.singularPascal}Cursor(${names.singularCamel}, query));
2634
2758
  const pageItems = all${names.pluralPascal}.slice(0, query.page.limit);
@@ -2643,11 +2767,11 @@ ${activeFilter}${options.tenant ? `\t\t\t\t.filter((${names.singularCamel}) => $
2643
2767
  nextCursor,
2644
2768
  );
2645
2769
  },
2646
- async create(input${options.tenant ? ", scope" : ""}) {
2770
+ async create(input${options.tenantScoped ? ", scope" : ""}) {
2647
2771
  const now = new Date().toISOString();
2648
- ${options.tenant ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel}: ${names.singularPascal} = {
2772
+ ${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel}: ${names.singularPascal} = {
2649
2773
  id: crypto.randomUUID(),
2650
- ${options.tenant ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
2774
+ ${options.tenantScoped ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
2651
2775
  version: 1,
2652
2776
  createdAt: now,
2653
2777
  updatedAt: now,
@@ -2657,7 +2781,7 @@ ${options.tenant ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
2657
2781
  ${newDeletedAtField} });
2658
2782
  return ${names.singularCamel};
2659
2783
  },
2660
- ${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` : ""}
2784
+ ${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` : ""}
2661
2785
  };
2662
2786
  }
2663
2787
  `;
@@ -2667,7 +2791,7 @@ function drizzleSchemaFile(names, options, dialect) {
2667
2791
 
2668
2792
  export const ${names.pluralCamel} = ${dialect.tableFunction}("${names.pluralKebab.replaceAll("-", "_")}", {
2669
2793
  id: ${dialect.idColumn("id")}.primaryKey(),
2670
- ${options.tenant ? `\ttenantId: ${dialect.idColumn("tenant_id")}.notNull(),\n` : ""} name: text("name").notNull(),
2794
+ ${options.tenantScoped ? `\ttenantId: ${dialect.idColumn("tenant_id")}.notNull(),\n` : ""} name: text("name").notNull(),
2671
2795
  version: ${dialect.integerColumn("version")}.notNull(),
2672
2796
  createdAt: ${dialect.timestampColumn("created_at")}.notNull(),
2673
2797
  updatedAt: ${dialect.timestampColumn("updated_at")}.notNull(),
@@ -2764,20 +2888,20 @@ function drizzleRepositoryFile(names, config, mode, options, dialect) {
2764
2888
  const schemaPath = drizzleSchemaIndexPath(config);
2765
2889
  const findWhere = drizzleResourceWhere(names, options, [
2766
2890
  `eq(schema.${names.pluralCamel}.id, id)`,
2767
- ...(options.tenant
2891
+ ...(options.tenantScoped
2768
2892
  ? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
2769
2893
  : []),
2770
2894
  ]);
2771
2895
  const updateWhere = drizzleResourceWhere(names, options, [
2772
2896
  `eq(schema.${names.pluralCamel}.id, input.id)`,
2773
2897
  `eq(schema.${names.pluralCamel}.version, input.version)`,
2774
- ...(options.tenant
2898
+ ...(options.tenantScoped
2775
2899
  ? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
2776
2900
  : []),
2777
2901
  ]);
2778
2902
  const deleteWhere = drizzleResourceWhere(names, options, [
2779
2903
  `eq(schema.${names.pluralCamel}.id, id)`,
2780
- ...(options.tenant
2904
+ ...(options.tenantScoped
2781
2905
  ? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
2782
2906
  : []),
2783
2907
  ]);
@@ -2795,7 +2919,7 @@ function drizzleRepositoryFile(names, config, mode, options, dialect) {
2795
2919
  ].filter((name) => Boolean(name));
2796
2920
  // After a version-guarded update succeeds, re-select by identity only —
2797
2921
  // matching what `.returning()` yields on the dialects that support it.
2798
- const updateReselectWhere = options.tenant
2922
+ const updateReselectWhere = options.tenantScoped
2799
2923
  ? `and(eq(schema.${names.pluralCamel}.id, input.id), eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)))`
2800
2924
  : `eq(schema.${names.pluralCamel}.id, input.id)`;
2801
2925
  // drizzle-orm/mysql2 has no `.returning(...)`; mutations resolve to a
@@ -2817,13 +2941,13 @@ function affectedRows(result: unknown): number {
2817
2941
  `
2818
2942
  : "";
2819
2943
  const createMethod = dialect.supportsReturning
2820
- ? ` async create(input${options.tenant ? ", scope" : ""}) {
2944
+ ? ` async create(input${options.tenantScoped ? ", scope" : ""}) {
2821
2945
  const now = new Date().toISOString();
2822
2946
  const [row] = await db
2823
2947
  .insert(schema.${names.pluralCamel})
2824
2948
  .values({
2825
2949
  id: crypto.randomUUID(),
2826
- ${options.tenant ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
2950
+ ${options.tenantScoped ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
2827
2951
  version: 1,
2828
2952
  createdAt: now,
2829
2953
  updatedAt: now,
@@ -2837,11 +2961,11 @@ ${options.tenant ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name:
2837
2961
  return to${names.singularPascal}(row);
2838
2962
  },
2839
2963
  `
2840
- : ` async create(input${options.tenant ? ", scope" : ""}) {
2964
+ : ` async create(input${options.tenantScoped ? ", scope" : ""}) {
2841
2965
  const now = new Date().toISOString();
2842
2966
  const ${names.singularCamel}: ${names.singularPascal} = {
2843
2967
  id: crypto.randomUUID(),
2844
- ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
2968
+ ${options.tenantScoped ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
2845
2969
  version: 1,
2846
2970
  createdAt: now,
2847
2971
  updatedAt: now,
@@ -2854,7 +2978,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
2854
2978
  const resourceMethods = mode !== "resource"
2855
2979
  ? ""
2856
2980
  : dialect.supportsReturning
2857
- ? ` async findById(id: string${options.tenant ? ", scope" : ""}) {
2981
+ ? ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
2858
2982
  const [row] = await db
2859
2983
  .select()
2860
2984
  .from(schema.${names.pluralCamel})
@@ -2863,7 +2987,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
2863
2987
 
2864
2988
  return row ? to${names.singularPascal}(row) : null;
2865
2989
  },
2866
- async update(input${options.tenant ? ", scope" : ""}) {
2990
+ async update(input${options.tenantScoped ? ", scope" : ""}) {
2867
2991
  const [row] = await db
2868
2992
  .update(schema.${names.pluralCamel})
2869
2993
  .set({
@@ -2876,12 +3000,12 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
2876
3000
 
2877
3001
  return row ? to${names.singularPascal}(row) : null;
2878
3002
  },
2879
- async delete(id: string${options.tenant ? ", scope" : ""}) {
3003
+ async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
2880
3004
  ${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`}
2881
3005
  return rows.length > 0;
2882
3006
  },
2883
3007
  `
2884
- : ` async findById(id: string${options.tenant ? ", scope" : ""}) {
3008
+ : ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
2885
3009
  const [row] = await db
2886
3010
  .select()
2887
3011
  .from(schema.${names.pluralCamel})
@@ -2890,7 +3014,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
2890
3014
 
2891
3015
  return row ? to${names.singularPascal}(row) : null;
2892
3016
  },
2893
- async update(input${options.tenant ? ", scope" : ""}) {
3017
+ async update(input${options.tenantScoped ? ", scope" : ""}) {
2894
3018
  const result = await db
2895
3019
  .update(schema.${names.pluralCamel})
2896
3020
  .set({
@@ -2910,14 +3034,14 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
2910
3034
 
2911
3035
  return row ? to${names.singularPascal}(row) : null;
2912
3036
  },
2913
- async delete(id: string${options.tenant ? ", scope" : ""}) {
3037
+ async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
2914
3038
  ${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`}
2915
3039
  return affectedRows(result) > 0;
2916
3040
  },
2917
3041
  `;
2918
3042
  return `import "@beignet/core/server-only";
2919
3043
  import { cursorPageResult } from "@beignet/core/pagination";
2920
- ${options.tenant ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
3044
+ ${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
2921
3045
  import { ${drizzleImports.join(", ")} } from "drizzle-orm";
2922
3046
  import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
2923
3047
  import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
@@ -2932,7 +3056,7 @@ type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelec
2932
3056
  function to${names.singularPascal}(row: ${names.singularPascal}Row): ${names.singularPascal} {
2933
3057
  return {
2934
3058
  id: row.id,
2935
- ${options.tenant ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
3059
+ ${options.tenantScoped ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
2936
3060
  version: row.version,
2937
3061
  createdAt: row.createdAt,
2938
3062
  updatedAt: row.updatedAt,
@@ -2964,9 +3088,9 @@ function ${names.singularCamel}CursorFilter(
2964
3088
 
2965
3089
  function ${names.singularCamel}ListWhere(
2966
3090
  query: List${names.pluralPascal}Query,
2967
- ${options.tenant ? `\tscope: Parameters<${names.singularPascal}Repository["list"]>[1],\n` : ""}): SQL<unknown> | undefined {
3091
+ ${options.tenantScoped ? `\tscope: Parameters<${names.singularPascal}Repository["list"]>[1],\n` : ""}): SQL<unknown> | undefined {
2968
3092
  const filters: SQL<unknown>[] = [
2969
- ${options.tenant ? `\t\teq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
3093
+ ${options.tenantScoped ? `\t\teq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
2970
3094
  const cursor = ${names.singularCamel}CursorFilter(query);
2971
3095
 
2972
3096
  if (query.name) {
@@ -3000,8 +3124,8 @@ export function createDrizzle${names.singularPascal}Repository(
3000
3124
  db: ${dialect.dbTypeName}<typeof schema>,
3001
3125
  ): ${names.singularPascal}Repository {
3002
3126
  return {
3003
- async list(query${options.tenant ? ", scope" : ""}) {
3004
- const where = ${names.singularCamel}ListWhere(query${options.tenant ? ", scope" : ""});
3127
+ async list(query${options.tenantScoped ? ", scope" : ""}) {
3128
+ const where = ${names.singularCamel}ListWhere(query${options.tenantScoped ? ", scope" : ""});
3005
3129
  const rows = await db
3006
3130
  .select()
3007
3131
  .from(schema.${names.pluralCamel})
@@ -3057,7 +3181,7 @@ export const list${names.pluralPascal} = ${names.pluralCamel}
3057
3181
  export const create${names.singularPascal} = ${names.pluralCamel}
3058
3182
  .post("/api/${names.pluralKebab}")
3059
3183
  .body(Create${names.singularPascal}InputSchema)
3060
- ${options.auth ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
3184
+ ${options.authorization ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
3061
3185
  201: ${names.singularPascal}Schema,
3062
3186
  });
3063
3187
  ${mode === "resource"
@@ -3074,7 +3198,7 @@ export const update${names.singularPascal} = ${names.pluralCamel}
3074
3198
  .patch("/api/${names.pluralKebab}/:id")
3075
3199
  .pathParams(${names.singularPascal}IdInputSchema)
3076
3200
  .body(Update${names.singularPascal}BodySchema)
3077
- ${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 })
3201
+ ${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 })
3078
3202
  .responses({
3079
3203
  200: ${names.singularPascal}Schema,
3080
3204
  });
@@ -3082,7 +3206,7 @@ ${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability
3082
3206
  export const delete${names.singularPascal} = ${names.pluralCamel}
3083
3207
  .delete("/api/${names.pluralKebab}/:id")
3084
3208
  .pathParams(${names.singularPascal}IdInputSchema)
3085
- ${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 })
3209
+ ${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 })
3086
3210
  .responses({
3087
3211
  204: null,
3088
3212
  });
@@ -3266,7 +3390,7 @@ function isAuthenticated(ctx: AuthorizationContext) {
3266
3390
 
3267
3391
  function canWrite(ctx: AuthorizationContext) {
3268
3392
  if (!isAuthenticated(ctx)) return deny("You must be signed in.");
3269
- ${options.tenant ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
3393
+ ${options.tenantScoped ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
3270
3394
  }
3271
3395
 
3272
3396
  export const ${names.singularCamel}Policy = definePolicy({
@@ -3276,13 +3400,13 @@ export const ${names.singularCamel}Policy = definePolicy({
3276
3400
  "${names.pluralCamel}.update": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
3277
3401
  const decision = canWrite(ctx);
3278
3402
  if (decision !== true) return decision;
3279
- ${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` : ""}
3403
+ ${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` : ""}
3280
3404
  return true;
3281
3405
  },
3282
3406
  "${names.pluralCamel}.delete": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
3283
3407
  const decision = canWrite(ctx);
3284
3408
  if (decision !== true) return decision;
3285
- ${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` : ""}
3409
+ ${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` : ""}
3286
3410
  return true;
3287
3411
  },
3288
3412
  "${names.pluralCamel}.manage": (_ctx: AuthorizationContext) =>
@@ -3329,7 +3453,7 @@ function create${names.singularPascal}(): ${names.singularPascal} {
3329
3453
  const now = new Date().toISOString();
3330
3454
  return {
3331
3455
  id: "00000000-0000-4000-8000-000000000001",
3332
- ${options.tenant ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
3456
+ ${options.tenantScoped ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
3333
3457
  version: 1,
3334
3458
  createdAt: now,
3335
3459
  updatedAt: now,
@@ -3344,7 +3468,7 @@ describe("${names.singularCamel}Policy", () => {
3344
3468
  const ${names.singularCamel} = create${names.singularPascal}();
3345
3469
  const ctx = {
3346
3470
  actor: { type: "user" as const, id: "user_test" },
3347
- ${options.tenant ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
3471
+ ${options.tenantScoped ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
3348
3472
 
3349
3473
  await expect(
3350
3474
  tester.assertMatrix([
@@ -3546,8 +3670,32 @@ ${names.timezone ? `\t\ttimezone: "${names.timezone}",\n` : ""}\t\tpayload: ${na
3546
3670
  );
3547
3671
  `;
3548
3672
  }
3673
+ function uploadManifestFile(names) {
3674
+ return `import type {
3675
+ \tUploadFileConstraints,
3676
+ \tUploadManifestEntry,
3677
+ } from "@beignet/core/uploads";
3678
+
3679
+ export const ${names.fileConstraintsExportName} = {
3680
+ \tcontentTypes: ["application/pdf", "text/plain"],
3681
+ \tmaxSizeBytes: 5 * 1024 * 1024,
3682
+ \tmaxFiles: 1,
3683
+ \tvisibility: "private",
3684
+ \tcacheControl: "private, max-age=0",
3685
+ } satisfies UploadFileConstraints;
3686
+
3687
+ export const ${names.manifestExportName} = [
3688
+ \t{
3689
+ \t\tname: "${names.uploadName}",
3690
+ \t\tdescription: "${names.artifact.pascal} upload",
3691
+ \t\tfile: ${names.fileConstraintsExportName},
3692
+ \t},
3693
+ ] satisfies readonly UploadManifestEntry[];
3694
+ `;
3695
+ }
3549
3696
  function uploadFile(names, config) {
3550
3697
  return `import { z } from "zod";
3698
+ import { ${names.fileConstraintsExportName} } from "${relativeModule(uploadFilePath(names, config), uploadManifestFilePath(names, config))}";
3551
3699
  import { defineUpload } from "${aliasModule(config.paths.uploadsBuilder)}";
3552
3700
 
3553
3701
  export const ${names.metadataSchemaName} = z.object({
@@ -3558,13 +3706,7 @@ export type ${names.metadataTypeName} = z.infer<typeof ${names.metadataSchemaNam
3558
3706
 
3559
3707
  export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
3560
3708
  \tmetadata: ${names.metadataSchemaName},
3561
- \tfile: {
3562
- \t\tcontentTypes: ["application/pdf", "text/plain"],
3563
- \t\tmaxSizeBytes: 5 * 1024 * 1024,
3564
- \t\tmaxFiles: 1,
3565
- \t\tvisibility: "private",
3566
- \t\tcacheControl: "private, max-age=0",
3567
- \t},
3709
+ \tfile: ${names.fileConstraintsExportName},
3568
3710
  \tauthorize({ ctx }) {
3569
3711
  \t\treturn ctx.actor.type === "user";
3570
3712
  \t},
@@ -3594,6 +3736,127 @@ export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
3594
3736
  });
3595
3737
  `;
3596
3738
  }
3739
+ function uploadUiClientFile(names, config) {
3740
+ const clientPath = uploadUiClientFilePath(names, config);
3741
+ const registryPath = path.join(featureArtifactDir(names, "uploads", config), "index.ts");
3742
+ return `import { createUploadClient } from "@beignet/core/uploads/client";
3743
+ import { createReactUploads } from "@beignet/react-uploads";
3744
+ import { ${names.manifestExportName} } from "${relativeModule(clientPath, uploadManifestFilePath(names, config))}";
3745
+ import type { ${names.featureSingularCamel}Uploads } from "${relativeModule(clientPath, registryPath)}";
3746
+
3747
+ const ${names.artifact.camel}UploadClient = createUploadClient<typeof ${names.featureSingularCamel}Uploads>({
3748
+ \tbaseUrl: "/api/uploads",
3749
+ \tmanifest: ${names.manifestExportName},
3750
+ });
3751
+
3752
+ export const ${names.reactUploadsExportName} = createReactUploads({
3753
+ \tuploads: ${names.artifact.camel}UploadClient,
3754
+ });
3755
+ `;
3756
+ }
3757
+ function uploadUiComponentFile(names, config) {
3758
+ const componentPath = uploadUiComponentFilePath(names, config);
3759
+ return `"use client";
3760
+
3761
+ import { useId } from "react";
3762
+ import { ${names.reactUploadsExportName} } from "${relativeModule(componentPath, uploadUiClientFilePath(names, config))}";
3763
+
3764
+ export type ${names.componentExportName}Props = {
3765
+ \tresourceId: string;
3766
+ \tdisabled?: boolean;
3767
+ \tonComplete?(objectKeys: readonly string[]): void | Promise<void>;
3768
+ };
3769
+
3770
+ export function ${names.componentExportName}({
3771
+ \tresourceId,
3772
+ \tdisabled = false,
3773
+ \tonComplete,
3774
+ }: ${names.componentExportName}Props) {
3775
+ \tconst inputId = useId();
3776
+ \tconst upload = ${names.reactUploadsExportName}.useUpload("${names.uploadName}", {
3777
+ \t\tonSuccess({ result }) {
3778
+ \t\t\treturn onComplete?.(result.objectKeys);
3779
+ \t\t},
3780
+ \t});
3781
+ \tconst maxSizeMb = upload.constraints?.maxSizeBytes
3782
+ \t\t? Math.round(upload.constraints.maxSizeBytes / 1024 / 1024)
3783
+ \t\t: undefined;
3784
+
3785
+ \treturn (
3786
+ \t\t<section className="${names.artifact.kebab}-uploader">
3787
+ \t\t\t<label htmlFor={inputId}>${names.artifact.pascal}</label>
3788
+ \t\t\t<input
3789
+ \t\t\t\tid={inputId}
3790
+ \t\t\t\ttype="file"
3791
+ \t\t\t\taccept={upload.accept}
3792
+ \t\t\t\tdisabled={disabled || upload.isUploading}
3793
+ \t\t\t\tonChange={(event) => {
3794
+ \t\t\t\t\tconst file = event.currentTarget.files?.[0];
3795
+ \t\t\t\t\tevent.currentTarget.value = "";
3796
+ \t\t\t\t\tif (!file) return;
3797
+
3798
+ \t\t\t\t\tupload.uploadFile(file, {
3799
+ \t\t\t\t\t\tmetadata: { resourceId },
3800
+ \t\t\t\t\t});
3801
+ \t\t\t\t}}
3802
+ \t\t\t/>
3803
+ \t\t\t<p>
3804
+ \t\t\t\tPDF or plain text{maxSizeMb ? <> up to {maxSizeMb} MB</> : null}.
3805
+ \t\t\t</p>
3806
+
3807
+ \t\t\t{upload.files[0] ? <p>{upload.files[0].fileName}</p> : null}
3808
+ \t\t\t{upload.isUploading ? (
3809
+ \t\t\t\t<div aria-live="polite">
3810
+ \t\t\t\t\t<progress
3811
+ \t\t\t\t\t\taria-label="Upload progress"
3812
+ \t\t\t\t\t\tmax={100}
3813
+ \t\t\t\t\t\tvalue={upload.progress}
3814
+ \t\t\t\t\t/>
3815
+ \t\t\t\t\t<span>{Math.round(upload.progress)}%</span>
3816
+ \t\t\t\t\t<button type="button" onClick={upload.abort}>
3817
+ \t\t\t\t\t\tCancel upload
3818
+ \t\t\t\t\t</button>
3819
+ \t\t\t\t</div>
3820
+ \t\t\t) : null}
3821
+ \t\t\t{upload.isError ? (
3822
+ \t\t\t\t<p role="alert">{uploadErrorMessage(upload.error)}</p>
3823
+ \t\t\t) : null}
3824
+ \t\t\t{upload.isSuccess ? <p role="status">Upload complete.</p> : null}
3825
+ \t\t\t{upload.isError || upload.isSuccess ? (
3826
+ \t\t\t\t<button type="button" onClick={upload.reset}>
3827
+ \t\t\t\t\tReset upload
3828
+ \t\t\t\t</button>
3829
+ \t\t\t) : null}
3830
+ \t\t</section>
3831
+ \t);
3832
+ }
3833
+
3834
+ function uploadErrorMessage(error: unknown): string {
3835
+ \treturn error instanceof Error ? error.message : "Upload failed.";
3836
+ }
3837
+ `;
3838
+ }
3839
+ function uploadUiComponentTestFile(names, config) {
3840
+ const testPath = uploadUiComponentTestFilePath(names, config);
3841
+ const testHelperPath = path.join(path.dirname(config.paths.useCaseBuilder), "beignet-test.ts");
3842
+ return `import { renderToStaticMarkup } from "react-dom/server";
3843
+ import { describe, expect, test } from "${relativeModule(testPath, testHelperPath)}";
3844
+ import { ${names.manifestExportName} } from "${relativeModule(testPath, uploadManifestFilePath(names, config))}";
3845
+ import { ${names.componentExportName} } from "${relativeModule(testPath, uploadUiComponentFilePath(names, config))}";
3846
+
3847
+ describe("${names.componentExportName}", () => {
3848
+ \ttest("renders the generated upload constraints", () => {
3849
+ \t\tconst markup = renderToStaticMarkup(
3850
+ \t\t\t<${names.componentExportName} resourceId="resource-1" />,
3851
+ \t\t);
3852
+
3853
+ \t\texpect(${names.manifestExportName}[0]?.file.maxSizeBytes).toBe(5 * 1024 * 1024);
3854
+ \t\texpect(markup).toContain('accept="application/pdf,text/plain"');
3855
+ \t\texpect(markup).toContain("PDF or plain text");
3856
+ \t});
3857
+ });
3858
+ `;
3859
+ }
3597
3860
  function uploadRouteFile(names, config) {
3598
3861
  const registryName = `${names.featureSingularCamel}Uploads`;
3599
3862
  return `import {
@@ -3682,63 +3945,70 @@ function featureUiComponentFile(names, config) {
3682
3945
  const clientQueriesPath = featureUiClientQueriesFilePath(names, config);
3683
3946
  return `"use client";
3684
3947
 
3685
- import { contractErrorMessage } from "@beignet/core/client";
3948
+ import { rootFormError } from "@beignet/react-hook-form";
3686
3949
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3687
- import { useState } from "react";
3950
+ import { rhf } from "${aliasModule(clientFormsPath(config))}";
3951
+ import { create${names.singularPascal} } from "${relativeModule(componentPath, resourceContractFilePath(names, config))}";
3688
3952
  import {
3689
3953
  \tcreate${names.singularPascal}MutationOptions,
3690
3954
  \tinvalidate${names.pluralPascal},
3691
3955
  \tlist${names.pluralPascal}QueryOptions,
3692
3956
  } from "${relativeModule(componentPath, clientQueriesPath)}";
3693
3957
 
3958
+ const create${names.singularPascal}Form = rhf(create${names.singularPascal});
3959
+
3694
3960
  export function ${names.componentExportName}() {
3695
- \tconst [name, setName] = useState("");
3696
3961
  \tconst queryClient = useQueryClient();
3697
3962
  \tconst ${names.pluralCamel}Query = useQuery(list${names.pluralPascal}QueryOptions());
3963
+ \tconst form = create${names.singularPascal}Form.useForm({
3964
+ \t\tdefaultValues: { name: "" },
3965
+ \t});
3966
+ \tconst name = form.watch("name");
3698
3967
  \tconst create${names.singularPascal}Mutation = useMutation({
3699
3968
  \t\t...create${names.singularPascal}MutationOptions(),
3700
3969
  \t\tonSuccess: async () => {
3701
- \t\t\tsetName("");
3970
+ \t\t\tform.reset({ name: "" });
3702
3971
  \t\t\tawait invalidate${names.pluralPascal}(queryClient);
3703
3972
  \t\t},
3973
+ \t\tonError: (error) => {
3974
+ \t\t\tform.setError(
3975
+ \t\t\t\t"root",
3976
+ \t\t\t\trootFormError(error, "Could not create ${names.singularKebab}."),
3977
+ \t\t\t);
3978
+ \t\t},
3704
3979
  \t});
3705
3980
 
3706
3981
  \treturn (
3707
3982
  \t\t<section className="${names.pluralKebab}-panel">
3708
3983
  \t\t\t<form
3709
3984
  \t\t\t\tclassName="${names.pluralKebab}-composer"
3710
- \t\t\t\tonSubmit={(event) => {
3711
- \t\t\t\t\tevent.preventDefault();
3712
- \t\t\t\t\tconst trimmedName = name.trim();
3713
- \t\t\t\t\tif (!trimmedName) return;
3985
+ \t\t\t\tonSubmit={form.handleSubmit((values) => {
3986
+ \t\t\t\t\tform.clearErrors("root");
3714
3987
  \t\t\t\t\tcreate${names.singularPascal}Mutation.reset();
3715
3988
  \t\t\t\t\tcreate${names.singularPascal}Mutation.mutate({
3716
- \t\t\t\t\t\tbody: { name: trimmedName },
3989
+ \t\t\t\t\t\tbody: values,
3717
3990
  \t\t\t\t\t});
3718
- \t\t\t\t}}
3991
+ \t\t\t\t})}
3719
3992
  \t\t\t>
3720
3993
  \t\t\t\t<label htmlFor="${names.singularKebab}-name">New ${names.singularKebab}</label>
3721
3994
  \t\t\t\t<div className="${names.pluralKebab}-composer-row">
3722
3995
  \t\t\t\t\t<input
3723
3996
  \t\t\t\t\t\tid="${names.singularKebab}-name"
3724
- \t\t\t\t\t\tvalue={name}
3725
- \t\t\t\t\t\tonChange={(event) => setName(event.currentTarget.value)}
3997
+ \t\t\t\t\t\t{...form.register("name")}
3726
3998
  \t\t\t\t\t\tplaceholder="Name"
3727
3999
  \t\t\t\t\t/>
3728
4000
  \t\t\t\t\t<button
3729
4001
  \t\t\t\t\t\ttype="submit"
3730
- \t\t\t\t\t\tdisabled={!name.trim() || create${names.singularPascal}Mutation.isPending}
4002
+ \t\t\t\t\t\tdisabled={!name?.trim() || create${names.singularPascal}Mutation.isPending}
3731
4003
  \t\t\t\t\t>
3732
4004
  \t\t\t\t\t\t{create${names.singularPascal}Mutation.isPending ? "Creating" : "Create"}
3733
4005
  \t\t\t\t\t</button>
3734
4006
  \t\t\t\t</div>
3735
- \t\t\t\t{create${names.singularPascal}Mutation.isError ? (
3736
- \t\t\t\t\t<p role="alert">
3737
- \t\t\t\t\t\t{contractErrorMessage(
3738
- \t\t\t\t\t\t\tcreate${names.singularPascal}Mutation.error,
3739
- \t\t\t\t\t\t\t"Could not create ${names.singularKebab}.",
3740
- \t\t\t\t\t\t)}
3741
- \t\t\t\t\t</p>
4007
+ \t\t\t\t{form.formState.errors.name ? (
4008
+ \t\t\t\t\t<p role="alert">{form.formState.errors.name.message}</p>
4009
+ \t\t\t\t) : null}
4010
+ \t\t\t\t{form.formState.errors.root ? (
4011
+ \t\t\t\t\t<p role="alert">{form.formState.errors.root.message}</p>
3742
4012
  \t\t\t\t) : null}
3743
4013
  \t\t\t</form>
3744
4014
 
@@ -3852,7 +4122,7 @@ ${mode === "resource" ? `import { isAppError } from "@beignet/core/errors";\n` :
3852
4122
  import { createTestApp } from "@beignet/web/testing";
3853
4123
  import { createInMemoryDevtools } from "@beignet/devtools";
3854
4124
  import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
3855
- import { ${options.auth ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenant ? ", createTestTenant" : ""} } from "@beignet/core/testing";
4125
+ import { ${options.authorization ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenantScoped ? ", createTestTenant" : ""} } from "@beignet/core/testing";
3856
4126
  import type { AppContext } from "${aliasModule(config.paths.appContext)}";
3857
4127
  import { initialPorts } from "${aliasModule(config.paths.portWiring)}";
3858
4128
  import { appContext } from "${aliasModule(serverContextPath)}";
@@ -3872,39 +4142,39 @@ describe("${names.pluralCamel} resource", () => {
3872
4142
  const seed${names.pluralPascal} = [
3873
4143
  {
3874
4144
  id: "00000000-0000-4000-8000-000000000101",
3875
- ${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
4145
+ ${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
3876
4146
  version: 1,
3877
4147
  createdAt: "2024-01-01T00:00:00.000Z",
3878
4148
  updatedAt: "2024-01-01T00:00:00.000Z",
3879
4149
  },
3880
4150
  {
3881
4151
  id: "00000000-0000-4000-8000-000000000102",
3882
- ${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
4152
+ ${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
3883
4153
  version: 1,
3884
4154
  createdAt: "2024-01-02T00:00:00.000Z",
3885
4155
  updatedAt: "2024-01-02T00:00:00.000Z",
3886
4156
  },
3887
4157
  {
3888
4158
  id: "00000000-0000-4000-8000-000000000103",
3889
- ${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
4159
+ ${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
3890
4160
  version: 1,
3891
4161
  createdAt: "2024-01-03T00:00:00.000Z",
3892
4162
  updatedAt: "2024-01-03T00:00:00.000Z",
3893
4163
  },
3894
- ${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` : ""} ];
4164
+ ${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` : ""} ];
3895
4165
  const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository(seed${names.pluralPascal});
3896
4166
  const testFixture = createTestPorts<AppContext["ports"]>({
3897
4167
  base: initialPorts,
3898
4168
  overrides: {
3899
4169
  ${names.pluralCamel},
3900
- ${options.auth || options.tenant
4170
+ ${options.authorization || options.tenantScoped
3901
4171
  ? `\t\t\t\tauth: {
3902
4172
  getSession: async () => ({
3903
- user: { id: "user_test", name: "Test User"${options.tenant ? `, tenantId: "tenant_test"` : ""} },
3904
- ${options.tenant ? `\t\t\t\t\t\tsession: { tenantId: "tenant_test" },\n` : ""}
4173
+ user: { id: "user_test", name: "Test User"${options.tenantScoped ? `, tenantId: "tenant_test"` : ""} },
4174
+ ${options.tenantScoped ? `\t\t\t\t\t\tsession: { tenantId: "tenant_test" },\n` : ""}
3905
4175
  }),
3906
4176
  },
3907
- ${options.auth ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
4177
+ ${options.authorization ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
3908
4178
  `
3909
4179
  : `\t\t\t\tauth: {
3910
4180
  getSession: async () => null,
@@ -3919,8 +4189,8 @@ ${options.auth ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
3919
4189
  });
3920
4190
  const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
3921
4191
  ports: testFixture.ports,
3922
- actor: ${options.auth ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
3923
- tenant: ${options.tenant ? `createTestTenant("tenant_test")` : "null"},
4192
+ actor: ${options.authorization ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
4193
+ tenant: ${options.tenantScoped ? `createTestTenant("tenant_test")` : "null"},
3924
4194
  });
3925
4195
  const tester = createUseCaseTester<AppContext>(createTestContext);
3926
4196