@beignet/cli 0.0.37 → 0.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +138 -101
  3. package/dist/choices.d.ts +17 -21
  4. package/dist/choices.d.ts.map +1 -1
  5. package/dist/choices.js +21 -67
  6. package/dist/choices.js.map +1 -1
  7. package/dist/config.d.ts +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +1 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/create-prompts.d.ts +5 -6
  12. package/dist/create-prompts.d.ts.map +1 -1
  13. package/dist/create-prompts.js +15 -15
  14. package/dist/create-prompts.js.map +1 -1
  15. package/dist/create.d.ts +11 -2
  16. package/dist/create.d.ts.map +1 -1
  17. package/dist/create.js +38 -28
  18. package/dist/create.js.map +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +149 -47
  21. package/dist/index.js.map +1 -1
  22. package/dist/inspect.js +69 -16
  23. package/dist/inspect.js.map +1 -1
  24. package/dist/lib.d.ts +1 -1
  25. package/dist/lib.d.ts.map +1 -1
  26. package/dist/lint.d.ts.map +1 -1
  27. package/dist/lint.js +27 -6
  28. package/dist/lint.js.map +1 -1
  29. package/dist/make/inbox.js +10 -10
  30. package/dist/make/inbox.js.map +1 -1
  31. package/dist/make/payments.d.ts +2 -1
  32. package/dist/make/payments.d.ts.map +1 -1
  33. package/dist/make/payments.js +18 -14
  34. package/dist/make/payments.js.map +1 -1
  35. package/dist/make/shared.d.ts +11 -4
  36. package/dist/make/shared.d.ts.map +1 -1
  37. package/dist/make/shared.js +56 -45
  38. package/dist/make/shared.js.map +1 -1
  39. package/dist/make/tenancy.js +8 -8
  40. package/dist/make/tenancy.js.map +1 -1
  41. package/dist/make.d.ts +3 -2
  42. package/dist/make.d.ts.map +1 -1
  43. package/dist/make.js +481 -200
  44. package/dist/make.js.map +1 -1
  45. package/dist/mcp.d.ts.map +1 -1
  46. package/dist/mcp.js +15 -11
  47. package/dist/mcp.js.map +1 -1
  48. package/dist/operational-error-reporting.d.ts +15 -0
  49. package/dist/operational-error-reporting.d.ts.map +1 -0
  50. package/dist/operational-error-reporting.js +42 -0
  51. package/dist/operational-error-reporting.js.map +1 -0
  52. package/dist/outbox.d.ts.map +1 -1
  53. package/dist/outbox.js +73 -0
  54. package/dist/outbox.js.map +1 -1
  55. package/dist/provider-add.d.ts +21 -3
  56. package/dist/provider-add.d.ts.map +1 -1
  57. package/dist/provider-add.js +117 -81
  58. package/dist/provider-add.js.map +1 -1
  59. package/dist/provider-audit.js +3 -3
  60. package/dist/provider-audit.js.map +1 -1
  61. package/dist/schedule.d.ts.map +1 -1
  62. package/dist/schedule.js +28 -1
  63. package/dist/schedule.js.map +1 -1
  64. package/dist/task.d.ts.map +1 -1
  65. package/dist/task.js +38 -5
  66. package/dist/task.js.map +1 -1
  67. package/dist/templates/base.d.ts +1 -1
  68. package/dist/templates/base.d.ts.map +1 -1
  69. package/dist/templates/base.js +15 -15
  70. package/dist/templates/base.js.map +1 -1
  71. package/dist/templates/index.d.ts +2 -2
  72. package/dist/templates/index.d.ts.map +1 -1
  73. package/dist/templates/index.js +7 -7
  74. package/dist/templates/index.js.map +1 -1
  75. package/dist/templates/server.d.ts +1 -1
  76. package/dist/templates/server.d.ts.map +1 -1
  77. package/dist/templates/server.js +22 -18
  78. package/dist/templates/server.js.map +1 -1
  79. package/dist/templates/shared.d.ts +4 -4
  80. package/dist/templates/shared.d.ts.map +1 -1
  81. package/dist/templates/shared.js +6 -6
  82. package/dist/templates/shared.js.map +1 -1
  83. package/dist/templates/testing.js +1 -1
  84. package/dist/templates/todos.js +6 -6
  85. package/package.json +4 -4
  86. package/skills/app-structure/SKILL.md +13 -8
  87. package/src/choices.ts +38 -88
  88. package/src/config.ts +2 -2
  89. package/src/create-prompts.ts +20 -21
  90. package/src/create.ts +54 -36
  91. package/src/index.ts +193 -65
  92. package/src/inspect.ts +116 -18
  93. package/src/lib.ts +1 -1
  94. package/src/lint.ts +35 -4
  95. package/src/make/inbox.ts +10 -10
  96. package/src/make/payments.ts +26 -18
  97. package/src/make/shared.ts +72 -50
  98. package/src/make/tenancy.ts +8 -8
  99. package/src/make.ts +607 -191
  100. package/src/mcp.ts +20 -13
  101. package/src/operational-error-reporting.ts +60 -0
  102. package/src/outbox.ts +77 -0
  103. package/src/provider-add.ts +179 -95
  104. package/src/provider-audit.ts +3 -3
  105. package/src/schedule.ts +32 -1
  106. package/src/task.ts +42 -5
  107. package/src/templates/base.ts +16 -16
  108. package/src/templates/index.ts +8 -8
  109. package/src/templates/server.ts +22 -18
  110. package/src/templates/shared.ts +10 -10
  111. package/src/templates/testing.ts +1 -1
  112. package/src/templates/todos.ts +6 -6
  113. package/src/test-helpers/generated-app.ts +1 -0
package/dist/make.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { clientIndexPath, loadBeignetConfig, resolveConfig, } from "./config.js";
4
+ import ts from "typescript";
5
+ import { clientFormsPath, clientIndexPath, loadBeignetConfig, resolveConfig, } from "./config.js";
5
6
  import { adapterFilePath, addNamedImport, addNamedTypeImport, aliasModule, appendDeferredPortKey, appendPickStringLiteralMember, applyPortProviderWiring, assertAdapterApp, assertFeatureArtifactApp, assertFeatureUiApp, assertGeneratedIdentifiers, assertPersistenceArtifactApp, assertPolicyApp, assertPortApp, assertServerRuntimeApp, assertStandardApp, assertTestApp, assertUseCaseApp, beignetDependencyVersion, constantCase, createListenersRegistrySource, defineUploadsInitializerInfo, detectResourceDatabase, detectResourcePersistence, drizzleDialects, drizzleRepositoriesPath, drizzleResourceRepositoryFilePath, drizzleSchemaIndexPath, eventBusPortWiring, eventFilePath, factoryFilePath, featureArtifactDir, featureArtifactIndexFile, featureArtifactNames, featureResourceNames, featureUiClientQueriesFilePath, featureUiComponentFilePath, featureUiIndexFilePath, featureUiNames, fileExists, hasTopLevelObjectProperty, identifierNames, infrastructureDir, insertTypeProperty, jobFilePath, listenerFilePath, listenersFilePath, mergeMakeResultInto, mergeMakeResults, missingCapabilityBuilderFiles, notificationFilePath, notificationPortWirings, planGeneratedFile, planGeneratedFiles, portFilePath, portNames, providersFilePath, readOptionalFile, relativeModule, resourceContractFilePath, resourceFeatureDir, resourceNames, resourcePortFilePath, resourceSharedErrorsPath, scheduleFilePath, seedEntrypointPath, seedFilePath, seedNames, singularize, storagePortWiring, tanstackReactQueryDependencyVersion, taskFilePath, typeBodySource, uniqueStrings, updateDrizzleRepositories, updateDrizzleSchemaIndex, updateFeatureArtifactIndex, updateInfrastructureAdapterWiring, updateInfrastructureDeferredPorts, updateInfrastructureGatePolicies, updateInfrastructurePortStub, updateInfrastructurePorts, updateListenersRegistrySource, updateOpenApiRoute, updatePackageDependencies, updatePackageJson, updatePackageScripts, updatePortIndex, updatePortsIndex, updateSeedEntrypoint, updateServerRoutes, updateServerTransactionPorts, updateSharedErrors, uploadFilePath, useCaseFeatureDir, useCaseFilePath, usesFeatureOwnedPolicies, usesFeatureOwnedTests, wireListenersProviderSource, wirePortProviders, writeGeneratedFile, writePlannedGeneratedFile, } from "./make/shared.js";
6
7
  import { appendToArrayExpression, appendToOutboxRegistryArray, arrayInitializerInfo, identifiersFromArrayExpression, insertAfterImports, } from "./registry-edits.js";
8
+ import { externalVersions } from "./templates/shared.js";
7
9
  import { testSupportTemplateFiles } from "./templates/testing.js";
8
10
  export { makeFeatureAddonChoices, makeFeatureRecipeChoices, } from "./choices.js";
9
11
  export * from "./make/inbox.js";
@@ -17,7 +19,7 @@ export async function makeFeature(options) {
17
19
  const addons = normalizeMakeFeatureAddons([
18
20
  ...makeFeatureRecipeAddons(options.recipe),
19
21
  ...(options.with ?? []),
20
- ...(options.events ? ["events"] : []),
22
+ ...(options.events ? ["event"] : []),
21
23
  ]);
22
24
  const featureOptions = {
23
25
  ...options,
@@ -122,30 +124,36 @@ async function makeResourceSlice(options, mode) {
122
124
  function resourceGenerationOptions(options, mode) {
123
125
  if (mode === "feature") {
124
126
  return {
125
- auth: false,
126
- tenant: false,
127
+ authorization: false,
128
+ tenantScoped: false,
127
129
  events: Boolean(options.events),
128
130
  softDelete: false,
129
131
  };
130
132
  }
131
133
  if (mode !== "resource") {
132
- return { auth: false, tenant: false, events: false, softDelete: false };
134
+ return {
135
+ authorization: false,
136
+ tenantScoped: false,
137
+ events: false,
138
+ softDelete: false,
139
+ };
133
140
  }
134
141
  return {
135
- auth: Boolean(options.auth),
136
- tenant: Boolean(options.tenant),
142
+ authorization: Boolean(options.authorization),
143
+ tenantScoped: Boolean(options.tenantScoped),
137
144
  events: Boolean(options.events),
138
145
  softDelete: Boolean(options.softDelete),
139
146
  };
140
147
  }
141
148
  async function makeFeatureSlice(options, addons) {
142
149
  let result = await makeFeatureResource(options);
150
+ const includeUploadUi = addons.includes("ui") && addons.includes("upload");
143
151
  for (const addon of addons) {
144
- result = mergeMakeResults(result, await makeFeatureAddon(addon, result.name, options));
152
+ result = mergeMakeResults(result, await makeFeatureAddon(addon, result.name, options, includeUploadUi));
145
153
  }
146
154
  return result;
147
155
  }
148
- async function makeFeatureAddon(addon, feature, options) {
156
+ async function makeFeatureAddon(addon, feature, options, includeUploadUi) {
149
157
  const shared = {
150
158
  cwd: options.cwd,
151
159
  force: options.force,
@@ -158,69 +166,53 @@ async function makeFeatureAddon(addon, feature, options) {
158
166
  if (addon === "factory") {
159
167
  return makeFactory({
160
168
  ...shared,
161
- name: `${feature}/${singularize(feature)}`,
169
+ name: `${feature}.${singularize(feature)}`,
162
170
  skipPersistenceAssert: true,
163
171
  });
164
172
  }
165
173
  if (addon === "task") {
166
- return makeTask({ ...shared, name: `${feature}/backfill` });
174
+ return makeTask({ ...shared, name: `${feature}.backfill` });
167
175
  }
168
176
  if (addon === "event") {
169
- return makeEvent({ ...shared, name: `${feature}/created` });
177
+ return makeEvent({ ...shared, name: `${feature}.created` });
170
178
  }
171
179
  if (addon === "job") {
172
- return makeJob({ ...shared, name: `${feature}/process` });
180
+ return makeJob({ ...shared, name: `${feature}.process` });
173
181
  }
174
182
  if (addon === "listener") {
175
183
  return makeListener({
176
184
  ...shared,
177
- name: `${feature}/handle-created`,
178
- event: `${feature}/created`,
185
+ name: `${feature}.handle-created`,
186
+ event: `${feature}.created`,
179
187
  skipEventAssert: true,
180
188
  });
181
189
  }
182
190
  if (addon === "notification") {
183
- return makeNotification({ ...shared, name: `${feature}/created` });
191
+ return makeNotification({ ...shared, name: `${feature}.created` });
184
192
  }
185
193
  if (addon === "schedule") {
186
- return makeSchedule({ ...shared, name: `${feature}/daily-summary` });
194
+ return makeSchedule({ ...shared, name: `${feature}.daily-summary` });
187
195
  }
188
196
  if (addon === "seed") {
189
197
  return makeSeed({
190
198
  ...shared,
191
- name: `${feature}/demo-${feature}`,
199
+ name: `${feature}.demo-${feature}`,
192
200
  skipPersistenceAssert: true,
193
201
  });
194
202
  }
195
203
  if (addon === "ui") {
196
204
  return makeFeatureUi({ ...shared, name: feature });
197
205
  }
198
- return makeUpload({ ...shared, name: `${feature}/attachment` });
206
+ return makeUpload({
207
+ ...shared,
208
+ name: `${feature}.attachment`,
209
+ ui: includeUploadUi,
210
+ });
199
211
  }
200
212
  function normalizeMakeFeatureAddons(addons) {
201
213
  const normalized = new Set();
202
214
  for (const addon of addons) {
203
- if (addon === "factories")
204
- normalized.add("factory");
205
- else if (addon === "events")
206
- normalized.add("event");
207
- else if (addon === "listeners") {
208
- normalized.add("event");
209
- normalized.add("listener");
210
- }
211
- else if (addon === "tasks")
212
- normalized.add("task");
213
- else if (addon === "jobs")
214
- normalized.add("job");
215
- else if (addon === "notifications")
216
- normalized.add("notification");
217
- else if (addon === "schedules")
218
- normalized.add("schedule");
219
- else if (addon === "seeds")
220
- normalized.add("seed");
221
- else if (addon === "uploads")
222
- normalized.add("upload");
223
- else if (addon === "listener") {
215
+ if (addon === "listener") {
224
216
  normalized.add("event");
225
217
  normalized.add("listener");
226
218
  }
@@ -233,16 +225,16 @@ function makeFeatureRecipeAddons(recipe) {
233
225
  if (recipe === "full-slice") {
234
226
  return [
235
227
  "policy",
236
- "factories",
237
- "seeds",
238
- "tasks",
239
- "events",
240
- "listeners",
241
- "jobs",
242
- "notifications",
243
- "schedules",
228
+ "factory",
229
+ "seed",
230
+ "task",
231
+ "event",
232
+ "listener",
233
+ "job",
234
+ "notification",
235
+ "schedule",
244
236
  "ui",
245
- "uploads",
237
+ "upload",
246
238
  ];
247
239
  }
248
240
  return [];
@@ -328,7 +320,7 @@ export async function makeUseCase(options) {
328
320
  skippedFiles.push(indexFile.path);
329
321
  return {
330
322
  schemaVersion: 1,
331
- name: `${names.feature.kebab}/${names.action.kebab}`,
323
+ name: `${names.feature.kebab}.${names.action.kebab}`,
332
324
  targetDir,
333
325
  dryRun: Boolean(options.dryRun),
334
326
  files: [...generatedFiles.map((file) => file.path), indexFile.path],
@@ -367,7 +359,7 @@ export async function makeTest(options) {
367
359
  }
368
360
  return {
369
361
  schemaVersion: 1,
370
- name: `${names.feature.kebab}/${names.action.kebab}`,
362
+ name: `${names.feature.kebab}.${names.action.kebab}`,
371
363
  targetDir,
372
364
  dryRun: Boolean(options.dryRun),
373
365
  files: generatedFiles.map((file) => file.path),
@@ -415,7 +407,7 @@ export async function makePort(options) {
415
407
  if (await updateInfrastructurePortStub(targetDir, names, config, {
416
408
  dryRun: Boolean(options.dryRun),
417
409
  })) {
418
- updatedFiles.push(config.paths.infrastructurePorts);
410
+ updatedFiles.push(config.paths.portWiring);
419
411
  }
420
412
  return {
421
413
  schemaVersion: 1,
@@ -454,7 +446,7 @@ export async function makeAdapter(options) {
454
446
  if (await updateInfrastructureAdapterWiring(targetDir, names, config, {
455
447
  dryRun: Boolean(options.dryRun),
456
448
  })) {
457
- updatedFiles.push(config.paths.infrastructurePorts);
449
+ updatedFiles.push(config.paths.portWiring);
458
450
  }
459
451
  return {
460
452
  schemaVersion: 1,
@@ -521,12 +513,15 @@ export async function makeEvent(options) {
521
513
  const outboxDrainPlan = await planOutboxDrainWiring(targetDir, config, {
522
514
  force: Boolean(options.force),
523
515
  });
516
+ await updateOutboxRegistry(targetDir, names, "events", config, {
517
+ dryRun: true,
518
+ });
524
519
  const result = await makeFeatureArtifact({
525
520
  targetDir,
526
521
  config,
527
522
  force: Boolean(options.force),
528
523
  dryRun: Boolean(options.dryRun),
529
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
524
+ name: names.stableName,
530
525
  files: eventFiles(names, config),
531
526
  index: featureArtifactIndexFile("event", names, config),
532
527
  dependencies: {
@@ -559,12 +554,15 @@ export async function makeJob(options) {
559
554
  const outboxDrainPlan = await planOutboxDrainWiring(targetDir, config, {
560
555
  force: Boolean(options.force),
561
556
  });
557
+ await updateOutboxRegistry(targetDir, names, "jobs", config, {
558
+ dryRun: true,
559
+ });
562
560
  const result = await makeFeatureArtifact({
563
561
  targetDir,
564
562
  config,
565
563
  force: Boolean(options.force),
566
564
  dryRun: Boolean(options.dryRun),
567
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
565
+ name: names.stableName,
568
566
  files: [
569
567
  ...(await missingCapabilityBuilderFiles(targetDir, "jobs", config)),
570
568
  ...jobFiles(names, config),
@@ -597,7 +595,7 @@ export async function makeTask(options) {
597
595
  config,
598
596
  force: Boolean(options.force),
599
597
  dryRun: Boolean(options.dryRun),
600
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
598
+ name: names.stableName,
601
599
  files: [
602
600
  ...(await missingCapabilityBuilderFiles(targetDir, "tasks", config)),
603
601
  ...taskFiles(names, config),
@@ -635,7 +633,7 @@ export async function makeFactory(options) {
635
633
  config,
636
634
  force: Boolean(options.force),
637
635
  dryRun: Boolean(options.dryRun),
638
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
636
+ name: names.stableName,
639
637
  files: factoryFiles(names, config),
640
638
  index: featureArtifactIndexFile("factory", names, config),
641
639
  dependencies: {
@@ -657,7 +655,7 @@ export async function makeSeed(options) {
657
655
  config,
658
656
  force: Boolean(options.force),
659
657
  dryRun: Boolean(options.dryRun),
660
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
658
+ name: names.stableName,
661
659
  files: seedFiles(names, config),
662
660
  index: featureArtifactIndexFile("seed", names, config),
663
661
  dependencies: {
@@ -671,7 +669,7 @@ export async function makeSeed(options) {
671
669
  config,
672
670
  force: Boolean(options.force),
673
671
  dryRun: Boolean(options.dryRun),
674
- name: `${factoryNamesForSeed.feature.kebab}/${factoryNamesForSeed.artifact.kebab}`,
672
+ name: factoryNamesForSeed.stableName,
675
673
  files: factoryFiles(factoryNamesForSeed, config),
676
674
  index: featureArtifactIndexFile("factory", factoryNamesForSeed, config),
677
675
  dependencies: {
@@ -708,7 +706,7 @@ export async function makeNotification(options) {
708
706
  // notifications uses ctx.ports.notifications, so the generator wires both
709
707
  // ports with dev-default providers. Each port is wired independently and
710
708
  // skipped when the app already has it, for example when the resend
711
- // integration contributed the mailer. Plan the wiring before writing any
709
+ // provider contributed the mailer. Plan the wiring before writing any
712
710
  // files so an anchor miss aborts the generator without leaving partial
713
711
  // output.
714
712
  const notificationWirings = notificationPortWirings("make notification");
@@ -720,7 +718,7 @@ export async function makeNotification(options) {
720
718
  config,
721
719
  force: Boolean(options.force),
722
720
  dryRun: Boolean(options.dryRun),
723
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
721
+ name: names.stableName,
724
722
  files: [
725
723
  ...(await missingCapabilityBuilderFiles(targetDir, "notifications", config)),
726
724
  ...notificationFiles(names, config),
@@ -754,7 +752,7 @@ export async function makeListener(options) {
754
752
  config,
755
753
  force: Boolean(options.force),
756
754
  dryRun: Boolean(options.dryRun),
757
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
755
+ name: names.stableName,
758
756
  files: [
759
757
  ...(await missingCapabilityBuilderFiles(targetDir, "listeners", config)),
760
758
  ...listenerFiles(names, config),
@@ -826,7 +824,7 @@ async function assertListenerEventExists(targetDir, names, config) {
826
824
  await stat(path.join(targetDir, file));
827
825
  }
828
826
  catch {
829
- throw new Error(`beignet make listener expected event ${names.eventName} at ${file}. Run beignet make event ${names.feature.kebab}/${names.artifact.kebab} first, or pass an event that exists.`);
827
+ throw new Error(`beignet make listener expected event ${names.eventName} at ${file}. Run beignet make event ${names.eventName} first, or pass an event that exists.`);
830
828
  }
831
829
  }
832
830
  async function updateListenerProviderWiring(targetDir, names, config, options) {
@@ -878,7 +876,7 @@ export async function makeSchedule(options) {
878
876
  config,
879
877
  force: Boolean(options.force),
880
878
  dryRun: Boolean(options.dryRun),
881
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
879
+ name: names.stableName,
882
880
  files,
883
881
  index: featureArtifactIndexFile("schedule", names, config),
884
882
  dependencies: {
@@ -1003,7 +1001,7 @@ async function updateOutboxPortWiring(targetDir, config, options) {
1003
1001
  changes.updatedFiles.push(config.paths.ports);
1004
1002
  }
1005
1003
  if (await updateOutboxInfrastructurePorts(targetDir, config, options)) {
1006
- changes.updatedFiles.push(config.paths.infrastructurePorts);
1004
+ changes.updatedFiles.push(config.paths.portWiring);
1007
1005
  }
1008
1006
  if (await updateOutboxDatabaseProvider(targetDir, config, options)) {
1009
1007
  changes.updatedFiles.push(path.join(infrastructureDir(config), "db/provider.ts"));
@@ -1075,7 +1073,7 @@ async function updateOutboxPorts(targetDir, config, options) {
1075
1073
  return true;
1076
1074
  }
1077
1075
  async function updateOutboxInfrastructurePorts(targetDir, config, options) {
1078
- const filePath = path.join(targetDir, config.paths.infrastructurePorts);
1076
+ const filePath = path.join(targetDir, config.paths.portWiring);
1079
1077
  const original = await readFile(filePath, "utf8");
1080
1078
  let next = appendDeferredPortKey(original, "outbox");
1081
1079
  next = appendDeferredPortKey(next, "outboxAdmin");
@@ -1246,6 +1244,9 @@ export async function makeUpload(options) {
1246
1244
  ? resolveConfig(options.config)
1247
1245
  : await loadBeignetConfig(targetDir);
1248
1246
  await assertFeatureArtifactApp(targetDir, config, "upload");
1247
+ if (options.ui) {
1248
+ await assertUploadUiApp(targetDir, config);
1249
+ }
1249
1250
  await wirePortProviders(targetDir, config, [storagePortWiring("make upload")], {
1250
1251
  dryRun: true,
1251
1252
  });
@@ -1254,10 +1255,10 @@ export async function makeUpload(options) {
1254
1255
  config,
1255
1256
  force: Boolean(options.force),
1256
1257
  dryRun: Boolean(options.dryRun),
1257
- name: `${names.feature.kebab}/${names.artifact.kebab}`,
1258
+ name: names.stableName,
1258
1259
  files: [
1259
1260
  ...(await missingCapabilityBuilderFiles(targetDir, "uploads", config)),
1260
- ...uploadFiles(names, config),
1261
+ ...uploadFiles(names, config, { ui: Boolean(options.ui) }),
1261
1262
  ],
1262
1263
  index: featureArtifactIndexFile("upload", names, config),
1263
1264
  dependencies: {
@@ -1277,6 +1278,31 @@ export async function makeUpload(options) {
1277
1278
  if (!result.files.includes(uploadRoutePath(config))) {
1278
1279
  result.files.push(uploadRoutePath(config));
1279
1280
  }
1281
+ if (options.ui) {
1282
+ const componentIndex = uploadUiComponentIndexFile(names, config);
1283
+ const componentIndexResult = await updateFeatureUiIndex(targetDir, componentIndex, { dryRun: Boolean(options.dryRun) });
1284
+ if (componentIndexResult === "created") {
1285
+ result.createdFiles.push(componentIndex.path);
1286
+ }
1287
+ if (componentIndexResult === "updated") {
1288
+ result.updatedFiles.push(componentIndex.path);
1289
+ }
1290
+ if (componentIndexResult === "skipped") {
1291
+ result.skippedFiles.push(componentIndex.path);
1292
+ }
1293
+ if (!result.files.includes(componentIndex.path)) {
1294
+ result.files.push(componentIndex.path);
1295
+ }
1296
+ if (await updatePackageDependencies(targetDir, { "@beignet/react-uploads": beignetDependencyVersion }, { dryRun: Boolean(options.dryRun) })) {
1297
+ result.updatedFiles.push("package.json");
1298
+ if (!result.files.includes("package.json")) {
1299
+ result.files.push("package.json");
1300
+ }
1301
+ }
1302
+ }
1303
+ result.createdFiles = uniqueStrings(result.createdFiles);
1304
+ result.updatedFiles = uniqueStrings(result.updatedFiles);
1305
+ result.skippedFiles = uniqueStrings(result.skippedFiles);
1280
1306
  return result;
1281
1307
  }
1282
1308
  export async function makeOutbox(options = {}) {
@@ -1325,7 +1351,7 @@ export async function makeOutbox(options = {}) {
1325
1351
  ...cronSecretPlan.files.map((file) => file.path),
1326
1352
  ...cronSecretPlan.skippedFiles,
1327
1353
  config.paths.ports,
1328
- config.paths.infrastructurePorts,
1354
+ config.paths.portWiring,
1329
1355
  path.join(infrastructureDir(config), "db/provider.ts"),
1330
1356
  drizzleRepositoriesPath(config),
1331
1357
  outboxDrizzleSchemaFilePath(config),
@@ -1343,7 +1369,10 @@ async function makeFeatureUi(options) {
1343
1369
  ? resolveConfig(options.config)
1344
1370
  : await loadBeignetConfig(targetDir);
1345
1371
  await assertFeatureUiApp(targetDir, config);
1346
- const componentFiles = featureUiComponentFiles(names, config);
1372
+ const componentFiles = [
1373
+ ...(await missingFeatureUiFormsFiles(targetDir, config)),
1374
+ ...featureUiComponentFiles(names, config),
1375
+ ];
1347
1376
  const indexFile = featureUiIndexFile(names, config);
1348
1377
  const plannedFiles = await planGeneratedFiles(targetDir, componentFiles, {
1349
1378
  force: Boolean(options.force),
@@ -1372,8 +1401,13 @@ async function makeFeatureUi(options) {
1372
1401
  if (indexResult === "skipped")
1373
1402
  skippedFiles.push(indexFile.path);
1374
1403
  if (await updatePackageDependencies(targetDir, {
1404
+ "@beignet/react-hook-form": beignetDependencyVersion,
1375
1405
  "@beignet/react-query": beignetDependencyVersion,
1406
+ "@hookform/resolvers": (packageJson) => packageJson.dependencies?.["@hookform/resolvers"] ??
1407
+ externalVersions.hookformResolvers,
1376
1408
  "@tanstack/react-query": tanstackReactQueryDependencyVersion,
1409
+ "react-hook-form": (packageJson) => packageJson.dependencies?.["react-hook-form"] ??
1410
+ externalVersions.reactHookForm,
1377
1411
  }, { dryRun: Boolean(options.dryRun) })) {
1378
1412
  updatedFiles.push("package.json");
1379
1413
  }
@@ -1459,15 +1493,15 @@ async function updateResourceWiring(targetDir, names, config, persistenceOrOptio
1459
1493
  }
1460
1494
  if (persistence === "memory" &&
1461
1495
  (await updateInfrastructurePorts(targetDir, names, config, options))) {
1462
- updated.add(config.paths.infrastructurePorts);
1496
+ updated.add(config.paths.portWiring);
1463
1497
  }
1464
- if (generationOptions.auth &&
1498
+ if (generationOptions.authorization &&
1465
1499
  (await updateInfrastructureGatePolicies(targetDir, names, config, options))) {
1466
- updated.add(config.paths.infrastructurePorts);
1500
+ updated.add(config.paths.portWiring);
1467
1501
  }
1468
1502
  if (persistence === "drizzle" &&
1469
1503
  (await updateInfrastructureDeferredPorts(targetDir, names, config, options))) {
1470
- updated.add(config.paths.infrastructurePorts);
1504
+ updated.add(config.paths.portWiring);
1471
1505
  }
1472
1506
  if (persistence === "drizzle" &&
1473
1507
  (await updateDrizzleSchemaIndex(targetDir, names, config, options))) {
@@ -1499,11 +1533,10 @@ function typeHasTopLevelProperty(source, typeName, propertyName) {
1499
1533
  function useCaseNames(input) {
1500
1534
  const parts = input
1501
1535
  .trim()
1502
- .split("/")
1503
- .map((part) => part.trim())
1504
- .filter(Boolean);
1505
- if (parts.length !== 2) {
1506
- throw new Error("Use case name must use feature/action format, for example projects/archive-project.");
1536
+ .split(".")
1537
+ .map((part) => part.trim());
1538
+ if (parts.length !== 2 || parts.some((part) => part.length === 0)) {
1539
+ throw new Error("Use case name must use feature.action format, for example projects.archive-project.");
1507
1540
  }
1508
1541
  const feature = identifierNames(parts[0], "Use case feature");
1509
1542
  const action = identifierNames(parts[1], "Use case action");
@@ -1535,7 +1568,7 @@ function policyNames(input) {
1535
1568
  return resolvedNames;
1536
1569
  }
1537
1570
  function eventNames(input) {
1538
- const names = featureArtifactNames(normalizeEventReference(input), "Event");
1571
+ const names = featureArtifactNames(input, "Event");
1539
1572
  const eventExportName = `${names.featureSingularPascal}${names.artifact.pascal}`;
1540
1573
  assertGeneratedIdentifiers(input, "Event name", [eventExportName]);
1541
1574
  return {
@@ -1581,7 +1614,7 @@ function factoryNames(input) {
1581
1614
  };
1582
1615
  }
1583
1616
  function defaultFactoryNamesForSeed(names) {
1584
- return factoryNames(`${names.feature.kebab}/${names.featureSingularKebab}`);
1617
+ return factoryNames(`${names.feature.kebab}.${names.featureSingularKebab}`);
1585
1618
  }
1586
1619
  function notificationNames(input) {
1587
1620
  const names = featureArtifactNames(input, "Notification");
@@ -1625,24 +1658,26 @@ function scheduleNames(input, options) {
1625
1658
  function uploadNames(input) {
1626
1659
  const names = featureArtifactNames(input, "Upload");
1627
1660
  const uploadExportName = `${names.artifact.pascal}Upload`;
1628
- assertGeneratedIdentifiers(input, "Upload name", [uploadExportName]);
1661
+ const componentExportName = `${names.artifact.pascal}Uploader`;
1662
+ assertGeneratedIdentifiers(input, "Upload name", [
1663
+ uploadExportName,
1664
+ componentExportName,
1665
+ ]);
1629
1666
  return {
1630
1667
  ...names,
1631
1668
  uploadName: names.stableName,
1632
1669
  uploadExportName,
1633
1670
  metadataSchemaName: `${uploadExportName}MetadataSchema`,
1634
1671
  metadataTypeName: `${uploadExportName}Metadata`,
1672
+ fileConstraintsExportName: `${names.artifact.camel}UploadFile`,
1673
+ manifestExportName: `${names.artifact.camel}UploadManifest`,
1674
+ manifestFileName: `${names.artifact.kebab}-upload-manifest`,
1675
+ reactUploadsExportName: `${names.artifact.camel}ReactUploads`,
1676
+ clientFileName: `${names.artifact.kebab}-upload`,
1677
+ componentExportName,
1678
+ componentFileName: `${names.artifact.kebab}-uploader`,
1635
1679
  };
1636
1680
  }
1637
- function normalizeEventReference(input) {
1638
- const trimmed = input.trim();
1639
- if (trimmed.includes("/"))
1640
- return trimmed;
1641
- const dotIndex = trimmed.indexOf(".");
1642
- if (dotIndex === -1)
1643
- return trimmed;
1644
- return `${trimmed.slice(0, dotIndex)}/${trimmed.slice(dotIndex + 1)}`;
1645
- }
1646
1681
  function inferUseCaseKind(action) {
1647
1682
  if (/^(count|find|get|list|search)-/.test(`${action}-`)) {
1648
1683
  return "query";
@@ -1650,8 +1685,8 @@ function inferUseCaseKind(action) {
1650
1685
  return "command";
1651
1686
  }
1652
1687
  function resourceFiles(names, config, persistence = "memory", mode = "feature", options = {
1653
- auth: false,
1654
- tenant: false,
1688
+ authorization: false,
1689
+ tenantScoped: false,
1655
1690
  events: false,
1656
1691
  softDelete: false,
1657
1692
  }, database = "sqlite") {
@@ -1704,7 +1739,7 @@ function resourceFiles(names, config, persistence = "memory", mode = "feature",
1704
1739
  },
1705
1740
  ]
1706
1741
  : []),
1707
- ...(mode === "resource" && options.auth
1742
+ ...(mode === "resource" && options.authorization
1708
1743
  ? [
1709
1744
  {
1710
1745
  path: path.join(featureDir, "tests/policy.test.ts"),
@@ -1759,6 +1794,59 @@ function featureUiComponentFiles(names, config) {
1759
1794
  },
1760
1795
  ];
1761
1796
  }
1797
+ async function missingFeatureUiFormsFiles(targetDir, config) {
1798
+ const formsPath = clientFormsPath(config);
1799
+ const existing = await readOptionalFile(path.join(targetDir, formsPath));
1800
+ if (existing === undefined) {
1801
+ return [
1802
+ {
1803
+ path: formsPath,
1804
+ content: `import { createReactHookForm } from "@beignet/react-hook-form";
1805
+
1806
+ export const rhf = createReactHookForm();
1807
+ `,
1808
+ },
1809
+ ];
1810
+ }
1811
+ if (!sourceExportsValue(existing, "rhf")) {
1812
+ throw new Error(`beignet make feature --with ui expects ${formsPath} to export rhf. Add \`export const rhf = createReactHookForm();\` from @beignet/react-hook-form, or move the existing helper before generating UI.`);
1813
+ }
1814
+ return [];
1815
+ }
1816
+ async function assertUploadUiApp(targetDir, config) {
1817
+ const clientPath = clientIndexPath(config);
1818
+ const clientSource = await readOptionalFile(path.join(targetDir, clientPath));
1819
+ if (!clientSource || !sourceExportsValue(clientSource, "rq")) {
1820
+ throw new Error(`beignet make upload --ui expects a full-stack Beignet app with ${clientPath} exporting rq. API-only apps stay backend-only; omit --ui or add the full-stack client setup first.`);
1821
+ }
1822
+ }
1823
+ function sourceExportsValue(source, name) {
1824
+ const sourceFile = ts.createSourceFile("client-forms.ts", source, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
1825
+ for (const statement of sourceFile.statements) {
1826
+ const exported = ts.canHaveModifiers(statement) &&
1827
+ ts
1828
+ .getModifiers(statement)
1829
+ ?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
1830
+ if (exported && ts.isVariableStatement(statement)) {
1831
+ if (statement.declarationList.declarations.some((declaration) => ts.isIdentifier(declaration.name) && declaration.name.text === name)) {
1832
+ return true;
1833
+ }
1834
+ }
1835
+ if (exported &&
1836
+ ts.isFunctionDeclaration(statement) &&
1837
+ statement.name?.text === name) {
1838
+ return true;
1839
+ }
1840
+ if (ts.isExportDeclaration(statement) &&
1841
+ !statement.isTypeOnly &&
1842
+ statement.exportClause &&
1843
+ ts.isNamedExports(statement.exportClause) &&
1844
+ statement.exportClause.elements.some((element) => !element.isTypeOnly && element.name.text === name)) {
1845
+ return true;
1846
+ }
1847
+ }
1848
+ return false;
1849
+ }
1762
1850
  function featureUiIndexFile(names, config) {
1763
1851
  return {
1764
1852
  path: featureUiIndexFilePath(names, config),
@@ -1887,12 +1975,54 @@ function scheduleFiles(names, config, options) {
1887
1975
  }
1888
1976
  return files;
1889
1977
  }
1890
- function uploadFiles(names, config) {
1978
+ function uploadFiles(names, config, options) {
1891
1979
  return [
1980
+ {
1981
+ path: uploadManifestFilePath(names, config),
1982
+ content: uploadManifestFile(names),
1983
+ },
1892
1984
  {
1893
1985
  path: uploadFilePath(names, config),
1894
1986
  content: uploadFile(names, config),
1895
1987
  },
1988
+ ...(options.ui ? uploadUiFiles(names, config) : []),
1989
+ ];
1990
+ }
1991
+ function uploadFeatureDir(names, config) {
1992
+ return path.join(config.paths.features, names.feature.kebab);
1993
+ }
1994
+ function uploadManifestFilePath(names, config) {
1995
+ return path.join(uploadFeatureDir(names, config), `${names.manifestFileName}.ts`);
1996
+ }
1997
+ function uploadUiClientFilePath(names, config) {
1998
+ return path.join(uploadFeatureDir(names, config), "client", `${names.clientFileName}.ts`);
1999
+ }
2000
+ function uploadUiComponentFilePath(names, config) {
2001
+ return path.join(uploadFeatureDir(names, config), "components", `${names.componentFileName}.tsx`);
2002
+ }
2003
+ function uploadUiComponentTestFilePath(names, config) {
2004
+ return path.join(uploadFeatureDir(names, config), "tests", `${names.componentFileName}.test.tsx`);
2005
+ }
2006
+ function uploadUiComponentIndexFile(names, config) {
2007
+ return {
2008
+ path: path.join(uploadFeatureDir(names, config), "components", "index.ts"),
2009
+ content: `export { ${names.componentExportName} } from "./${names.componentFileName}";\n`,
2010
+ };
2011
+ }
2012
+ function uploadUiFiles(names, config) {
2013
+ return [
2014
+ {
2015
+ path: uploadUiClientFilePath(names, config),
2016
+ content: uploadUiClientFile(names, config),
2017
+ },
2018
+ {
2019
+ path: uploadUiComponentFilePath(names, config),
2020
+ content: uploadUiComponentFile(names, config),
2021
+ },
2022
+ {
2023
+ path: uploadUiComponentTestFilePath(names, config),
2024
+ content: uploadUiComponentTestFile(names, config),
2025
+ },
1896
2026
  ];
1897
2027
  }
1898
2028
  function outboxFiles(config) {
@@ -2166,7 +2296,10 @@ async function updateOutboxRegistry(targetDir, names, kind, config, options) {
2166
2296
  const registryName = `${names.featureSingularCamel}${kind === "events" ? "Events" : "Jobs"}`;
2167
2297
  const importLine = `import { ${registryName} } from "${aliasModule(featureIndexPath)}";`;
2168
2298
  const appended = appendToOutboxRegistryArray(existing, kind, `...${registryName}`, importLine);
2169
- if (appended.kind === "missing" || appended.kind === "unchanged") {
2299
+ if (appended.kind === "missing") {
2300
+ throw new Error(`Could not find the ${kind} array in ${config.paths.outbox}. Register ${registryName} manually, or restore the generated outbox file before running this command.`);
2301
+ }
2302
+ if (appended.kind === "unchanged") {
2170
2303
  return "skipped";
2171
2304
  }
2172
2305
  if (!options.dryRun)
@@ -2195,7 +2328,7 @@ function schemasFile(names, mode, options) {
2195
2328
 
2196
2329
  export const ${names.singularPascal}Schema = z.object({
2197
2330
  id: z.string().uuid(),
2198
- ${options.tenant ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
2331
+ ${options.tenantScoped ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
2199
2332
  version: z.number().int().min(1),
2200
2333
  createdAt: z.string().datetime(),
2201
2334
  updatedAt: z.string().datetime(),
@@ -2295,7 +2428,7 @@ export const List${names.pluralPascal}OutputSchema = z.object({
2295
2428
  });
2296
2429
 
2297
2430
  export const Create${names.singularPascal}InputSchema = z.object({
2298
- name: z.string().min(1).max(120),
2431
+ name: z.string().trim().min(1).max(120),
2299
2432
  });
2300
2433
  ${mode === "resource"
2301
2434
  ? `
@@ -2304,7 +2437,7 @@ export const ${names.singularPascal}IdInputSchema = z.object({
2304
2437
  });
2305
2438
 
2306
2439
  export const Update${names.singularPascal}BodySchema = z.object({
2307
- name: z.string().min(1).max(120),
2440
+ name: z.string().trim().min(1).max(120),
2308
2441
  version: z.number().int().min(1),
2309
2442
  });
2310
2443
 
@@ -2338,7 +2471,7 @@ function listUseCaseFile(names, config, options) {
2338
2471
  const filePath = path.join(resourceUseCaseDir(names, config), `list-${names.pluralKebab}.ts`);
2339
2472
  return `import "@beignet/core/server-only";
2340
2473
  import { normalizeCursorPage } from "@beignet/core/pagination";
2341
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2474
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2342
2475
  import {
2343
2476
  decode${names.singularPascal}Cursor,
2344
2477
  List${names.pluralPascal}InputSchema,
@@ -2350,7 +2483,7 @@ export const list${names.pluralPascal}UseCase = useCase
2350
2483
  .input(List${names.pluralPascal}InputSchema)
2351
2484
  .output(List${names.pluralPascal}OutputSchema)
2352
2485
  .run(async ({ ctx, input }) => {
2353
- ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const page = normalizeCursorPage(input, {
2486
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const page = normalizeCursorPage(input, {
2354
2487
  defaultLimit: 20,
2355
2488
  maxLimit: 100,
2356
2489
  });
@@ -2361,14 +2494,14 @@ ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const
2361
2494
  name: input.name,
2362
2495
  sortBy: input.sortBy,
2363
2496
  sortDirection: input.sortDirection,
2364
- }${options.tenant ? ", scope" : ""});
2497
+ }${options.tenantScoped ? ", scope" : ""});
2365
2498
  });
2366
2499
  `;
2367
2500
  }
2368
2501
  function createUseCaseFile(names, config, options) {
2369
2502
  const filePath = path.join(resourceUseCaseDir(names, config), `create-${names.singularKebab}.ts`);
2370
2503
  return `import "@beignet/core/server-only";
2371
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2504
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2372
2505
  import {
2373
2506
  Create${names.singularPascal}InputSchema,
2374
2507
  ${names.singularPascal}Schema,
@@ -2380,7 +2513,7 @@ export const create${names.singularPascal}UseCase = useCase
2380
2513
  .input(Create${names.singularPascal}InputSchema)
2381
2514
  .output(${names.singularPascal}Schema)
2382
2515
  .run(async ({ ctx, input }) => {
2383
- ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""}${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.create");\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.create(input${options.tenant ? ", scope" : ""});
2516
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""}${options.authorization ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.create");\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.create(input${options.tenantScoped ? ", scope" : ""});
2384
2517
  ${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Created, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
2385
2518
  return ${names.singularCamel};
2386
2519
  });
@@ -2389,7 +2522,7 @@ ${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPasca
2389
2522
  function getUseCaseFile(names, config, options) {
2390
2523
  const filePath = path.join(resourceUseCaseDir(names, config), `get-${names.singularKebab}.ts`);
2391
2524
  return `import "@beignet/core/server-only";
2392
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
2525
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
2393
2526
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2394
2527
  import {
2395
2528
  ${names.singularPascal}IdInputSchema,
@@ -2401,7 +2534,7 @@ export const get${names.singularPascal}UseCase = useCase
2401
2534
  .input(${names.singularPascal}IdInputSchema)
2402
2535
  .output(${names.singularPascal}Schema)
2403
2536
  .run(async ({ ctx, input }) => {
2404
- ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenant ? ", scope" : ""});
2537
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
2405
2538
  if (!${names.singularCamel}) {
2406
2539
  throw appError("${names.singularPascal}NotFound", {
2407
2540
  details: { id: input.id },
@@ -2415,7 +2548,7 @@ ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const
2415
2548
  function updateUseCaseFile(names, config, options) {
2416
2549
  const filePath = path.join(resourceUseCaseDir(names, config), `update-${names.singularKebab}.ts`);
2417
2550
  return `import "@beignet/core/server-only";
2418
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
2551
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
2419
2552
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2420
2553
  ${options.events ? `import { ${names.singularPascal}Updated } from "../domain/events";\n` : ""}import {
2421
2554
  Update${names.singularPascal}InputSchema,
@@ -2427,13 +2560,13 @@ export const update${names.singularPascal}UseCase = useCase
2427
2560
  .input(Update${names.singularPascal}InputSchema)
2428
2561
  .output(${names.singularPascal}Schema)
2429
2562
  .run(async ({ ctx, input }) => {
2430
- ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenant ? ", scope" : ""});
2563
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
2431
2564
  if (!existing) {
2432
2565
  throw appError("${names.singularPascal}NotFound", {
2433
2566
  details: { id: input.id },
2434
2567
  });
2435
2568
  }
2436
- ${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
2569
+ ${options.authorization ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
2437
2570
  throw appError("${names.singularPascal}Conflict", {
2438
2571
  details: { id: input.id, expectedVersion: existing.version },
2439
2572
  });
@@ -2441,7 +2574,7 @@ ${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", ex
2441
2574
 
2442
2575
  const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.update({
2443
2576
  ...input,
2444
- }${options.tenant ? ", scope" : ""});
2577
+ }${options.tenantScoped ? ", scope" : ""});
2445
2578
  if (!${names.singularCamel}) {
2446
2579
  throw appError("${names.singularPascal}Conflict", {
2447
2580
  details: { id: input.id, expectedVersion: existing.version },
@@ -2456,7 +2589,7 @@ function deleteUseCaseFile(names, config, options) {
2456
2589
  const filePath = path.join(resourceUseCaseDir(names, config), `delete-${names.singularKebab}.ts`);
2457
2590
  return `import "@beignet/core/server-only";
2458
2591
  import { z } from "zod";
2459
- ${options.tenant ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
2592
+ ${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
2460
2593
  import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
2461
2594
  ${options.events ? `import { ${names.singularPascal}Deleted } from "../domain/events";\n` : ""}import { ${names.singularPascal}IdInputSchema } from "../schemas";
2462
2595
 
@@ -2465,7 +2598,7 @@ export const delete${names.singularPascal}UseCase = useCase
2465
2598
  .input(${names.singularPascal}IdInputSchema)
2466
2599
  .output(z.void())
2467
2600
  .run(async ({ ctx, input }) => {
2468
- ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""}${options.auth ? `\t\tconst existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenant ? ", scope" : ""});\n\t\tif (!existing) {\n\t\t\tthrow appError("${names.singularPascal}NotFound", {\n\t\t\t\tdetails: { id: input.id },\n\t\t\t});\n\t\t}\n\t\tawait ctx.gate.authorize("${names.pluralCamel}.delete", existing);\n\n` : ""} const deleted = await ctx.ports.${names.pluralCamel}.delete(input.id${options.tenant ? ", scope" : ""});
2601
+ ${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""}${options.authorization ? `\t\tconst existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});\n\t\tif (!existing) {\n\t\t\tthrow appError("${names.singularPascal}NotFound", {\n\t\t\t\tdetails: { id: input.id },\n\t\t\t});\n\t\t}\n\t\tawait ctx.gate.authorize("${names.pluralCamel}.delete", existing);\n\n` : ""} const deleted = await ctx.ports.${names.pluralCamel}.delete(input.id${options.tenantScoped ? ", scope" : ""});
2469
2602
  if (!deleted) {
2470
2603
  throw appError("${names.singularPascal}NotFound", {
2471
2604
  details: { id: input.id },
@@ -2495,7 +2628,7 @@ function repositoryPortFile(names, config, mode, options) {
2495
2628
  PageResult,
2496
2629
  SortDirection,
2497
2630
  } from "@beignet/core/pagination";
2498
- ${options.tenant ? `import type { TenantScope } from "@beignet/core/tenancy";\n` : ""}import type {
2631
+ ${options.tenantScoped ? `import type { TenantScope } from "@beignet/core/tenancy";\n` : ""}import type {
2499
2632
  Create${names.singularPascal}Input,
2500
2633
  ${names.singularPascal}Cursor,
2501
2634
  ${names.singularPascal}SortBy,
@@ -2512,9 +2645,9 @@ export type List${names.pluralPascal}Query = {
2512
2645
  };
2513
2646
 
2514
2647
  export interface ${names.singularPascal}Repository {
2515
- list(query: List${names.pluralPascal}Query${options.tenant ? ", scope: TenantScope" : ""}): Promise<List${names.pluralPascal}Result>;
2516
- create(input: Create${names.singularPascal}Input${options.tenant ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal}>;
2517
- ${mode === "resource" ? ` findById(id: string${options.tenant ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal} | null>;\n update(input: Update${names.singularPascal}Input${options.tenant ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal} | null>;\n delete(id: string${options.tenant ? ", scope: TenantScope" : ""}): Promise<boolean>;\n` : ""}}
2648
+ list(query: List${names.pluralPascal}Query${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<List${names.pluralPascal}Result>;
2649
+ create(input: Create${names.singularPascal}Input${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal}>;
2650
+ ${mode === "resource" ? ` findById(id: string${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal} | null>;\n update(input: Update${names.singularPascal}Input${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal} | null>;\n delete(id: string${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<boolean>;\n` : ""}}
2518
2651
  `;
2519
2652
  }
2520
2653
  function inMemoryRepositoryFile(names, config, mode, options) {
@@ -2542,13 +2675,13 @@ function inMemoryRepositoryFile(names, config, mode, options) {
2542
2675
  ? " || existing.deletedAt !== null"
2543
2676
  : "";
2544
2677
  const deleteImplementation = options.softDelete
2545
- ? `${options.tenant ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.tenantId !== tenantId || existing.deletedAt !== null) return false;\n` : `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.deletedAt !== null) return false;\n`} const now = new Date().toISOString();
2678
+ ? `${options.tenantScoped ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.tenantId !== tenantId || existing.deletedAt !== null) return false;\n` : `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.deletedAt !== null) return false;\n`} const now = new Date().toISOString();
2546
2679
  ${names.pluralCamel}.set(id, { ...existing, deletedAt: now, updatedAt: now });
2547
2680
  return true;`
2548
- : `${options.tenant ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (existing?.tenantId !== tenantId) return false;\n` : ""} return ${names.pluralCamel}.delete(id);`;
2681
+ : `${options.tenantScoped ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (existing?.tenantId !== tenantId) return false;\n` : ""} return ${names.pluralCamel}.delete(id);`;
2549
2682
  return `import "@beignet/core/server-only";
2550
2683
  import { cursorPageResult } from "@beignet/core/pagination";
2551
- ${options.tenant ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
2684
+ ${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
2552
2685
  import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
2553
2686
  import type {
2554
2687
  Create${names.singularPascal}Input,
@@ -2558,7 +2691,7 @@ ${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.s
2558
2691
  ${recordType}function to${names.singularPascal}(${names.singularCamel}: ${mapValueType}): ${names.singularPascal} {
2559
2692
  return {
2560
2693
  id: ${names.singularCamel}.id,
2561
- ${options.tenant ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
2694
+ ${options.tenantScoped ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
2562
2695
  version: ${names.singularCamel}.version,
2563
2696
  createdAt: ${names.singularCamel}.createdAt,
2564
2697
  updatedAt: ${names.singularCamel}.updatedAt,
@@ -2616,10 +2749,10 @@ export function createInMemory${names.singularPascal}Repository(
2616
2749
  );
2617
2750
 
2618
2751
  return {
2619
- async list(query${options.tenant ? ", scope" : ""}) {
2752
+ async list(query${options.tenantScoped ? ", scope" : ""}) {
2620
2753
  const name = query.name?.toLocaleLowerCase();
2621
- ${options.tenant ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values())
2622
- ${activeFilter}${options.tenant ? `\t\t\t\t.filter((${names.singularCamel}) => ${names.singularCamel}.tenantId === tenantId)\n` : ""} .filter((${names.singularCamel}) => !name || ${names.singularCamel}.name.toLocaleLowerCase().includes(name))
2754
+ ${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values())
2755
+ ${activeFilter}${options.tenantScoped ? `\t\t\t\t.filter((${names.singularCamel}) => ${names.singularCamel}.tenantId === tenantId)\n` : ""} .filter((${names.singularCamel}) => !name || ${names.singularCamel}.name.toLocaleLowerCase().includes(name))
2623
2756
  .sort((left, right) => compare${names.pluralPascal}(left, right, query))
2624
2757
  .filter((${names.singularCamel}) => isAfter${names.singularPascal}Cursor(${names.singularCamel}, query));
2625
2758
  const pageItems = all${names.pluralPascal}.slice(0, query.page.limit);
@@ -2634,11 +2767,11 @@ ${activeFilter}${options.tenant ? `\t\t\t\t.filter((${names.singularCamel}) => $
2634
2767
  nextCursor,
2635
2768
  );
2636
2769
  },
2637
- async create(input${options.tenant ? ", scope" : ""}) {
2770
+ async create(input${options.tenantScoped ? ", scope" : ""}) {
2638
2771
  const now = new Date().toISOString();
2639
- ${options.tenant ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel}: ${names.singularPascal} = {
2772
+ ${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel}: ${names.singularPascal} = {
2640
2773
  id: crypto.randomUUID(),
2641
- ${options.tenant ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
2774
+ ${options.tenantScoped ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
2642
2775
  version: 1,
2643
2776
  createdAt: now,
2644
2777
  updatedAt: now,
@@ -2648,7 +2781,7 @@ ${options.tenant ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
2648
2781
  ${newDeletedAtField} });
2649
2782
  return ${names.singularCamel};
2650
2783
  },
2651
- ${mode === "resource" ? ` async findById(id: string${options.tenant ? ", scope" : ""}) {\n${options.tenant ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel} = ${names.pluralCamel}.get(id) ?? null;\n${options.tenant ? `\t\t\tif (${names.singularCamel}?.tenantId !== tenantId) return null;\n` : ""}${findDeletedGuard} return ${names.singularCamel} ? to${names.singularPascal}(${names.singularCamel}) : null;\n },\n async update(input${options.tenant ? ", scope" : ""}) {\n${options.tenant ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const existing = ${names.pluralCamel}.get(input.id);\n if (!existing${options.tenant ? " || existing.tenantId !== tenantId" : ""}${updateDeletedGuard} || existing.version !== input.version) return null;\n\n const next: ${mapValueType} = {\n ...existing,\n name: input.name,\n version: existing.version + 1,\n updatedAt: new Date().toISOString(),\n };\n ${names.pluralCamel}.set(input.id, next);\n return to${names.singularPascal}(next);\n },\n async delete(id: string${options.tenant ? ", scope" : ""}) {\n${options.tenant ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""}${deleteImplementation}\n },\n` : ""}
2784
+ ${mode === "resource" ? ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {\n${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel} = ${names.pluralCamel}.get(id) ?? null;\n${options.tenantScoped ? `\t\t\tif (${names.singularCamel}?.tenantId !== tenantId) return null;\n` : ""}${findDeletedGuard} return ${names.singularCamel} ? to${names.singularPascal}(${names.singularCamel}) : null;\n },\n async update(input${options.tenantScoped ? ", scope" : ""}) {\n${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const existing = ${names.pluralCamel}.get(input.id);\n if (!existing${options.tenantScoped ? " || existing.tenantId !== tenantId" : ""}${updateDeletedGuard} || existing.version !== input.version) return null;\n\n const next: ${mapValueType} = {\n ...existing,\n name: input.name,\n version: existing.version + 1,\n updatedAt: new Date().toISOString(),\n };\n ${names.pluralCamel}.set(input.id, next);\n return to${names.singularPascal}(next);\n },\n async delete(id: string${options.tenantScoped ? ", scope" : ""}) {\n${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""}${deleteImplementation}\n },\n` : ""}
2652
2785
  };
2653
2786
  }
2654
2787
  `;
@@ -2658,7 +2791,7 @@ function drizzleSchemaFile(names, options, dialect) {
2658
2791
 
2659
2792
  export const ${names.pluralCamel} = ${dialect.tableFunction}("${names.pluralKebab.replaceAll("-", "_")}", {
2660
2793
  id: ${dialect.idColumn("id")}.primaryKey(),
2661
- ${options.tenant ? `\ttenantId: ${dialect.idColumn("tenant_id")}.notNull(),\n` : ""} name: text("name").notNull(),
2794
+ ${options.tenantScoped ? `\ttenantId: ${dialect.idColumn("tenant_id")}.notNull(),\n` : ""} name: text("name").notNull(),
2662
2795
  version: ${dialect.integerColumn("version")}.notNull(),
2663
2796
  createdAt: ${dialect.timestampColumn("created_at")}.notNull(),
2664
2797
  updatedAt: ${dialect.timestampColumn("updated_at")}.notNull(),
@@ -2676,6 +2809,7 @@ export const outboxMessages = mysqlTable(
2676
2809
  \t\tkind: varchar("kind", { length: 32 }).notNull(),
2677
2810
  \t\tname: varchar("name", { length: 255 }).notNull(),
2678
2811
  \t\tpayloadJson: longtext("payload_json").notNull(),
2812
+ \t\ttraceContextJson: longtext("trace_context_json"),
2679
2813
  \t\tstatus: varchar("status", { length: 32 }).notNull(),
2680
2814
  \t\tattempts: int("attempts").notNull().default(0),
2681
2815
  \t\tmaxAttempts: int("max_attempts").notNull().default(3),
@@ -2710,6 +2844,7 @@ export const outboxMessages = ${dialect.tableFunction}(
2710
2844
  \t\tkind: text("kind").notNull(),
2711
2845
  \t\tname: text("name").notNull(),
2712
2846
  \t\tpayloadJson: text("payload_json").notNull(),
2847
+ \t\ttraceContextJson: text("trace_context_json"),
2713
2848
  \t\tstatus: text("status").notNull(),
2714
2849
  \t\tattempts: integer("attempts").notNull().default(0),
2715
2850
  \t\tmaxAttempts: integer("max_attempts").notNull().default(3),
@@ -2753,20 +2888,20 @@ function drizzleRepositoryFile(names, config, mode, options, dialect) {
2753
2888
  const schemaPath = drizzleSchemaIndexPath(config);
2754
2889
  const findWhere = drizzleResourceWhere(names, options, [
2755
2890
  `eq(schema.${names.pluralCamel}.id, id)`,
2756
- ...(options.tenant
2891
+ ...(options.tenantScoped
2757
2892
  ? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
2758
2893
  : []),
2759
2894
  ]);
2760
2895
  const updateWhere = drizzleResourceWhere(names, options, [
2761
2896
  `eq(schema.${names.pluralCamel}.id, input.id)`,
2762
2897
  `eq(schema.${names.pluralCamel}.version, input.version)`,
2763
- ...(options.tenant
2898
+ ...(options.tenantScoped
2764
2899
  ? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
2765
2900
  : []),
2766
2901
  ]);
2767
2902
  const deleteWhere = drizzleResourceWhere(names, options, [
2768
2903
  `eq(schema.${names.pluralCamel}.id, id)`,
2769
- ...(options.tenant
2904
+ ...(options.tenantScoped
2770
2905
  ? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
2771
2906
  : []),
2772
2907
  ]);
@@ -2784,7 +2919,7 @@ function drizzleRepositoryFile(names, config, mode, options, dialect) {
2784
2919
  ].filter((name) => Boolean(name));
2785
2920
  // After a version-guarded update succeeds, re-select by identity only —
2786
2921
  // matching what `.returning()` yields on the dialects that support it.
2787
- const updateReselectWhere = options.tenant
2922
+ const updateReselectWhere = options.tenantScoped
2788
2923
  ? `and(eq(schema.${names.pluralCamel}.id, input.id), eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)))`
2789
2924
  : `eq(schema.${names.pluralCamel}.id, input.id)`;
2790
2925
  // drizzle-orm/mysql2 has no `.returning(...)`; mutations resolve to a
@@ -2806,13 +2941,13 @@ function affectedRows(result: unknown): number {
2806
2941
  `
2807
2942
  : "";
2808
2943
  const createMethod = dialect.supportsReturning
2809
- ? ` async create(input${options.tenant ? ", scope" : ""}) {
2944
+ ? ` async create(input${options.tenantScoped ? ", scope" : ""}) {
2810
2945
  const now = new Date().toISOString();
2811
2946
  const [row] = await db
2812
2947
  .insert(schema.${names.pluralCamel})
2813
2948
  .values({
2814
2949
  id: crypto.randomUUID(),
2815
- ${options.tenant ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
2950
+ ${options.tenantScoped ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
2816
2951
  version: 1,
2817
2952
  createdAt: now,
2818
2953
  updatedAt: now,
@@ -2826,11 +2961,11 @@ ${options.tenant ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name:
2826
2961
  return to${names.singularPascal}(row);
2827
2962
  },
2828
2963
  `
2829
- : ` async create(input${options.tenant ? ", scope" : ""}) {
2964
+ : ` async create(input${options.tenantScoped ? ", scope" : ""}) {
2830
2965
  const now = new Date().toISOString();
2831
2966
  const ${names.singularCamel}: ${names.singularPascal} = {
2832
2967
  id: crypto.randomUUID(),
2833
- ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
2968
+ ${options.tenantScoped ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
2834
2969
  version: 1,
2835
2970
  createdAt: now,
2836
2971
  updatedAt: now,
@@ -2843,7 +2978,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
2843
2978
  const resourceMethods = mode !== "resource"
2844
2979
  ? ""
2845
2980
  : dialect.supportsReturning
2846
- ? ` async findById(id: string${options.tenant ? ", scope" : ""}) {
2981
+ ? ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
2847
2982
  const [row] = await db
2848
2983
  .select()
2849
2984
  .from(schema.${names.pluralCamel})
@@ -2852,7 +2987,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
2852
2987
 
2853
2988
  return row ? to${names.singularPascal}(row) : null;
2854
2989
  },
2855
- async update(input${options.tenant ? ", scope" : ""}) {
2990
+ async update(input${options.tenantScoped ? ", scope" : ""}) {
2856
2991
  const [row] = await db
2857
2992
  .update(schema.${names.pluralCamel})
2858
2993
  .set({
@@ -2865,12 +3000,12 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
2865
3000
 
2866
3001
  return row ? to${names.singularPascal}(row) : null;
2867
3002
  },
2868
- async delete(id: string${options.tenant ? ", scope" : ""}) {
3003
+ async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
2869
3004
  ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst rows = await db\n\t\t\t\t.update(schema.${names.pluralCamel})\n\t\t\t\t.set({ deletedAt: now, updatedAt: now })\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n` : `\t\t\tconst rows = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n`}
2870
3005
  return rows.length > 0;
2871
3006
  },
2872
3007
  `
2873
- : ` async findById(id: string${options.tenant ? ", scope" : ""}) {
3008
+ : ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
2874
3009
  const [row] = await db
2875
3010
  .select()
2876
3011
  .from(schema.${names.pluralCamel})
@@ -2879,7 +3014,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
2879
3014
 
2880
3015
  return row ? to${names.singularPascal}(row) : null;
2881
3016
  },
2882
- async update(input${options.tenant ? ", scope" : ""}) {
3017
+ async update(input${options.tenantScoped ? ", scope" : ""}) {
2883
3018
  const result = await db
2884
3019
  .update(schema.${names.pluralCamel})
2885
3020
  .set({
@@ -2899,14 +3034,14 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
2899
3034
 
2900
3035
  return row ? to${names.singularPascal}(row) : null;
2901
3036
  },
2902
- async delete(id: string${options.tenant ? ", scope" : ""}) {
3037
+ async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
2903
3038
  ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst result = await db\n\t\t\t\t.update(schema.${names.pluralCamel})\n\t\t\t\t.set({ deletedAt: now, updatedAt: now })\n\t\t\t\t.where(${deleteWhere});\n` : `\t\t\tconst result = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere});\n`}
2904
3039
  return affectedRows(result) > 0;
2905
3040
  },
2906
3041
  `;
2907
3042
  return `import "@beignet/core/server-only";
2908
3043
  import { cursorPageResult } from "@beignet/core/pagination";
2909
- ${options.tenant ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
3044
+ ${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
2910
3045
  import { ${drizzleImports.join(", ")} } from "drizzle-orm";
2911
3046
  import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
2912
3047
  import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
@@ -2921,7 +3056,7 @@ type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelec
2921
3056
  function to${names.singularPascal}(row: ${names.singularPascal}Row): ${names.singularPascal} {
2922
3057
  return {
2923
3058
  id: row.id,
2924
- ${options.tenant ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
3059
+ ${options.tenantScoped ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
2925
3060
  version: row.version,
2926
3061
  createdAt: row.createdAt,
2927
3062
  updatedAt: row.updatedAt,
@@ -2953,9 +3088,9 @@ function ${names.singularCamel}CursorFilter(
2953
3088
 
2954
3089
  function ${names.singularCamel}ListWhere(
2955
3090
  query: List${names.pluralPascal}Query,
2956
- ${options.tenant ? `\tscope: Parameters<${names.singularPascal}Repository["list"]>[1],\n` : ""}): SQL<unknown> | undefined {
3091
+ ${options.tenantScoped ? `\tscope: Parameters<${names.singularPascal}Repository["list"]>[1],\n` : ""}): SQL<unknown> | undefined {
2957
3092
  const filters: SQL<unknown>[] = [
2958
- ${options.tenant ? `\t\teq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
3093
+ ${options.tenantScoped ? `\t\teq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
2959
3094
  const cursor = ${names.singularCamel}CursorFilter(query);
2960
3095
 
2961
3096
  if (query.name) {
@@ -2989,8 +3124,8 @@ export function createDrizzle${names.singularPascal}Repository(
2989
3124
  db: ${dialect.dbTypeName}<typeof schema>,
2990
3125
  ): ${names.singularPascal}Repository {
2991
3126
  return {
2992
- async list(query${options.tenant ? ", scope" : ""}) {
2993
- const where = ${names.singularCamel}ListWhere(query${options.tenant ? ", scope" : ""});
3127
+ async list(query${options.tenantScoped ? ", scope" : ""}) {
3128
+ const where = ${names.singularCamel}ListWhere(query${options.tenantScoped ? ", scope" : ""});
2994
3129
  const rows = await db
2995
3130
  .select()
2996
3131
  .from(schema.${names.pluralCamel})
@@ -3046,7 +3181,7 @@ export const list${names.pluralPascal} = ${names.pluralCamel}
3046
3181
  export const create${names.singularPascal} = ${names.pluralCamel}
3047
3182
  .post("/api/${names.pluralKebab}")
3048
3183
  .body(Create${names.singularPascal}InputSchema)
3049
- ${options.auth ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
3184
+ ${options.authorization ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
3050
3185
  201: ${names.singularPascal}Schema,
3051
3186
  });
3052
3187
  ${mode === "resource"
@@ -3063,7 +3198,7 @@ export const update${names.singularPascal} = ${names.pluralCamel}
3063
3198
  .patch("/api/${names.pluralKebab}/:id")
3064
3199
  .pathParams(${names.singularPascal}IdInputSchema)
3065
3200
  .body(Update${names.singularPascal}BodySchema)
3066
- ${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.update" },\n\t})\n` : ""} .errors({${options.auth ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound, ${names.singularPascal}Conflict: errors.${names.singularPascal}Conflict })
3201
+ ${options.authorization ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.update" },\n\t})\n` : ""} .errors({${options.authorization ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound, ${names.singularPascal}Conflict: errors.${names.singularPascal}Conflict })
3067
3202
  .responses({
3068
3203
  200: ${names.singularPascal}Schema,
3069
3204
  });
@@ -3071,7 +3206,7 @@ ${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability
3071
3206
  export const delete${names.singularPascal} = ${names.pluralCamel}
3072
3207
  .delete("/api/${names.pluralKebab}/:id")
3073
3208
  .pathParams(${names.singularPascal}IdInputSchema)
3074
- ${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.delete" },\n\t})\n` : ""} .errors({${options.auth ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound })
3209
+ ${options.authorization ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.delete" },\n\t})\n` : ""} .errors({${options.authorization ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound })
3075
3210
  .responses({
3076
3211
  204: null,
3077
3212
  });
@@ -3157,15 +3292,15 @@ import { createInMemoryDevtools } from "@beignet/devtools";
3157
3292
  import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
3158
3293
  import { createTestAnonymousActor } from "@beignet/core/testing";
3159
3294
  import type { AppContext } from "${aliasModule(config.paths.appContext)}";
3160
- import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
3295
+ import { initialPorts } from "${aliasModule(config.paths.portWiring)}";
3161
3296
  import { ${names.exportName} } from "${relativeModule(filePath, useCaseFilePath(names, config))}";
3162
3297
 
3163
3298
  describe("${names.exportName}", () => {
3164
3299
  it("runs ${names.action.camel}", async () => {
3165
3300
  const testFixture = createTestPorts<AppContext["ports"]>({
3166
- base: appPorts,
3301
+ base: initialPorts,
3167
3302
  overrides: {
3168
- gate: appPorts.gate,
3303
+ gate: initialPorts.gate,
3169
3304
  devtools: createInMemoryDevtools(),
3170
3305
  },
3171
3306
  });
@@ -3255,7 +3390,7 @@ function isAuthenticated(ctx: AuthorizationContext) {
3255
3390
 
3256
3391
  function canWrite(ctx: AuthorizationContext) {
3257
3392
  if (!isAuthenticated(ctx)) return deny("You must be signed in.");
3258
- ${options.tenant ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
3393
+ ${options.tenantScoped ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
3259
3394
  }
3260
3395
 
3261
3396
  export const ${names.singularCamel}Policy = definePolicy({
@@ -3265,13 +3400,13 @@ export const ${names.singularCamel}Policy = definePolicy({
3265
3400
  "${names.pluralCamel}.update": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
3266
3401
  const decision = canWrite(ctx);
3267
3402
  if (decision !== true) return decision;
3268
- ${options.tenant ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot update a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
3403
+ ${options.tenantScoped ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot update a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
3269
3404
  return true;
3270
3405
  },
3271
3406
  "${names.pluralCamel}.delete": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
3272
3407
  const decision = canWrite(ctx);
3273
3408
  if (decision !== true) return decision;
3274
- ${options.tenant ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot delete a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
3409
+ ${options.tenantScoped ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot delete a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
3275
3410
  return true;
3276
3411
  },
3277
3412
  "${names.pluralCamel}.manage": (_ctx: AuthorizationContext) =>
@@ -3318,7 +3453,7 @@ function create${names.singularPascal}(): ${names.singularPascal} {
3318
3453
  const now = new Date().toISOString();
3319
3454
  return {
3320
3455
  id: "00000000-0000-4000-8000-000000000001",
3321
- ${options.tenant ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
3456
+ ${options.tenantScoped ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
3322
3457
  version: 1,
3323
3458
  createdAt: now,
3324
3459
  updatedAt: now,
@@ -3333,7 +3468,7 @@ describe("${names.singularCamel}Policy", () => {
3333
3468
  const ${names.singularCamel} = create${names.singularPascal}();
3334
3469
  const ctx = {
3335
3470
  actor: { type: "user" as const, id: "user_test" },
3336
- ${options.tenant ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
3471
+ ${options.tenantScoped ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
3337
3472
 
3338
3473
  await expect(
3339
3474
  tester.assertMatrix([
@@ -3535,8 +3670,32 @@ ${names.timezone ? `\t\ttimezone: "${names.timezone}",\n` : ""}\t\tpayload: ${na
3535
3670
  );
3536
3671
  `;
3537
3672
  }
3673
+ function uploadManifestFile(names) {
3674
+ return `import type {
3675
+ \tUploadFileConstraints,
3676
+ \tUploadManifestEntry,
3677
+ } from "@beignet/core/uploads";
3678
+
3679
+ export const ${names.fileConstraintsExportName} = {
3680
+ \tcontentTypes: ["application/pdf", "text/plain"],
3681
+ \tmaxSizeBytes: 5 * 1024 * 1024,
3682
+ \tmaxFiles: 1,
3683
+ \tvisibility: "private",
3684
+ \tcacheControl: "private, max-age=0",
3685
+ } satisfies UploadFileConstraints;
3686
+
3687
+ export const ${names.manifestExportName} = [
3688
+ \t{
3689
+ \t\tname: "${names.uploadName}",
3690
+ \t\tdescription: "${names.artifact.pascal} upload",
3691
+ \t\tfile: ${names.fileConstraintsExportName},
3692
+ \t},
3693
+ ] satisfies readonly UploadManifestEntry[];
3694
+ `;
3695
+ }
3538
3696
  function uploadFile(names, config) {
3539
3697
  return `import { z } from "zod";
3698
+ import { ${names.fileConstraintsExportName} } from "${relativeModule(uploadFilePath(names, config), uploadManifestFilePath(names, config))}";
3540
3699
  import { defineUpload } from "${aliasModule(config.paths.uploadsBuilder)}";
3541
3700
 
3542
3701
  export const ${names.metadataSchemaName} = z.object({
@@ -3547,13 +3706,7 @@ export type ${names.metadataTypeName} = z.infer<typeof ${names.metadataSchemaNam
3547
3706
 
3548
3707
  export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
3549
3708
  \tmetadata: ${names.metadataSchemaName},
3550
- \tfile: {
3551
- \t\tcontentTypes: ["application/pdf", "text/plain"],
3552
- \t\tmaxSizeBytes: 5 * 1024 * 1024,
3553
- \t\tmaxFiles: 1,
3554
- \t\tvisibility: "private",
3555
- \t\tcacheControl: "private, max-age=0",
3556
- \t},
3709
+ \tfile: ${names.fileConstraintsExportName},
3557
3710
  \tauthorize({ ctx }) {
3558
3711
  \t\treturn ctx.actor.type === "user";
3559
3712
  \t},
@@ -3583,6 +3736,127 @@ export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
3583
3736
  });
3584
3737
  `;
3585
3738
  }
3739
+ function uploadUiClientFile(names, config) {
3740
+ const clientPath = uploadUiClientFilePath(names, config);
3741
+ const registryPath = path.join(featureArtifactDir(names, "uploads", config), "index.ts");
3742
+ return `import { createUploadClient } from "@beignet/core/uploads/client";
3743
+ import { createReactUploads } from "@beignet/react-uploads";
3744
+ import { ${names.manifestExportName} } from "${relativeModule(clientPath, uploadManifestFilePath(names, config))}";
3745
+ import type { ${names.featureSingularCamel}Uploads } from "${relativeModule(clientPath, registryPath)}";
3746
+
3747
+ const ${names.artifact.camel}UploadClient = createUploadClient<typeof ${names.featureSingularCamel}Uploads>({
3748
+ \tbaseUrl: "/api/uploads",
3749
+ \tmanifest: ${names.manifestExportName},
3750
+ });
3751
+
3752
+ export const ${names.reactUploadsExportName} = createReactUploads({
3753
+ \tuploads: ${names.artifact.camel}UploadClient,
3754
+ });
3755
+ `;
3756
+ }
3757
+ function uploadUiComponentFile(names, config) {
3758
+ const componentPath = uploadUiComponentFilePath(names, config);
3759
+ return `"use client";
3760
+
3761
+ import { useId } from "react";
3762
+ import { ${names.reactUploadsExportName} } from "${relativeModule(componentPath, uploadUiClientFilePath(names, config))}";
3763
+
3764
+ export type ${names.componentExportName}Props = {
3765
+ \tresourceId: string;
3766
+ \tdisabled?: boolean;
3767
+ \tonComplete?(objectKeys: readonly string[]): void | Promise<void>;
3768
+ };
3769
+
3770
+ export function ${names.componentExportName}({
3771
+ \tresourceId,
3772
+ \tdisabled = false,
3773
+ \tonComplete,
3774
+ }: ${names.componentExportName}Props) {
3775
+ \tconst inputId = useId();
3776
+ \tconst upload = ${names.reactUploadsExportName}.useUpload("${names.uploadName}", {
3777
+ \t\tonSuccess({ result }) {
3778
+ \t\t\treturn onComplete?.(result.objectKeys);
3779
+ \t\t},
3780
+ \t});
3781
+ \tconst maxSizeMb = upload.constraints?.maxSizeBytes
3782
+ \t\t? Math.round(upload.constraints.maxSizeBytes / 1024 / 1024)
3783
+ \t\t: undefined;
3784
+
3785
+ \treturn (
3786
+ \t\t<section className="${names.artifact.kebab}-uploader">
3787
+ \t\t\t<label htmlFor={inputId}>${names.artifact.pascal}</label>
3788
+ \t\t\t<input
3789
+ \t\t\t\tid={inputId}
3790
+ \t\t\t\ttype="file"
3791
+ \t\t\t\taccept={upload.accept}
3792
+ \t\t\t\tdisabled={disabled || upload.isUploading}
3793
+ \t\t\t\tonChange={(event) => {
3794
+ \t\t\t\t\tconst file = event.currentTarget.files?.[0];
3795
+ \t\t\t\t\tevent.currentTarget.value = "";
3796
+ \t\t\t\t\tif (!file) return;
3797
+
3798
+ \t\t\t\t\tupload.uploadFile(file, {
3799
+ \t\t\t\t\t\tmetadata: { resourceId },
3800
+ \t\t\t\t\t});
3801
+ \t\t\t\t}}
3802
+ \t\t\t/>
3803
+ \t\t\t<p>
3804
+ \t\t\t\tPDF or plain text{maxSizeMb ? <> up to {maxSizeMb} MB</> : null}.
3805
+ \t\t\t</p>
3806
+
3807
+ \t\t\t{upload.files[0] ? <p>{upload.files[0].fileName}</p> : null}
3808
+ \t\t\t{upload.isUploading ? (
3809
+ \t\t\t\t<div aria-live="polite">
3810
+ \t\t\t\t\t<progress
3811
+ \t\t\t\t\t\taria-label="Upload progress"
3812
+ \t\t\t\t\t\tmax={100}
3813
+ \t\t\t\t\t\tvalue={upload.progress}
3814
+ \t\t\t\t\t/>
3815
+ \t\t\t\t\t<span>{Math.round(upload.progress)}%</span>
3816
+ \t\t\t\t\t<button type="button" onClick={upload.abort}>
3817
+ \t\t\t\t\t\tCancel upload
3818
+ \t\t\t\t\t</button>
3819
+ \t\t\t\t</div>
3820
+ \t\t\t) : null}
3821
+ \t\t\t{upload.isError ? (
3822
+ \t\t\t\t<p role="alert">{uploadErrorMessage(upload.error)}</p>
3823
+ \t\t\t) : null}
3824
+ \t\t\t{upload.isSuccess ? <p role="status">Upload complete.</p> : null}
3825
+ \t\t\t{upload.isError || upload.isSuccess ? (
3826
+ \t\t\t\t<button type="button" onClick={upload.reset}>
3827
+ \t\t\t\t\tReset upload
3828
+ \t\t\t\t</button>
3829
+ \t\t\t) : null}
3830
+ \t\t</section>
3831
+ \t);
3832
+ }
3833
+
3834
+ function uploadErrorMessage(error: unknown): string {
3835
+ \treturn error instanceof Error ? error.message : "Upload failed.";
3836
+ }
3837
+ `;
3838
+ }
3839
+ function uploadUiComponentTestFile(names, config) {
3840
+ const testPath = uploadUiComponentTestFilePath(names, config);
3841
+ const testHelperPath = path.join(path.dirname(config.paths.useCaseBuilder), "beignet-test.ts");
3842
+ return `import { renderToStaticMarkup } from "react-dom/server";
3843
+ import { describe, expect, test } from "${relativeModule(testPath, testHelperPath)}";
3844
+ import { ${names.manifestExportName} } from "${relativeModule(testPath, uploadManifestFilePath(names, config))}";
3845
+ import { ${names.componentExportName} } from "${relativeModule(testPath, uploadUiComponentFilePath(names, config))}";
3846
+
3847
+ describe("${names.componentExportName}", () => {
3848
+ \ttest("renders the generated upload constraints", () => {
3849
+ \t\tconst markup = renderToStaticMarkup(
3850
+ \t\t\t<${names.componentExportName} resourceId="resource-1" />,
3851
+ \t\t);
3852
+
3853
+ \t\texpect(${names.manifestExportName}[0]?.file.maxSizeBytes).toBe(5 * 1024 * 1024);
3854
+ \t\texpect(markup).toContain('accept="application/pdf,text/plain"');
3855
+ \t\texpect(markup).toContain("PDF or plain text");
3856
+ \t});
3857
+ });
3858
+ `;
3859
+ }
3586
3860
  function uploadRouteFile(names, config) {
3587
3861
  const registryName = `${names.featureSingularCamel}Uploads`;
3588
3862
  return `import {
@@ -3671,63 +3945,70 @@ function featureUiComponentFile(names, config) {
3671
3945
  const clientQueriesPath = featureUiClientQueriesFilePath(names, config);
3672
3946
  return `"use client";
3673
3947
 
3674
- import { contractErrorMessage } from "@beignet/core/client";
3948
+ import { rootFormError } from "@beignet/react-hook-form";
3675
3949
  import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3676
- import { useState } from "react";
3950
+ import { rhf } from "${aliasModule(clientFormsPath(config))}";
3951
+ import { create${names.singularPascal} } from "${relativeModule(componentPath, resourceContractFilePath(names, config))}";
3677
3952
  import {
3678
3953
  \tcreate${names.singularPascal}MutationOptions,
3679
3954
  \tinvalidate${names.pluralPascal},
3680
3955
  \tlist${names.pluralPascal}QueryOptions,
3681
3956
  } from "${relativeModule(componentPath, clientQueriesPath)}";
3682
3957
 
3958
+ const create${names.singularPascal}Form = rhf(create${names.singularPascal});
3959
+
3683
3960
  export function ${names.componentExportName}() {
3684
- \tconst [name, setName] = useState("");
3685
3961
  \tconst queryClient = useQueryClient();
3686
3962
  \tconst ${names.pluralCamel}Query = useQuery(list${names.pluralPascal}QueryOptions());
3963
+ \tconst form = create${names.singularPascal}Form.useForm({
3964
+ \t\tdefaultValues: { name: "" },
3965
+ \t});
3966
+ \tconst name = form.watch("name");
3687
3967
  \tconst create${names.singularPascal}Mutation = useMutation({
3688
3968
  \t\t...create${names.singularPascal}MutationOptions(),
3689
3969
  \t\tonSuccess: async () => {
3690
- \t\t\tsetName("");
3970
+ \t\t\tform.reset({ name: "" });
3691
3971
  \t\t\tawait invalidate${names.pluralPascal}(queryClient);
3692
3972
  \t\t},
3973
+ \t\tonError: (error) => {
3974
+ \t\t\tform.setError(
3975
+ \t\t\t\t"root",
3976
+ \t\t\t\trootFormError(error, "Could not create ${names.singularKebab}."),
3977
+ \t\t\t);
3978
+ \t\t},
3693
3979
  \t});
3694
3980
 
3695
3981
  \treturn (
3696
3982
  \t\t<section className="${names.pluralKebab}-panel">
3697
3983
  \t\t\t<form
3698
3984
  \t\t\t\tclassName="${names.pluralKebab}-composer"
3699
- \t\t\t\tonSubmit={(event) => {
3700
- \t\t\t\t\tevent.preventDefault();
3701
- \t\t\t\t\tconst trimmedName = name.trim();
3702
- \t\t\t\t\tif (!trimmedName) return;
3985
+ \t\t\t\tonSubmit={form.handleSubmit((values) => {
3986
+ \t\t\t\t\tform.clearErrors("root");
3703
3987
  \t\t\t\t\tcreate${names.singularPascal}Mutation.reset();
3704
3988
  \t\t\t\t\tcreate${names.singularPascal}Mutation.mutate({
3705
- \t\t\t\t\t\tbody: { name: trimmedName },
3989
+ \t\t\t\t\t\tbody: values,
3706
3990
  \t\t\t\t\t});
3707
- \t\t\t\t}}
3991
+ \t\t\t\t})}
3708
3992
  \t\t\t>
3709
3993
  \t\t\t\t<label htmlFor="${names.singularKebab}-name">New ${names.singularKebab}</label>
3710
3994
  \t\t\t\t<div className="${names.pluralKebab}-composer-row">
3711
3995
  \t\t\t\t\t<input
3712
3996
  \t\t\t\t\t\tid="${names.singularKebab}-name"
3713
- \t\t\t\t\t\tvalue={name}
3714
- \t\t\t\t\t\tonChange={(event) => setName(event.currentTarget.value)}
3997
+ \t\t\t\t\t\t{...form.register("name")}
3715
3998
  \t\t\t\t\t\tplaceholder="Name"
3716
3999
  \t\t\t\t\t/>
3717
4000
  \t\t\t\t\t<button
3718
4001
  \t\t\t\t\t\ttype="submit"
3719
- \t\t\t\t\t\tdisabled={!name.trim() || create${names.singularPascal}Mutation.isPending}
4002
+ \t\t\t\t\t\tdisabled={!name?.trim() || create${names.singularPascal}Mutation.isPending}
3720
4003
  \t\t\t\t\t>
3721
4004
  \t\t\t\t\t\t{create${names.singularPascal}Mutation.isPending ? "Creating" : "Create"}
3722
4005
  \t\t\t\t\t</button>
3723
4006
  \t\t\t\t</div>
3724
- \t\t\t\t{create${names.singularPascal}Mutation.isError ? (
3725
- \t\t\t\t\t<p role="alert">
3726
- \t\t\t\t\t\t{contractErrorMessage(
3727
- \t\t\t\t\t\t\tcreate${names.singularPascal}Mutation.error,
3728
- \t\t\t\t\t\t\t"Could not create ${names.singularKebab}.",
3729
- \t\t\t\t\t\t)}
3730
- \t\t\t\t\t</p>
4007
+ \t\t\t\t{form.formState.errors.name ? (
4008
+ \t\t\t\t\t<p role="alert">{form.formState.errors.name.message}</p>
4009
+ \t\t\t\t) : null}
4010
+ \t\t\t\t{form.formState.errors.root ? (
4011
+ \t\t\t\t\t<p role="alert">{form.formState.errors.root.message}</p>
3731
4012
  \t\t\t\t) : null}
3732
4013
  \t\t\t</form>
3733
4014
 
@@ -3832,7 +4113,7 @@ ${mode === "resource" ? ` { contract: get${names.singularPascal}, useCase: get$
3832
4113
  `;
3833
4114
  }
3834
4115
  function testFile(names, config, mode, options) {
3835
- const repositoryPath = path.join(path.dirname(config.paths.infrastructurePorts), names.pluralKebab, `in-memory-${names.singularKebab}-repository.ts`);
4116
+ const repositoryPath = path.join(path.dirname(config.paths.portWiring), names.pluralKebab, `in-memory-${names.singularKebab}-repository.ts`);
3836
4117
  const serverContextPath = path.join(path.dirname(config.paths.server), "context.ts");
3837
4118
  const requestTarget = "app";
3838
4119
  return `import { describe, expect, it } from "@/lib/beignet-test";
@@ -3841,9 +4122,9 @@ ${mode === "resource" ? `import { isAppError } from "@beignet/core/errors";\n` :
3841
4122
  import { createTestApp } from "@beignet/web/testing";
3842
4123
  import { createInMemoryDevtools } from "@beignet/devtools";
3843
4124
  import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
3844
- import { ${options.auth ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenant ? ", createTestTenant" : ""} } from "@beignet/core/testing";
4125
+ import { ${options.authorization ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenantScoped ? ", createTestTenant" : ""} } from "@beignet/core/testing";
3845
4126
  import type { AppContext } from "${aliasModule(config.paths.appContext)}";
3846
- import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
4127
+ import { initialPorts } from "${aliasModule(config.paths.portWiring)}";
3847
4128
  import { appContext } from "${aliasModule(serverContextPath)}";
3848
4129
  import {
3849
4130
  create${names.singularPascal},
@@ -3861,39 +4142,39 @@ describe("${names.pluralCamel} resource", () => {
3861
4142
  const seed${names.pluralPascal} = [
3862
4143
  {
3863
4144
  id: "00000000-0000-4000-8000-000000000101",
3864
- ${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
4145
+ ${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
3865
4146
  version: 1,
3866
4147
  createdAt: "2024-01-01T00:00:00.000Z",
3867
4148
  updatedAt: "2024-01-01T00:00:00.000Z",
3868
4149
  },
3869
4150
  {
3870
4151
  id: "00000000-0000-4000-8000-000000000102",
3871
- ${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
4152
+ ${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
3872
4153
  version: 1,
3873
4154
  createdAt: "2024-01-02T00:00:00.000Z",
3874
4155
  updatedAt: "2024-01-02T00:00:00.000Z",
3875
4156
  },
3876
4157
  {
3877
4158
  id: "00000000-0000-4000-8000-000000000103",
3878
- ${options.tenant ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
4159
+ ${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
3879
4160
  version: 1,
3880
4161
  createdAt: "2024-01-03T00:00:00.000Z",
3881
4162
  updatedAt: "2024-01-03T00:00:00.000Z",
3882
4163
  },
3883
- ${options.tenant ? `\t\t\t{\n\t\t\t\tid: "00000000-0000-4000-8000-000000000104",\n\t\t\t\ttenantId: "tenant_other",\n\t\t\t\tname: "Other Tenant ${names.singularPascal}",\n\t\t\t\tversion: 1,\n\t\t\t\tcreatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t\tupdatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t},\n` : ""} ];
4164
+ ${options.tenantScoped ? `\t\t\t{\n\t\t\t\tid: "00000000-0000-4000-8000-000000000104",\n\t\t\t\ttenantId: "tenant_other",\n\t\t\t\tname: "Other Tenant ${names.singularPascal}",\n\t\t\t\tversion: 1,\n\t\t\t\tcreatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t\tupdatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t},\n` : ""} ];
3884
4165
  const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository(seed${names.pluralPascal});
3885
4166
  const testFixture = createTestPorts<AppContext["ports"]>({
3886
- base: appPorts,
4167
+ base: initialPorts,
3887
4168
  overrides: {
3888
4169
  ${names.pluralCamel},
3889
- ${options.auth || options.tenant
4170
+ ${options.authorization || options.tenantScoped
3890
4171
  ? `\t\t\t\tauth: {
3891
4172
  getSession: async () => ({
3892
- user: { id: "user_test", name: "Test User"${options.tenant ? `, tenantId: "tenant_test"` : ""} },
3893
- ${options.tenant ? `\t\t\t\t\t\tsession: { tenantId: "tenant_test" },\n` : ""}
4173
+ user: { id: "user_test", name: "Test User"${options.tenantScoped ? `, tenantId: "tenant_test"` : ""} },
4174
+ ${options.tenantScoped ? `\t\t\t\t\t\tsession: { tenantId: "tenant_test" },\n` : ""}
3894
4175
  }),
3895
4176
  },
3896
- ${options.auth ? `\t\t\t\tgate: appPorts.gate,\n` : ""}
4177
+ ${options.authorization ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
3897
4178
  `
3898
4179
  : `\t\t\t\tauth: {
3899
4180
  getSession: async () => null,
@@ -3908,8 +4189,8 @@ ${options.auth ? `\t\t\t\tgate: appPorts.gate,\n` : ""}
3908
4189
  });
3909
4190
  const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
3910
4191
  ports: testFixture.ports,
3911
- actor: ${options.auth ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
3912
- tenant: ${options.tenant ? `createTestTenant("tenant_test")` : "null"},
4192
+ actor: ${options.authorization ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
4193
+ tenant: ${options.tenantScoped ? `createTestTenant("tenant_test")` : "null"},
3913
4194
  });
3914
4195
  const tester = createUseCaseTester<AppContext>(createTestContext);
3915
4196