@beignet/cli 0.0.38 → 0.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +154 -102
  3. package/dist/choices.d.ts +17 -21
  4. package/dist/choices.d.ts.map +1 -1
  5. package/dist/choices.js +21 -67
  6. package/dist/choices.js.map +1 -1
  7. package/dist/config.d.ts +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +4 -2
  10. package/dist/config.js.map +1 -1
  11. package/dist/create-prompts.d.ts +5 -6
  12. package/dist/create-prompts.d.ts.map +1 -1
  13. package/dist/create-prompts.js +15 -15
  14. package/dist/create-prompts.js.map +1 -1
  15. package/dist/create.d.ts +11 -2
  16. package/dist/create.d.ts.map +1 -1
  17. package/dist/create.js +38 -28
  18. package/dist/create.js.map +1 -1
  19. package/dist/framework.d.ts +3 -0
  20. package/dist/framework.d.ts.map +1 -0
  21. package/dist/framework.js +6 -0
  22. package/dist/framework.js.map +1 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +149 -47
  25. package/dist/index.js.map +1 -1
  26. package/dist/inspect.d.ts +4 -2
  27. package/dist/inspect.d.ts.map +1 -1
  28. package/dist/inspect.js +174 -34
  29. package/dist/inspect.js.map +1 -1
  30. package/dist/lib.d.ts +1 -1
  31. package/dist/lib.d.ts.map +1 -1
  32. package/dist/make/inbox.js +3 -3
  33. package/dist/make/inbox.js.map +1 -1
  34. package/dist/make/payments.d.ts.map +1 -1
  35. package/dist/make/payments.js +5 -3
  36. package/dist/make/payments.js.map +1 -1
  37. package/dist/make/shared.d.ts +10 -3
  38. package/dist/make/shared.d.ts.map +1 -1
  39. package/dist/make/shared.js +7 -8
  40. package/dist/make/shared.js.map +1 -1
  41. package/dist/make.d.ts +3 -2
  42. package/dist/make.d.ts.map +1 -1
  43. package/dist/make.js +549 -186
  44. package/dist/make.js.map +1 -1
  45. package/dist/mcp.d.ts.map +1 -1
  46. package/dist/mcp.js +15 -11
  47. package/dist/mcp.js.map +1 -1
  48. package/dist/provider-add.d.ts +21 -3
  49. package/dist/provider-add.d.ts.map +1 -1
  50. package/dist/provider-add.js +101 -65
  51. package/dist/provider-add.js.map +1 -1
  52. package/dist/task.js +1 -1
  53. package/dist/task.js.map +1 -1
  54. package/dist/templates/base.d.ts +1 -1
  55. package/dist/templates/base.d.ts.map +1 -1
  56. package/dist/templates/base.js +15 -15
  57. package/dist/templates/base.js.map +1 -1
  58. package/dist/templates/index.d.ts +2 -2
  59. package/dist/templates/index.d.ts.map +1 -1
  60. package/dist/templates/index.js +5 -5
  61. package/dist/templates/index.js.map +1 -1
  62. package/dist/templates/server.d.ts.map +1 -1
  63. package/dist/templates/server.js +17 -13
  64. package/dist/templates/server.js.map +1 -1
  65. package/dist/templates/shadcn.js +1 -1
  66. package/dist/templates/shared.d.ts +4 -4
  67. package/dist/templates/shared.d.ts.map +1 -1
  68. package/dist/templates/shared.js +6 -6
  69. package/dist/templates/shared.js.map +1 -1
  70. package/package.json +2 -2
  71. package/skills/app-structure/SKILL.md +29 -9
  72. package/src/choices.ts +38 -88
  73. package/src/config.ts +7 -3
  74. package/src/create-prompts.ts +20 -21
  75. package/src/create.ts +54 -36
  76. package/src/framework.ts +13 -0
  77. package/src/index.ts +193 -65
  78. package/src/inspect.ts +290 -39
  79. package/src/lib.ts +1 -1
  80. package/src/make/inbox.ts +3 -3
  81. package/src/make/payments.ts +10 -3
  82. package/src/make/shared.ts +16 -10
  83. package/src/make.ts +720 -176
  84. package/src/mcp.ts +20 -13
  85. package/src/provider-add.ts +159 -79
  86. package/src/task.ts +1 -1
  87. package/src/templates/base.ts +16 -16
  88. package/src/templates/index.ts +6 -6
  89. package/src/templates/server.ts +17 -13
  90. package/src/templates/shadcn.ts +1 -1
  91. package/src/templates/shared.ts +10 -10
  92. package/src/test-helpers/generated-app.ts +2 -1
package/dist/make.js CHANGED
@@ -1,9 +1,12 @@
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";
6
+ import { assertNextFramework } from "./framework.js";
5
7
  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
- import { appendToArrayExpression, appendToOutboxRegistryArray, arrayInitializerInfo, identifiersFromArrayExpression, insertAfterImports, } from "./registry-edits.js";
8
+ import { appendToArrayExpression, appendToNamedArray, appendToOutboxRegistryArray, arrayInitializerInfo, identifiersFromArrayExpression, insertAfterImports, } from "./registry-edits.js";
9
+ import { externalVersions } from "./templates/shared.js";
7
10
  import { testSupportTemplateFiles } from "./templates/testing.js";
8
11
  export { makeFeatureAddonChoices, makeFeatureRecipeChoices, } from "./choices.js";
9
12
  export * from "./make/inbox.js";
@@ -17,7 +20,7 @@ export async function makeFeature(options) {
17
20
  const addons = normalizeMakeFeatureAddons([
18
21
  ...makeFeatureRecipeAddons(options.recipe),
19
22
  ...(options.with ?? []),
20
- ...(options.events ? ["events"] : []),
23
+ ...(options.events ? ["event"] : []),
21
24
  ]);
22
25
  const featureOptions = {
23
26
  ...options,
@@ -122,30 +125,36 @@ async function makeResourceSlice(options, mode) {
122
125
  function resourceGenerationOptions(options, mode) {
123
126
  if (mode === "feature") {
124
127
  return {
125
- auth: false,
126
- tenant: false,
128
+ authorization: false,
129
+ tenantScoped: false,
127
130
  events: Boolean(options.events),
128
131
  softDelete: false,
129
132
  };
130
133
  }
131
134
  if (mode !== "resource") {
132
- return { auth: false, tenant: false, events: false, softDelete: false };
135
+ return {
136
+ authorization: false,
137
+ tenantScoped: false,
138
+ events: false,
139
+ softDelete: false,
140
+ };
133
141
  }
134
142
  return {
135
- auth: Boolean(options.auth),
136
- tenant: Boolean(options.tenant),
143
+ authorization: Boolean(options.authorization),
144
+ tenantScoped: Boolean(options.tenantScoped),
137
145
  events: Boolean(options.events),
138
146
  softDelete: Boolean(options.softDelete),
139
147
  };
140
148
  }
141
149
  async function makeFeatureSlice(options, addons) {
142
150
  let result = await makeFeatureResource(options);
151
+ const includeUploadUi = addons.includes("ui") && addons.includes("upload");
143
152
  for (const addon of addons) {
144
- result = mergeMakeResults(result, await makeFeatureAddon(addon, result.name, options));
153
+ result = mergeMakeResults(result, await makeFeatureAddon(addon, result.name, options, includeUploadUi));
145
154
  }
146
155
  return result;
147
156
  }
148
- async function makeFeatureAddon(addon, feature, options) {
157
+ async function makeFeatureAddon(addon, feature, options, includeUploadUi) {
149
158
  const shared = {
150
159
  cwd: options.cwd,
151
160
  force: options.force,
@@ -158,69 +167,53 @@ async function makeFeatureAddon(addon, feature, options) {
158
167
  if (addon === "factory") {
159
168
  return makeFactory({
160
169
  ...shared,
161
- name: `${feature}/${singularize(feature)}`,
170
+ name: `${feature}.${singularize(feature)}`,
162
171
  skipPersistenceAssert: true,
163
172
  });
164
173
  }
165
174
  if (addon === "task") {
166
- return makeTask({ ...shared, name: `${feature}/backfill` });
175
+ return makeTask({ ...shared, name: `${feature}.backfill` });
167
176
  }
168
177
  if (addon === "event") {
169
- return makeEvent({ ...shared, name: `${feature}/created` });
178
+ return makeEvent({ ...shared, name: `${feature}.created` });
170
179
  }
171
180
  if (addon === "job") {
172
- return makeJob({ ...shared, name: `${feature}/process` });
181
+ return makeJob({ ...shared, name: `${feature}.process` });
173
182
  }
174
183
  if (addon === "listener") {
175
184
  return makeListener({
176
185
  ...shared,
177
- name: `${feature}/handle-created`,
178
- event: `${feature}/created`,
186
+ name: `${feature}.handle-created`,
187
+ event: `${feature}.created`,
179
188
  skipEventAssert: true,
180
189
  });
181
190
  }
182
191
  if (addon === "notification") {
183
- return makeNotification({ ...shared, name: `${feature}/created` });
192
+ return makeNotification({ ...shared, name: `${feature}.created` });
184
193
  }
185
194
  if (addon === "schedule") {
186
- return makeSchedule({ ...shared, name: `${feature}/daily-summary` });
195
+ return makeSchedule({ ...shared, name: `${feature}.daily-summary` });
187
196
  }
188
197
  if (addon === "seed") {
189
198
  return makeSeed({
190
199
  ...shared,
191
- name: `${feature}/demo-${feature}`,
200
+ name: `${feature}.demo-${feature}`,
192
201
  skipPersistenceAssert: true,
193
202
  });
194
203
  }
195
204
  if (addon === "ui") {
196
205
  return makeFeatureUi({ ...shared, name: feature });
197
206
  }
198
- return makeUpload({ ...shared, name: `${feature}/attachment` });
207
+ return makeUpload({
208
+ ...shared,
209
+ name: `${feature}.attachment`,
210
+ ui: includeUploadUi,
211
+ });
199
212
  }
200
213
  function normalizeMakeFeatureAddons(addons) {
201
214
  const normalized = new Set();
202
215
  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") {
216
+ if (addon === "listener") {
224
217
  normalized.add("event");
225
218
  normalized.add("listener");
226
219
  }
@@ -233,16 +226,16 @@ function makeFeatureRecipeAddons(recipe) {
233
226
  if (recipe === "full-slice") {
234
227
  return [
235
228
  "policy",
236
- "factories",
237
- "seeds",
238
- "tasks",
239
- "events",
240
- "listeners",
241
- "jobs",
242
- "notifications",
243
- "schedules",
229
+ "factory",
230
+ "seed",
231
+ "task",
232
+ "event",
233
+ "listener",
234
+ "job",
235
+ "notification",
236
+ "schedule",
244
237
  "ui",
245
- "uploads",
238
+ "upload",
246
239
  ];
247
240
  }
248
241
  return [];
@@ -328,7 +321,7 @@ export async function makeUseCase(options) {
328
321
  skippedFiles.push(indexFile.path);
329
322
  return {
330
323
  schemaVersion: 1,
331
- name: `${names.feature.kebab}/${names.action.kebab}`,
324
+ name: `${names.feature.kebab}.${names.action.kebab}`,
332
325
  targetDir,
333
326
  dryRun: Boolean(options.dryRun),
334
327
  files: [...generatedFiles.map((file) => file.path), indexFile.path],
@@ -367,7 +360,7 @@ export async function makeTest(options) {
367
360
  }
368
361
  return {
369
362
  schemaVersion: 1,
370
- name: `${names.feature.kebab}/${names.action.kebab}`,
363
+ name: `${names.feature.kebab}.${names.action.kebab}`,
371
364
  targetDir,
372
365
  dryRun: Boolean(options.dryRun),
373
366
  files: generatedFiles.map((file) => file.path),
@@ -529,7 +522,7 @@ export async function makeEvent(options) {
529
522
  config,
530
523
  force: Boolean(options.force),
531
524
  dryRun: Boolean(options.dryRun),
532
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
525
+ name: names.stableName,
533
526
  files: eventFiles(names, config),
534
527
  index: featureArtifactIndexFile("event", names, config),
535
528
  dependencies: {
@@ -570,7 +563,7 @@ export async function makeJob(options) {
570
563
  config,
571
564
  force: Boolean(options.force),
572
565
  dryRun: Boolean(options.dryRun),
573
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
566
+ name: names.stableName,
574
567
  files: [
575
568
  ...(await missingCapabilityBuilderFiles(targetDir, "jobs", config)),
576
569
  ...jobFiles(names, config),
@@ -603,7 +596,7 @@ export async function makeTask(options) {
603
596
  config,
604
597
  force: Boolean(options.force),
605
598
  dryRun: Boolean(options.dryRun),
606
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
599
+ name: names.stableName,
607
600
  files: [
608
601
  ...(await missingCapabilityBuilderFiles(targetDir, "tasks", config)),
609
602
  ...taskFiles(names, config),
@@ -641,7 +634,7 @@ export async function makeFactory(options) {
641
634
  config,
642
635
  force: Boolean(options.force),
643
636
  dryRun: Boolean(options.dryRun),
644
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
637
+ name: names.stableName,
645
638
  files: factoryFiles(names, config),
646
639
  index: featureArtifactIndexFile("factory", names, config),
647
640
  dependencies: {
@@ -663,7 +656,7 @@ export async function makeSeed(options) {
663
656
  config,
664
657
  force: Boolean(options.force),
665
658
  dryRun: Boolean(options.dryRun),
666
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
659
+ name: names.stableName,
667
660
  files: seedFiles(names, config),
668
661
  index: featureArtifactIndexFile("seed", names, config),
669
662
  dependencies: {
@@ -677,7 +670,7 @@ export async function makeSeed(options) {
677
670
  config,
678
671
  force: Boolean(options.force),
679
672
  dryRun: Boolean(options.dryRun),
680
- name: `${factoryNamesForSeed.feature.kebab}/${factoryNamesForSeed.artifact.kebab}`,
673
+ name: factoryNamesForSeed.stableName,
681
674
  files: factoryFiles(factoryNamesForSeed, config),
682
675
  index: featureArtifactIndexFile("factory", factoryNamesForSeed, config),
683
676
  dependencies: {
@@ -714,7 +707,7 @@ export async function makeNotification(options) {
714
707
  // notifications uses ctx.ports.notifications, so the generator wires both
715
708
  // ports with dev-default providers. Each port is wired independently and
716
709
  // skipped when the app already has it, for example when the resend
717
- // integration contributed the mailer. Plan the wiring before writing any
710
+ // provider contributed the mailer. Plan the wiring before writing any
718
711
  // files so an anchor miss aborts the generator without leaving partial
719
712
  // output.
720
713
  const notificationWirings = notificationPortWirings("make notification");
@@ -726,7 +719,7 @@ export async function makeNotification(options) {
726
719
  config,
727
720
  force: Boolean(options.force),
728
721
  dryRun: Boolean(options.dryRun),
729
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
722
+ name: names.stableName,
730
723
  files: [
731
724
  ...(await missingCapabilityBuilderFiles(targetDir, "notifications", config)),
732
725
  ...notificationFiles(names, config),
@@ -760,7 +753,7 @@ export async function makeListener(options) {
760
753
  config,
761
754
  force: Boolean(options.force),
762
755
  dryRun: Boolean(options.dryRun),
763
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
756
+ name: names.stableName,
764
757
  files: [
765
758
  ...(await missingCapabilityBuilderFiles(targetDir, "listeners", config)),
766
759
  ...listenerFiles(names, config),
@@ -832,7 +825,7 @@ async function assertListenerEventExists(targetDir, names, config) {
832
825
  await stat(path.join(targetDir, file));
833
826
  }
834
827
  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.`);
828
+ 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
829
  }
837
830
  }
838
831
  async function updateListenerProviderWiring(targetDir, names, config, options) {
@@ -867,6 +860,9 @@ export async function makeSchedule(options) {
867
860
  const config = options.config
868
861
  ? resolveConfig(options.config)
869
862
  : await loadBeignetConfig(targetDir);
863
+ if (options.route) {
864
+ assertNextFramework(config, "beignet make schedule --route", "a Next.js App Router cron handler");
865
+ }
870
866
  await assertFeatureArtifactApp(targetDir, config, "schedule", {
871
867
  requireServer: Boolean(options.route),
872
868
  });
@@ -884,7 +880,7 @@ export async function makeSchedule(options) {
884
880
  config,
885
881
  force: Boolean(options.force),
886
882
  dryRun: Boolean(options.dryRun),
887
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
883
+ name: names.stableName,
888
884
  files,
889
885
  index: featureArtifactIndexFile("schedule", names, config),
890
886
  dependencies: {
@@ -1014,6 +1010,9 @@ async function updateOutboxPortWiring(targetDir, config, options) {
1014
1010
  if (await updateOutboxDatabaseProvider(targetDir, config, options)) {
1015
1011
  changes.updatedFiles.push(path.join(infrastructureDir(config), "db/provider.ts"));
1016
1012
  }
1013
+ if (await updateOutboxDrainProvider(targetDir, config, options)) {
1014
+ changes.updatedFiles.push(providersFilePath(config));
1015
+ }
1017
1016
  if (await updateOutboxRepositoriesReturnType(targetDir, config, options)) {
1018
1017
  changes.updatedFiles.push(drizzleRepositoriesPath(config));
1019
1018
  }
@@ -1091,6 +1090,25 @@ async function updateOutboxInfrastructurePorts(targetDir, config, options) {
1091
1090
  await writeFile(filePath, next);
1092
1091
  return true;
1093
1092
  }
1093
+ async function projectSupportsNextAfter(targetDir) {
1094
+ const packageJson = JSON.parse(await readFile(path.join(targetDir, "package.json"), "utf8"));
1095
+ const version = packageJson.dependencies?.next ?? packageJson.devDependencies?.next;
1096
+ if (!version)
1097
+ return false;
1098
+ return version.split("||").every((range) => {
1099
+ const lowerBound = /^\s*(\^|~|>=|>|=|<=|<)?\s*v?(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?([+-][0-9A-Za-z.-]+)?(?=\s|$)/.exec(range);
1100
+ if (!lowerBound)
1101
+ return false;
1102
+ const operator = lowerBound[1];
1103
+ if (operator === "<" || operator === "<=")
1104
+ return false;
1105
+ if (lowerBound[5]?.startsWith("-"))
1106
+ return false;
1107
+ const major = Number(lowerBound[2]);
1108
+ const minor = /^\d+$/.test(lowerBound[3] ?? "") ? Number(lowerBound[3]) : 0;
1109
+ return major > 15 || (major === 15 && minor >= 1);
1110
+ });
1111
+ }
1094
1112
  async function updateOutboxDatabaseProvider(targetDir, config, options) {
1095
1113
  const file = path.join(infrastructureDir(config), "db/provider.ts");
1096
1114
  const filePath = path.join(targetDir, file);
@@ -1145,6 +1163,68 @@ async function updateOutboxDatabaseProvider(targetDir, config, options) {
1145
1163
  await writeFile(filePath, next);
1146
1164
  return true;
1147
1165
  }
1166
+ async function updateOutboxDrainProvider(targetDir, config, options) {
1167
+ if (!(await projectSupportsNextAfter(targetDir)))
1168
+ return false;
1169
+ const file = providersFilePath(config);
1170
+ const filePath = path.join(targetDir, file);
1171
+ const original = await readOptionalFile(filePath);
1172
+ if (original === undefined) {
1173
+ throw new Error(`Could not find the generated providers file ${file}. Register the outbox drain trigger manually, or restore the generated providers file before running make outbox.`);
1174
+ }
1175
+ let next = addNamedImport(original, "createObservedUnitOfWork", "@beignet/core/ports");
1176
+ next = addNamedImport(next, "createProvider", "@beignet/core/providers");
1177
+ next = addNamedImport(next, "createNextOutboxDrainTrigger", "@beignet/next");
1178
+ next = addNamedImport(next, "after", "next/server");
1179
+ next = addNamedTypeImport(next, "AppContext", aliasModule(config.paths.appContext));
1180
+ next = addNamedTypeImport(next, "AppPorts", aliasModule(config.paths.ports));
1181
+ next = addNamedTypeImport(next, "AppServiceContextInput", aliasModule(path.join(path.dirname(config.paths.server), "context.ts")));
1182
+ const providerName = "outboxDrainProvider";
1183
+ if (!new RegExp(`\\bconst\\s+${providerName}\\b`).test(next)) {
1184
+ const outboxModule = aliasModule(config.paths.outbox);
1185
+ const providerDefinition = `const ${providerName} = createProvider<
1186
+ \tPick<AppPorts, "uow">,
1187
+ \tAppContext,
1188
+ \tAppServiceContextInput
1189
+ >()({
1190
+ \tname: "outbox-drain-trigger",
1191
+ \tsetup({ ports, createServiceContext }): { ports: Pick<AppPorts, "uow"> } {
1192
+ \t\tconst triggerOutboxDrain: () => void = createNextOutboxDrainTrigger({
1193
+ \t\t\tdefer: after,
1194
+ \t\t\tcreateContext: () => createServiceContext(undefined),
1195
+ \t\t\tregistry: async () => (await import("${outboxModule}")).outboxRegistry,
1196
+ \t\t\tbatchSize: 100,
1197
+ \t\t});
1198
+
1199
+ \t\treturn {
1200
+ \t\t\tports: {
1201
+ \t\t\t\tuow: createObservedUnitOfWork({
1202
+ \t\t\t\t\tunitOfWork: ports.uow,
1203
+ \t\t\t\t\tafterCommit: triggerOutboxDrain,
1204
+ \t\t\t\t}),
1205
+ \t\t\t},
1206
+ \t\t};
1207
+ \t},
1208
+ });
1209
+ `;
1210
+ const withProvider = next.replace(/\nexport const providers = \[/, `\n${providerDefinition}\nexport const providers = [`);
1211
+ if (withProvider === next) {
1212
+ throw new Error(`Could not find the exported providers array in ${file}. Register outboxDrainProvider manually, or restore the generated providers file before running make outbox.`);
1213
+ }
1214
+ next = withProvider;
1215
+ }
1216
+ const appended = appendToNamedArray(next, "providers", providerName);
1217
+ if (appended.kind === "missing") {
1218
+ throw new Error(`Could not find the exported providers array in ${file}. Register outboxDrainProvider manually, or restore the generated providers file before running make outbox.`);
1219
+ }
1220
+ if (appended.kind === "updated")
1221
+ next = appended.source;
1222
+ if (next === original)
1223
+ return false;
1224
+ if (!options.dryRun)
1225
+ await writeFile(filePath, next);
1226
+ return true;
1227
+ }
1148
1228
  async function updateOutboxRepositoriesReturnType(targetDir, config, options) {
1149
1229
  const file = drizzleRepositoriesPath(config);
1150
1230
  const filePath = path.join(targetDir, file);
@@ -1251,7 +1331,11 @@ export async function makeUpload(options) {
1251
1331
  const config = options.config
1252
1332
  ? resolveConfig(options.config)
1253
1333
  : await loadBeignetConfig(targetDir);
1334
+ assertNextFramework(config, "beignet make upload", "a Next.js App Router upload handler");
1254
1335
  await assertFeatureArtifactApp(targetDir, config, "upload");
1336
+ if (options.ui) {
1337
+ await assertUploadUiApp(targetDir, config);
1338
+ }
1255
1339
  await wirePortProviders(targetDir, config, [storagePortWiring("make upload")], {
1256
1340
  dryRun: true,
1257
1341
  });
@@ -1260,10 +1344,10 @@ export async function makeUpload(options) {
1260
1344
  config,
1261
1345
  force: Boolean(options.force),
1262
1346
  dryRun: Boolean(options.dryRun),
1263
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1347
+ name: names.stableName,
1264
1348
  files: [
1265
1349
  ...(await missingCapabilityBuilderFiles(targetDir, "uploads", config)),
1266
- ...uploadFiles(names, config),
1350
+ ...uploadFiles(names, config, { ui: Boolean(options.ui) }),
1267
1351
  ],
1268
1352
  index: featureArtifactIndexFile("upload", names, config),
1269
1353
  dependencies: {
@@ -1283,6 +1367,31 @@ export async function makeUpload(options) {
1283
1367
  if (!result.files.includes(uploadRoutePath(config))) {
1284
1368
  result.files.push(uploadRoutePath(config));
1285
1369
  }
1370
+ if (options.ui) {
1371
+ const componentIndex = uploadUiComponentIndexFile(names, config);
1372
+ const componentIndexResult = await updateFeatureUiIndex(targetDir, componentIndex, { dryRun: Boolean(options.dryRun) });
1373
+ if (componentIndexResult === "created") {
1374
+ result.createdFiles.push(componentIndex.path);
1375
+ }
1376
+ if (componentIndexResult === "updated") {
1377
+ result.updatedFiles.push(componentIndex.path);
1378
+ }
1379
+ if (componentIndexResult === "skipped") {
1380
+ result.skippedFiles.push(componentIndex.path);
1381
+ }
1382
+ if (!result.files.includes(componentIndex.path)) {
1383
+ result.files.push(componentIndex.path);
1384
+ }
1385
+ if (await updatePackageDependencies(targetDir, { "@beignet/react-uploads": beignetDependencyVersion }, { dryRun: Boolean(options.dryRun) })) {
1386
+ result.updatedFiles.push("package.json");
1387
+ if (!result.files.includes("package.json")) {
1388
+ result.files.push("package.json");
1389
+ }
1390
+ }
1391
+ }
1392
+ result.createdFiles = uniqueStrings(result.createdFiles);
1393
+ result.updatedFiles = uniqueStrings(result.updatedFiles);
1394
+ result.skippedFiles = uniqueStrings(result.skippedFiles);
1286
1395
  return result;
1287
1396
  }
1288
1397
  export async function makeOutbox(options = {}) {
@@ -1290,6 +1399,7 @@ export async function makeOutbox(options = {}) {
1290
1399
  const config = options.config
1291
1400
  ? resolveConfig(options.config)
1292
1401
  : await loadBeignetConfig(targetDir);
1402
+ assertNextFramework(config, "beignet make outbox", "a Next.js App Router recovery drain handler");
1293
1403
  await assertServerRuntimeApp(targetDir, config, "outbox");
1294
1404
  await updateOutboxPortWiring(targetDir, config, { dryRun: true });
1295
1405
  const cronSecretPlan = await planCronSecretWiring(targetDir);
@@ -1333,6 +1443,9 @@ export async function makeOutbox(options = {}) {
1333
1443
  config.paths.ports,
1334
1444
  config.paths.portWiring,
1335
1445
  path.join(infrastructureDir(config), "db/provider.ts"),
1446
+ ...((await projectSupportsNextAfter(targetDir))
1447
+ ? [providersFilePath(config)]
1448
+ : []),
1336
1449
  drizzleRepositoriesPath(config),
1337
1450
  outboxDrizzleSchemaFilePath(config),
1338
1451
  drizzleSchemaIndexPath(config),
@@ -1349,7 +1462,10 @@ async function makeFeatureUi(options) {
1349
1462
  ? resolveConfig(options.config)
1350
1463
  : await loadBeignetConfig(targetDir);
1351
1464
  await assertFeatureUiApp(targetDir, config);
1352
- const componentFiles = featureUiComponentFiles(names, config);
1465
+ const componentFiles = [
1466
+ ...(await missingFeatureUiFormsFiles(targetDir, config)),
1467
+ ...featureUiComponentFiles(names, config),
1468
+ ];
1353
1469
  const indexFile = featureUiIndexFile(names, config);
1354
1470
  const plannedFiles = await planGeneratedFiles(targetDir, componentFiles, {
1355
1471
  force: Boolean(options.force),
@@ -1378,8 +1494,13 @@ async function makeFeatureUi(options) {
1378
1494
  if (indexResult === "skipped")
1379
1495
  skippedFiles.push(indexFile.path);
1380
1496
  if (await updatePackageDependencies(targetDir, {
1497
+ "@beignet/react-hook-form": beignetDependencyVersion,
1381
1498
  "@beignet/react-query": beignetDependencyVersion,
1499
+ "@hookform/resolvers": (packageJson) => packageJson.dependencies?.["@hookform/resolvers"] ??
1500
+ externalVersions.hookformResolvers,
1382
1501
  "@tanstack/react-query": tanstackReactQueryDependencyVersion,
1502
+ "react-hook-form": (packageJson) => packageJson.dependencies?.["react-hook-form"] ??
1503
+ externalVersions.reactHookForm,
1383
1504
  }, { dryRun: Boolean(options.dryRun) })) {
1384
1505
  updatedFiles.push("package.json");
1385
1506
  }
@@ -1467,7 +1588,7 @@ async function updateResourceWiring(targetDir, names, config, persistenceOrOptio
1467
1588
  (await updateInfrastructurePorts(targetDir, names, config, options))) {
1468
1589
  updated.add(config.paths.portWiring);
1469
1590
  }
1470
- if (generationOptions.auth &&
1591
+ if (generationOptions.authorization &&
1471
1592
  (await updateInfrastructureGatePolicies(targetDir, names, config, options))) {
1472
1593
  updated.add(config.paths.portWiring);
1473
1594
  }
@@ -1505,11 +1626,10 @@ function typeHasTopLevelProperty(source, typeName, propertyName) {
1505
1626
  function useCaseNames(input) {
1506
1627
  const parts = input
1507
1628
  .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.");
1629
+ .split(".")
1630
+ .map((part) => part.trim());
1631
+ if (parts.length !== 2 || parts.some((part) => part.length === 0)) {
1632
+ throw new Error("Use case name must use feature.action format, for example projects.archive-project.");
1513
1633
  }
1514
1634
  const feature = identifierNames(parts[0], "Use case feature");
1515
1635
  const action = identifierNames(parts[1], "Use case action");
@@ -1541,7 +1661,7 @@ function policyNames(input) {
1541
1661
  return resolvedNames;
1542
1662
  }
1543
1663
  function eventNames(input) {
1544
- const names = featureArtifactNames(normalizeEventReference(input), "Event");
1664
+ const names = featureArtifactNames(input, "Event");
1545
1665
  const eventExportName = `${names.featureSingularPascal}${names.artifact.pascal}`;
1546
1666
  assertGeneratedIdentifiers(input, "Event name", [eventExportName]);
1547
1667
  return {
@@ -1587,7 +1707,7 @@ function factoryNames(input) {
1587
1707
  };
1588
1708
  }
1589
1709
  function defaultFactoryNamesForSeed(names) {
1590
- return factoryNames(`${names.feature.kebab}/${names.featureSingularKebab}`);
1710
+ return factoryNames(`${names.feature.kebab}.${names.featureSingularKebab}`);
1591
1711
  }
1592
1712
  function notificationNames(input) {
1593
1713
  const names = featureArtifactNames(input, "Notification");
@@ -1631,24 +1751,26 @@ function scheduleNames(input, options) {
1631
1751
  function uploadNames(input) {
1632
1752
  const names = featureArtifactNames(input, "Upload");
1633
1753
  const uploadExportName = `${names.artifact.pascal}Upload`;
1634
- assertGeneratedIdentifiers(input, "Upload name", [uploadExportName]);
1754
+ const componentExportName = `${names.artifact.pascal}Uploader`;
1755
+ assertGeneratedIdentifiers(input, "Upload name", [
1756
+ uploadExportName,
1757
+ componentExportName,
1758
+ ]);
1635
1759
  return {
1636
1760
  ...names,
1637
1761
  uploadName: names.stableName,
1638
1762
  uploadExportName,
1639
1763
  metadataSchemaName: `${uploadExportName}MetadataSchema`,
1640
1764
  metadataTypeName: `${uploadExportName}Metadata`,
1765
+ fileConstraintsExportName: `${names.artifact.camel}UploadFile`,
1766
+ manifestExportName: `${names.artifact.camel}UploadManifest`,
1767
+ manifestFileName: `${names.artifact.kebab}-upload-manifest`,
1768
+ reactUploadsExportName: `${names.artifact.camel}ReactUploads`,
1769
+ clientFileName: `${names.artifact.kebab}-upload`,
1770
+ componentExportName,
1771
+ componentFileName: `${names.artifact.kebab}-uploader`,
1641
1772
  };
1642
1773
  }
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
1774
  function inferUseCaseKind(action) {
1653
1775
  if (/^(count|find|get|list|search)-/.test(`${action}-`)) {
1654
1776
  return "query";
@@ -1656,8 +1778,8 @@ function inferUseCaseKind(action) {
1656
1778
  return "command";
1657
1779
  }
1658
1780
  function resourceFiles(names, config, persistence = "memory", mode = "feature", options = {
1659
- auth: false,
1660
- tenant: false,
1781
+ authorization: false,
1782
+ tenantScoped: false,
1661
1783
  events: false,
1662
1784
  softDelete: false,
1663
1785
  }, database = "sqlite") {
@@ -1710,7 +1832,7 @@ function resourceFiles(names, config, persistence = "memory", mode = "feature",
1710
1832
  },
1711
1833
  ]
1712
1834
  : []),
1713
- ...(mode === "resource" && options.auth
1835
+ ...(mode === "resource" && options.authorization
1714
1836
  ? [
1715
1837
  {
1716
1838
  path: path.join(featureDir, "tests/policy.test.ts"),
@@ -1765,6 +1887,59 @@ function featureUiComponentFiles(names, config) {
1765
1887
  },
1766
1888
  ];
1767
1889
  }
1890
+ async function missingFeatureUiFormsFiles(targetDir, config) {
1891
+ const formsPath = clientFormsPath(config);
1892
+ const existing = await readOptionalFile(path.join(targetDir, formsPath));
1893
+ if (existing === undefined) {
1894
+ return [
1895
+ {
1896
+ path: formsPath,
1897
+ content: `import { createReactHookForm } from "@beignet/react-hook-form";
1898
+
1899
+ export const rhf = createReactHookForm();
1900
+ `,
1901
+ },
1902
+ ];
1903
+ }
1904
+ if (!sourceExportsValue(existing, "rhf")) {
1905
+ 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.`);
1906
+ }
1907
+ return [];
1908
+ }
1909
+ async function assertUploadUiApp(targetDir, config) {
1910
+ const clientPath = clientIndexPath(config);
1911
+ const clientSource = await readOptionalFile(path.join(targetDir, clientPath));
1912
+ if (!clientSource || !sourceExportsValue(clientSource, "rq")) {
1913
+ 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.`);
1914
+ }
1915
+ }
1916
+ function sourceExportsValue(source, name) {
1917
+ const sourceFile = ts.createSourceFile("client-forms.ts", source, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
1918
+ for (const statement of sourceFile.statements) {
1919
+ const exported = ts.canHaveModifiers(statement) &&
1920
+ ts
1921
+ .getModifiers(statement)
1922
+ ?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
1923
+ if (exported && ts.isVariableStatement(statement)) {
1924
+ if (statement.declarationList.declarations.some((declaration) => ts.isIdentifier(declaration.name) && declaration.name.text === name)) {
1925
+ return true;
1926
+ }
1927
+ }
1928
+ if (exported &&
1929
+ ts.isFunctionDeclaration(statement) &&
1930
+ statement.name?.text === name) {
1931
+ return true;
1932
+ }
1933
+ if (ts.isExportDeclaration(statement) &&
1934
+ !statement.isTypeOnly &&
1935
+ statement.exportClause &&
1936
+ ts.isNamedExports(statement.exportClause) &&
1937
+ statement.exportClause.elements.some((element) => !element.isTypeOnly && element.name.text === name)) {
1938
+ return true;
1939
+ }
1940
+ }
1941
+ return false;
1942
+ }
1768
1943
  function featureUiIndexFile(names, config) {
1769
1944
  return {
1770
1945
  path: featureUiIndexFilePath(names, config),
@@ -1893,12 +2068,54 @@ function scheduleFiles(names, config, options) {
1893
2068
  }
1894
2069
  return files;
1895
2070
  }
1896
- function uploadFiles(names, config) {
2071
+ function uploadFiles(names, config, options) {
1897
2072
  return [
2073
+ {
2074
+ path: uploadManifestFilePath(names, config),
2075
+ content: uploadManifestFile(names),
2076
+ },
1898
2077
  {
1899
2078
  path: uploadFilePath(names, config),
1900
2079
  content: uploadFile(names, config),
1901
2080
  },
2081
+ ...(options.ui ? uploadUiFiles(names, config) : []),
2082
+ ];
2083
+ }
2084
+ function uploadFeatureDir(names, config) {
2085
+ return path.join(config.paths.features, names.feature.kebab);
2086
+ }
2087
+ function uploadManifestFilePath(names, config) {
2088
+ return path.join(uploadFeatureDir(names, config), `${names.manifestFileName}.ts`);
2089
+ }
2090
+ function uploadUiClientFilePath(names, config) {
2091
+ return path.join(uploadFeatureDir(names, config), "client", `${names.clientFileName}.ts`);
2092
+ }
2093
+ function uploadUiComponentFilePath(names, config) {
2094
+ return path.join(uploadFeatureDir(names, config), "components", `${names.componentFileName}.tsx`);
2095
+ }
2096
+ function uploadUiComponentTestFilePath(names, config) {
2097
+ return path.join(uploadFeatureDir(names, config), "tests", `${names.componentFileName}.test.tsx`);
2098
+ }
2099
+ function uploadUiComponentIndexFile(names, config) {
2100
+ return {
2101
+ path: path.join(uploadFeatureDir(names, config), "components", "index.ts"),
2102
+ content: `export { ${names.componentExportName} } from "./${names.componentFileName}";\n`,
2103
+ };
2104
+ }
2105
+ function uploadUiFiles(names, config) {
2106
+ return [
2107
+ {
2108
+ path: uploadUiClientFilePath(names, config),
2109
+ content: uploadUiClientFile(names, config),
2110
+ },
2111
+ {
2112
+ path: uploadUiComponentFilePath(names, config),
2113
+ content: uploadUiComponentFile(names, config),
2114
+ },
2115
+ {
2116
+ path: uploadUiComponentTestFilePath(names, config),
2117
+ content: uploadUiComponentTestFile(names, config),
2118
+ },
1902
2119
  ];
1903
2120
  }
1904
2121
  function outboxFiles(config) {
@@ -2204,7 +2421,7 @@ function schemasFile(names, mode, options) {
2204
2421
 
2205
2422
  export const ${names.singularPascal}Schema = z.object({
2206
2423
  id: z.string().uuid(),
2207
- ${options.tenant ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
2424
+ ${options.tenantScoped ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
2208
2425
  version: z.number().int().min(1),
2209
2426
  createdAt: z.string().datetime(),
2210
2427
  updatedAt: z.string().datetime(),
@@ -2304,7 +2521,7 @@ export const List${names.pluralPascal}OutputSchema = z.object({
2304
2521
  });
2305
2522
 
2306
2523
  export const Create${names.singularPascal}InputSchema = z.object({
2307
- name: z.string().min(1).max(120),
2524
+ name: z.string().trim().min(1).max(120),
2308
2525
  });
2309
2526
  ${mode === "resource"
2310
2527
  ? `
@@ -2313,7 +2530,7 @@ export const ${names.singularPascal}IdInputSchema = z.object({
2313
2530
  });
2314
2531
 
2315
2532
  export const Update${names.singularPascal}BodySchema = z.object({
2316
- name: z.string().min(1).max(120),
2533
+ name: z.string().trim().min(1).max(120),
2317
2534
  version: z.number().int().min(1),
2318
2535
  });
2319
2536
 
@@ -2347,7 +2564,7 @@ function listUseCaseFile(names, config, options) {
2347
2564
  const filePath = path.join(resourceUseCaseDir(names, config), `list-${names.pluralKebab}.ts`);
2348
2565
  return `import "@beignet/core/server-only";
2349
2566
  import { normalizeCursorPage } from "@beignet/core/pagination";
2350
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2567
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2351
2568
  import {
2352
2569
  decode${names.singularPascal}Cursor,
2353
2570
  List${names.pluralPascal}InputSchema,
@@ -2359,7 +2576,7 @@ export const list${names.pluralPascal}UseCase = useCase
2359
2576
  .input(List${names.pluralPascal}InputSchema)
2360
2577
  .output(List${names.pluralPascal}OutputSchema)
2361
2578
  .run(async ({ ctx, input }) => {
2362
- ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const page = normalizeCursorPage(input, {
2579
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const page = normalizeCursorPage(input, {
2363
2580
  defaultLimit: 20,
2364
2581
  maxLimit: 100,
2365
2582
  });
@@ -2370,14 +2587,14 @@ ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const
2370
2587
  name: input.name,
2371
2588
  sortBy: input.sortBy,
2372
2589
  sortDirection: input.sortDirection,
2373
- }${options.tenant ? ", scope" : ""});
2590
+ }${options.tenantScoped ? ", scope" : ""});
2374
2591
  });
2375
2592
  `;
2376
2593
  }
2377
2594
  function createUseCaseFile(names, config, options) {
2378
2595
  const filePath = path.join(resourceUseCaseDir(names, config), `create-${names.singularKebab}.ts`);
2379
2596
  return `import "@beignet/core/server-only";
2380
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2597
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2381
2598
  import {
2382
2599
  Create${names.singularPascal}InputSchema,
2383
2600
  ${names.singularPascal}Schema,
@@ -2389,7 +2606,7 @@ export const create${names.singularPascal}UseCase = useCase
2389
2606
  .input(Create${names.singularPascal}InputSchema)
2390
2607
  .output(${names.singularPascal}Schema)
2391
2608
  .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" : ""});
2609
+ ${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
2610
  ${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Created, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
2394
2611
  return ${names.singularCamel};
2395
2612
  });
@@ -2398,7 +2615,7 @@ ${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPasca
2398
2615
  function getUseCaseFile(names, config, options) {
2399
2616
  const filePath = path.join(resourceUseCaseDir(names, config), `get-${names.singularKebab}.ts`);
2400
2617
  return `import "@beignet/core/server-only";
2401
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
2618
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
2402
2619
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2403
2620
  import {
2404
2621
  ${names.singularPascal}IdInputSchema,
@@ -2410,7 +2627,7 @@ export const get${names.singularPascal}UseCase = useCase
2410
2627
  .input(${names.singularPascal}IdInputSchema)
2411
2628
  .output(${names.singularPascal}Schema)
2412
2629
  .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" : ""});
2630
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
2414
2631
  if (!${names.singularCamel}) {
2415
2632
  throw appError("${names.singularPascal}NotFound", {
2416
2633
  details: { id: input.id },
@@ -2424,7 +2641,7 @@ ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const
2424
2641
  function updateUseCaseFile(names, config, options) {
2425
2642
  const filePath = path.join(resourceUseCaseDir(names, config), `update-${names.singularKebab}.ts`);
2426
2643
  return `import "@beignet/core/server-only";
2427
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
2644
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
2428
2645
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2429
2646
  ${options.events ? `import { ${names.singularPascal}Updated } from "../domain/events";\n` : ""}import {
2430
2647
  Update${names.singularPascal}InputSchema,
@@ -2436,13 +2653,13 @@ export const update${names.singularPascal}UseCase = useCase
2436
2653
  .input(Update${names.singularPascal}InputSchema)
2437
2654
  .output(${names.singularPascal}Schema)
2438
2655
  .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" : ""});
2656
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
2440
2657
  if (!existing) {
2441
2658
  throw appError("${names.singularPascal}NotFound", {
2442
2659
  details: { id: input.id },
2443
2660
  });
2444
2661
  }
2445
- ${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
2662
+ ${options.authorization ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
2446
2663
  throw appError("${names.singularPascal}Conflict", {
2447
2664
  details: { id: input.id, expectedVersion: existing.version },
2448
2665
  });
@@ -2450,7 +2667,7 @@ ${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", ex
2450
2667
 
2451
2668
  const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.update({
2452
2669
  ...input,
2453
- }${options.tenant ? ", scope" : ""});
2670
+ }${options.tenantScoped ? ", scope" : ""});
2454
2671
  if (!${names.singularCamel}) {
2455
2672
  throw appError("${names.singularPascal}Conflict", {
2456
2673
  details: { id: input.id, expectedVersion: existing.version },
@@ -2465,7 +2682,7 @@ function deleteUseCaseFile(names, config, options) {
2465
2682
  const filePath = path.join(resourceUseCaseDir(names, config), `delete-${names.singularKebab}.ts`);
2466
2683
  return `import "@beignet/core/server-only";
2467
2684
  import { z } from "zod";
2468
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
2685
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
2469
2686
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2470
2687
  ${options.events ? `import { ${names.singularPascal}Deleted } from "../domain/events";\n` : ""}import { ${names.singularPascal}IdInputSchema } from "../schemas";
2471
2688
 
@@ -2474,7 +2691,7 @@ export const delete${names.singularPascal}UseCase = useCase
2474
2691
  .input(${names.singularPascal}IdInputSchema)
2475
2692
  .output(z.void())
2476
2693
  .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" : ""});
2694
+ ${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
2695
  if (!deleted) {
2479
2696
  throw appError("${names.singularPascal}NotFound", {
2480
2697
  details: { id: input.id },
@@ -2504,7 +2721,7 @@ function repositoryPortFile(names, config, mode, options) {
2504
2721
  PageResult,
2505
2722
  SortDirection,
2506
2723
  } from "@beignet/core/pagination";
2507
- ${options.tenant ? `import type { TenantScope } from "@beignet/core/tenancy";\n` : ""}import type {
2724
+ ${options.tenantScoped ? `import type { TenantScope } from "@beignet/core/tenancy";\n` : ""}import type {
2508
2725
  Create${names.singularPascal}Input,
2509
2726
  ${names.singularPascal}Cursor,
2510
2727
  ${names.singularPascal}SortBy,
@@ -2521,9 +2738,9 @@ export type List${names.pluralPascal}Query = {
2521
2738
  };
2522
2739
 
2523
2740
  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` : ""}}
2741
+ list(query: List${names.pluralPascal}Query${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<List${names.pluralPascal}Result>;
2742
+ create(input: Create${names.singularPascal}Input${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal}>;
2743
+ ${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
2744
  `;
2528
2745
  }
2529
2746
  function inMemoryRepositoryFile(names, config, mode, options) {
@@ -2551,13 +2768,13 @@ function inMemoryRepositoryFile(names, config, mode, options) {
2551
2768
  ? " || existing.deletedAt !== null"
2552
2769
  : "";
2553
2770
  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();
2771
+ ? `${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
2772
  ${names.pluralCamel}.set(id, { ...existing, deletedAt: now, updatedAt: now });
2556
2773
  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);`;
2774
+ : `${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
2775
  return `import "@beignet/core/server-only";
2559
2776
  import { cursorPageResult } from "@beignet/core/pagination";
2560
- ${options.tenant ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
2777
+ ${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
2561
2778
  import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
2562
2779
  import type {
2563
2780
  Create${names.singularPascal}Input,
@@ -2567,7 +2784,7 @@ ${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.s
2567
2784
  ${recordType}function to${names.singularPascal}(${names.singularCamel}: ${mapValueType}): ${names.singularPascal} {
2568
2785
  return {
2569
2786
  id: ${names.singularCamel}.id,
2570
- ${options.tenant ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
2787
+ ${options.tenantScoped ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
2571
2788
  version: ${names.singularCamel}.version,
2572
2789
  createdAt: ${names.singularCamel}.createdAt,
2573
2790
  updatedAt: ${names.singularCamel}.updatedAt,
@@ -2625,10 +2842,10 @@ export function createInMemory${names.singularPascal}Repository(
2625
2842
  );
2626
2843
 
2627
2844
  return {
2628
- async list(query${options.tenant ? ", scope" : ""}) {
2845
+ async list(query${options.tenantScoped ? ", scope" : ""}) {
2629
2846
  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))
2847
+ ${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values())
2848
+ ${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
2849
  .sort((left, right) => compare${names.pluralPascal}(left, right, query))
2633
2850
  .filter((${names.singularCamel}) => isAfter${names.singularPascal}Cursor(${names.singularCamel}, query));
2634
2851
  const pageItems = all${names.pluralPascal}.slice(0, query.page.limit);
@@ -2643,11 +2860,11 @@ ${activeFilter}${options.tenant ? `\t\t\t\t.filter((${names.singularCamel}) => $
2643
2860
  nextCursor,
2644
2861
  );
2645
2862
  },
2646
- async create(input${options.tenant ? ", scope" : ""}) {
2863
+ async create(input${options.tenantScoped ? ", scope" : ""}) {
2647
2864
  const now = new Date().toISOString();
2648
- ${options.tenant ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel}: ${names.singularPascal} = {
2865
+ ${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel}: ${names.singularPascal} = {
2649
2866
  id: crypto.randomUUID(),
2650
- ${options.tenant ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
2867
+ ${options.tenantScoped ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
2651
2868
  version: 1,
2652
2869
  createdAt: now,
2653
2870
  updatedAt: now,
@@ -2657,7 +2874,7 @@ ${options.tenant ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
2657
2874
  ${newDeletedAtField} });
2658
2875
  return ${names.singularCamel};
2659
2876
  },
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` : ""}
2877
+ ${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
2878
  };
2662
2879
  }
2663
2880
  `;
@@ -2667,7 +2884,7 @@ function drizzleSchemaFile(names, options, dialect) {
2667
2884
 
2668
2885
  export const ${names.pluralCamel} = ${dialect.tableFunction}("${names.pluralKebab.replaceAll("-", "_")}", {
2669
2886
  id: ${dialect.idColumn("id")}.primaryKey(),
2670
- ${options.tenant ? `\ttenantId: ${dialect.idColumn("tenant_id")}.notNull(),\n` : ""} name: text("name").notNull(),
2887
+ ${options.tenantScoped ? `\ttenantId: ${dialect.idColumn("tenant_id")}.notNull(),\n` : ""} name: text("name").notNull(),
2671
2888
  version: ${dialect.integerColumn("version")}.notNull(),
2672
2889
  createdAt: ${dialect.timestampColumn("created_at")}.notNull(),
2673
2890
  updatedAt: ${dialect.timestampColumn("updated_at")}.notNull(),
@@ -2764,20 +2981,20 @@ function drizzleRepositoryFile(names, config, mode, options, dialect) {
2764
2981
  const schemaPath = drizzleSchemaIndexPath(config);
2765
2982
  const findWhere = drizzleResourceWhere(names, options, [
2766
2983
  `eq(schema.${names.pluralCamel}.id, id)`,
2767
- ...(options.tenant
2984
+ ...(options.tenantScoped
2768
2985
  ? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
2769
2986
  : []),
2770
2987
  ]);
2771
2988
  const updateWhere = drizzleResourceWhere(names, options, [
2772
2989
  `eq(schema.${names.pluralCamel}.id, input.id)`,
2773
2990
  `eq(schema.${names.pluralCamel}.version, input.version)`,
2774
- ...(options.tenant
2991
+ ...(options.tenantScoped
2775
2992
  ? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
2776
2993
  : []),
2777
2994
  ]);
2778
2995
  const deleteWhere = drizzleResourceWhere(names, options, [
2779
2996
  `eq(schema.${names.pluralCamel}.id, id)`,
2780
- ...(options.tenant
2997
+ ...(options.tenantScoped
2781
2998
  ? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
2782
2999
  : []),
2783
3000
  ]);
@@ -2795,7 +3012,7 @@ function drizzleRepositoryFile(names, config, mode, options, dialect) {
2795
3012
  ].filter((name) => Boolean(name));
2796
3013
  // After a version-guarded update succeeds, re-select by identity only —
2797
3014
  // matching what `.returning()` yields on the dialects that support it.
2798
- const updateReselectWhere = options.tenant
3015
+ const updateReselectWhere = options.tenantScoped
2799
3016
  ? `and(eq(schema.${names.pluralCamel}.id, input.id), eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)))`
2800
3017
  : `eq(schema.${names.pluralCamel}.id, input.id)`;
2801
3018
  // drizzle-orm/mysql2 has no `.returning(...)`; mutations resolve to a
@@ -2817,13 +3034,13 @@ function affectedRows(result: unknown): number {
2817
3034
  `
2818
3035
  : "";
2819
3036
  const createMethod = dialect.supportsReturning
2820
- ? ` async create(input${options.tenant ? ", scope" : ""}) {
3037
+ ? ` async create(input${options.tenantScoped ? ", scope" : ""}) {
2821
3038
  const now = new Date().toISOString();
2822
3039
  const [row] = await db
2823
3040
  .insert(schema.${names.pluralCamel})
2824
3041
  .values({
2825
3042
  id: crypto.randomUUID(),
2826
- ${options.tenant ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
3043
+ ${options.tenantScoped ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
2827
3044
  version: 1,
2828
3045
  createdAt: now,
2829
3046
  updatedAt: now,
@@ -2837,11 +3054,11 @@ ${options.tenant ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name:
2837
3054
  return to${names.singularPascal}(row);
2838
3055
  },
2839
3056
  `
2840
- : ` async create(input${options.tenant ? ", scope" : ""}) {
3057
+ : ` async create(input${options.tenantScoped ? ", scope" : ""}) {
2841
3058
  const now = new Date().toISOString();
2842
3059
  const ${names.singularCamel}: ${names.singularPascal} = {
2843
3060
  id: crypto.randomUUID(),
2844
- ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
3061
+ ${options.tenantScoped ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
2845
3062
  version: 1,
2846
3063
  createdAt: now,
2847
3064
  updatedAt: now,
@@ -2854,7 +3071,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
2854
3071
  const resourceMethods = mode !== "resource"
2855
3072
  ? ""
2856
3073
  : dialect.supportsReturning
2857
- ? ` async findById(id: string${options.tenant ? ", scope" : ""}) {
3074
+ ? ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
2858
3075
  const [row] = await db
2859
3076
  .select()
2860
3077
  .from(schema.${names.pluralCamel})
@@ -2863,7 +3080,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
2863
3080
 
2864
3081
  return row ? to${names.singularPascal}(row) : null;
2865
3082
  },
2866
- async update(input${options.tenant ? ", scope" : ""}) {
3083
+ async update(input${options.tenantScoped ? ", scope" : ""}) {
2867
3084
  const [row] = await db
2868
3085
  .update(schema.${names.pluralCamel})
2869
3086
  .set({
@@ -2876,12 +3093,12 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
2876
3093
 
2877
3094
  return row ? to${names.singularPascal}(row) : null;
2878
3095
  },
2879
- async delete(id: string${options.tenant ? ", scope" : ""}) {
3096
+ async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
2880
3097
  ${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
3098
  return rows.length > 0;
2882
3099
  },
2883
3100
  `
2884
- : ` async findById(id: string${options.tenant ? ", scope" : ""}) {
3101
+ : ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
2885
3102
  const [row] = await db
2886
3103
  .select()
2887
3104
  .from(schema.${names.pluralCamel})
@@ -2890,7 +3107,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
2890
3107
 
2891
3108
  return row ? to${names.singularPascal}(row) : null;
2892
3109
  },
2893
- async update(input${options.tenant ? ", scope" : ""}) {
3110
+ async update(input${options.tenantScoped ? ", scope" : ""}) {
2894
3111
  const result = await db
2895
3112
  .update(schema.${names.pluralCamel})
2896
3113
  .set({
@@ -2910,14 +3127,14 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
2910
3127
 
2911
3128
  return row ? to${names.singularPascal}(row) : null;
2912
3129
  },
2913
- async delete(id: string${options.tenant ? ", scope" : ""}) {
3130
+ async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
2914
3131
  ${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
3132
  return affectedRows(result) > 0;
2916
3133
  },
2917
3134
  `;
2918
3135
  return `import "@beignet/core/server-only";
2919
3136
  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}";
3137
+ ${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
2921
3138
  import { ${drizzleImports.join(", ")} } from "drizzle-orm";
2922
3139
  import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
2923
3140
  import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
@@ -2932,7 +3149,7 @@ type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelec
2932
3149
  function to${names.singularPascal}(row: ${names.singularPascal}Row): ${names.singularPascal} {
2933
3150
  return {
2934
3151
  id: row.id,
2935
- ${options.tenant ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
3152
+ ${options.tenantScoped ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
2936
3153
  version: row.version,
2937
3154
  createdAt: row.createdAt,
2938
3155
  updatedAt: row.updatedAt,
@@ -2964,9 +3181,9 @@ function ${names.singularCamel}CursorFilter(
2964
3181
 
2965
3182
  function ${names.singularCamel}ListWhere(
2966
3183
  query: List${names.pluralPascal}Query,
2967
- ${options.tenant ? `\tscope: Parameters<${names.singularPascal}Repository["list"]>[1],\n` : ""}): SQL<unknown> | undefined {
3184
+ ${options.tenantScoped ? `\tscope: Parameters<${names.singularPascal}Repository["list"]>[1],\n` : ""}): SQL<unknown> | undefined {
2968
3185
  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` : ""} ];
3186
+ ${options.tenantScoped ? `\t\teq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
2970
3187
  const cursor = ${names.singularCamel}CursorFilter(query);
2971
3188
 
2972
3189
  if (query.name) {
@@ -3000,8 +3217,8 @@ export function createDrizzle${names.singularPascal}Repository(
3000
3217
  db: ${dialect.dbTypeName}<typeof schema>,
3001
3218
  ): ${names.singularPascal}Repository {
3002
3219
  return {
3003
- async list(query${options.tenant ? ", scope" : ""}) {
3004
- const where = ${names.singularCamel}ListWhere(query${options.tenant ? ", scope" : ""});
3220
+ async list(query${options.tenantScoped ? ", scope" : ""}) {
3221
+ const where = ${names.singularCamel}ListWhere(query${options.tenantScoped ? ", scope" : ""});
3005
3222
  const rows = await db
3006
3223
  .select()
3007
3224
  .from(schema.${names.pluralCamel})
@@ -3057,7 +3274,7 @@ export const list${names.pluralPascal} = ${names.pluralCamel}
3057
3274
  export const create${names.singularPascal} = ${names.pluralCamel}
3058
3275
  .post("/api/${names.pluralKebab}")
3059
3276
  .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({
3277
+ ${options.authorization ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
3061
3278
  201: ${names.singularPascal}Schema,
3062
3279
  });
3063
3280
  ${mode === "resource"
@@ -3074,7 +3291,7 @@ export const update${names.singularPascal} = ${names.pluralCamel}
3074
3291
  .patch("/api/${names.pluralKebab}/:id")
3075
3292
  .pathParams(${names.singularPascal}IdInputSchema)
3076
3293
  .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 })
3294
+ ${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
3295
  .responses({
3079
3296
  200: ${names.singularPascal}Schema,
3080
3297
  });
@@ -3082,7 +3299,7 @@ ${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability
3082
3299
  export const delete${names.singularPascal} = ${names.pluralCamel}
3083
3300
  .delete("/api/${names.pluralKebab}/:id")
3084
3301
  .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 })
3302
+ ${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
3303
  .responses({
3087
3304
  204: null,
3088
3305
  });
@@ -3266,7 +3483,7 @@ function isAuthenticated(ctx: AuthorizationContext) {
3266
3483
 
3267
3484
  function canWrite(ctx: AuthorizationContext) {
3268
3485
  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;
3486
+ ${options.tenantScoped ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
3270
3487
  }
3271
3488
 
3272
3489
  export const ${names.singularCamel}Policy = definePolicy({
@@ -3276,13 +3493,13 @@ export const ${names.singularCamel}Policy = definePolicy({
3276
3493
  "${names.pluralCamel}.update": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
3277
3494
  const decision = canWrite(ctx);
3278
3495
  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` : ""}
3496
+ ${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
3497
  return true;
3281
3498
  },
3282
3499
  "${names.pluralCamel}.delete": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
3283
3500
  const decision = canWrite(ctx);
3284
3501
  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` : ""}
3502
+ ${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
3503
  return true;
3287
3504
  },
3288
3505
  "${names.pluralCamel}.manage": (_ctx: AuthorizationContext) =>
@@ -3329,7 +3546,7 @@ function create${names.singularPascal}(): ${names.singularPascal} {
3329
3546
  const now = new Date().toISOString();
3330
3547
  return {
3331
3548
  id: "00000000-0000-4000-8000-000000000001",
3332
- ${options.tenant ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
3549
+ ${options.tenantScoped ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
3333
3550
  version: 1,
3334
3551
  createdAt: now,
3335
3552
  updatedAt: now,
@@ -3344,7 +3561,7 @@ describe("${names.singularCamel}Policy", () => {
3344
3561
  const ${names.singularCamel} = create${names.singularPascal}();
3345
3562
  const ctx = {
3346
3563
  actor: { type: "user" as const, id: "user_test" },
3347
- ${options.tenant ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
3564
+ ${options.tenantScoped ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
3348
3565
 
3349
3566
  await expect(
3350
3567
  tester.assertMatrix([
@@ -3546,8 +3763,32 @@ ${names.timezone ? `\t\ttimezone: "${names.timezone}",\n` : ""}\t\tpayload: ${na
3546
3763
  );
3547
3764
  `;
3548
3765
  }
3766
+ function uploadManifestFile(names) {
3767
+ return `import type {
3768
+ \tUploadFileConstraints,
3769
+ \tUploadManifestEntry,
3770
+ } from "@beignet/core/uploads";
3771
+
3772
+ export const ${names.fileConstraintsExportName} = {
3773
+ \tcontentTypes: ["application/pdf", "text/plain"],
3774
+ \tmaxSizeBytes: 5 * 1024 * 1024,
3775
+ \tmaxFiles: 1,
3776
+ \tvisibility: "private",
3777
+ \tcacheControl: "private, max-age=0",
3778
+ } satisfies UploadFileConstraints;
3779
+
3780
+ export const ${names.manifestExportName} = [
3781
+ \t{
3782
+ \t\tname: "${names.uploadName}",
3783
+ \t\tdescription: "${names.artifact.pascal} upload",
3784
+ \t\tfile: ${names.fileConstraintsExportName},
3785
+ \t},
3786
+ ] satisfies readonly UploadManifestEntry[];
3787
+ `;
3788
+ }
3549
3789
  function uploadFile(names, config) {
3550
3790
  return `import { z } from "zod";
3791
+ import { ${names.fileConstraintsExportName} } from "${relativeModule(uploadFilePath(names, config), uploadManifestFilePath(names, config))}";
3551
3792
  import { defineUpload } from "${aliasModule(config.paths.uploadsBuilder)}";
3552
3793
 
3553
3794
  export const ${names.metadataSchemaName} = z.object({
@@ -3558,13 +3799,7 @@ export type ${names.metadataTypeName} = z.infer<typeof ${names.metadataSchemaNam
3558
3799
 
3559
3800
  export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
3560
3801
  \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},
3802
+ \tfile: ${names.fileConstraintsExportName},
3568
3803
  \tauthorize({ ctx }) {
3569
3804
  \t\treturn ctx.actor.type === "user";
3570
3805
  \t},
@@ -3594,6 +3829,127 @@ export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
3594
3829
  });
3595
3830
  `;
3596
3831
  }
3832
+ function uploadUiClientFile(names, config) {
3833
+ const clientPath = uploadUiClientFilePath(names, config);
3834
+ const registryPath = path.join(featureArtifactDir(names, "uploads", config), "index.ts");
3835
+ return `import { createUploadClient } from "@beignet/core/uploads/client";
3836
+ import { createReactUploads } from "@beignet/react-uploads";
3837
+ import { ${names.manifestExportName} } from "${relativeModule(clientPath, uploadManifestFilePath(names, config))}";
3838
+ import type { ${names.featureSingularCamel}Uploads } from "${relativeModule(clientPath, registryPath)}";
3839
+
3840
+ const ${names.artifact.camel}UploadClient = createUploadClient<typeof ${names.featureSingularCamel}Uploads>({
3841
+ \tbaseUrl: "/api/uploads",
3842
+ \tmanifest: ${names.manifestExportName},
3843
+ });
3844
+
3845
+ export const ${names.reactUploadsExportName} = createReactUploads({
3846
+ \tuploads: ${names.artifact.camel}UploadClient,
3847
+ });
3848
+ `;
3849
+ }
3850
+ function uploadUiComponentFile(names, config) {
3851
+ const componentPath = uploadUiComponentFilePath(names, config);
3852
+ return `"use client";
3853
+
3854
+ import { useId } from "react";
3855
+ import { ${names.reactUploadsExportName} } from "${relativeModule(componentPath, uploadUiClientFilePath(names, config))}";
3856
+
3857
+ export type ${names.componentExportName}Props = {
3858
+ \tresourceId: string;
3859
+ \tdisabled?: boolean;
3860
+ \tonComplete?(objectKeys: readonly string[]): void | Promise<void>;
3861
+ };
3862
+
3863
+ export function ${names.componentExportName}({
3864
+ \tresourceId,
3865
+ \tdisabled = false,
3866
+ \tonComplete,
3867
+ }: ${names.componentExportName}Props) {
3868
+ \tconst inputId = useId();
3869
+ \tconst upload = ${names.reactUploadsExportName}.useUpload("${names.uploadName}", {
3870
+ \t\tonSuccess({ result }) {
3871
+ \t\t\treturn onComplete?.(result.objectKeys);
3872
+ \t\t},
3873
+ \t});
3874
+ \tconst maxSizeMb = upload.constraints?.maxSizeBytes
3875
+ \t\t? Math.round(upload.constraints.maxSizeBytes / 1024 / 1024)
3876
+ \t\t: undefined;
3877
+
3878
+ \treturn (
3879
+ \t\t<section className="${names.artifact.kebab}-uploader">
3880
+ \t\t\t<label htmlFor={inputId}>${names.artifact.pascal}</label>
3881
+ \t\t\t<input
3882
+ \t\t\t\tid={inputId}
3883
+ \t\t\t\ttype="file"
3884
+ \t\t\t\taccept={upload.accept}
3885
+ \t\t\t\tdisabled={disabled || upload.isUploading}
3886
+ \t\t\t\tonChange={(event) => {
3887
+ \t\t\t\t\tconst file = event.currentTarget.files?.[0];
3888
+ \t\t\t\t\tevent.currentTarget.value = "";
3889
+ \t\t\t\t\tif (!file) return;
3890
+
3891
+ \t\t\t\t\tupload.uploadFile(file, {
3892
+ \t\t\t\t\t\tmetadata: { resourceId },
3893
+ \t\t\t\t\t});
3894
+ \t\t\t\t}}
3895
+ \t\t\t/>
3896
+ \t\t\t<p>
3897
+ \t\t\t\tPDF or plain text{maxSizeMb ? <> up to {maxSizeMb} MB</> : null}.
3898
+ \t\t\t</p>
3899
+
3900
+ \t\t\t{upload.files[0] ? <p>{upload.files[0].fileName}</p> : null}
3901
+ \t\t\t{upload.isUploading ? (
3902
+ \t\t\t\t<div aria-live="polite">
3903
+ \t\t\t\t\t<progress
3904
+ \t\t\t\t\t\taria-label="Upload progress"
3905
+ \t\t\t\t\t\tmax={100}
3906
+ \t\t\t\t\t\tvalue={upload.progress}
3907
+ \t\t\t\t\t/>
3908
+ \t\t\t\t\t<span>{Math.round(upload.progress)}%</span>
3909
+ \t\t\t\t\t<button type="button" onClick={upload.abort}>
3910
+ \t\t\t\t\t\tCancel upload
3911
+ \t\t\t\t\t</button>
3912
+ \t\t\t\t</div>
3913
+ \t\t\t) : null}
3914
+ \t\t\t{upload.isError ? (
3915
+ \t\t\t\t<p role="alert">{uploadErrorMessage(upload.error)}</p>
3916
+ \t\t\t) : null}
3917
+ \t\t\t{upload.isSuccess ? <p role="status">Upload complete.</p> : null}
3918
+ \t\t\t{upload.isError || upload.isSuccess ? (
3919
+ \t\t\t\t<button type="button" onClick={upload.reset}>
3920
+ \t\t\t\t\tReset upload
3921
+ \t\t\t\t</button>
3922
+ \t\t\t) : null}
3923
+ \t\t</section>
3924
+ \t);
3925
+ }
3926
+
3927
+ function uploadErrorMessage(error: unknown): string {
3928
+ \treturn error instanceof Error ? error.message : "Upload failed.";
3929
+ }
3930
+ `;
3931
+ }
3932
+ function uploadUiComponentTestFile(names, config) {
3933
+ const testPath = uploadUiComponentTestFilePath(names, config);
3934
+ const testHelperPath = path.join(path.dirname(config.paths.useCaseBuilder), "beignet-test.ts");
3935
+ return `import { renderToStaticMarkup } from "react-dom/server";
3936
+ import { describe, expect, test } from "${relativeModule(testPath, testHelperPath)}";
3937
+ import { ${names.manifestExportName} } from "${relativeModule(testPath, uploadManifestFilePath(names, config))}";
3938
+ import { ${names.componentExportName} } from "${relativeModule(testPath, uploadUiComponentFilePath(names, config))}";
3939
+
3940
+ describe("${names.componentExportName}", () => {
3941
+ \ttest("renders the generated upload constraints", () => {
3942
+ \t\tconst markup = renderToStaticMarkup(
3943
+ \t\t\t<${names.componentExportName} resourceId="resource-1" />,
3944
+ \t\t);
3945
+
3946
+ \t\texpect(${names.manifestExportName}[0]?.file.maxSizeBytes).toBe(5 * 1024 * 1024);
3947
+ \t\texpect(markup).toContain('accept="application/pdf,text/plain"');
3948
+ \t\texpect(markup).toContain("PDF or plain text");
3949
+ \t});
3950
+ });
3951
+ `;
3952
+ }
3597
3953
  function uploadRouteFile(names, config) {
3598
3954
  const registryName = `${names.featureSingularCamel}Uploads`;
3599
3955
  return `import {
@@ -3682,63 +4038,70 @@ function featureUiComponentFile(names, config) {
3682
4038
  const clientQueriesPath = featureUiClientQueriesFilePath(names, config);
3683
4039
  return `"use client";
3684
4040
 
3685
- import { contractErrorMessage } from "@beignet/core/client";
4041
+ import { rootFormError } from "@beignet/react-hook-form";
3686
4042
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3687
- import { useState } from "react";
4043
+ import { rhf } from "${aliasModule(clientFormsPath(config))}";
4044
+ import { create${names.singularPascal} } from "${relativeModule(componentPath, resourceContractFilePath(names, config))}";
3688
4045
  import {
3689
4046
  \tcreate${names.singularPascal}MutationOptions,
3690
4047
  \tinvalidate${names.pluralPascal},
3691
4048
  \tlist${names.pluralPascal}QueryOptions,
3692
4049
  } from "${relativeModule(componentPath, clientQueriesPath)}";
3693
4050
 
4051
+ const create${names.singularPascal}Form = rhf(create${names.singularPascal});
4052
+
3694
4053
  export function ${names.componentExportName}() {
3695
- \tconst [name, setName] = useState("");
3696
4054
  \tconst queryClient = useQueryClient();
3697
4055
  \tconst ${names.pluralCamel}Query = useQuery(list${names.pluralPascal}QueryOptions());
4056
+ \tconst form = create${names.singularPascal}Form.useForm({
4057
+ \t\tdefaultValues: { name: "" },
4058
+ \t});
4059
+ \tconst name = form.watch("name");
3698
4060
  \tconst create${names.singularPascal}Mutation = useMutation({
3699
4061
  \t\t...create${names.singularPascal}MutationOptions(),
3700
4062
  \t\tonSuccess: async () => {
3701
- \t\t\tsetName("");
4063
+ \t\t\tform.reset({ name: "" });
3702
4064
  \t\t\tawait invalidate${names.pluralPascal}(queryClient);
3703
4065
  \t\t},
4066
+ \t\tonError: (error) => {
4067
+ \t\t\tform.setError(
4068
+ \t\t\t\t"root",
4069
+ \t\t\t\trootFormError(error, "Could not create ${names.singularKebab}."),
4070
+ \t\t\t);
4071
+ \t\t},
3704
4072
  \t});
3705
4073
 
3706
4074
  \treturn (
3707
4075
  \t\t<section className="${names.pluralKebab}-panel">
3708
4076
  \t\t\t<form
3709
4077
  \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;
4078
+ \t\t\t\tonSubmit={form.handleSubmit((values) => {
4079
+ \t\t\t\t\tform.clearErrors("root");
3714
4080
  \t\t\t\t\tcreate${names.singularPascal}Mutation.reset();
3715
4081
  \t\t\t\t\tcreate${names.singularPascal}Mutation.mutate({
3716
- \t\t\t\t\t\tbody: { name: trimmedName },
4082
+ \t\t\t\t\t\tbody: values,
3717
4083
  \t\t\t\t\t});
3718
- \t\t\t\t}}
4084
+ \t\t\t\t})}
3719
4085
  \t\t\t>
3720
4086
  \t\t\t\t<label htmlFor="${names.singularKebab}-name">New ${names.singularKebab}</label>
3721
4087
  \t\t\t\t<div className="${names.pluralKebab}-composer-row">
3722
4088
  \t\t\t\t\t<input
3723
4089
  \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)}
4090
+ \t\t\t\t\t\t{...form.register("name")}
3726
4091
  \t\t\t\t\t\tplaceholder="Name"
3727
4092
  \t\t\t\t\t/>
3728
4093
  \t\t\t\t\t<button
3729
4094
  \t\t\t\t\t\ttype="submit"
3730
- \t\t\t\t\t\tdisabled={!name.trim() || create${names.singularPascal}Mutation.isPending}
4095
+ \t\t\t\t\t\tdisabled={!name?.trim() || create${names.singularPascal}Mutation.isPending}
3731
4096
  \t\t\t\t\t>
3732
4097
  \t\t\t\t\t\t{create${names.singularPascal}Mutation.isPending ? "Creating" : "Create"}
3733
4098
  \t\t\t\t\t</button>
3734
4099
  \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>
4100
+ \t\t\t\t{form.formState.errors.name ? (
4101
+ \t\t\t\t\t<p role="alert">{form.formState.errors.name.message}</p>
4102
+ \t\t\t\t) : null}
4103
+ \t\t\t\t{form.formState.errors.root ? (
4104
+ \t\t\t\t\t<p role="alert">{form.formState.errors.root.message}</p>
3742
4105
  \t\t\t\t) : null}
3743
4106
  \t\t\t</form>
3744
4107
 
@@ -3852,7 +4215,7 @@ ${mode === "resource" ? `import { isAppError } from "@beignet/core/errors";\n` :
3852
4215
  import { createTestApp } from "@beignet/web/testing";
3853
4216
  import { createInMemoryDevtools } from "@beignet/devtools";
3854
4217
  import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
3855
- import { ${options.auth ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenant ? ", createTestTenant" : ""} } from "@beignet/core/testing";
4218
+ import { ${options.authorization ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenantScoped ? ", createTestTenant" : ""} } from "@beignet/core/testing";
3856
4219
  import type { AppContext } from "${aliasModule(config.paths.appContext)}";
3857
4220
  import { initialPorts } from "${aliasModule(config.paths.portWiring)}";
3858
4221
  import { appContext } from "${aliasModule(serverContextPath)}";
@@ -3872,39 +4235,39 @@ describe("${names.pluralCamel} resource", () => {
3872
4235
  const seed${names.pluralPascal} = [
3873
4236
  {
3874
4237
  id: "00000000-0000-4000-8000-000000000101",
3875
- ${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
4238
+ ${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
3876
4239
  version: 1,
3877
4240
  createdAt: "2024-01-01T00:00:00.000Z",
3878
4241
  updatedAt: "2024-01-01T00:00:00.000Z",
3879
4242
  },
3880
4243
  {
3881
4244
  id: "00000000-0000-4000-8000-000000000102",
3882
- ${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
4245
+ ${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
3883
4246
  version: 1,
3884
4247
  createdAt: "2024-01-02T00:00:00.000Z",
3885
4248
  updatedAt: "2024-01-02T00:00:00.000Z",
3886
4249
  },
3887
4250
  {
3888
4251
  id: "00000000-0000-4000-8000-000000000103",
3889
- ${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
4252
+ ${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
3890
4253
  version: 1,
3891
4254
  createdAt: "2024-01-03T00:00:00.000Z",
3892
4255
  updatedAt: "2024-01-03T00:00:00.000Z",
3893
4256
  },
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` : ""} ];
4257
+ ${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
4258
  const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository(seed${names.pluralPascal});
3896
4259
  const testFixture = createTestPorts<AppContext["ports"]>({
3897
4260
  base: initialPorts,
3898
4261
  overrides: {
3899
4262
  ${names.pluralCamel},
3900
- ${options.auth || options.tenant
4263
+ ${options.authorization || options.tenantScoped
3901
4264
  ? `\t\t\t\tauth: {
3902
4265
  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` : ""}
4266
+ user: { id: "user_test", name: "Test User"${options.tenantScoped ? `, tenantId: "tenant_test"` : ""} },
4267
+ ${options.tenantScoped ? `\t\t\t\t\t\tsession: { tenantId: "tenant_test" },\n` : ""}
3905
4268
  }),
3906
4269
  },
3907
- ${options.auth ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
4270
+ ${options.authorization ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
3908
4271
  `
3909
4272
  : `\t\t\t\tauth: {
3910
4273
  getSession: async () => null,
@@ -3919,8 +4282,8 @@ ${options.auth ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
3919
4282
  });
3920
4283
  const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
3921
4284
  ports: testFixture.ports,
3922
- actor: ${options.auth ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
3923
- tenant: ${options.tenant ? `createTestTenant("tenant_test")` : "null"},
4285
+ actor: ${options.authorization ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
4286
+ tenant: ${options.tenantScoped ? `createTestTenant("tenant_test")` : "null"},
3924
4287
  });
3925
4288
  const tester = createUseCaseTester<AppContext>(createTestContext);
3926
4289