@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.
- package/CHANGELOG.md +26 -0
- package/README.md +138 -101
- package/dist/choices.d.ts +17 -21
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +21 -67
- package/dist/choices.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/create-prompts.d.ts +5 -6
- package/dist/create-prompts.d.ts.map +1 -1
- package/dist/create-prompts.js +15 -15
- package/dist/create-prompts.js.map +1 -1
- package/dist/create.d.ts +11 -2
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +38 -28
- package/dist/create.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +149 -47
- package/dist/index.js.map +1 -1
- package/dist/inspect.js +69 -16
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +1 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +27 -6
- package/dist/lint.js.map +1 -1
- package/dist/make/inbox.js +10 -10
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.d.ts +2 -1
- package/dist/make/payments.d.ts.map +1 -1
- package/dist/make/payments.js +18 -14
- package/dist/make/payments.js.map +1 -1
- package/dist/make/shared.d.ts +11 -4
- package/dist/make/shared.d.ts.map +1 -1
- package/dist/make/shared.js +56 -45
- package/dist/make/shared.js.map +1 -1
- package/dist/make/tenancy.js +8 -8
- package/dist/make/tenancy.js.map +1 -1
- package/dist/make.d.ts +3 -2
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +481 -200
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +15 -11
- package/dist/mcp.js.map +1 -1
- package/dist/operational-error-reporting.d.ts +15 -0
- package/dist/operational-error-reporting.d.ts.map +1 -0
- package/dist/operational-error-reporting.js +42 -0
- package/dist/operational-error-reporting.js.map +1 -0
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +73 -0
- package/dist/outbox.js.map +1 -1
- package/dist/provider-add.d.ts +21 -3
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +117 -81
- package/dist/provider-add.js.map +1 -1
- package/dist/provider-audit.js +3 -3
- package/dist/provider-audit.js.map +1 -1
- package/dist/schedule.d.ts.map +1 -1
- package/dist/schedule.js +28 -1
- package/dist/schedule.js.map +1 -1
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +38 -5
- package/dist/task.js.map +1 -1
- package/dist/templates/base.d.ts +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +15 -15
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/index.d.ts +2 -2
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +7 -7
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/server.d.ts +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +22 -18
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +4 -4
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +6 -6
- package/dist/templates/shared.js.map +1 -1
- package/dist/templates/testing.js +1 -1
- package/dist/templates/todos.js +6 -6
- package/package.json +4 -4
- package/skills/app-structure/SKILL.md +13 -8
- package/src/choices.ts +38 -88
- package/src/config.ts +2 -2
- package/src/create-prompts.ts +20 -21
- package/src/create.ts +54 -36
- package/src/index.ts +193 -65
- package/src/inspect.ts +116 -18
- package/src/lib.ts +1 -1
- package/src/lint.ts +35 -4
- package/src/make/inbox.ts +10 -10
- package/src/make/payments.ts +26 -18
- package/src/make/shared.ts +72 -50
- package/src/make/tenancy.ts +8 -8
- package/src/make.ts +607 -191
- package/src/mcp.ts +20 -13
- package/src/operational-error-reporting.ts +60 -0
- package/src/outbox.ts +77 -0
- package/src/provider-add.ts +179 -95
- package/src/provider-audit.ts +3 -3
- package/src/schedule.ts +32 -1
- package/src/task.ts +42 -5
- package/src/templates/base.ts +16 -16
- package/src/templates/index.ts +8 -8
- package/src/templates/server.ts +22 -18
- package/src/templates/shared.ts +10 -10
- package/src/templates/testing.ts +1 -1
- package/src/templates/todos.ts +6 -6
- package/src/test-helpers/generated-app.ts +1 -0
package/src/make.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import ts from "typescript";
|
|
4
5
|
import type { MakeFeatureAddon, MakeFeatureRecipe } from "./choices.js";
|
|
5
6
|
import {
|
|
6
7
|
type BeignetConfig,
|
|
8
|
+
clientFormsPath,
|
|
7
9
|
clientIndexPath,
|
|
8
10
|
loadBeignetConfig,
|
|
9
11
|
type ResolvedBeignetConfig,
|
|
@@ -146,6 +148,7 @@ import {
|
|
|
146
148
|
identifiersFromArrayExpression,
|
|
147
149
|
insertAfterImports,
|
|
148
150
|
} from "./registry-edits.js";
|
|
151
|
+
import { externalVersions } from "./templates/shared.js";
|
|
149
152
|
import { testSupportTemplateFiles } from "./templates/testing.js";
|
|
150
153
|
|
|
151
154
|
export type { MakeFeatureAddon, MakeFeatureRecipe } from "./choices.js";
|
|
@@ -164,8 +167,8 @@ type MakeResourceOptions = {
|
|
|
164
167
|
force?: boolean;
|
|
165
168
|
dryRun?: boolean;
|
|
166
169
|
config?: BeignetConfig;
|
|
167
|
-
|
|
168
|
-
|
|
170
|
+
authorization?: boolean;
|
|
171
|
+
tenantScoped?: boolean;
|
|
169
172
|
events?: boolean;
|
|
170
173
|
softDelete?: boolean;
|
|
171
174
|
};
|
|
@@ -313,6 +316,7 @@ type MakeUploadOptions = {
|
|
|
313
316
|
force?: boolean;
|
|
314
317
|
dryRun?: boolean;
|
|
315
318
|
config?: BeignetConfig;
|
|
319
|
+
ui?: boolean;
|
|
316
320
|
};
|
|
317
321
|
|
|
318
322
|
type MakeOutboxOptions = {
|
|
@@ -428,7 +432,7 @@ export async function makeFeature(
|
|
|
428
432
|
const addons = normalizeMakeFeatureAddons([
|
|
429
433
|
...makeFeatureRecipeAddons(options.recipe),
|
|
430
434
|
...(options.with ?? []),
|
|
431
|
-
...(options.events ? (["
|
|
435
|
+
...(options.events ? (["event"] as const) : []),
|
|
432
436
|
]);
|
|
433
437
|
const featureOptions = {
|
|
434
438
|
...options,
|
|
@@ -580,20 +584,25 @@ function resourceGenerationOptions(
|
|
|
580
584
|
): ResourceGenerationOptions {
|
|
581
585
|
if (mode === "feature") {
|
|
582
586
|
return {
|
|
583
|
-
|
|
584
|
-
|
|
587
|
+
authorization: false,
|
|
588
|
+
tenantScoped: false,
|
|
585
589
|
events: Boolean(options.events),
|
|
586
590
|
softDelete: false,
|
|
587
591
|
};
|
|
588
592
|
}
|
|
589
593
|
|
|
590
594
|
if (mode !== "resource") {
|
|
591
|
-
return {
|
|
595
|
+
return {
|
|
596
|
+
authorization: false,
|
|
597
|
+
tenantScoped: false,
|
|
598
|
+
events: false,
|
|
599
|
+
softDelete: false,
|
|
600
|
+
};
|
|
592
601
|
}
|
|
593
602
|
|
|
594
603
|
return {
|
|
595
|
-
|
|
596
|
-
|
|
604
|
+
authorization: Boolean(options.authorization),
|
|
605
|
+
tenantScoped: Boolean(options.tenantScoped),
|
|
597
606
|
events: Boolean(options.events),
|
|
598
607
|
softDelete: Boolean(options.softDelete),
|
|
599
608
|
};
|
|
@@ -604,11 +613,12 @@ async function makeFeatureSlice(
|
|
|
604
613
|
addons: readonly NormalizedMakeFeatureAddon[],
|
|
605
614
|
): Promise<MakeFeatureResult> {
|
|
606
615
|
let result = await makeFeatureResource(options);
|
|
616
|
+
const includeUploadUi = addons.includes("ui") && addons.includes("upload");
|
|
607
617
|
|
|
608
618
|
for (const addon of addons) {
|
|
609
619
|
result = mergeMakeResults(
|
|
610
620
|
result,
|
|
611
|
-
await makeFeatureAddon(addon, result.name, options),
|
|
621
|
+
await makeFeatureAddon(addon, result.name, options, includeUploadUi),
|
|
612
622
|
);
|
|
613
623
|
}
|
|
614
624
|
|
|
@@ -619,6 +629,7 @@ async function makeFeatureAddon(
|
|
|
619
629
|
addon: NormalizedMakeFeatureAddon,
|
|
620
630
|
feature: string,
|
|
621
631
|
options: MakeFeatureOptions,
|
|
632
|
+
includeUploadUi: boolean,
|
|
622
633
|
): Promise<MakeResult> {
|
|
623
634
|
const shared = {
|
|
624
635
|
cwd: options.cwd,
|
|
@@ -633,37 +644,37 @@ async function makeFeatureAddon(
|
|
|
633
644
|
if (addon === "factory") {
|
|
634
645
|
return makeFactory({
|
|
635
646
|
...shared,
|
|
636
|
-
name: `${feature}
|
|
647
|
+
name: `${feature}.${singularize(feature)}`,
|
|
637
648
|
skipPersistenceAssert: true,
|
|
638
649
|
});
|
|
639
650
|
}
|
|
640
651
|
if (addon === "task") {
|
|
641
|
-
return makeTask({ ...shared, name: `${feature}
|
|
652
|
+
return makeTask({ ...shared, name: `${feature}.backfill` });
|
|
642
653
|
}
|
|
643
654
|
if (addon === "event") {
|
|
644
|
-
return makeEvent({ ...shared, name: `${feature}
|
|
655
|
+
return makeEvent({ ...shared, name: `${feature}.created` });
|
|
645
656
|
}
|
|
646
657
|
if (addon === "job") {
|
|
647
|
-
return makeJob({ ...shared, name: `${feature}
|
|
658
|
+
return makeJob({ ...shared, name: `${feature}.process` });
|
|
648
659
|
}
|
|
649
660
|
if (addon === "listener") {
|
|
650
661
|
return makeListener({
|
|
651
662
|
...shared,
|
|
652
|
-
name: `${feature}
|
|
653
|
-
event: `${feature}
|
|
663
|
+
name: `${feature}.handle-created`,
|
|
664
|
+
event: `${feature}.created`,
|
|
654
665
|
skipEventAssert: true,
|
|
655
666
|
});
|
|
656
667
|
}
|
|
657
668
|
if (addon === "notification") {
|
|
658
|
-
return makeNotification({ ...shared, name: `${feature}
|
|
669
|
+
return makeNotification({ ...shared, name: `${feature}.created` });
|
|
659
670
|
}
|
|
660
671
|
if (addon === "schedule") {
|
|
661
|
-
return makeSchedule({ ...shared, name: `${feature}
|
|
672
|
+
return makeSchedule({ ...shared, name: `${feature}.daily-summary` });
|
|
662
673
|
}
|
|
663
674
|
if (addon === "seed") {
|
|
664
675
|
return makeSeed({
|
|
665
676
|
...shared,
|
|
666
|
-
name: `${feature}
|
|
677
|
+
name: `${feature}.demo-${feature}`,
|
|
667
678
|
skipPersistenceAssert: true,
|
|
668
679
|
});
|
|
669
680
|
}
|
|
@@ -671,7 +682,11 @@ async function makeFeatureAddon(
|
|
|
671
682
|
return makeFeatureUi({ ...shared, name: feature });
|
|
672
683
|
}
|
|
673
684
|
|
|
674
|
-
return makeUpload({
|
|
685
|
+
return makeUpload({
|
|
686
|
+
...shared,
|
|
687
|
+
name: `${feature}.attachment`,
|
|
688
|
+
ui: includeUploadUi,
|
|
689
|
+
});
|
|
675
690
|
}
|
|
676
691
|
|
|
677
692
|
function normalizeMakeFeatureAddons(
|
|
@@ -680,18 +695,7 @@ function normalizeMakeFeatureAddons(
|
|
|
680
695
|
const normalized = new Set<NormalizedMakeFeatureAddon>();
|
|
681
696
|
|
|
682
697
|
for (const addon of addons) {
|
|
683
|
-
if (addon === "
|
|
684
|
-
else if (addon === "events") normalized.add("event");
|
|
685
|
-
else if (addon === "listeners") {
|
|
686
|
-
normalized.add("event");
|
|
687
|
-
normalized.add("listener");
|
|
688
|
-
} else if (addon === "tasks") normalized.add("task");
|
|
689
|
-
else if (addon === "jobs") normalized.add("job");
|
|
690
|
-
else if (addon === "notifications") normalized.add("notification");
|
|
691
|
-
else if (addon === "schedules") normalized.add("schedule");
|
|
692
|
-
else if (addon === "seeds") normalized.add("seed");
|
|
693
|
-
else if (addon === "uploads") normalized.add("upload");
|
|
694
|
-
else if (addon === "listener") {
|
|
698
|
+
if (addon === "listener") {
|
|
695
699
|
normalized.add("event");
|
|
696
700
|
normalized.add("listener");
|
|
697
701
|
} else normalized.add(addon);
|
|
@@ -706,16 +710,16 @@ function makeFeatureRecipeAddons(
|
|
|
706
710
|
if (recipe === "full-slice") {
|
|
707
711
|
return [
|
|
708
712
|
"policy",
|
|
709
|
-
"
|
|
710
|
-
"
|
|
711
|
-
"
|
|
712
|
-
"
|
|
713
|
-
"
|
|
714
|
-
"
|
|
715
|
-
"
|
|
716
|
-
"
|
|
713
|
+
"factory",
|
|
714
|
+
"seed",
|
|
715
|
+
"task",
|
|
716
|
+
"event",
|
|
717
|
+
"listener",
|
|
718
|
+
"job",
|
|
719
|
+
"notification",
|
|
720
|
+
"schedule",
|
|
717
721
|
"ui",
|
|
718
|
-
"
|
|
722
|
+
"upload",
|
|
719
723
|
];
|
|
720
724
|
}
|
|
721
725
|
|
|
@@ -807,7 +811,7 @@ export async function makeUseCase(
|
|
|
807
811
|
|
|
808
812
|
return {
|
|
809
813
|
schemaVersion: 1,
|
|
810
|
-
name: `${names.feature.kebab}
|
|
814
|
+
name: `${names.feature.kebab}.${names.action.kebab}`,
|
|
811
815
|
targetDir,
|
|
812
816
|
dryRun: Boolean(options.dryRun),
|
|
813
817
|
files: [...generatedFiles.map((file) => file.path), indexFile.path],
|
|
@@ -853,7 +857,7 @@ export async function makeTest(
|
|
|
853
857
|
|
|
854
858
|
return {
|
|
855
859
|
schemaVersion: 1,
|
|
856
|
-
name: `${names.feature.kebab}
|
|
860
|
+
name: `${names.feature.kebab}.${names.action.kebab}`,
|
|
857
861
|
targetDir,
|
|
858
862
|
dryRun: Boolean(options.dryRun),
|
|
859
863
|
files: generatedFiles.map((file) => file.path),
|
|
@@ -911,7 +915,7 @@ export async function makePort(
|
|
|
911
915
|
dryRun: Boolean(options.dryRun),
|
|
912
916
|
})
|
|
913
917
|
) {
|
|
914
|
-
updatedFiles.push(config.paths.
|
|
918
|
+
updatedFiles.push(config.paths.portWiring);
|
|
915
919
|
}
|
|
916
920
|
|
|
917
921
|
return {
|
|
@@ -957,7 +961,7 @@ export async function makeAdapter(
|
|
|
957
961
|
dryRun: Boolean(options.dryRun),
|
|
958
962
|
})
|
|
959
963
|
) {
|
|
960
|
-
updatedFiles.push(config.paths.
|
|
964
|
+
updatedFiles.push(config.paths.portWiring);
|
|
961
965
|
}
|
|
962
966
|
|
|
963
967
|
return {
|
|
@@ -1034,13 +1038,16 @@ export async function makeEvent(
|
|
|
1034
1038
|
const outboxDrainPlan = await planOutboxDrainWiring(targetDir, config, {
|
|
1035
1039
|
force: Boolean(options.force),
|
|
1036
1040
|
});
|
|
1041
|
+
await updateOutboxRegistry(targetDir, names, "events", config, {
|
|
1042
|
+
dryRun: true,
|
|
1043
|
+
});
|
|
1037
1044
|
|
|
1038
1045
|
const result = await makeFeatureArtifact({
|
|
1039
1046
|
targetDir,
|
|
1040
1047
|
config,
|
|
1041
1048
|
force: Boolean(options.force),
|
|
1042
1049
|
dryRun: Boolean(options.dryRun),
|
|
1043
|
-
name:
|
|
1050
|
+
name: names.stableName,
|
|
1044
1051
|
files: eventFiles(names, config),
|
|
1045
1052
|
index: featureArtifactIndexFile("event", names, config),
|
|
1046
1053
|
dependencies: {
|
|
@@ -1078,13 +1085,16 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
|
|
|
1078
1085
|
const outboxDrainPlan = await planOutboxDrainWiring(targetDir, config, {
|
|
1079
1086
|
force: Boolean(options.force),
|
|
1080
1087
|
});
|
|
1088
|
+
await updateOutboxRegistry(targetDir, names, "jobs", config, {
|
|
1089
|
+
dryRun: true,
|
|
1090
|
+
});
|
|
1081
1091
|
|
|
1082
1092
|
const result = await makeFeatureArtifact({
|
|
1083
1093
|
targetDir,
|
|
1084
1094
|
config,
|
|
1085
1095
|
force: Boolean(options.force),
|
|
1086
1096
|
dryRun: Boolean(options.dryRun),
|
|
1087
|
-
name:
|
|
1097
|
+
name: names.stableName,
|
|
1088
1098
|
files: [
|
|
1089
1099
|
...(await missingCapabilityBuilderFiles(targetDir, "jobs", config)),
|
|
1090
1100
|
...jobFiles(names, config),
|
|
@@ -1124,7 +1134,7 @@ export async function makeTask(
|
|
|
1124
1134
|
config,
|
|
1125
1135
|
force: Boolean(options.force),
|
|
1126
1136
|
dryRun: Boolean(options.dryRun),
|
|
1127
|
-
name:
|
|
1137
|
+
name: names.stableName,
|
|
1128
1138
|
files: [
|
|
1129
1139
|
...(await missingCapabilityBuilderFiles(targetDir, "tasks", config)),
|
|
1130
1140
|
...taskFiles(names, config),
|
|
@@ -1169,7 +1179,7 @@ export async function makeFactory(
|
|
|
1169
1179
|
config,
|
|
1170
1180
|
force: Boolean(options.force),
|
|
1171
1181
|
dryRun: Boolean(options.dryRun),
|
|
1172
|
-
name:
|
|
1182
|
+
name: names.stableName,
|
|
1173
1183
|
files: factoryFiles(names, config),
|
|
1174
1184
|
index: featureArtifactIndexFile("factory", names, config),
|
|
1175
1185
|
dependencies: {
|
|
@@ -1196,7 +1206,7 @@ export async function makeSeed(
|
|
|
1196
1206
|
config,
|
|
1197
1207
|
force: Boolean(options.force),
|
|
1198
1208
|
dryRun: Boolean(options.dryRun),
|
|
1199
|
-
name:
|
|
1209
|
+
name: names.stableName,
|
|
1200
1210
|
files: seedFiles(names, config),
|
|
1201
1211
|
index: featureArtifactIndexFile("seed", names, config),
|
|
1202
1212
|
dependencies: {
|
|
@@ -1215,7 +1225,7 @@ export async function makeSeed(
|
|
|
1215
1225
|
config,
|
|
1216
1226
|
force: Boolean(options.force),
|
|
1217
1227
|
dryRun: Boolean(options.dryRun),
|
|
1218
|
-
name:
|
|
1228
|
+
name: factoryNamesForSeed.stableName,
|
|
1219
1229
|
files: factoryFiles(factoryNamesForSeed, config),
|
|
1220
1230
|
index: featureArtifactIndexFile("factory", factoryNamesForSeed, config),
|
|
1221
1231
|
dependencies: {
|
|
@@ -1271,7 +1281,7 @@ export async function makeNotification(
|
|
|
1271
1281
|
// notifications uses ctx.ports.notifications, so the generator wires both
|
|
1272
1282
|
// ports with dev-default providers. Each port is wired independently and
|
|
1273
1283
|
// skipped when the app already has it, for example when the resend
|
|
1274
|
-
//
|
|
1284
|
+
// provider contributed the mailer. Plan the wiring before writing any
|
|
1275
1285
|
// files so an anchor miss aborts the generator without leaving partial
|
|
1276
1286
|
// output.
|
|
1277
1287
|
const notificationWirings = notificationPortWirings("make notification");
|
|
@@ -1284,7 +1294,7 @@ export async function makeNotification(
|
|
|
1284
1294
|
config,
|
|
1285
1295
|
force: Boolean(options.force),
|
|
1286
1296
|
dryRun: Boolean(options.dryRun),
|
|
1287
|
-
name:
|
|
1297
|
+
name: names.stableName,
|
|
1288
1298
|
files: [
|
|
1289
1299
|
...(await missingCapabilityBuilderFiles(
|
|
1290
1300
|
targetDir,
|
|
@@ -1335,7 +1345,7 @@ export async function makeListener(
|
|
|
1335
1345
|
config,
|
|
1336
1346
|
force: Boolean(options.force),
|
|
1337
1347
|
dryRun: Boolean(options.dryRun),
|
|
1338
|
-
name:
|
|
1348
|
+
name: names.stableName,
|
|
1339
1349
|
files: [
|
|
1340
1350
|
...(await missingCapabilityBuilderFiles(targetDir, "listeners", config)),
|
|
1341
1351
|
...listenerFiles(names, config),
|
|
@@ -1439,7 +1449,7 @@ async function assertListenerEventExists(
|
|
|
1439
1449
|
await stat(path.join(targetDir, file));
|
|
1440
1450
|
} catch {
|
|
1441
1451
|
throw new Error(
|
|
1442
|
-
`beignet make listener expected event ${names.eventName} at ${file}. Run beignet make event ${names.
|
|
1452
|
+
`beignet make listener expected event ${names.eventName} at ${file}. Run beignet make event ${names.eventName} first, or pass an event that exists.`,
|
|
1443
1453
|
);
|
|
1444
1454
|
}
|
|
1445
1455
|
}
|
|
@@ -1509,7 +1519,7 @@ export async function makeSchedule(
|
|
|
1509
1519
|
config,
|
|
1510
1520
|
force: Boolean(options.force),
|
|
1511
1521
|
dryRun: Boolean(options.dryRun),
|
|
1512
|
-
name:
|
|
1522
|
+
name: names.stableName,
|
|
1513
1523
|
files,
|
|
1514
1524
|
index: featureArtifactIndexFile("schedule", names, config),
|
|
1515
1525
|
dependencies: {
|
|
@@ -1697,7 +1707,7 @@ async function updateOutboxPortWiring(
|
|
|
1697
1707
|
changes.updatedFiles.push(config.paths.ports);
|
|
1698
1708
|
}
|
|
1699
1709
|
if (await updateOutboxInfrastructurePorts(targetDir, config, options)) {
|
|
1700
|
-
changes.updatedFiles.push(config.paths.
|
|
1710
|
+
changes.updatedFiles.push(config.paths.portWiring);
|
|
1701
1711
|
}
|
|
1702
1712
|
if (await updateOutboxDatabaseProvider(targetDir, config, options)) {
|
|
1703
1713
|
changes.updatedFiles.push(
|
|
@@ -1805,7 +1815,7 @@ async function updateOutboxInfrastructurePorts(
|
|
|
1805
1815
|
config: ResolvedBeignetConfig,
|
|
1806
1816
|
options: { dryRun: boolean },
|
|
1807
1817
|
): Promise<boolean> {
|
|
1808
|
-
const filePath = path.join(targetDir, config.paths.
|
|
1818
|
+
const filePath = path.join(targetDir, config.paths.portWiring);
|
|
1809
1819
|
const original = await readFile(filePath, "utf8");
|
|
1810
1820
|
let next = appendDeferredPortKey(original, "outbox");
|
|
1811
1821
|
next = appendDeferredPortKey(next, "outboxAdmin");
|
|
@@ -2059,6 +2069,9 @@ export async function makeUpload(
|
|
|
2059
2069
|
: await loadBeignetConfig(targetDir);
|
|
2060
2070
|
|
|
2061
2071
|
await assertFeatureArtifactApp(targetDir, config, "upload");
|
|
2072
|
+
if (options.ui) {
|
|
2073
|
+
await assertUploadUiApp(targetDir, config);
|
|
2074
|
+
}
|
|
2062
2075
|
|
|
2063
2076
|
await wirePortProviders(
|
|
2064
2077
|
targetDir,
|
|
@@ -2074,10 +2087,10 @@ export async function makeUpload(
|
|
|
2074
2087
|
config,
|
|
2075
2088
|
force: Boolean(options.force),
|
|
2076
2089
|
dryRun: Boolean(options.dryRun),
|
|
2077
|
-
name:
|
|
2090
|
+
name: names.stableName,
|
|
2078
2091
|
files: [
|
|
2079
2092
|
...(await missingCapabilityBuilderFiles(targetDir, "uploads", config)),
|
|
2080
|
-
...uploadFiles(names, config),
|
|
2093
|
+
...uploadFiles(names, config, { ui: Boolean(options.ui) }),
|
|
2081
2094
|
],
|
|
2082
2095
|
index: featureArtifactIndexFile("upload", names, config),
|
|
2083
2096
|
dependencies: {
|
|
@@ -2106,6 +2119,44 @@ export async function makeUpload(
|
|
|
2106
2119
|
result.files.push(uploadRoutePath(config));
|
|
2107
2120
|
}
|
|
2108
2121
|
|
|
2122
|
+
if (options.ui) {
|
|
2123
|
+
const componentIndex = uploadUiComponentIndexFile(names, config);
|
|
2124
|
+
const componentIndexResult = await updateFeatureUiIndex(
|
|
2125
|
+
targetDir,
|
|
2126
|
+
componentIndex,
|
|
2127
|
+
{ dryRun: Boolean(options.dryRun) },
|
|
2128
|
+
);
|
|
2129
|
+
if (componentIndexResult === "created") {
|
|
2130
|
+
result.createdFiles.push(componentIndex.path);
|
|
2131
|
+
}
|
|
2132
|
+
if (componentIndexResult === "updated") {
|
|
2133
|
+
result.updatedFiles.push(componentIndex.path);
|
|
2134
|
+
}
|
|
2135
|
+
if (componentIndexResult === "skipped") {
|
|
2136
|
+
result.skippedFiles.push(componentIndex.path);
|
|
2137
|
+
}
|
|
2138
|
+
if (!result.files.includes(componentIndex.path)) {
|
|
2139
|
+
result.files.push(componentIndex.path);
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
if (
|
|
2143
|
+
await updatePackageDependencies(
|
|
2144
|
+
targetDir,
|
|
2145
|
+
{ "@beignet/react-uploads": beignetDependencyVersion },
|
|
2146
|
+
{ dryRun: Boolean(options.dryRun) },
|
|
2147
|
+
)
|
|
2148
|
+
) {
|
|
2149
|
+
result.updatedFiles.push("package.json");
|
|
2150
|
+
if (!result.files.includes("package.json")) {
|
|
2151
|
+
result.files.push("package.json");
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
result.createdFiles = uniqueStrings(result.createdFiles);
|
|
2157
|
+
result.updatedFiles = uniqueStrings(result.updatedFiles);
|
|
2158
|
+
result.skippedFiles = uniqueStrings(result.skippedFiles);
|
|
2159
|
+
|
|
2109
2160
|
return result;
|
|
2110
2161
|
}
|
|
2111
2162
|
|
|
@@ -2166,7 +2217,7 @@ export async function makeOutbox(
|
|
|
2166
2217
|
...cronSecretPlan.files.map((file) => file.path),
|
|
2167
2218
|
...cronSecretPlan.skippedFiles,
|
|
2168
2219
|
config.paths.ports,
|
|
2169
|
-
config.paths.
|
|
2220
|
+
config.paths.portWiring,
|
|
2170
2221
|
path.join(infrastructureDir(config), "db/provider.ts"),
|
|
2171
2222
|
drizzleRepositoriesPath(config),
|
|
2172
2223
|
outboxDrizzleSchemaFilePath(config),
|
|
@@ -2189,7 +2240,10 @@ async function makeFeatureUi(
|
|
|
2189
2240
|
|
|
2190
2241
|
await assertFeatureUiApp(targetDir, config);
|
|
2191
2242
|
|
|
2192
|
-
const componentFiles =
|
|
2243
|
+
const componentFiles = [
|
|
2244
|
+
...(await missingFeatureUiFormsFiles(targetDir, config)),
|
|
2245
|
+
...featureUiComponentFiles(names, config),
|
|
2246
|
+
];
|
|
2193
2247
|
const indexFile = featureUiIndexFile(names, config);
|
|
2194
2248
|
const plannedFiles = await planGeneratedFiles(targetDir, componentFiles, {
|
|
2195
2249
|
force: Boolean(options.force),
|
|
@@ -2221,8 +2275,15 @@ async function makeFeatureUi(
|
|
|
2221
2275
|
await updatePackageDependencies(
|
|
2222
2276
|
targetDir,
|
|
2223
2277
|
{
|
|
2278
|
+
"@beignet/react-hook-form": beignetDependencyVersion,
|
|
2224
2279
|
"@beignet/react-query": beignetDependencyVersion,
|
|
2280
|
+
"@hookform/resolvers": (packageJson) =>
|
|
2281
|
+
packageJson.dependencies?.["@hookform/resolvers"] ??
|
|
2282
|
+
externalVersions.hookformResolvers,
|
|
2225
2283
|
"@tanstack/react-query": tanstackReactQueryDependencyVersion,
|
|
2284
|
+
"react-hook-form": (packageJson) =>
|
|
2285
|
+
packageJson.dependencies?.["react-hook-form"] ??
|
|
2286
|
+
externalVersions.reactHookForm,
|
|
2226
2287
|
},
|
|
2227
2288
|
{ dryRun: Boolean(options.dryRun) },
|
|
2228
2289
|
)
|
|
@@ -2361,19 +2422,19 @@ async function updateResourceWiring(
|
|
|
2361
2422
|
persistence === "memory" &&
|
|
2362
2423
|
(await updateInfrastructurePorts(targetDir, names, config, options))
|
|
2363
2424
|
) {
|
|
2364
|
-
updated.add(config.paths.
|
|
2425
|
+
updated.add(config.paths.portWiring);
|
|
2365
2426
|
}
|
|
2366
2427
|
if (
|
|
2367
|
-
generationOptions.
|
|
2428
|
+
generationOptions.authorization &&
|
|
2368
2429
|
(await updateInfrastructureGatePolicies(targetDir, names, config, options))
|
|
2369
2430
|
) {
|
|
2370
|
-
updated.add(config.paths.
|
|
2431
|
+
updated.add(config.paths.portWiring);
|
|
2371
2432
|
}
|
|
2372
2433
|
if (
|
|
2373
2434
|
persistence === "drizzle" &&
|
|
2374
2435
|
(await updateInfrastructureDeferredPorts(targetDir, names, config, options))
|
|
2375
2436
|
) {
|
|
2376
|
-
updated.add(config.paths.
|
|
2437
|
+
updated.add(config.paths.portWiring);
|
|
2377
2438
|
}
|
|
2378
2439
|
if (
|
|
2379
2440
|
persistence === "drizzle" &&
|
|
@@ -2426,13 +2487,12 @@ function typeHasTopLevelProperty(
|
|
|
2426
2487
|
function useCaseNames(input: string): UseCaseNames {
|
|
2427
2488
|
const parts = input
|
|
2428
2489
|
.trim()
|
|
2429
|
-
.split("
|
|
2430
|
-
.map((part) => part.trim())
|
|
2431
|
-
.filter(Boolean);
|
|
2490
|
+
.split(".")
|
|
2491
|
+
.map((part) => part.trim());
|
|
2432
2492
|
|
|
2433
|
-
if (parts.length !== 2) {
|
|
2493
|
+
if (parts.length !== 2 || parts.some((part) => part.length === 0)) {
|
|
2434
2494
|
throw new Error(
|
|
2435
|
-
"Use case name must use feature
|
|
2495
|
+
"Use case name must use feature.action format, for example projects.archive-project.",
|
|
2436
2496
|
);
|
|
2437
2497
|
}
|
|
2438
2498
|
|
|
@@ -2472,7 +2532,7 @@ function policyNames(input: string): PolicyNames {
|
|
|
2472
2532
|
}
|
|
2473
2533
|
|
|
2474
2534
|
function eventNames(input: string): EventNames {
|
|
2475
|
-
const names = featureArtifactNames(
|
|
2535
|
+
const names = featureArtifactNames(input, "Event");
|
|
2476
2536
|
const eventExportName = `${names.featureSingularPascal}${names.artifact.pascal}`;
|
|
2477
2537
|
|
|
2478
2538
|
assertGeneratedIdentifiers(input, "Event name", [eventExportName]);
|
|
@@ -2530,7 +2590,7 @@ function factoryNames(input: string): FactoryNames {
|
|
|
2530
2590
|
}
|
|
2531
2591
|
|
|
2532
2592
|
function defaultFactoryNamesForSeed(names: SeedNames): FactoryNames {
|
|
2533
|
-
return factoryNames(`${names.feature.kebab}
|
|
2593
|
+
return factoryNames(`${names.feature.kebab}.${names.featureSingularKebab}`);
|
|
2534
2594
|
}
|
|
2535
2595
|
|
|
2536
2596
|
function notificationNames(input: string): NotificationNames {
|
|
@@ -2587,8 +2647,12 @@ function scheduleNames(
|
|
|
2587
2647
|
function uploadNames(input: string): UploadNames {
|
|
2588
2648
|
const names = featureArtifactNames(input, "Upload");
|
|
2589
2649
|
const uploadExportName = `${names.artifact.pascal}Upload`;
|
|
2650
|
+
const componentExportName = `${names.artifact.pascal}Uploader`;
|
|
2590
2651
|
|
|
2591
|
-
assertGeneratedIdentifiers(input, "Upload name", [
|
|
2652
|
+
assertGeneratedIdentifiers(input, "Upload name", [
|
|
2653
|
+
uploadExportName,
|
|
2654
|
+
componentExportName,
|
|
2655
|
+
]);
|
|
2592
2656
|
|
|
2593
2657
|
return {
|
|
2594
2658
|
...names,
|
|
@@ -2596,19 +2660,16 @@ function uploadNames(input: string): UploadNames {
|
|
|
2596
2660
|
uploadExportName,
|
|
2597
2661
|
metadataSchemaName: `${uploadExportName}MetadataSchema`,
|
|
2598
2662
|
metadataTypeName: `${uploadExportName}Metadata`,
|
|
2663
|
+
fileConstraintsExportName: `${names.artifact.camel}UploadFile`,
|
|
2664
|
+
manifestExportName: `${names.artifact.camel}UploadManifest`,
|
|
2665
|
+
manifestFileName: `${names.artifact.kebab}-upload-manifest`,
|
|
2666
|
+
reactUploadsExportName: `${names.artifact.camel}ReactUploads`,
|
|
2667
|
+
clientFileName: `${names.artifact.kebab}-upload`,
|
|
2668
|
+
componentExportName,
|
|
2669
|
+
componentFileName: `${names.artifact.kebab}-uploader`,
|
|
2599
2670
|
};
|
|
2600
2671
|
}
|
|
2601
2672
|
|
|
2602
|
-
function normalizeEventReference(input: string): string {
|
|
2603
|
-
const trimmed = input.trim();
|
|
2604
|
-
if (trimmed.includes("/")) return trimmed;
|
|
2605
|
-
|
|
2606
|
-
const dotIndex = trimmed.indexOf(".");
|
|
2607
|
-
if (dotIndex === -1) return trimmed;
|
|
2608
|
-
|
|
2609
|
-
return `${trimmed.slice(0, dotIndex)}/${trimmed.slice(dotIndex + 1)}`;
|
|
2610
|
-
}
|
|
2611
|
-
|
|
2612
2673
|
function inferUseCaseKind(action: string): "command" | "query" {
|
|
2613
2674
|
if (/^(count|find|get|list|search)-/.test(`${action}-`)) {
|
|
2614
2675
|
return "query";
|
|
@@ -2622,8 +2683,8 @@ function resourceFiles(
|
|
|
2622
2683
|
persistence: ResourcePersistence = "memory",
|
|
2623
2684
|
mode: ResourceGenerationMode = "feature",
|
|
2624
2685
|
options: ResourceGenerationOptions = {
|
|
2625
|
-
|
|
2626
|
-
|
|
2686
|
+
authorization: false,
|
|
2687
|
+
tenantScoped: false,
|
|
2627
2688
|
events: false,
|
|
2628
2689
|
softDelete: false,
|
|
2629
2690
|
},
|
|
@@ -2678,7 +2739,7 @@ function resourceFiles(
|
|
|
2678
2739
|
},
|
|
2679
2740
|
]
|
|
2680
2741
|
: []),
|
|
2681
|
-
...(mode === "resource" && options.
|
|
2742
|
+
...(mode === "resource" && options.authorization
|
|
2682
2743
|
? [
|
|
2683
2744
|
{
|
|
2684
2745
|
path: path.join(featureDir, "tests/policy.test.ts"),
|
|
@@ -2748,6 +2809,99 @@ function featureUiComponentFiles(
|
|
|
2748
2809
|
];
|
|
2749
2810
|
}
|
|
2750
2811
|
|
|
2812
|
+
async function missingFeatureUiFormsFiles(
|
|
2813
|
+
targetDir: string,
|
|
2814
|
+
config: ResolvedBeignetConfig,
|
|
2815
|
+
): Promise<GeneratedFile[]> {
|
|
2816
|
+
const formsPath = clientFormsPath(config);
|
|
2817
|
+
const existing = await readOptionalFile(path.join(targetDir, formsPath));
|
|
2818
|
+
|
|
2819
|
+
if (existing === undefined) {
|
|
2820
|
+
return [
|
|
2821
|
+
{
|
|
2822
|
+
path: formsPath,
|
|
2823
|
+
content: `import { createReactHookForm } from "@beignet/react-hook-form";
|
|
2824
|
+
|
|
2825
|
+
export const rhf = createReactHookForm();
|
|
2826
|
+
`,
|
|
2827
|
+
},
|
|
2828
|
+
];
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
if (!sourceExportsValue(existing, "rhf")) {
|
|
2832
|
+
throw new Error(
|
|
2833
|
+
`beignet make feature --with ui expects ${formsPath} to export rhf. Add \`export const rhf = createReactHookForm();\` from @beignet/react-hook-form, or move the existing helper before generating UI.`,
|
|
2834
|
+
);
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
return [];
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
async function assertUploadUiApp(
|
|
2841
|
+
targetDir: string,
|
|
2842
|
+
config: ResolvedBeignetConfig,
|
|
2843
|
+
): Promise<void> {
|
|
2844
|
+
const clientPath = clientIndexPath(config);
|
|
2845
|
+
const clientSource = await readOptionalFile(path.join(targetDir, clientPath));
|
|
2846
|
+
|
|
2847
|
+
if (!clientSource || !sourceExportsValue(clientSource, "rq")) {
|
|
2848
|
+
throw new Error(
|
|
2849
|
+
`beignet make upload --ui expects a full-stack Beignet app with ${clientPath} exporting rq. API-only apps stay backend-only; omit --ui or add the full-stack client setup first.`,
|
|
2850
|
+
);
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
function sourceExportsValue(source: string, name: string): boolean {
|
|
2855
|
+
const sourceFile = ts.createSourceFile(
|
|
2856
|
+
"client-forms.ts",
|
|
2857
|
+
source,
|
|
2858
|
+
ts.ScriptTarget.Latest,
|
|
2859
|
+
false,
|
|
2860
|
+
ts.ScriptKind.TS,
|
|
2861
|
+
);
|
|
2862
|
+
|
|
2863
|
+
for (const statement of sourceFile.statements) {
|
|
2864
|
+
const exported =
|
|
2865
|
+
ts.canHaveModifiers(statement) &&
|
|
2866
|
+
ts
|
|
2867
|
+
.getModifiers(statement)
|
|
2868
|
+
?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
2869
|
+
|
|
2870
|
+
if (exported && ts.isVariableStatement(statement)) {
|
|
2871
|
+
if (
|
|
2872
|
+
statement.declarationList.declarations.some(
|
|
2873
|
+
(declaration) =>
|
|
2874
|
+
ts.isIdentifier(declaration.name) && declaration.name.text === name,
|
|
2875
|
+
)
|
|
2876
|
+
) {
|
|
2877
|
+
return true;
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
if (
|
|
2882
|
+
exported &&
|
|
2883
|
+
ts.isFunctionDeclaration(statement) &&
|
|
2884
|
+
statement.name?.text === name
|
|
2885
|
+
) {
|
|
2886
|
+
return true;
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
if (
|
|
2890
|
+
ts.isExportDeclaration(statement) &&
|
|
2891
|
+
!statement.isTypeOnly &&
|
|
2892
|
+
statement.exportClause &&
|
|
2893
|
+
ts.isNamedExports(statement.exportClause) &&
|
|
2894
|
+
statement.exportClause.elements.some(
|
|
2895
|
+
(element) => !element.isTypeOnly && element.name.text === name,
|
|
2896
|
+
)
|
|
2897
|
+
) {
|
|
2898
|
+
return true;
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
return false;
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2751
2905
|
function featureUiIndexFile(
|
|
2752
2906
|
names: FeatureUiNames,
|
|
2753
2907
|
config: ResolvedBeignetConfig,
|
|
@@ -2943,12 +3097,98 @@ function scheduleFiles(
|
|
|
2943
3097
|
function uploadFiles(
|
|
2944
3098
|
names: UploadNames,
|
|
2945
3099
|
config: ResolvedBeignetConfig,
|
|
3100
|
+
options: { ui: boolean },
|
|
2946
3101
|
): GeneratedFile[] {
|
|
2947
3102
|
return [
|
|
3103
|
+
{
|
|
3104
|
+
path: uploadManifestFilePath(names, config),
|
|
3105
|
+
content: uploadManifestFile(names),
|
|
3106
|
+
},
|
|
2948
3107
|
{
|
|
2949
3108
|
path: uploadFilePath(names, config),
|
|
2950
3109
|
content: uploadFile(names, config),
|
|
2951
3110
|
},
|
|
3111
|
+
...(options.ui ? uploadUiFiles(names, config) : []),
|
|
3112
|
+
];
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
function uploadFeatureDir(
|
|
3116
|
+
names: UploadNames,
|
|
3117
|
+
config: ResolvedBeignetConfig,
|
|
3118
|
+
): string {
|
|
3119
|
+
return path.join(config.paths.features, names.feature.kebab);
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
function uploadManifestFilePath(
|
|
3123
|
+
names: UploadNames,
|
|
3124
|
+
config: ResolvedBeignetConfig,
|
|
3125
|
+
): string {
|
|
3126
|
+
return path.join(
|
|
3127
|
+
uploadFeatureDir(names, config),
|
|
3128
|
+
`${names.manifestFileName}.ts`,
|
|
3129
|
+
);
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
function uploadUiClientFilePath(
|
|
3133
|
+
names: UploadNames,
|
|
3134
|
+
config: ResolvedBeignetConfig,
|
|
3135
|
+
): string {
|
|
3136
|
+
return path.join(
|
|
3137
|
+
uploadFeatureDir(names, config),
|
|
3138
|
+
"client",
|
|
3139
|
+
`${names.clientFileName}.ts`,
|
|
3140
|
+
);
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
function uploadUiComponentFilePath(
|
|
3144
|
+
names: UploadNames,
|
|
3145
|
+
config: ResolvedBeignetConfig,
|
|
3146
|
+
): string {
|
|
3147
|
+
return path.join(
|
|
3148
|
+
uploadFeatureDir(names, config),
|
|
3149
|
+
"components",
|
|
3150
|
+
`${names.componentFileName}.tsx`,
|
|
3151
|
+
);
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
function uploadUiComponentTestFilePath(
|
|
3155
|
+
names: UploadNames,
|
|
3156
|
+
config: ResolvedBeignetConfig,
|
|
3157
|
+
): string {
|
|
3158
|
+
return path.join(
|
|
3159
|
+
uploadFeatureDir(names, config),
|
|
3160
|
+
"tests",
|
|
3161
|
+
`${names.componentFileName}.test.tsx`,
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
function uploadUiComponentIndexFile(
|
|
3166
|
+
names: UploadNames,
|
|
3167
|
+
config: ResolvedBeignetConfig,
|
|
3168
|
+
): GeneratedFile {
|
|
3169
|
+
return {
|
|
3170
|
+
path: path.join(uploadFeatureDir(names, config), "components", "index.ts"),
|
|
3171
|
+
content: `export { ${names.componentExportName} } from "./${names.componentFileName}";\n`,
|
|
3172
|
+
};
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
function uploadUiFiles(
|
|
3176
|
+
names: UploadNames,
|
|
3177
|
+
config: ResolvedBeignetConfig,
|
|
3178
|
+
): GeneratedFile[] {
|
|
3179
|
+
return [
|
|
3180
|
+
{
|
|
3181
|
+
path: uploadUiClientFilePath(names, config),
|
|
3182
|
+
content: uploadUiClientFile(names, config),
|
|
3183
|
+
},
|
|
3184
|
+
{
|
|
3185
|
+
path: uploadUiComponentFilePath(names, config),
|
|
3186
|
+
content: uploadUiComponentFile(names, config),
|
|
3187
|
+
},
|
|
3188
|
+
{
|
|
3189
|
+
path: uploadUiComponentTestFilePath(names, config),
|
|
3190
|
+
content: uploadUiComponentTestFile(names, config),
|
|
3191
|
+
},
|
|
2952
3192
|
];
|
|
2953
3193
|
}
|
|
2954
3194
|
|
|
@@ -3381,7 +3621,13 @@ async function updateOutboxRegistry(
|
|
|
3381
3621
|
importLine,
|
|
3382
3622
|
);
|
|
3383
3623
|
|
|
3384
|
-
if (appended.kind === "missing"
|
|
3624
|
+
if (appended.kind === "missing") {
|
|
3625
|
+
throw new Error(
|
|
3626
|
+
`Could not find the ${kind} array in ${config.paths.outbox}. Register ${registryName} manually, or restore the generated outbox file before running this command.`,
|
|
3627
|
+
);
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
if (appended.kind === "unchanged") {
|
|
3385
3631
|
return "skipped";
|
|
3386
3632
|
}
|
|
3387
3633
|
|
|
@@ -3435,7 +3681,7 @@ function schemasFile(
|
|
|
3435
3681
|
|
|
3436
3682
|
export const ${names.singularPascal}Schema = z.object({
|
|
3437
3683
|
id: z.string().uuid(),
|
|
3438
|
-
${options.
|
|
3684
|
+
${options.tenantScoped ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
|
|
3439
3685
|
version: z.number().int().min(1),
|
|
3440
3686
|
createdAt: z.string().datetime(),
|
|
3441
3687
|
updatedAt: z.string().datetime(),
|
|
@@ -3535,7 +3781,7 @@ export const List${names.pluralPascal}OutputSchema = z.object({
|
|
|
3535
3781
|
});
|
|
3536
3782
|
|
|
3537
3783
|
export const Create${names.singularPascal}InputSchema = z.object({
|
|
3538
|
-
name: z.string().min(1).max(120),
|
|
3784
|
+
name: z.string().trim().min(1).max(120),
|
|
3539
3785
|
});
|
|
3540
3786
|
${
|
|
3541
3787
|
mode === "resource"
|
|
@@ -3545,7 +3791,7 @@ export const ${names.singularPascal}IdInputSchema = z.object({
|
|
|
3545
3791
|
});
|
|
3546
3792
|
|
|
3547
3793
|
export const Update${names.singularPascal}BodySchema = z.object({
|
|
3548
|
-
name: z.string().min(1).max(120),
|
|
3794
|
+
name: z.string().trim().min(1).max(120),
|
|
3549
3795
|
version: z.number().int().min(1),
|
|
3550
3796
|
});
|
|
3551
3797
|
|
|
@@ -3591,7 +3837,7 @@ function listUseCaseFile(
|
|
|
3591
3837
|
|
|
3592
3838
|
return `import "@beignet/core/server-only";
|
|
3593
3839
|
import { normalizeCursorPage } from "@beignet/core/pagination";
|
|
3594
|
-
${options.
|
|
3840
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3595
3841
|
import {
|
|
3596
3842
|
decode${names.singularPascal}Cursor,
|
|
3597
3843
|
List${names.pluralPascal}InputSchema,
|
|
@@ -3603,7 +3849,7 @@ export const list${names.pluralPascal}UseCase = useCase
|
|
|
3603
3849
|
.input(List${names.pluralPascal}InputSchema)
|
|
3604
3850
|
.output(List${names.pluralPascal}OutputSchema)
|
|
3605
3851
|
.run(async ({ ctx, input }) => {
|
|
3606
|
-
${options.
|
|
3852
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const page = normalizeCursorPage(input, {
|
|
3607
3853
|
defaultLimit: 20,
|
|
3608
3854
|
maxLimit: 100,
|
|
3609
3855
|
});
|
|
@@ -3614,7 +3860,7 @@ ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const
|
|
|
3614
3860
|
name: input.name,
|
|
3615
3861
|
sortBy: input.sortBy,
|
|
3616
3862
|
sortDirection: input.sortDirection,
|
|
3617
|
-
}${options.
|
|
3863
|
+
}${options.tenantScoped ? ", scope" : ""});
|
|
3618
3864
|
});
|
|
3619
3865
|
`;
|
|
3620
3866
|
}
|
|
@@ -3630,7 +3876,7 @@ function createUseCaseFile(
|
|
|
3630
3876
|
);
|
|
3631
3877
|
|
|
3632
3878
|
return `import "@beignet/core/server-only";
|
|
3633
|
-
${options.
|
|
3879
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3634
3880
|
import {
|
|
3635
3881
|
Create${names.singularPascal}InputSchema,
|
|
3636
3882
|
${names.singularPascal}Schema,
|
|
@@ -3642,7 +3888,7 @@ export const create${names.singularPascal}UseCase = useCase
|
|
|
3642
3888
|
.input(Create${names.singularPascal}InputSchema)
|
|
3643
3889
|
.output(${names.singularPascal}Schema)
|
|
3644
3890
|
.run(async ({ ctx, input }) => {
|
|
3645
|
-
${options.
|
|
3891
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""}${options.authorization ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.create");\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.create(input${options.tenantScoped ? ", scope" : ""});
|
|
3646
3892
|
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Created, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
|
|
3647
3893
|
return ${names.singularCamel};
|
|
3648
3894
|
});
|
|
@@ -3660,7 +3906,7 @@ function getUseCaseFile(
|
|
|
3660
3906
|
);
|
|
3661
3907
|
|
|
3662
3908
|
return `import "@beignet/core/server-only";
|
|
3663
|
-
${options.
|
|
3909
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
3664
3910
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3665
3911
|
import {
|
|
3666
3912
|
${names.singularPascal}IdInputSchema,
|
|
@@ -3672,7 +3918,7 @@ export const get${names.singularPascal}UseCase = useCase
|
|
|
3672
3918
|
.input(${names.singularPascal}IdInputSchema)
|
|
3673
3919
|
.output(${names.singularPascal}Schema)
|
|
3674
3920
|
.run(async ({ ctx, input }) => {
|
|
3675
|
-
${options.
|
|
3921
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
|
|
3676
3922
|
if (!${names.singularCamel}) {
|
|
3677
3923
|
throw appError("${names.singularPascal}NotFound", {
|
|
3678
3924
|
details: { id: input.id },
|
|
@@ -3695,7 +3941,7 @@ function updateUseCaseFile(
|
|
|
3695
3941
|
);
|
|
3696
3942
|
|
|
3697
3943
|
return `import "@beignet/core/server-only";
|
|
3698
|
-
${options.
|
|
3944
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
3699
3945
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3700
3946
|
${options.events ? `import { ${names.singularPascal}Updated } from "../domain/events";\n` : ""}import {
|
|
3701
3947
|
Update${names.singularPascal}InputSchema,
|
|
@@ -3707,13 +3953,13 @@ export const update${names.singularPascal}UseCase = useCase
|
|
|
3707
3953
|
.input(Update${names.singularPascal}InputSchema)
|
|
3708
3954
|
.output(${names.singularPascal}Schema)
|
|
3709
3955
|
.run(async ({ ctx, input }) => {
|
|
3710
|
-
${options.
|
|
3956
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
|
|
3711
3957
|
if (!existing) {
|
|
3712
3958
|
throw appError("${names.singularPascal}NotFound", {
|
|
3713
3959
|
details: { id: input.id },
|
|
3714
3960
|
});
|
|
3715
3961
|
}
|
|
3716
|
-
${options.
|
|
3962
|
+
${options.authorization ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
|
|
3717
3963
|
throw appError("${names.singularPascal}Conflict", {
|
|
3718
3964
|
details: { id: input.id, expectedVersion: existing.version },
|
|
3719
3965
|
});
|
|
@@ -3721,7 +3967,7 @@ ${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", ex
|
|
|
3721
3967
|
|
|
3722
3968
|
const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.update({
|
|
3723
3969
|
...input,
|
|
3724
|
-
}${options.
|
|
3970
|
+
}${options.tenantScoped ? ", scope" : ""});
|
|
3725
3971
|
if (!${names.singularCamel}) {
|
|
3726
3972
|
throw appError("${names.singularPascal}Conflict", {
|
|
3727
3973
|
details: { id: input.id, expectedVersion: existing.version },
|
|
@@ -3745,7 +3991,7 @@ function deleteUseCaseFile(
|
|
|
3745
3991
|
|
|
3746
3992
|
return `import "@beignet/core/server-only";
|
|
3747
3993
|
import { z } from "zod";
|
|
3748
|
-
${options.
|
|
3994
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
3749
3995
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3750
3996
|
${options.events ? `import { ${names.singularPascal}Deleted } from "../domain/events";\n` : ""}import { ${names.singularPascal}IdInputSchema } from "../schemas";
|
|
3751
3997
|
|
|
@@ -3754,7 +4000,7 @@ export const delete${names.singularPascal}UseCase = useCase
|
|
|
3754
4000
|
.input(${names.singularPascal}IdInputSchema)
|
|
3755
4001
|
.output(z.void())
|
|
3756
4002
|
.run(async ({ ctx, input }) => {
|
|
3757
|
-
${options.
|
|
4003
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""}${options.authorization ? `\t\tconst existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});\n\t\tif (!existing) {\n\t\t\tthrow appError("${names.singularPascal}NotFound", {\n\t\t\t\tdetails: { id: input.id },\n\t\t\t});\n\t\t}\n\t\tawait ctx.gate.authorize("${names.pluralCamel}.delete", existing);\n\n` : ""} const deleted = await ctx.ports.${names.pluralCamel}.delete(input.id${options.tenantScoped ? ", scope" : ""});
|
|
3758
4004
|
if (!deleted) {
|
|
3759
4005
|
throw appError("${names.singularPascal}NotFound", {
|
|
3760
4006
|
details: { id: input.id },
|
|
@@ -3794,7 +4040,7 @@ function repositoryPortFile(
|
|
|
3794
4040
|
PageResult,
|
|
3795
4041
|
SortDirection,
|
|
3796
4042
|
} from "@beignet/core/pagination";
|
|
3797
|
-
${options.
|
|
4043
|
+
${options.tenantScoped ? `import type { TenantScope } from "@beignet/core/tenancy";\n` : ""}import type {
|
|
3798
4044
|
Create${names.singularPascal}Input,
|
|
3799
4045
|
${names.singularPascal}Cursor,
|
|
3800
4046
|
${names.singularPascal}SortBy,
|
|
@@ -3811,9 +4057,9 @@ export type List${names.pluralPascal}Query = {
|
|
|
3811
4057
|
};
|
|
3812
4058
|
|
|
3813
4059
|
export interface ${names.singularPascal}Repository {
|
|
3814
|
-
list(query: List${names.pluralPascal}Query${options.
|
|
3815
|
-
create(input: Create${names.singularPascal}Input${options.
|
|
3816
|
-
${mode === "resource" ? ` findById(id: string${options.
|
|
4060
|
+
list(query: List${names.pluralPascal}Query${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<List${names.pluralPascal}Result>;
|
|
4061
|
+
create(input: Create${names.singularPascal}Input${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal}>;
|
|
4062
|
+
${mode === "resource" ? ` findById(id: string${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal} | null>;\n update(input: Update${names.singularPascal}Input${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal} | null>;\n delete(id: string${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<boolean>;\n` : ""}}
|
|
3817
4063
|
`;
|
|
3818
4064
|
}
|
|
3819
4065
|
|
|
@@ -3847,14 +4093,14 @@ function inMemoryRepositoryFile(
|
|
|
3847
4093
|
? " || existing.deletedAt !== null"
|
|
3848
4094
|
: "";
|
|
3849
4095
|
const deleteImplementation = options.softDelete
|
|
3850
|
-
? `${options.
|
|
4096
|
+
? `${options.tenantScoped ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.tenantId !== tenantId || existing.deletedAt !== null) return false;\n` : `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.deletedAt !== null) return false;\n`} const now = new Date().toISOString();
|
|
3851
4097
|
${names.pluralCamel}.set(id, { ...existing, deletedAt: now, updatedAt: now });
|
|
3852
4098
|
return true;`
|
|
3853
|
-
: `${options.
|
|
4099
|
+
: `${options.tenantScoped ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (existing?.tenantId !== tenantId) return false;\n` : ""} return ${names.pluralCamel}.delete(id);`;
|
|
3854
4100
|
|
|
3855
4101
|
return `import "@beignet/core/server-only";
|
|
3856
4102
|
import { cursorPageResult } from "@beignet/core/pagination";
|
|
3857
|
-
${options.
|
|
4103
|
+
${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
3858
4104
|
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
3859
4105
|
import type {
|
|
3860
4106
|
Create${names.singularPascal}Input,
|
|
@@ -3864,7 +4110,7 @@ ${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.s
|
|
|
3864
4110
|
${recordType}function to${names.singularPascal}(${names.singularCamel}: ${mapValueType}): ${names.singularPascal} {
|
|
3865
4111
|
return {
|
|
3866
4112
|
id: ${names.singularCamel}.id,
|
|
3867
|
-
${options.
|
|
4113
|
+
${options.tenantScoped ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
|
|
3868
4114
|
version: ${names.singularCamel}.version,
|
|
3869
4115
|
createdAt: ${names.singularCamel}.createdAt,
|
|
3870
4116
|
updatedAt: ${names.singularCamel}.updatedAt,
|
|
@@ -3922,10 +4168,10 @@ export function createInMemory${names.singularPascal}Repository(
|
|
|
3922
4168
|
);
|
|
3923
4169
|
|
|
3924
4170
|
return {
|
|
3925
|
-
async list(query${options.
|
|
4171
|
+
async list(query${options.tenantScoped ? ", scope" : ""}) {
|
|
3926
4172
|
const name = query.name?.toLocaleLowerCase();
|
|
3927
|
-
${options.
|
|
3928
|
-
${activeFilter}${options.
|
|
4173
|
+
${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values())
|
|
4174
|
+
${activeFilter}${options.tenantScoped ? `\t\t\t\t.filter((${names.singularCamel}) => ${names.singularCamel}.tenantId === tenantId)\n` : ""} .filter((${names.singularCamel}) => !name || ${names.singularCamel}.name.toLocaleLowerCase().includes(name))
|
|
3929
4175
|
.sort((left, right) => compare${names.pluralPascal}(left, right, query))
|
|
3930
4176
|
.filter((${names.singularCamel}) => isAfter${names.singularPascal}Cursor(${names.singularCamel}, query));
|
|
3931
4177
|
const pageItems = all${names.pluralPascal}.slice(0, query.page.limit);
|
|
@@ -3940,11 +4186,11 @@ ${activeFilter}${options.tenant ? `\t\t\t\t.filter((${names.singularCamel}) => $
|
|
|
3940
4186
|
nextCursor,
|
|
3941
4187
|
);
|
|
3942
4188
|
},
|
|
3943
|
-
async create(input${options.
|
|
4189
|
+
async create(input${options.tenantScoped ? ", scope" : ""}) {
|
|
3944
4190
|
const now = new Date().toISOString();
|
|
3945
|
-
${options.
|
|
4191
|
+
${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel}: ${names.singularPascal} = {
|
|
3946
4192
|
id: crypto.randomUUID(),
|
|
3947
|
-
${options.
|
|
4193
|
+
${options.tenantScoped ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
|
|
3948
4194
|
version: 1,
|
|
3949
4195
|
createdAt: now,
|
|
3950
4196
|
updatedAt: now,
|
|
@@ -3954,7 +4200,7 @@ ${options.tenant ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
|
|
|
3954
4200
|
${newDeletedAtField} });
|
|
3955
4201
|
return ${names.singularCamel};
|
|
3956
4202
|
},
|
|
3957
|
-
${mode === "resource" ? ` async findById(id: string${options.
|
|
4203
|
+
${mode === "resource" ? ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {\n${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel} = ${names.pluralCamel}.get(id) ?? null;\n${options.tenantScoped ? `\t\t\tif (${names.singularCamel}?.tenantId !== tenantId) return null;\n` : ""}${findDeletedGuard} return ${names.singularCamel} ? to${names.singularPascal}(${names.singularCamel}) : null;\n },\n async update(input${options.tenantScoped ? ", scope" : ""}) {\n${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const existing = ${names.pluralCamel}.get(input.id);\n if (!existing${options.tenantScoped ? " || existing.tenantId !== tenantId" : ""}${updateDeletedGuard} || existing.version !== input.version) return null;\n\n const next: ${mapValueType} = {\n ...existing,\n name: input.name,\n version: existing.version + 1,\n updatedAt: new Date().toISOString(),\n };\n ${names.pluralCamel}.set(input.id, next);\n return to${names.singularPascal}(next);\n },\n async delete(id: string${options.tenantScoped ? ", scope" : ""}) {\n${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""}${deleteImplementation}\n },\n` : ""}
|
|
3958
4204
|
};
|
|
3959
4205
|
}
|
|
3960
4206
|
`;
|
|
@@ -3969,7 +4215,7 @@ function drizzleSchemaFile(
|
|
|
3969
4215
|
|
|
3970
4216
|
export const ${names.pluralCamel} = ${dialect.tableFunction}("${names.pluralKebab.replaceAll("-", "_")}", {
|
|
3971
4217
|
id: ${dialect.idColumn("id")}.primaryKey(),
|
|
3972
|
-
${options.
|
|
4218
|
+
${options.tenantScoped ? `\ttenantId: ${dialect.idColumn("tenant_id")}.notNull(),\n` : ""} name: text("name").notNull(),
|
|
3973
4219
|
version: ${dialect.integerColumn("version")}.notNull(),
|
|
3974
4220
|
createdAt: ${dialect.timestampColumn("created_at")}.notNull(),
|
|
3975
4221
|
updatedAt: ${dialect.timestampColumn("updated_at")}.notNull(),
|
|
@@ -3988,6 +4234,7 @@ export const outboxMessages = mysqlTable(
|
|
|
3988
4234
|
\t\tkind: varchar("kind", { length: 32 }).notNull(),
|
|
3989
4235
|
\t\tname: varchar("name", { length: 255 }).notNull(),
|
|
3990
4236
|
\t\tpayloadJson: longtext("payload_json").notNull(),
|
|
4237
|
+
\t\ttraceContextJson: longtext("trace_context_json"),
|
|
3991
4238
|
\t\tstatus: varchar("status", { length: 32 }).notNull(),
|
|
3992
4239
|
\t\tattempts: int("attempts").notNull().default(0),
|
|
3993
4240
|
\t\tmaxAttempts: int("max_attempts").notNull().default(3),
|
|
@@ -4023,6 +4270,7 @@ export const outboxMessages = ${dialect.tableFunction}(
|
|
|
4023
4270
|
\t\tkind: text("kind").notNull(),
|
|
4024
4271
|
\t\tname: text("name").notNull(),
|
|
4025
4272
|
\t\tpayloadJson: text("payload_json").notNull(),
|
|
4273
|
+
\t\ttraceContextJson: text("trace_context_json"),
|
|
4026
4274
|
\t\tstatus: text("status").notNull(),
|
|
4027
4275
|
\t\tattempts: integer("attempts").notNull().default(0),
|
|
4028
4276
|
\t\tmaxAttempts: integer("max_attempts").notNull().default(3),
|
|
@@ -4077,20 +4325,20 @@ function drizzleRepositoryFile(
|
|
|
4077
4325
|
const schemaPath = drizzleSchemaIndexPath(config);
|
|
4078
4326
|
const findWhere = drizzleResourceWhere(names, options, [
|
|
4079
4327
|
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
4080
|
-
...(options.
|
|
4328
|
+
...(options.tenantScoped
|
|
4081
4329
|
? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
|
|
4082
4330
|
: []),
|
|
4083
4331
|
]);
|
|
4084
4332
|
const updateWhere = drizzleResourceWhere(names, options, [
|
|
4085
4333
|
`eq(schema.${names.pluralCamel}.id, input.id)`,
|
|
4086
4334
|
`eq(schema.${names.pluralCamel}.version, input.version)`,
|
|
4087
|
-
...(options.
|
|
4335
|
+
...(options.tenantScoped
|
|
4088
4336
|
? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
|
|
4089
4337
|
: []),
|
|
4090
4338
|
]);
|
|
4091
4339
|
const deleteWhere = drizzleResourceWhere(names, options, [
|
|
4092
4340
|
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
4093
|
-
...(options.
|
|
4341
|
+
...(options.tenantScoped
|
|
4094
4342
|
? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
|
|
4095
4343
|
: []),
|
|
4096
4344
|
]);
|
|
@@ -4108,7 +4356,7 @@ function drizzleRepositoryFile(
|
|
|
4108
4356
|
].filter((name): name is string => Boolean(name));
|
|
4109
4357
|
// After a version-guarded update succeeds, re-select by identity only —
|
|
4110
4358
|
// matching what `.returning()` yields on the dialects that support it.
|
|
4111
|
-
const updateReselectWhere = options.
|
|
4359
|
+
const updateReselectWhere = options.tenantScoped
|
|
4112
4360
|
? `and(eq(schema.${names.pluralCamel}.id, input.id), eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)))`
|
|
4113
4361
|
: `eq(schema.${names.pluralCamel}.id, input.id)`;
|
|
4114
4362
|
// drizzle-orm/mysql2 has no `.returning(...)`; mutations resolve to a
|
|
@@ -4131,13 +4379,13 @@ function affectedRows(result: unknown): number {
|
|
|
4131
4379
|
`
|
|
4132
4380
|
: "";
|
|
4133
4381
|
const createMethod = dialect.supportsReturning
|
|
4134
|
-
? ` async create(input${options.
|
|
4382
|
+
? ` async create(input${options.tenantScoped ? ", scope" : ""}) {
|
|
4135
4383
|
const now = new Date().toISOString();
|
|
4136
4384
|
const [row] = await db
|
|
4137
4385
|
.insert(schema.${names.pluralCamel})
|
|
4138
4386
|
.values({
|
|
4139
4387
|
id: crypto.randomUUID(),
|
|
4140
|
-
${options.
|
|
4388
|
+
${options.tenantScoped ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
|
|
4141
4389
|
version: 1,
|
|
4142
4390
|
createdAt: now,
|
|
4143
4391
|
updatedAt: now,
|
|
@@ -4151,11 +4399,11 @@ ${options.tenant ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name:
|
|
|
4151
4399
|
return to${names.singularPascal}(row);
|
|
4152
4400
|
},
|
|
4153
4401
|
`
|
|
4154
|
-
: ` async create(input${options.
|
|
4402
|
+
: ` async create(input${options.tenantScoped ? ", scope" : ""}) {
|
|
4155
4403
|
const now = new Date().toISOString();
|
|
4156
4404
|
const ${names.singularCamel}: ${names.singularPascal} = {
|
|
4157
4405
|
id: crypto.randomUUID(),
|
|
4158
|
-
${options.
|
|
4406
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
|
|
4159
4407
|
version: 1,
|
|
4160
4408
|
createdAt: now,
|
|
4161
4409
|
updatedAt: now,
|
|
@@ -4169,7 +4417,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
|
|
|
4169
4417
|
mode !== "resource"
|
|
4170
4418
|
? ""
|
|
4171
4419
|
: dialect.supportsReturning
|
|
4172
|
-
? ` async findById(id: string${options.
|
|
4420
|
+
? ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
4173
4421
|
const [row] = await db
|
|
4174
4422
|
.select()
|
|
4175
4423
|
.from(schema.${names.pluralCamel})
|
|
@@ -4178,7 +4426,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
|
|
|
4178
4426
|
|
|
4179
4427
|
return row ? to${names.singularPascal}(row) : null;
|
|
4180
4428
|
},
|
|
4181
|
-
async update(input${options.
|
|
4429
|
+
async update(input${options.tenantScoped ? ", scope" : ""}) {
|
|
4182
4430
|
const [row] = await db
|
|
4183
4431
|
.update(schema.${names.pluralCamel})
|
|
4184
4432
|
.set({
|
|
@@ -4191,12 +4439,12 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
|
|
|
4191
4439
|
|
|
4192
4440
|
return row ? to${names.singularPascal}(row) : null;
|
|
4193
4441
|
},
|
|
4194
|
-
async delete(id: string${options.
|
|
4442
|
+
async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
4195
4443
|
${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst rows = await db\n\t\t\t\t.update(schema.${names.pluralCamel})\n\t\t\t\t.set({ deletedAt: now, updatedAt: now })\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n` : `\t\t\tconst rows = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n`}
|
|
4196
4444
|
return rows.length > 0;
|
|
4197
4445
|
},
|
|
4198
4446
|
`
|
|
4199
|
-
: ` async findById(id: string${options.
|
|
4447
|
+
: ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
4200
4448
|
const [row] = await db
|
|
4201
4449
|
.select()
|
|
4202
4450
|
.from(schema.${names.pluralCamel})
|
|
@@ -4205,7 +4453,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
|
|
|
4205
4453
|
|
|
4206
4454
|
return row ? to${names.singularPascal}(row) : null;
|
|
4207
4455
|
},
|
|
4208
|
-
async update(input${options.
|
|
4456
|
+
async update(input${options.tenantScoped ? ", scope" : ""}) {
|
|
4209
4457
|
const result = await db
|
|
4210
4458
|
.update(schema.${names.pluralCamel})
|
|
4211
4459
|
.set({
|
|
@@ -4225,7 +4473,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
|
|
|
4225
4473
|
|
|
4226
4474
|
return row ? to${names.singularPascal}(row) : null;
|
|
4227
4475
|
},
|
|
4228
|
-
async delete(id: string${options.
|
|
4476
|
+
async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
4229
4477
|
${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst result = await db\n\t\t\t\t.update(schema.${names.pluralCamel})\n\t\t\t\t.set({ deletedAt: now, updatedAt: now })\n\t\t\t\t.where(${deleteWhere});\n` : `\t\t\tconst result = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere});\n`}
|
|
4230
4478
|
return affectedRows(result) > 0;
|
|
4231
4479
|
},
|
|
@@ -4233,7 +4481,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
|
|
|
4233
4481
|
|
|
4234
4482
|
return `import "@beignet/core/server-only";
|
|
4235
4483
|
import { cursorPageResult } from "@beignet/core/pagination";
|
|
4236
|
-
${options.
|
|
4484
|
+
${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
|
|
4237
4485
|
import { ${drizzleImports.join(", ")} } from "drizzle-orm";
|
|
4238
4486
|
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
4239
4487
|
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
@@ -4248,7 +4496,7 @@ type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelec
|
|
|
4248
4496
|
function to${names.singularPascal}(row: ${names.singularPascal}Row): ${names.singularPascal} {
|
|
4249
4497
|
return {
|
|
4250
4498
|
id: row.id,
|
|
4251
|
-
${options.
|
|
4499
|
+
${options.tenantScoped ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
|
|
4252
4500
|
version: row.version,
|
|
4253
4501
|
createdAt: row.createdAt,
|
|
4254
4502
|
updatedAt: row.updatedAt,
|
|
@@ -4280,9 +4528,9 @@ function ${names.singularCamel}CursorFilter(
|
|
|
4280
4528
|
|
|
4281
4529
|
function ${names.singularCamel}ListWhere(
|
|
4282
4530
|
query: List${names.pluralPascal}Query,
|
|
4283
|
-
${options.
|
|
4531
|
+
${options.tenantScoped ? `\tscope: Parameters<${names.singularPascal}Repository["list"]>[1],\n` : ""}): SQL<unknown> | undefined {
|
|
4284
4532
|
const filters: SQL<unknown>[] = [
|
|
4285
|
-
${options.
|
|
4533
|
+
${options.tenantScoped ? `\t\teq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
|
|
4286
4534
|
const cursor = ${names.singularCamel}CursorFilter(query);
|
|
4287
4535
|
|
|
4288
4536
|
if (query.name) {
|
|
@@ -4316,8 +4564,8 @@ export function createDrizzle${names.singularPascal}Repository(
|
|
|
4316
4564
|
db: ${dialect.dbTypeName}<typeof schema>,
|
|
4317
4565
|
): ${names.singularPascal}Repository {
|
|
4318
4566
|
return {
|
|
4319
|
-
async list(query${options.
|
|
4320
|
-
const where = ${names.singularCamel}ListWhere(query${options.
|
|
4567
|
+
async list(query${options.tenantScoped ? ", scope" : ""}) {
|
|
4568
|
+
const where = ${names.singularCamel}ListWhere(query${options.tenantScoped ? ", scope" : ""});
|
|
4321
4569
|
const rows = await db
|
|
4322
4570
|
.select()
|
|
4323
4571
|
.from(schema.${names.pluralCamel})
|
|
@@ -4379,7 +4627,7 @@ export const list${names.pluralPascal} = ${names.pluralCamel}
|
|
|
4379
4627
|
export const create${names.singularPascal} = ${names.pluralCamel}
|
|
4380
4628
|
.post("/api/${names.pluralKebab}")
|
|
4381
4629
|
.body(Create${names.singularPascal}InputSchema)
|
|
4382
|
-
${options.
|
|
4630
|
+
${options.authorization ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
|
|
4383
4631
|
201: ${names.singularPascal}Schema,
|
|
4384
4632
|
});
|
|
4385
4633
|
${
|
|
@@ -4397,7 +4645,7 @@ export const update${names.singularPascal} = ${names.pluralCamel}
|
|
|
4397
4645
|
.patch("/api/${names.pluralKebab}/:id")
|
|
4398
4646
|
.pathParams(${names.singularPascal}IdInputSchema)
|
|
4399
4647
|
.body(Update${names.singularPascal}BodySchema)
|
|
4400
|
-
${options.
|
|
4648
|
+
${options.authorization ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.update" },\n\t})\n` : ""} .errors({${options.authorization ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound, ${names.singularPascal}Conflict: errors.${names.singularPascal}Conflict })
|
|
4401
4649
|
.responses({
|
|
4402
4650
|
200: ${names.singularPascal}Schema,
|
|
4403
4651
|
});
|
|
@@ -4405,7 +4653,7 @@ ${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability
|
|
|
4405
4653
|
export const delete${names.singularPascal} = ${names.pluralCamel}
|
|
4406
4654
|
.delete("/api/${names.pluralKebab}/:id")
|
|
4407
4655
|
.pathParams(${names.singularPascal}IdInputSchema)
|
|
4408
|
-
${options.
|
|
4656
|
+
${options.authorization ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.delete" },\n\t})\n` : ""} .errors({${options.authorization ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound })
|
|
4409
4657
|
.responses({
|
|
4410
4658
|
204: null,
|
|
4411
4659
|
});
|
|
@@ -4503,15 +4751,15 @@ import { createInMemoryDevtools } from "@beignet/devtools";
|
|
|
4503
4751
|
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
4504
4752
|
import { createTestAnonymousActor } from "@beignet/core/testing";
|
|
4505
4753
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
4506
|
-
import {
|
|
4754
|
+
import { initialPorts } from "${aliasModule(config.paths.portWiring)}";
|
|
4507
4755
|
import { ${names.exportName} } from "${relativeModule(filePath, useCaseFilePath(names, config))}";
|
|
4508
4756
|
|
|
4509
4757
|
describe("${names.exportName}", () => {
|
|
4510
4758
|
it("runs ${names.action.camel}", async () => {
|
|
4511
4759
|
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
4512
|
-
base:
|
|
4760
|
+
base: initialPorts,
|
|
4513
4761
|
overrides: {
|
|
4514
|
-
gate:
|
|
4762
|
+
gate: initialPorts.gate,
|
|
4515
4763
|
devtools: createInMemoryDevtools(),
|
|
4516
4764
|
},
|
|
4517
4765
|
});
|
|
@@ -4610,7 +4858,7 @@ function isAuthenticated(ctx: AuthorizationContext) {
|
|
|
4610
4858
|
|
|
4611
4859
|
function canWrite(ctx: AuthorizationContext) {
|
|
4612
4860
|
if (!isAuthenticated(ctx)) return deny("You must be signed in.");
|
|
4613
|
-
${options.
|
|
4861
|
+
${options.tenantScoped ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
|
|
4614
4862
|
}
|
|
4615
4863
|
|
|
4616
4864
|
export const ${names.singularCamel}Policy = definePolicy({
|
|
@@ -4620,13 +4868,13 @@ export const ${names.singularCamel}Policy = definePolicy({
|
|
|
4620
4868
|
"${names.pluralCamel}.update": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
4621
4869
|
const decision = canWrite(ctx);
|
|
4622
4870
|
if (decision !== true) return decision;
|
|
4623
|
-
${options.
|
|
4871
|
+
${options.tenantScoped ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot update a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
|
|
4624
4872
|
return true;
|
|
4625
4873
|
},
|
|
4626
4874
|
"${names.pluralCamel}.delete": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
4627
4875
|
const decision = canWrite(ctx);
|
|
4628
4876
|
if (decision !== true) return decision;
|
|
4629
|
-
${options.
|
|
4877
|
+
${options.tenantScoped ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot delete a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
|
|
4630
4878
|
return true;
|
|
4631
4879
|
},
|
|
4632
4880
|
"${names.pluralCamel}.manage": (_ctx: AuthorizationContext) =>
|
|
@@ -4678,7 +4926,7 @@ function create${names.singularPascal}(): ${names.singularPascal} {
|
|
|
4678
4926
|
const now = new Date().toISOString();
|
|
4679
4927
|
return {
|
|
4680
4928
|
id: "00000000-0000-4000-8000-000000000001",
|
|
4681
|
-
${options.
|
|
4929
|
+
${options.tenantScoped ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
|
|
4682
4930
|
version: 1,
|
|
4683
4931
|
createdAt: now,
|
|
4684
4932
|
updatedAt: now,
|
|
@@ -4693,7 +4941,7 @@ describe("${names.singularCamel}Policy", () => {
|
|
|
4693
4941
|
const ${names.singularCamel} = create${names.singularPascal}();
|
|
4694
4942
|
const ctx = {
|
|
4695
4943
|
actor: { type: "user" as const, id: "user_test" },
|
|
4696
|
-
${options.
|
|
4944
|
+
${options.tenantScoped ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
|
|
4697
4945
|
|
|
4698
4946
|
await expect(
|
|
4699
4947
|
tester.assertMatrix([
|
|
@@ -4919,8 +5167,33 @@ ${names.timezone ? `\t\ttimezone: "${names.timezone}",\n` : ""}\t\tpayload: ${na
|
|
|
4919
5167
|
`;
|
|
4920
5168
|
}
|
|
4921
5169
|
|
|
5170
|
+
function uploadManifestFile(names: UploadNames): string {
|
|
5171
|
+
return `import type {
|
|
5172
|
+
\tUploadFileConstraints,
|
|
5173
|
+
\tUploadManifestEntry,
|
|
5174
|
+
} from "@beignet/core/uploads";
|
|
5175
|
+
|
|
5176
|
+
export const ${names.fileConstraintsExportName} = {
|
|
5177
|
+
\tcontentTypes: ["application/pdf", "text/plain"],
|
|
5178
|
+
\tmaxSizeBytes: 5 * 1024 * 1024,
|
|
5179
|
+
\tmaxFiles: 1,
|
|
5180
|
+
\tvisibility: "private",
|
|
5181
|
+
\tcacheControl: "private, max-age=0",
|
|
5182
|
+
} satisfies UploadFileConstraints;
|
|
5183
|
+
|
|
5184
|
+
export const ${names.manifestExportName} = [
|
|
5185
|
+
\t{
|
|
5186
|
+
\t\tname: "${names.uploadName}",
|
|
5187
|
+
\t\tdescription: "${names.artifact.pascal} upload",
|
|
5188
|
+
\t\tfile: ${names.fileConstraintsExportName},
|
|
5189
|
+
\t},
|
|
5190
|
+
] satisfies readonly UploadManifestEntry[];
|
|
5191
|
+
`;
|
|
5192
|
+
}
|
|
5193
|
+
|
|
4922
5194
|
function uploadFile(names: UploadNames, config: ResolvedBeignetConfig): string {
|
|
4923
5195
|
return `import { z } from "zod";
|
|
5196
|
+
import { ${names.fileConstraintsExportName} } from "${relativeModule(uploadFilePath(names, config), uploadManifestFilePath(names, config))}";
|
|
4924
5197
|
import { defineUpload } from "${aliasModule(config.paths.uploadsBuilder)}";
|
|
4925
5198
|
|
|
4926
5199
|
export const ${names.metadataSchemaName} = z.object({
|
|
@@ -4931,13 +5204,7 @@ export type ${names.metadataTypeName} = z.infer<typeof ${names.metadataSchemaNam
|
|
|
4931
5204
|
|
|
4932
5205
|
export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
|
|
4933
5206
|
\tmetadata: ${names.metadataSchemaName},
|
|
4934
|
-
\tfile: {
|
|
4935
|
-
\t\tcontentTypes: ["application/pdf", "text/plain"],
|
|
4936
|
-
\t\tmaxSizeBytes: 5 * 1024 * 1024,
|
|
4937
|
-
\t\tmaxFiles: 1,
|
|
4938
|
-
\t\tvisibility: "private",
|
|
4939
|
-
\t\tcacheControl: "private, max-age=0",
|
|
4940
|
-
\t},
|
|
5207
|
+
\tfile: ${names.fileConstraintsExportName},
|
|
4941
5208
|
\tauthorize({ ctx }) {
|
|
4942
5209
|
\t\treturn ctx.actor.type === "user";
|
|
4943
5210
|
\t},
|
|
@@ -4968,6 +5235,148 @@ export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
|
|
|
4968
5235
|
`;
|
|
4969
5236
|
}
|
|
4970
5237
|
|
|
5238
|
+
function uploadUiClientFile(
|
|
5239
|
+
names: UploadNames,
|
|
5240
|
+
config: ResolvedBeignetConfig,
|
|
5241
|
+
): string {
|
|
5242
|
+
const clientPath = uploadUiClientFilePath(names, config);
|
|
5243
|
+
const registryPath = path.join(
|
|
5244
|
+
featureArtifactDir(names, "uploads", config),
|
|
5245
|
+
"index.ts",
|
|
5246
|
+
);
|
|
5247
|
+
|
|
5248
|
+
return `import { createUploadClient } from "@beignet/core/uploads/client";
|
|
5249
|
+
import { createReactUploads } from "@beignet/react-uploads";
|
|
5250
|
+
import { ${names.manifestExportName} } from "${relativeModule(clientPath, uploadManifestFilePath(names, config))}";
|
|
5251
|
+
import type { ${names.featureSingularCamel}Uploads } from "${relativeModule(clientPath, registryPath)}";
|
|
5252
|
+
|
|
5253
|
+
const ${names.artifact.camel}UploadClient = createUploadClient<typeof ${names.featureSingularCamel}Uploads>({
|
|
5254
|
+
\tbaseUrl: "/api/uploads",
|
|
5255
|
+
\tmanifest: ${names.manifestExportName},
|
|
5256
|
+
});
|
|
5257
|
+
|
|
5258
|
+
export const ${names.reactUploadsExportName} = createReactUploads({
|
|
5259
|
+
\tuploads: ${names.artifact.camel}UploadClient,
|
|
5260
|
+
});
|
|
5261
|
+
`;
|
|
5262
|
+
}
|
|
5263
|
+
|
|
5264
|
+
function uploadUiComponentFile(
|
|
5265
|
+
names: UploadNames,
|
|
5266
|
+
config: ResolvedBeignetConfig,
|
|
5267
|
+
): string {
|
|
5268
|
+
const componentPath = uploadUiComponentFilePath(names, config);
|
|
5269
|
+
|
|
5270
|
+
return `"use client";
|
|
5271
|
+
|
|
5272
|
+
import { useId } from "react";
|
|
5273
|
+
import { ${names.reactUploadsExportName} } from "${relativeModule(componentPath, uploadUiClientFilePath(names, config))}";
|
|
5274
|
+
|
|
5275
|
+
export type ${names.componentExportName}Props = {
|
|
5276
|
+
\tresourceId: string;
|
|
5277
|
+
\tdisabled?: boolean;
|
|
5278
|
+
\tonComplete?(objectKeys: readonly string[]): void | Promise<void>;
|
|
5279
|
+
};
|
|
5280
|
+
|
|
5281
|
+
export function ${names.componentExportName}({
|
|
5282
|
+
\tresourceId,
|
|
5283
|
+
\tdisabled = false,
|
|
5284
|
+
\tonComplete,
|
|
5285
|
+
}: ${names.componentExportName}Props) {
|
|
5286
|
+
\tconst inputId = useId();
|
|
5287
|
+
\tconst upload = ${names.reactUploadsExportName}.useUpload("${names.uploadName}", {
|
|
5288
|
+
\t\tonSuccess({ result }) {
|
|
5289
|
+
\t\t\treturn onComplete?.(result.objectKeys);
|
|
5290
|
+
\t\t},
|
|
5291
|
+
\t});
|
|
5292
|
+
\tconst maxSizeMb = upload.constraints?.maxSizeBytes
|
|
5293
|
+
\t\t? Math.round(upload.constraints.maxSizeBytes / 1024 / 1024)
|
|
5294
|
+
\t\t: undefined;
|
|
5295
|
+
|
|
5296
|
+
\treturn (
|
|
5297
|
+
\t\t<section className="${names.artifact.kebab}-uploader">
|
|
5298
|
+
\t\t\t<label htmlFor={inputId}>${names.artifact.pascal}</label>
|
|
5299
|
+
\t\t\t<input
|
|
5300
|
+
\t\t\t\tid={inputId}
|
|
5301
|
+
\t\t\t\ttype="file"
|
|
5302
|
+
\t\t\t\taccept={upload.accept}
|
|
5303
|
+
\t\t\t\tdisabled={disabled || upload.isUploading}
|
|
5304
|
+
\t\t\t\tonChange={(event) => {
|
|
5305
|
+
\t\t\t\t\tconst file = event.currentTarget.files?.[0];
|
|
5306
|
+
\t\t\t\t\tevent.currentTarget.value = "";
|
|
5307
|
+
\t\t\t\t\tif (!file) return;
|
|
5308
|
+
|
|
5309
|
+
\t\t\t\t\tupload.uploadFile(file, {
|
|
5310
|
+
\t\t\t\t\t\tmetadata: { resourceId },
|
|
5311
|
+
\t\t\t\t\t});
|
|
5312
|
+
\t\t\t\t}}
|
|
5313
|
+
\t\t\t/>
|
|
5314
|
+
\t\t\t<p>
|
|
5315
|
+
\t\t\t\tPDF or plain text{maxSizeMb ? <> up to {maxSizeMb} MB</> : null}.
|
|
5316
|
+
\t\t\t</p>
|
|
5317
|
+
|
|
5318
|
+
\t\t\t{upload.files[0] ? <p>{upload.files[0].fileName}</p> : null}
|
|
5319
|
+
\t\t\t{upload.isUploading ? (
|
|
5320
|
+
\t\t\t\t<div aria-live="polite">
|
|
5321
|
+
\t\t\t\t\t<progress
|
|
5322
|
+
\t\t\t\t\t\taria-label="Upload progress"
|
|
5323
|
+
\t\t\t\t\t\tmax={100}
|
|
5324
|
+
\t\t\t\t\t\tvalue={upload.progress}
|
|
5325
|
+
\t\t\t\t\t/>
|
|
5326
|
+
\t\t\t\t\t<span>{Math.round(upload.progress)}%</span>
|
|
5327
|
+
\t\t\t\t\t<button type="button" onClick={upload.abort}>
|
|
5328
|
+
\t\t\t\t\t\tCancel upload
|
|
5329
|
+
\t\t\t\t\t</button>
|
|
5330
|
+
\t\t\t\t</div>
|
|
5331
|
+
\t\t\t) : null}
|
|
5332
|
+
\t\t\t{upload.isError ? (
|
|
5333
|
+
\t\t\t\t<p role="alert">{uploadErrorMessage(upload.error)}</p>
|
|
5334
|
+
\t\t\t) : null}
|
|
5335
|
+
\t\t\t{upload.isSuccess ? <p role="status">Upload complete.</p> : null}
|
|
5336
|
+
\t\t\t{upload.isError || upload.isSuccess ? (
|
|
5337
|
+
\t\t\t\t<button type="button" onClick={upload.reset}>
|
|
5338
|
+
\t\t\t\t\tReset upload
|
|
5339
|
+
\t\t\t\t</button>
|
|
5340
|
+
\t\t\t) : null}
|
|
5341
|
+
\t\t</section>
|
|
5342
|
+
\t);
|
|
5343
|
+
}
|
|
5344
|
+
|
|
5345
|
+
function uploadErrorMessage(error: unknown): string {
|
|
5346
|
+
\treturn error instanceof Error ? error.message : "Upload failed.";
|
|
5347
|
+
}
|
|
5348
|
+
`;
|
|
5349
|
+
}
|
|
5350
|
+
|
|
5351
|
+
function uploadUiComponentTestFile(
|
|
5352
|
+
names: UploadNames,
|
|
5353
|
+
config: ResolvedBeignetConfig,
|
|
5354
|
+
): string {
|
|
5355
|
+
const testPath = uploadUiComponentTestFilePath(names, config);
|
|
5356
|
+
const testHelperPath = path.join(
|
|
5357
|
+
path.dirname(config.paths.useCaseBuilder),
|
|
5358
|
+
"beignet-test.ts",
|
|
5359
|
+
);
|
|
5360
|
+
|
|
5361
|
+
return `import { renderToStaticMarkup } from "react-dom/server";
|
|
5362
|
+
import { describe, expect, test } from "${relativeModule(testPath, testHelperPath)}";
|
|
5363
|
+
import { ${names.manifestExportName} } from "${relativeModule(testPath, uploadManifestFilePath(names, config))}";
|
|
5364
|
+
import { ${names.componentExportName} } from "${relativeModule(testPath, uploadUiComponentFilePath(names, config))}";
|
|
5365
|
+
|
|
5366
|
+
describe("${names.componentExportName}", () => {
|
|
5367
|
+
\ttest("renders the generated upload constraints", () => {
|
|
5368
|
+
\t\tconst markup = renderToStaticMarkup(
|
|
5369
|
+
\t\t\t<${names.componentExportName} resourceId="resource-1" />,
|
|
5370
|
+
\t\t);
|
|
5371
|
+
|
|
5372
|
+
\t\texpect(${names.manifestExportName}[0]?.file.maxSizeBytes).toBe(5 * 1024 * 1024);
|
|
5373
|
+
\t\texpect(markup).toContain('accept="application/pdf,text/plain"');
|
|
5374
|
+
\t\texpect(markup).toContain("PDF or plain text");
|
|
5375
|
+
\t});
|
|
5376
|
+
});
|
|
5377
|
+
`;
|
|
5378
|
+
}
|
|
5379
|
+
|
|
4971
5380
|
function uploadRouteFile(
|
|
4972
5381
|
names: UploadNames,
|
|
4973
5382
|
config: ResolvedBeignetConfig,
|
|
@@ -5087,63 +5496,70 @@ function featureUiComponentFile(
|
|
|
5087
5496
|
|
|
5088
5497
|
return `"use client";
|
|
5089
5498
|
|
|
5090
|
-
import {
|
|
5499
|
+
import { rootFormError } from "@beignet/react-hook-form";
|
|
5091
5500
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5092
|
-
import {
|
|
5501
|
+
import { rhf } from "${aliasModule(clientFormsPath(config))}";
|
|
5502
|
+
import { create${names.singularPascal} } from "${relativeModule(componentPath, resourceContractFilePath(names, config))}";
|
|
5093
5503
|
import {
|
|
5094
5504
|
\tcreate${names.singularPascal}MutationOptions,
|
|
5095
5505
|
\tinvalidate${names.pluralPascal},
|
|
5096
5506
|
\tlist${names.pluralPascal}QueryOptions,
|
|
5097
5507
|
} from "${relativeModule(componentPath, clientQueriesPath)}";
|
|
5098
5508
|
|
|
5509
|
+
const create${names.singularPascal}Form = rhf(create${names.singularPascal});
|
|
5510
|
+
|
|
5099
5511
|
export function ${names.componentExportName}() {
|
|
5100
|
-
\tconst [name, setName] = useState("");
|
|
5101
5512
|
\tconst queryClient = useQueryClient();
|
|
5102
5513
|
\tconst ${names.pluralCamel}Query = useQuery(list${names.pluralPascal}QueryOptions());
|
|
5514
|
+
\tconst form = create${names.singularPascal}Form.useForm({
|
|
5515
|
+
\t\tdefaultValues: { name: "" },
|
|
5516
|
+
\t});
|
|
5517
|
+
\tconst name = form.watch("name");
|
|
5103
5518
|
\tconst create${names.singularPascal}Mutation = useMutation({
|
|
5104
5519
|
\t\t...create${names.singularPascal}MutationOptions(),
|
|
5105
5520
|
\t\tonSuccess: async () => {
|
|
5106
|
-
\t\t\
|
|
5521
|
+
\t\t\tform.reset({ name: "" });
|
|
5107
5522
|
\t\t\tawait invalidate${names.pluralPascal}(queryClient);
|
|
5108
5523
|
\t\t},
|
|
5524
|
+
\t\tonError: (error) => {
|
|
5525
|
+
\t\t\tform.setError(
|
|
5526
|
+
\t\t\t\t"root",
|
|
5527
|
+
\t\t\t\trootFormError(error, "Could not create ${names.singularKebab}."),
|
|
5528
|
+
\t\t\t);
|
|
5529
|
+
\t\t},
|
|
5109
5530
|
\t});
|
|
5110
5531
|
|
|
5111
5532
|
\treturn (
|
|
5112
5533
|
\t\t<section className="${names.pluralKebab}-panel">
|
|
5113
5534
|
\t\t\t<form
|
|
5114
5535
|
\t\t\t\tclassName="${names.pluralKebab}-composer"
|
|
5115
|
-
\t\t\t\tonSubmit={(
|
|
5116
|
-
\t\t\t\t\
|
|
5117
|
-
\t\t\t\t\tconst trimmedName = name.trim();
|
|
5118
|
-
\t\t\t\t\tif (!trimmedName) return;
|
|
5536
|
+
\t\t\t\tonSubmit={form.handleSubmit((values) => {
|
|
5537
|
+
\t\t\t\t\tform.clearErrors("root");
|
|
5119
5538
|
\t\t\t\t\tcreate${names.singularPascal}Mutation.reset();
|
|
5120
5539
|
\t\t\t\t\tcreate${names.singularPascal}Mutation.mutate({
|
|
5121
|
-
\t\t\t\t\t\tbody:
|
|
5540
|
+
\t\t\t\t\t\tbody: values,
|
|
5122
5541
|
\t\t\t\t\t});
|
|
5123
|
-
\t\t\t\t}}
|
|
5542
|
+
\t\t\t\t})}
|
|
5124
5543
|
\t\t\t>
|
|
5125
5544
|
\t\t\t\t<label htmlFor="${names.singularKebab}-name">New ${names.singularKebab}</label>
|
|
5126
5545
|
\t\t\t\t<div className="${names.pluralKebab}-composer-row">
|
|
5127
5546
|
\t\t\t\t\t<input
|
|
5128
5547
|
\t\t\t\t\t\tid="${names.singularKebab}-name"
|
|
5129
|
-
\t\t\t\t\t\
|
|
5130
|
-
\t\t\t\t\t\tonChange={(event) => setName(event.currentTarget.value)}
|
|
5548
|
+
\t\t\t\t\t\t{...form.register("name")}
|
|
5131
5549
|
\t\t\t\t\t\tplaceholder="Name"
|
|
5132
5550
|
\t\t\t\t\t/>
|
|
5133
5551
|
\t\t\t\t\t<button
|
|
5134
5552
|
\t\t\t\t\t\ttype="submit"
|
|
5135
|
-
\t\t\t\t\t\tdisabled={!name
|
|
5553
|
+
\t\t\t\t\t\tdisabled={!name?.trim() || create${names.singularPascal}Mutation.isPending}
|
|
5136
5554
|
\t\t\t\t\t>
|
|
5137
5555
|
\t\t\t\t\t\t{create${names.singularPascal}Mutation.isPending ? "Creating" : "Create"}
|
|
5138
5556
|
\t\t\t\t\t</button>
|
|
5139
5557
|
\t\t\t\t</div>
|
|
5140
|
-
\t\t\t\t{
|
|
5141
|
-
\t\t\t\t\t<p role="alert">
|
|
5142
|
-
\t\t\t\t
|
|
5143
|
-
\t\t\t\t
|
|
5144
|
-
\t\t\t\t\t
|
|
5145
|
-
\t\t\t\t\t\t)}
|
|
5146
|
-
\t\t\t\t\t</p>
|
|
5558
|
+
\t\t\t\t{form.formState.errors.name ? (
|
|
5559
|
+
\t\t\t\t\t<p role="alert">{form.formState.errors.name.message}</p>
|
|
5560
|
+
\t\t\t\t) : null}
|
|
5561
|
+
\t\t\t\t{form.formState.errors.root ? (
|
|
5562
|
+
\t\t\t\t\t<p role="alert">{form.formState.errors.root.message}</p>
|
|
5147
5563
|
\t\t\t\t) : null}
|
|
5148
5564
|
\t\t\t</form>
|
|
5149
5565
|
|
|
@@ -5276,7 +5692,7 @@ function testFile(
|
|
|
5276
5692
|
options: ResourceGenerationOptions,
|
|
5277
5693
|
): string {
|
|
5278
5694
|
const repositoryPath = path.join(
|
|
5279
|
-
path.dirname(config.paths.
|
|
5695
|
+
path.dirname(config.paths.portWiring),
|
|
5280
5696
|
names.pluralKebab,
|
|
5281
5697
|
`in-memory-${names.singularKebab}-repository.ts`,
|
|
5282
5698
|
);
|
|
@@ -5292,9 +5708,9 @@ ${mode === "resource" ? `import { isAppError } from "@beignet/core/errors";\n` :
|
|
|
5292
5708
|
import { createTestApp } from "@beignet/web/testing";
|
|
5293
5709
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
5294
5710
|
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
5295
|
-
import { ${options.
|
|
5711
|
+
import { ${options.authorization ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenantScoped ? ", createTestTenant" : ""} } from "@beignet/core/testing";
|
|
5296
5712
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
5297
|
-
import {
|
|
5713
|
+
import { initialPorts } from "${aliasModule(config.paths.portWiring)}";
|
|
5298
5714
|
import { appContext } from "${aliasModule(serverContextPath)}";
|
|
5299
5715
|
import {
|
|
5300
5716
|
create${names.singularPascal},
|
|
@@ -5312,40 +5728,40 @@ describe("${names.pluralCamel} resource", () => {
|
|
|
5312
5728
|
const seed${names.pluralPascal} = [
|
|
5313
5729
|
{
|
|
5314
5730
|
id: "00000000-0000-4000-8000-000000000101",
|
|
5315
|
-
${options.
|
|
5731
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
|
|
5316
5732
|
version: 1,
|
|
5317
5733
|
createdAt: "2024-01-01T00:00:00.000Z",
|
|
5318
5734
|
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
5319
5735
|
},
|
|
5320
5736
|
{
|
|
5321
5737
|
id: "00000000-0000-4000-8000-000000000102",
|
|
5322
|
-
${options.
|
|
5738
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
|
|
5323
5739
|
version: 1,
|
|
5324
5740
|
createdAt: "2024-01-02T00:00:00.000Z",
|
|
5325
5741
|
updatedAt: "2024-01-02T00:00:00.000Z",
|
|
5326
5742
|
},
|
|
5327
5743
|
{
|
|
5328
5744
|
id: "00000000-0000-4000-8000-000000000103",
|
|
5329
|
-
${options.
|
|
5745
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
|
|
5330
5746
|
version: 1,
|
|
5331
5747
|
createdAt: "2024-01-03T00:00:00.000Z",
|
|
5332
5748
|
updatedAt: "2024-01-03T00:00:00.000Z",
|
|
5333
5749
|
},
|
|
5334
|
-
${options.
|
|
5750
|
+
${options.tenantScoped ? `\t\t\t{\n\t\t\t\tid: "00000000-0000-4000-8000-000000000104",\n\t\t\t\ttenantId: "tenant_other",\n\t\t\t\tname: "Other Tenant ${names.singularPascal}",\n\t\t\t\tversion: 1,\n\t\t\t\tcreatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t\tupdatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t},\n` : ""} ];
|
|
5335
5751
|
const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository(seed${names.pluralPascal});
|
|
5336
5752
|
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
5337
|
-
base:
|
|
5753
|
+
base: initialPorts,
|
|
5338
5754
|
overrides: {
|
|
5339
5755
|
${names.pluralCamel},
|
|
5340
5756
|
${
|
|
5341
|
-
options.
|
|
5757
|
+
options.authorization || options.tenantScoped
|
|
5342
5758
|
? `\t\t\t\tauth: {
|
|
5343
5759
|
getSession: async () => ({
|
|
5344
|
-
user: { id: "user_test", name: "Test User"${options.
|
|
5345
|
-
${options.
|
|
5760
|
+
user: { id: "user_test", name: "Test User"${options.tenantScoped ? `, tenantId: "tenant_test"` : ""} },
|
|
5761
|
+
${options.tenantScoped ? `\t\t\t\t\t\tsession: { tenantId: "tenant_test" },\n` : ""}
|
|
5346
5762
|
}),
|
|
5347
5763
|
},
|
|
5348
|
-
${options.
|
|
5764
|
+
${options.authorization ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
|
|
5349
5765
|
`
|
|
5350
5766
|
: `\t\t\t\tauth: {
|
|
5351
5767
|
getSession: async () => null,
|
|
@@ -5361,8 +5777,8 @@ ${options.auth ? `\t\t\t\tgate: appPorts.gate,\n` : ""}
|
|
|
5361
5777
|
});
|
|
5362
5778
|
const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
|
|
5363
5779
|
ports: testFixture.ports,
|
|
5364
|
-
actor: ${options.
|
|
5365
|
-
tenant: ${options.
|
|
5780
|
+
actor: ${options.authorization ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
|
|
5781
|
+
tenant: ${options.tenantScoped ? `createTestTenant("tenant_test")` : "null"},
|
|
5366
5782
|
});
|
|
5367
5783
|
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
5368
5784
|
|