@beignet/cli 0.0.38 → 0.0.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +154 -102
- 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 +4 -2
- 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/framework.d.ts +3 -0
- package/dist/framework.d.ts.map +1 -0
- package/dist/framework.js +6 -0
- package/dist/framework.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +149 -47
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +4 -2
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +174 -34
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +1 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/make/inbox.js +3 -3
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.d.ts.map +1 -1
- package/dist/make/payments.js +5 -3
- package/dist/make/payments.js.map +1 -1
- package/dist/make/shared.d.ts +10 -3
- package/dist/make/shared.d.ts.map +1 -1
- package/dist/make/shared.js +7 -8
- package/dist/make/shared.js.map +1 -1
- package/dist/make.d.ts +3 -2
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +549 -186
- 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/provider-add.d.ts +21 -3
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +101 -65
- package/dist/provider-add.js.map +1 -1
- package/dist/task.js +1 -1
- 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 +5 -5
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +17 -13
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shadcn.js +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/package.json +2 -2
- package/skills/app-structure/SKILL.md +29 -9
- package/src/choices.ts +38 -88
- package/src/config.ts +7 -3
- package/src/create-prompts.ts +20 -21
- package/src/create.ts +54 -36
- package/src/framework.ts +13 -0
- package/src/index.ts +193 -65
- package/src/inspect.ts +290 -39
- package/src/lib.ts +1 -1
- package/src/make/inbox.ts +3 -3
- package/src/make/payments.ts +10 -3
- package/src/make/shared.ts +16 -10
- package/src/make.ts +720 -176
- package/src/mcp.ts +20 -13
- package/src/provider-add.ts +159 -79
- package/src/task.ts +1 -1
- package/src/templates/base.ts +16 -16
- package/src/templates/index.ts +6 -6
- package/src/templates/server.ts +17 -13
- package/src/templates/shadcn.ts +1 -1
- package/src/templates/shared.ts +10 -10
- package/src/test-helpers/generated-app.ts +2 -1
package/src/make.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
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,
|
|
10
12
|
resolveConfig,
|
|
11
13
|
} from "./config.js";
|
|
14
|
+
import { assertNextFramework } from "./framework.js";
|
|
12
15
|
import {
|
|
13
16
|
adapterFilePath,
|
|
14
17
|
addNamedImport,
|
|
@@ -141,11 +144,13 @@ import {
|
|
|
141
144
|
} from "./make/shared.js";
|
|
142
145
|
import {
|
|
143
146
|
appendToArrayExpression,
|
|
147
|
+
appendToNamedArray,
|
|
144
148
|
appendToOutboxRegistryArray,
|
|
145
149
|
arrayInitializerInfo,
|
|
146
150
|
identifiersFromArrayExpression,
|
|
147
151
|
insertAfterImports,
|
|
148
152
|
} from "./registry-edits.js";
|
|
153
|
+
import { externalVersions } from "./templates/shared.js";
|
|
149
154
|
import { testSupportTemplateFiles } from "./templates/testing.js";
|
|
150
155
|
|
|
151
156
|
export type { MakeFeatureAddon, MakeFeatureRecipe } from "./choices.js";
|
|
@@ -164,8 +169,8 @@ type MakeResourceOptions = {
|
|
|
164
169
|
force?: boolean;
|
|
165
170
|
dryRun?: boolean;
|
|
166
171
|
config?: BeignetConfig;
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
authorization?: boolean;
|
|
173
|
+
tenantScoped?: boolean;
|
|
169
174
|
events?: boolean;
|
|
170
175
|
softDelete?: boolean;
|
|
171
176
|
};
|
|
@@ -313,6 +318,7 @@ type MakeUploadOptions = {
|
|
|
313
318
|
force?: boolean;
|
|
314
319
|
dryRun?: boolean;
|
|
315
320
|
config?: BeignetConfig;
|
|
321
|
+
ui?: boolean;
|
|
316
322
|
};
|
|
317
323
|
|
|
318
324
|
type MakeOutboxOptions = {
|
|
@@ -428,7 +434,7 @@ export async function makeFeature(
|
|
|
428
434
|
const addons = normalizeMakeFeatureAddons([
|
|
429
435
|
...makeFeatureRecipeAddons(options.recipe),
|
|
430
436
|
...(options.with ?? []),
|
|
431
|
-
...(options.events ? (["
|
|
437
|
+
...(options.events ? (["event"] as const) : []),
|
|
432
438
|
]);
|
|
433
439
|
const featureOptions = {
|
|
434
440
|
...options,
|
|
@@ -580,20 +586,25 @@ function resourceGenerationOptions(
|
|
|
580
586
|
): ResourceGenerationOptions {
|
|
581
587
|
if (mode === "feature") {
|
|
582
588
|
return {
|
|
583
|
-
|
|
584
|
-
|
|
589
|
+
authorization: false,
|
|
590
|
+
tenantScoped: false,
|
|
585
591
|
events: Boolean(options.events),
|
|
586
592
|
softDelete: false,
|
|
587
593
|
};
|
|
588
594
|
}
|
|
589
595
|
|
|
590
596
|
if (mode !== "resource") {
|
|
591
|
-
return {
|
|
597
|
+
return {
|
|
598
|
+
authorization: false,
|
|
599
|
+
tenantScoped: false,
|
|
600
|
+
events: false,
|
|
601
|
+
softDelete: false,
|
|
602
|
+
};
|
|
592
603
|
}
|
|
593
604
|
|
|
594
605
|
return {
|
|
595
|
-
|
|
596
|
-
|
|
606
|
+
authorization: Boolean(options.authorization),
|
|
607
|
+
tenantScoped: Boolean(options.tenantScoped),
|
|
597
608
|
events: Boolean(options.events),
|
|
598
609
|
softDelete: Boolean(options.softDelete),
|
|
599
610
|
};
|
|
@@ -604,11 +615,12 @@ async function makeFeatureSlice(
|
|
|
604
615
|
addons: readonly NormalizedMakeFeatureAddon[],
|
|
605
616
|
): Promise<MakeFeatureResult> {
|
|
606
617
|
let result = await makeFeatureResource(options);
|
|
618
|
+
const includeUploadUi = addons.includes("ui") && addons.includes("upload");
|
|
607
619
|
|
|
608
620
|
for (const addon of addons) {
|
|
609
621
|
result = mergeMakeResults(
|
|
610
622
|
result,
|
|
611
|
-
await makeFeatureAddon(addon, result.name, options),
|
|
623
|
+
await makeFeatureAddon(addon, result.name, options, includeUploadUi),
|
|
612
624
|
);
|
|
613
625
|
}
|
|
614
626
|
|
|
@@ -619,6 +631,7 @@ async function makeFeatureAddon(
|
|
|
619
631
|
addon: NormalizedMakeFeatureAddon,
|
|
620
632
|
feature: string,
|
|
621
633
|
options: MakeFeatureOptions,
|
|
634
|
+
includeUploadUi: boolean,
|
|
622
635
|
): Promise<MakeResult> {
|
|
623
636
|
const shared = {
|
|
624
637
|
cwd: options.cwd,
|
|
@@ -633,37 +646,37 @@ async function makeFeatureAddon(
|
|
|
633
646
|
if (addon === "factory") {
|
|
634
647
|
return makeFactory({
|
|
635
648
|
...shared,
|
|
636
|
-
name: `${feature}
|
|
649
|
+
name: `${feature}.${singularize(feature)}`,
|
|
637
650
|
skipPersistenceAssert: true,
|
|
638
651
|
});
|
|
639
652
|
}
|
|
640
653
|
if (addon === "task") {
|
|
641
|
-
return makeTask({ ...shared, name: `${feature}
|
|
654
|
+
return makeTask({ ...shared, name: `${feature}.backfill` });
|
|
642
655
|
}
|
|
643
656
|
if (addon === "event") {
|
|
644
|
-
return makeEvent({ ...shared, name: `${feature}
|
|
657
|
+
return makeEvent({ ...shared, name: `${feature}.created` });
|
|
645
658
|
}
|
|
646
659
|
if (addon === "job") {
|
|
647
|
-
return makeJob({ ...shared, name: `${feature}
|
|
660
|
+
return makeJob({ ...shared, name: `${feature}.process` });
|
|
648
661
|
}
|
|
649
662
|
if (addon === "listener") {
|
|
650
663
|
return makeListener({
|
|
651
664
|
...shared,
|
|
652
|
-
name: `${feature}
|
|
653
|
-
event: `${feature}
|
|
665
|
+
name: `${feature}.handle-created`,
|
|
666
|
+
event: `${feature}.created`,
|
|
654
667
|
skipEventAssert: true,
|
|
655
668
|
});
|
|
656
669
|
}
|
|
657
670
|
if (addon === "notification") {
|
|
658
|
-
return makeNotification({ ...shared, name: `${feature}
|
|
671
|
+
return makeNotification({ ...shared, name: `${feature}.created` });
|
|
659
672
|
}
|
|
660
673
|
if (addon === "schedule") {
|
|
661
|
-
return makeSchedule({ ...shared, name: `${feature}
|
|
674
|
+
return makeSchedule({ ...shared, name: `${feature}.daily-summary` });
|
|
662
675
|
}
|
|
663
676
|
if (addon === "seed") {
|
|
664
677
|
return makeSeed({
|
|
665
678
|
...shared,
|
|
666
|
-
name: `${feature}
|
|
679
|
+
name: `${feature}.demo-${feature}`,
|
|
667
680
|
skipPersistenceAssert: true,
|
|
668
681
|
});
|
|
669
682
|
}
|
|
@@ -671,7 +684,11 @@ async function makeFeatureAddon(
|
|
|
671
684
|
return makeFeatureUi({ ...shared, name: feature });
|
|
672
685
|
}
|
|
673
686
|
|
|
674
|
-
return makeUpload({
|
|
687
|
+
return makeUpload({
|
|
688
|
+
...shared,
|
|
689
|
+
name: `${feature}.attachment`,
|
|
690
|
+
ui: includeUploadUi,
|
|
691
|
+
});
|
|
675
692
|
}
|
|
676
693
|
|
|
677
694
|
function normalizeMakeFeatureAddons(
|
|
@@ -680,18 +697,7 @@ function normalizeMakeFeatureAddons(
|
|
|
680
697
|
const normalized = new Set<NormalizedMakeFeatureAddon>();
|
|
681
698
|
|
|
682
699
|
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") {
|
|
700
|
+
if (addon === "listener") {
|
|
695
701
|
normalized.add("event");
|
|
696
702
|
normalized.add("listener");
|
|
697
703
|
} else normalized.add(addon);
|
|
@@ -706,16 +712,16 @@ function makeFeatureRecipeAddons(
|
|
|
706
712
|
if (recipe === "full-slice") {
|
|
707
713
|
return [
|
|
708
714
|
"policy",
|
|
709
|
-
"
|
|
710
|
-
"
|
|
711
|
-
"
|
|
712
|
-
"
|
|
713
|
-
"
|
|
714
|
-
"
|
|
715
|
-
"
|
|
716
|
-
"
|
|
715
|
+
"factory",
|
|
716
|
+
"seed",
|
|
717
|
+
"task",
|
|
718
|
+
"event",
|
|
719
|
+
"listener",
|
|
720
|
+
"job",
|
|
721
|
+
"notification",
|
|
722
|
+
"schedule",
|
|
717
723
|
"ui",
|
|
718
|
-
"
|
|
724
|
+
"upload",
|
|
719
725
|
];
|
|
720
726
|
}
|
|
721
727
|
|
|
@@ -807,7 +813,7 @@ export async function makeUseCase(
|
|
|
807
813
|
|
|
808
814
|
return {
|
|
809
815
|
schemaVersion: 1,
|
|
810
|
-
name: `${names.feature.kebab}
|
|
816
|
+
name: `${names.feature.kebab}.${names.action.kebab}`,
|
|
811
817
|
targetDir,
|
|
812
818
|
dryRun: Boolean(options.dryRun),
|
|
813
819
|
files: [...generatedFiles.map((file) => file.path), indexFile.path],
|
|
@@ -853,7 +859,7 @@ export async function makeTest(
|
|
|
853
859
|
|
|
854
860
|
return {
|
|
855
861
|
schemaVersion: 1,
|
|
856
|
-
name: `${names.feature.kebab}
|
|
862
|
+
name: `${names.feature.kebab}.${names.action.kebab}`,
|
|
857
863
|
targetDir,
|
|
858
864
|
dryRun: Boolean(options.dryRun),
|
|
859
865
|
files: generatedFiles.map((file) => file.path),
|
|
@@ -1043,7 +1049,7 @@ export async function makeEvent(
|
|
|
1043
1049
|
config,
|
|
1044
1050
|
force: Boolean(options.force),
|
|
1045
1051
|
dryRun: Boolean(options.dryRun),
|
|
1046
|
-
name:
|
|
1052
|
+
name: names.stableName,
|
|
1047
1053
|
files: eventFiles(names, config),
|
|
1048
1054
|
index: featureArtifactIndexFile("event", names, config),
|
|
1049
1055
|
dependencies: {
|
|
@@ -1090,7 +1096,7 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
|
|
|
1090
1096
|
config,
|
|
1091
1097
|
force: Boolean(options.force),
|
|
1092
1098
|
dryRun: Boolean(options.dryRun),
|
|
1093
|
-
name:
|
|
1099
|
+
name: names.stableName,
|
|
1094
1100
|
files: [
|
|
1095
1101
|
...(await missingCapabilityBuilderFiles(targetDir, "jobs", config)),
|
|
1096
1102
|
...jobFiles(names, config),
|
|
@@ -1130,7 +1136,7 @@ export async function makeTask(
|
|
|
1130
1136
|
config,
|
|
1131
1137
|
force: Boolean(options.force),
|
|
1132
1138
|
dryRun: Boolean(options.dryRun),
|
|
1133
|
-
name:
|
|
1139
|
+
name: names.stableName,
|
|
1134
1140
|
files: [
|
|
1135
1141
|
...(await missingCapabilityBuilderFiles(targetDir, "tasks", config)),
|
|
1136
1142
|
...taskFiles(names, config),
|
|
@@ -1175,7 +1181,7 @@ export async function makeFactory(
|
|
|
1175
1181
|
config,
|
|
1176
1182
|
force: Boolean(options.force),
|
|
1177
1183
|
dryRun: Boolean(options.dryRun),
|
|
1178
|
-
name:
|
|
1184
|
+
name: names.stableName,
|
|
1179
1185
|
files: factoryFiles(names, config),
|
|
1180
1186
|
index: featureArtifactIndexFile("factory", names, config),
|
|
1181
1187
|
dependencies: {
|
|
@@ -1202,7 +1208,7 @@ export async function makeSeed(
|
|
|
1202
1208
|
config,
|
|
1203
1209
|
force: Boolean(options.force),
|
|
1204
1210
|
dryRun: Boolean(options.dryRun),
|
|
1205
|
-
name:
|
|
1211
|
+
name: names.stableName,
|
|
1206
1212
|
files: seedFiles(names, config),
|
|
1207
1213
|
index: featureArtifactIndexFile("seed", names, config),
|
|
1208
1214
|
dependencies: {
|
|
@@ -1221,7 +1227,7 @@ export async function makeSeed(
|
|
|
1221
1227
|
config,
|
|
1222
1228
|
force: Boolean(options.force),
|
|
1223
1229
|
dryRun: Boolean(options.dryRun),
|
|
1224
|
-
name:
|
|
1230
|
+
name: factoryNamesForSeed.stableName,
|
|
1225
1231
|
files: factoryFiles(factoryNamesForSeed, config),
|
|
1226
1232
|
index: featureArtifactIndexFile("factory", factoryNamesForSeed, config),
|
|
1227
1233
|
dependencies: {
|
|
@@ -1277,7 +1283,7 @@ export async function makeNotification(
|
|
|
1277
1283
|
// notifications uses ctx.ports.notifications, so the generator wires both
|
|
1278
1284
|
// ports with dev-default providers. Each port is wired independently and
|
|
1279
1285
|
// skipped when the app already has it, for example when the resend
|
|
1280
|
-
//
|
|
1286
|
+
// provider contributed the mailer. Plan the wiring before writing any
|
|
1281
1287
|
// files so an anchor miss aborts the generator without leaving partial
|
|
1282
1288
|
// output.
|
|
1283
1289
|
const notificationWirings = notificationPortWirings("make notification");
|
|
@@ -1290,7 +1296,7 @@ export async function makeNotification(
|
|
|
1290
1296
|
config,
|
|
1291
1297
|
force: Boolean(options.force),
|
|
1292
1298
|
dryRun: Boolean(options.dryRun),
|
|
1293
|
-
name:
|
|
1299
|
+
name: names.stableName,
|
|
1294
1300
|
files: [
|
|
1295
1301
|
...(await missingCapabilityBuilderFiles(
|
|
1296
1302
|
targetDir,
|
|
@@ -1341,7 +1347,7 @@ export async function makeListener(
|
|
|
1341
1347
|
config,
|
|
1342
1348
|
force: Boolean(options.force),
|
|
1343
1349
|
dryRun: Boolean(options.dryRun),
|
|
1344
|
-
name:
|
|
1350
|
+
name: names.stableName,
|
|
1345
1351
|
files: [
|
|
1346
1352
|
...(await missingCapabilityBuilderFiles(targetDir, "listeners", config)),
|
|
1347
1353
|
...listenerFiles(names, config),
|
|
@@ -1445,7 +1451,7 @@ async function assertListenerEventExists(
|
|
|
1445
1451
|
await stat(path.join(targetDir, file));
|
|
1446
1452
|
} catch {
|
|
1447
1453
|
throw new Error(
|
|
1448
|
-
`beignet make listener expected event ${names.eventName} at ${file}. Run beignet make event ${names.
|
|
1454
|
+
`beignet make listener expected event ${names.eventName} at ${file}. Run beignet make event ${names.eventName} first, or pass an event that exists.`,
|
|
1449
1455
|
);
|
|
1450
1456
|
}
|
|
1451
1457
|
}
|
|
@@ -1495,6 +1501,14 @@ export async function makeSchedule(
|
|
|
1495
1501
|
? resolveConfig(options.config)
|
|
1496
1502
|
: await loadBeignetConfig(targetDir);
|
|
1497
1503
|
|
|
1504
|
+
if (options.route) {
|
|
1505
|
+
assertNextFramework(
|
|
1506
|
+
config,
|
|
1507
|
+
"beignet make schedule --route",
|
|
1508
|
+
"a Next.js App Router cron handler",
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1498
1512
|
await assertFeatureArtifactApp(targetDir, config, "schedule", {
|
|
1499
1513
|
requireServer: Boolean(options.route),
|
|
1500
1514
|
});
|
|
@@ -1515,7 +1529,7 @@ export async function makeSchedule(
|
|
|
1515
1529
|
config,
|
|
1516
1530
|
force: Boolean(options.force),
|
|
1517
1531
|
dryRun: Boolean(options.dryRun),
|
|
1518
|
-
name:
|
|
1532
|
+
name: names.stableName,
|
|
1519
1533
|
files,
|
|
1520
1534
|
index: featureArtifactIndexFile("schedule", names, config),
|
|
1521
1535
|
dependencies: {
|
|
@@ -1710,6 +1724,9 @@ async function updateOutboxPortWiring(
|
|
|
1710
1724
|
path.join(infrastructureDir(config), "db/provider.ts"),
|
|
1711
1725
|
);
|
|
1712
1726
|
}
|
|
1727
|
+
if (await updateOutboxDrainProvider(targetDir, config, options)) {
|
|
1728
|
+
changes.updatedFiles.push(providersFilePath(config));
|
|
1729
|
+
}
|
|
1713
1730
|
if (await updateOutboxRepositoriesReturnType(targetDir, config, options)) {
|
|
1714
1731
|
changes.updatedFiles.push(drizzleRepositoriesPath(config));
|
|
1715
1732
|
}
|
|
@@ -1821,6 +1838,31 @@ async function updateOutboxInfrastructurePorts(
|
|
|
1821
1838
|
return true;
|
|
1822
1839
|
}
|
|
1823
1840
|
|
|
1841
|
+
async function projectSupportsNextAfter(targetDir: string): Promise<boolean> {
|
|
1842
|
+
const packageJson = JSON.parse(
|
|
1843
|
+
await readFile(path.join(targetDir, "package.json"), "utf8"),
|
|
1844
|
+
) as PackageJsonLike;
|
|
1845
|
+
const version =
|
|
1846
|
+
packageJson.dependencies?.next ?? packageJson.devDependencies?.next;
|
|
1847
|
+
if (!version) return false;
|
|
1848
|
+
|
|
1849
|
+
return version.split("||").every((range) => {
|
|
1850
|
+
const lowerBound =
|
|
1851
|
+
/^\s*(\^|~|>=|>|=|<=|<)?\s*v?(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?([+-][0-9A-Za-z.-]+)?(?=\s|$)/.exec(
|
|
1852
|
+
range,
|
|
1853
|
+
);
|
|
1854
|
+
if (!lowerBound) return false;
|
|
1855
|
+
|
|
1856
|
+
const operator = lowerBound[1];
|
|
1857
|
+
if (operator === "<" || operator === "<=") return false;
|
|
1858
|
+
if (lowerBound[5]?.startsWith("-")) return false;
|
|
1859
|
+
|
|
1860
|
+
const major = Number(lowerBound[2]);
|
|
1861
|
+
const minor = /^\d+$/.test(lowerBound[3] ?? "") ? Number(lowerBound[3]) : 0;
|
|
1862
|
+
return major > 15 || (major === 15 && minor >= 1);
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1824
1866
|
async function updateOutboxDatabaseProvider(
|
|
1825
1867
|
targetDir: string,
|
|
1826
1868
|
config: ResolvedBeignetConfig,
|
|
@@ -1919,6 +1961,95 @@ async function updateOutboxDatabaseProvider(
|
|
|
1919
1961
|
return true;
|
|
1920
1962
|
}
|
|
1921
1963
|
|
|
1964
|
+
async function updateOutboxDrainProvider(
|
|
1965
|
+
targetDir: string,
|
|
1966
|
+
config: ResolvedBeignetConfig,
|
|
1967
|
+
options: { dryRun: boolean },
|
|
1968
|
+
): Promise<boolean> {
|
|
1969
|
+
if (!(await projectSupportsNextAfter(targetDir))) return false;
|
|
1970
|
+
|
|
1971
|
+
const file = providersFilePath(config);
|
|
1972
|
+
const filePath = path.join(targetDir, file);
|
|
1973
|
+
const original = await readOptionalFile(filePath);
|
|
1974
|
+
if (original === undefined) {
|
|
1975
|
+
throw new Error(
|
|
1976
|
+
`Could not find the generated providers file ${file}. Register the outbox drain trigger manually, or restore the generated providers file before running make outbox.`,
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
let next = addNamedImport(
|
|
1981
|
+
original,
|
|
1982
|
+
"createObservedUnitOfWork",
|
|
1983
|
+
"@beignet/core/ports",
|
|
1984
|
+
);
|
|
1985
|
+
next = addNamedImport(next, "createProvider", "@beignet/core/providers");
|
|
1986
|
+
next = addNamedImport(next, "createNextOutboxDrainTrigger", "@beignet/next");
|
|
1987
|
+
next = addNamedImport(next, "after", "next/server");
|
|
1988
|
+
next = addNamedTypeImport(
|
|
1989
|
+
next,
|
|
1990
|
+
"AppContext",
|
|
1991
|
+
aliasModule(config.paths.appContext),
|
|
1992
|
+
);
|
|
1993
|
+
next = addNamedTypeImport(next, "AppPorts", aliasModule(config.paths.ports));
|
|
1994
|
+
next = addNamedTypeImport(
|
|
1995
|
+
next,
|
|
1996
|
+
"AppServiceContextInput",
|
|
1997
|
+
aliasModule(path.join(path.dirname(config.paths.server), "context.ts")),
|
|
1998
|
+
);
|
|
1999
|
+
|
|
2000
|
+
const providerName = "outboxDrainProvider";
|
|
2001
|
+
if (!new RegExp(`\\bconst\\s+${providerName}\\b`).test(next)) {
|
|
2002
|
+
const outboxModule = aliasModule(config.paths.outbox);
|
|
2003
|
+
const providerDefinition = `const ${providerName} = createProvider<
|
|
2004
|
+
\tPick<AppPorts, "uow">,
|
|
2005
|
+
\tAppContext,
|
|
2006
|
+
\tAppServiceContextInput
|
|
2007
|
+
>()({
|
|
2008
|
+
\tname: "outbox-drain-trigger",
|
|
2009
|
+
\tsetup({ ports, createServiceContext }): { ports: Pick<AppPorts, "uow"> } {
|
|
2010
|
+
\t\tconst triggerOutboxDrain: () => void = createNextOutboxDrainTrigger({
|
|
2011
|
+
\t\t\tdefer: after,
|
|
2012
|
+
\t\t\tcreateContext: () => createServiceContext(undefined),
|
|
2013
|
+
\t\t\tregistry: async () => (await import("${outboxModule}")).outboxRegistry,
|
|
2014
|
+
\t\t\tbatchSize: 100,
|
|
2015
|
+
\t\t});
|
|
2016
|
+
|
|
2017
|
+
\t\treturn {
|
|
2018
|
+
\t\t\tports: {
|
|
2019
|
+
\t\t\t\tuow: createObservedUnitOfWork({
|
|
2020
|
+
\t\t\t\t\tunitOfWork: ports.uow,
|
|
2021
|
+
\t\t\t\t\tafterCommit: triggerOutboxDrain,
|
|
2022
|
+
\t\t\t\t}),
|
|
2023
|
+
\t\t\t},
|
|
2024
|
+
\t\t};
|
|
2025
|
+
\t},
|
|
2026
|
+
});
|
|
2027
|
+
`;
|
|
2028
|
+
const withProvider = next.replace(
|
|
2029
|
+
/\nexport const providers = \[/,
|
|
2030
|
+
`\n${providerDefinition}\nexport const providers = [`,
|
|
2031
|
+
);
|
|
2032
|
+
if (withProvider === next) {
|
|
2033
|
+
throw new Error(
|
|
2034
|
+
`Could not find the exported providers array in ${file}. Register outboxDrainProvider manually, or restore the generated providers file before running make outbox.`,
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
next = withProvider;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
const appended = appendToNamedArray(next, "providers", providerName);
|
|
2041
|
+
if (appended.kind === "missing") {
|
|
2042
|
+
throw new Error(
|
|
2043
|
+
`Could not find the exported providers array in ${file}. Register outboxDrainProvider manually, or restore the generated providers file before running make outbox.`,
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
if (appended.kind === "updated") next = appended.source;
|
|
2047
|
+
|
|
2048
|
+
if (next === original) return false;
|
|
2049
|
+
if (!options.dryRun) await writeFile(filePath, next);
|
|
2050
|
+
return true;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
1922
2053
|
async function updateOutboxRepositoriesReturnType(
|
|
1923
2054
|
targetDir: string,
|
|
1924
2055
|
config: ResolvedBeignetConfig,
|
|
@@ -2064,7 +2195,16 @@ export async function makeUpload(
|
|
|
2064
2195
|
? resolveConfig(options.config)
|
|
2065
2196
|
: await loadBeignetConfig(targetDir);
|
|
2066
2197
|
|
|
2198
|
+
assertNextFramework(
|
|
2199
|
+
config,
|
|
2200
|
+
"beignet make upload",
|
|
2201
|
+
"a Next.js App Router upload handler",
|
|
2202
|
+
);
|
|
2203
|
+
|
|
2067
2204
|
await assertFeatureArtifactApp(targetDir, config, "upload");
|
|
2205
|
+
if (options.ui) {
|
|
2206
|
+
await assertUploadUiApp(targetDir, config);
|
|
2207
|
+
}
|
|
2068
2208
|
|
|
2069
2209
|
await wirePortProviders(
|
|
2070
2210
|
targetDir,
|
|
@@ -2080,10 +2220,10 @@ export async function makeUpload(
|
|
|
2080
2220
|
config,
|
|
2081
2221
|
force: Boolean(options.force),
|
|
2082
2222
|
dryRun: Boolean(options.dryRun),
|
|
2083
|
-
name:
|
|
2223
|
+
name: names.stableName,
|
|
2084
2224
|
files: [
|
|
2085
2225
|
...(await missingCapabilityBuilderFiles(targetDir, "uploads", config)),
|
|
2086
|
-
...uploadFiles(names, config),
|
|
2226
|
+
...uploadFiles(names, config, { ui: Boolean(options.ui) }),
|
|
2087
2227
|
],
|
|
2088
2228
|
index: featureArtifactIndexFile("upload", names, config),
|
|
2089
2229
|
dependencies: {
|
|
@@ -2112,6 +2252,44 @@ export async function makeUpload(
|
|
|
2112
2252
|
result.files.push(uploadRoutePath(config));
|
|
2113
2253
|
}
|
|
2114
2254
|
|
|
2255
|
+
if (options.ui) {
|
|
2256
|
+
const componentIndex = uploadUiComponentIndexFile(names, config);
|
|
2257
|
+
const componentIndexResult = await updateFeatureUiIndex(
|
|
2258
|
+
targetDir,
|
|
2259
|
+
componentIndex,
|
|
2260
|
+
{ dryRun: Boolean(options.dryRun) },
|
|
2261
|
+
);
|
|
2262
|
+
if (componentIndexResult === "created") {
|
|
2263
|
+
result.createdFiles.push(componentIndex.path);
|
|
2264
|
+
}
|
|
2265
|
+
if (componentIndexResult === "updated") {
|
|
2266
|
+
result.updatedFiles.push(componentIndex.path);
|
|
2267
|
+
}
|
|
2268
|
+
if (componentIndexResult === "skipped") {
|
|
2269
|
+
result.skippedFiles.push(componentIndex.path);
|
|
2270
|
+
}
|
|
2271
|
+
if (!result.files.includes(componentIndex.path)) {
|
|
2272
|
+
result.files.push(componentIndex.path);
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
if (
|
|
2276
|
+
await updatePackageDependencies(
|
|
2277
|
+
targetDir,
|
|
2278
|
+
{ "@beignet/react-uploads": beignetDependencyVersion },
|
|
2279
|
+
{ dryRun: Boolean(options.dryRun) },
|
|
2280
|
+
)
|
|
2281
|
+
) {
|
|
2282
|
+
result.updatedFiles.push("package.json");
|
|
2283
|
+
if (!result.files.includes("package.json")) {
|
|
2284
|
+
result.files.push("package.json");
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
result.createdFiles = uniqueStrings(result.createdFiles);
|
|
2290
|
+
result.updatedFiles = uniqueStrings(result.updatedFiles);
|
|
2291
|
+
result.skippedFiles = uniqueStrings(result.skippedFiles);
|
|
2292
|
+
|
|
2115
2293
|
return result;
|
|
2116
2294
|
}
|
|
2117
2295
|
|
|
@@ -2123,6 +2301,12 @@ export async function makeOutbox(
|
|
|
2123
2301
|
? resolveConfig(options.config)
|
|
2124
2302
|
: await loadBeignetConfig(targetDir);
|
|
2125
2303
|
|
|
2304
|
+
assertNextFramework(
|
|
2305
|
+
config,
|
|
2306
|
+
"beignet make outbox",
|
|
2307
|
+
"a Next.js App Router recovery drain handler",
|
|
2308
|
+
);
|
|
2309
|
+
|
|
2126
2310
|
await assertServerRuntimeApp(targetDir, config, "outbox");
|
|
2127
2311
|
await updateOutboxPortWiring(targetDir, config, { dryRun: true });
|
|
2128
2312
|
|
|
@@ -2174,6 +2358,9 @@ export async function makeOutbox(
|
|
|
2174
2358
|
config.paths.ports,
|
|
2175
2359
|
config.paths.portWiring,
|
|
2176
2360
|
path.join(infrastructureDir(config), "db/provider.ts"),
|
|
2361
|
+
...((await projectSupportsNextAfter(targetDir))
|
|
2362
|
+
? [providersFilePath(config)]
|
|
2363
|
+
: []),
|
|
2177
2364
|
drizzleRepositoriesPath(config),
|
|
2178
2365
|
outboxDrizzleSchemaFilePath(config),
|
|
2179
2366
|
drizzleSchemaIndexPath(config),
|
|
@@ -2195,7 +2382,10 @@ async function makeFeatureUi(
|
|
|
2195
2382
|
|
|
2196
2383
|
await assertFeatureUiApp(targetDir, config);
|
|
2197
2384
|
|
|
2198
|
-
const componentFiles =
|
|
2385
|
+
const componentFiles = [
|
|
2386
|
+
...(await missingFeatureUiFormsFiles(targetDir, config)),
|
|
2387
|
+
...featureUiComponentFiles(names, config),
|
|
2388
|
+
];
|
|
2199
2389
|
const indexFile = featureUiIndexFile(names, config);
|
|
2200
2390
|
const plannedFiles = await planGeneratedFiles(targetDir, componentFiles, {
|
|
2201
2391
|
force: Boolean(options.force),
|
|
@@ -2227,8 +2417,15 @@ async function makeFeatureUi(
|
|
|
2227
2417
|
await updatePackageDependencies(
|
|
2228
2418
|
targetDir,
|
|
2229
2419
|
{
|
|
2420
|
+
"@beignet/react-hook-form": beignetDependencyVersion,
|
|
2230
2421
|
"@beignet/react-query": beignetDependencyVersion,
|
|
2422
|
+
"@hookform/resolvers": (packageJson) =>
|
|
2423
|
+
packageJson.dependencies?.["@hookform/resolvers"] ??
|
|
2424
|
+
externalVersions.hookformResolvers,
|
|
2231
2425
|
"@tanstack/react-query": tanstackReactQueryDependencyVersion,
|
|
2426
|
+
"react-hook-form": (packageJson) =>
|
|
2427
|
+
packageJson.dependencies?.["react-hook-form"] ??
|
|
2428
|
+
externalVersions.reactHookForm,
|
|
2232
2429
|
},
|
|
2233
2430
|
{ dryRun: Boolean(options.dryRun) },
|
|
2234
2431
|
)
|
|
@@ -2370,7 +2567,7 @@ async function updateResourceWiring(
|
|
|
2370
2567
|
updated.add(config.paths.portWiring);
|
|
2371
2568
|
}
|
|
2372
2569
|
if (
|
|
2373
|
-
generationOptions.
|
|
2570
|
+
generationOptions.authorization &&
|
|
2374
2571
|
(await updateInfrastructureGatePolicies(targetDir, names, config, options))
|
|
2375
2572
|
) {
|
|
2376
2573
|
updated.add(config.paths.portWiring);
|
|
@@ -2432,13 +2629,12 @@ function typeHasTopLevelProperty(
|
|
|
2432
2629
|
function useCaseNames(input: string): UseCaseNames {
|
|
2433
2630
|
const parts = input
|
|
2434
2631
|
.trim()
|
|
2435
|
-
.split("
|
|
2436
|
-
.map((part) => part.trim())
|
|
2437
|
-
.filter(Boolean);
|
|
2632
|
+
.split(".")
|
|
2633
|
+
.map((part) => part.trim());
|
|
2438
2634
|
|
|
2439
|
-
if (parts.length !== 2) {
|
|
2635
|
+
if (parts.length !== 2 || parts.some((part) => part.length === 0)) {
|
|
2440
2636
|
throw new Error(
|
|
2441
|
-
"Use case name must use feature
|
|
2637
|
+
"Use case name must use feature.action format, for example projects.archive-project.",
|
|
2442
2638
|
);
|
|
2443
2639
|
}
|
|
2444
2640
|
|
|
@@ -2478,7 +2674,7 @@ function policyNames(input: string): PolicyNames {
|
|
|
2478
2674
|
}
|
|
2479
2675
|
|
|
2480
2676
|
function eventNames(input: string): EventNames {
|
|
2481
|
-
const names = featureArtifactNames(
|
|
2677
|
+
const names = featureArtifactNames(input, "Event");
|
|
2482
2678
|
const eventExportName = `${names.featureSingularPascal}${names.artifact.pascal}`;
|
|
2483
2679
|
|
|
2484
2680
|
assertGeneratedIdentifiers(input, "Event name", [eventExportName]);
|
|
@@ -2536,7 +2732,7 @@ function factoryNames(input: string): FactoryNames {
|
|
|
2536
2732
|
}
|
|
2537
2733
|
|
|
2538
2734
|
function defaultFactoryNamesForSeed(names: SeedNames): FactoryNames {
|
|
2539
|
-
return factoryNames(`${names.feature.kebab}
|
|
2735
|
+
return factoryNames(`${names.feature.kebab}.${names.featureSingularKebab}`);
|
|
2540
2736
|
}
|
|
2541
2737
|
|
|
2542
2738
|
function notificationNames(input: string): NotificationNames {
|
|
@@ -2593,8 +2789,12 @@ function scheduleNames(
|
|
|
2593
2789
|
function uploadNames(input: string): UploadNames {
|
|
2594
2790
|
const names = featureArtifactNames(input, "Upload");
|
|
2595
2791
|
const uploadExportName = `${names.artifact.pascal}Upload`;
|
|
2792
|
+
const componentExportName = `${names.artifact.pascal}Uploader`;
|
|
2596
2793
|
|
|
2597
|
-
assertGeneratedIdentifiers(input, "Upload name", [
|
|
2794
|
+
assertGeneratedIdentifiers(input, "Upload name", [
|
|
2795
|
+
uploadExportName,
|
|
2796
|
+
componentExportName,
|
|
2797
|
+
]);
|
|
2598
2798
|
|
|
2599
2799
|
return {
|
|
2600
2800
|
...names,
|
|
@@ -2602,19 +2802,16 @@ function uploadNames(input: string): UploadNames {
|
|
|
2602
2802
|
uploadExportName,
|
|
2603
2803
|
metadataSchemaName: `${uploadExportName}MetadataSchema`,
|
|
2604
2804
|
metadataTypeName: `${uploadExportName}Metadata`,
|
|
2805
|
+
fileConstraintsExportName: `${names.artifact.camel}UploadFile`,
|
|
2806
|
+
manifestExportName: `${names.artifact.camel}UploadManifest`,
|
|
2807
|
+
manifestFileName: `${names.artifact.kebab}-upload-manifest`,
|
|
2808
|
+
reactUploadsExportName: `${names.artifact.camel}ReactUploads`,
|
|
2809
|
+
clientFileName: `${names.artifact.kebab}-upload`,
|
|
2810
|
+
componentExportName,
|
|
2811
|
+
componentFileName: `${names.artifact.kebab}-uploader`,
|
|
2605
2812
|
};
|
|
2606
2813
|
}
|
|
2607
2814
|
|
|
2608
|
-
function normalizeEventReference(input: string): string {
|
|
2609
|
-
const trimmed = input.trim();
|
|
2610
|
-
if (trimmed.includes("/")) return trimmed;
|
|
2611
|
-
|
|
2612
|
-
const dotIndex = trimmed.indexOf(".");
|
|
2613
|
-
if (dotIndex === -1) return trimmed;
|
|
2614
|
-
|
|
2615
|
-
return `${trimmed.slice(0, dotIndex)}/${trimmed.slice(dotIndex + 1)}`;
|
|
2616
|
-
}
|
|
2617
|
-
|
|
2618
2815
|
function inferUseCaseKind(action: string): "command" | "query" {
|
|
2619
2816
|
if (/^(count|find|get|list|search)-/.test(`${action}-`)) {
|
|
2620
2817
|
return "query";
|
|
@@ -2628,8 +2825,8 @@ function resourceFiles(
|
|
|
2628
2825
|
persistence: ResourcePersistence = "memory",
|
|
2629
2826
|
mode: ResourceGenerationMode = "feature",
|
|
2630
2827
|
options: ResourceGenerationOptions = {
|
|
2631
|
-
|
|
2632
|
-
|
|
2828
|
+
authorization: false,
|
|
2829
|
+
tenantScoped: false,
|
|
2633
2830
|
events: false,
|
|
2634
2831
|
softDelete: false,
|
|
2635
2832
|
},
|
|
@@ -2684,7 +2881,7 @@ function resourceFiles(
|
|
|
2684
2881
|
},
|
|
2685
2882
|
]
|
|
2686
2883
|
: []),
|
|
2687
|
-
...(mode === "resource" && options.
|
|
2884
|
+
...(mode === "resource" && options.authorization
|
|
2688
2885
|
? [
|
|
2689
2886
|
{
|
|
2690
2887
|
path: path.join(featureDir, "tests/policy.test.ts"),
|
|
@@ -2754,6 +2951,99 @@ function featureUiComponentFiles(
|
|
|
2754
2951
|
];
|
|
2755
2952
|
}
|
|
2756
2953
|
|
|
2954
|
+
async function missingFeatureUiFormsFiles(
|
|
2955
|
+
targetDir: string,
|
|
2956
|
+
config: ResolvedBeignetConfig,
|
|
2957
|
+
): Promise<GeneratedFile[]> {
|
|
2958
|
+
const formsPath = clientFormsPath(config);
|
|
2959
|
+
const existing = await readOptionalFile(path.join(targetDir, formsPath));
|
|
2960
|
+
|
|
2961
|
+
if (existing === undefined) {
|
|
2962
|
+
return [
|
|
2963
|
+
{
|
|
2964
|
+
path: formsPath,
|
|
2965
|
+
content: `import { createReactHookForm } from "@beignet/react-hook-form";
|
|
2966
|
+
|
|
2967
|
+
export const rhf = createReactHookForm();
|
|
2968
|
+
`,
|
|
2969
|
+
},
|
|
2970
|
+
];
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
if (!sourceExportsValue(existing, "rhf")) {
|
|
2974
|
+
throw new Error(
|
|
2975
|
+
`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.`,
|
|
2976
|
+
);
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
return [];
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
async function assertUploadUiApp(
|
|
2983
|
+
targetDir: string,
|
|
2984
|
+
config: ResolvedBeignetConfig,
|
|
2985
|
+
): Promise<void> {
|
|
2986
|
+
const clientPath = clientIndexPath(config);
|
|
2987
|
+
const clientSource = await readOptionalFile(path.join(targetDir, clientPath));
|
|
2988
|
+
|
|
2989
|
+
if (!clientSource || !sourceExportsValue(clientSource, "rq")) {
|
|
2990
|
+
throw new Error(
|
|
2991
|
+
`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.`,
|
|
2992
|
+
);
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
function sourceExportsValue(source: string, name: string): boolean {
|
|
2997
|
+
const sourceFile = ts.createSourceFile(
|
|
2998
|
+
"client-forms.ts",
|
|
2999
|
+
source,
|
|
3000
|
+
ts.ScriptTarget.Latest,
|
|
3001
|
+
false,
|
|
3002
|
+
ts.ScriptKind.TS,
|
|
3003
|
+
);
|
|
3004
|
+
|
|
3005
|
+
for (const statement of sourceFile.statements) {
|
|
3006
|
+
const exported =
|
|
3007
|
+
ts.canHaveModifiers(statement) &&
|
|
3008
|
+
ts
|
|
3009
|
+
.getModifiers(statement)
|
|
3010
|
+
?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
3011
|
+
|
|
3012
|
+
if (exported && ts.isVariableStatement(statement)) {
|
|
3013
|
+
if (
|
|
3014
|
+
statement.declarationList.declarations.some(
|
|
3015
|
+
(declaration) =>
|
|
3016
|
+
ts.isIdentifier(declaration.name) && declaration.name.text === name,
|
|
3017
|
+
)
|
|
3018
|
+
) {
|
|
3019
|
+
return true;
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
if (
|
|
3024
|
+
exported &&
|
|
3025
|
+
ts.isFunctionDeclaration(statement) &&
|
|
3026
|
+
statement.name?.text === name
|
|
3027
|
+
) {
|
|
3028
|
+
return true;
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
if (
|
|
3032
|
+
ts.isExportDeclaration(statement) &&
|
|
3033
|
+
!statement.isTypeOnly &&
|
|
3034
|
+
statement.exportClause &&
|
|
3035
|
+
ts.isNamedExports(statement.exportClause) &&
|
|
3036
|
+
statement.exportClause.elements.some(
|
|
3037
|
+
(element) => !element.isTypeOnly && element.name.text === name,
|
|
3038
|
+
)
|
|
3039
|
+
) {
|
|
3040
|
+
return true;
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
return false;
|
|
3045
|
+
}
|
|
3046
|
+
|
|
2757
3047
|
function featureUiIndexFile(
|
|
2758
3048
|
names: FeatureUiNames,
|
|
2759
3049
|
config: ResolvedBeignetConfig,
|
|
@@ -2949,12 +3239,98 @@ function scheduleFiles(
|
|
|
2949
3239
|
function uploadFiles(
|
|
2950
3240
|
names: UploadNames,
|
|
2951
3241
|
config: ResolvedBeignetConfig,
|
|
3242
|
+
options: { ui: boolean },
|
|
2952
3243
|
): GeneratedFile[] {
|
|
2953
3244
|
return [
|
|
3245
|
+
{
|
|
3246
|
+
path: uploadManifestFilePath(names, config),
|
|
3247
|
+
content: uploadManifestFile(names),
|
|
3248
|
+
},
|
|
2954
3249
|
{
|
|
2955
3250
|
path: uploadFilePath(names, config),
|
|
2956
3251
|
content: uploadFile(names, config),
|
|
2957
3252
|
},
|
|
3253
|
+
...(options.ui ? uploadUiFiles(names, config) : []),
|
|
3254
|
+
];
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
function uploadFeatureDir(
|
|
3258
|
+
names: UploadNames,
|
|
3259
|
+
config: ResolvedBeignetConfig,
|
|
3260
|
+
): string {
|
|
3261
|
+
return path.join(config.paths.features, names.feature.kebab);
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
function uploadManifestFilePath(
|
|
3265
|
+
names: UploadNames,
|
|
3266
|
+
config: ResolvedBeignetConfig,
|
|
3267
|
+
): string {
|
|
3268
|
+
return path.join(
|
|
3269
|
+
uploadFeatureDir(names, config),
|
|
3270
|
+
`${names.manifestFileName}.ts`,
|
|
3271
|
+
);
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
function uploadUiClientFilePath(
|
|
3275
|
+
names: UploadNames,
|
|
3276
|
+
config: ResolvedBeignetConfig,
|
|
3277
|
+
): string {
|
|
3278
|
+
return path.join(
|
|
3279
|
+
uploadFeatureDir(names, config),
|
|
3280
|
+
"client",
|
|
3281
|
+
`${names.clientFileName}.ts`,
|
|
3282
|
+
);
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
function uploadUiComponentFilePath(
|
|
3286
|
+
names: UploadNames,
|
|
3287
|
+
config: ResolvedBeignetConfig,
|
|
3288
|
+
): string {
|
|
3289
|
+
return path.join(
|
|
3290
|
+
uploadFeatureDir(names, config),
|
|
3291
|
+
"components",
|
|
3292
|
+
`${names.componentFileName}.tsx`,
|
|
3293
|
+
);
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
function uploadUiComponentTestFilePath(
|
|
3297
|
+
names: UploadNames,
|
|
3298
|
+
config: ResolvedBeignetConfig,
|
|
3299
|
+
): string {
|
|
3300
|
+
return path.join(
|
|
3301
|
+
uploadFeatureDir(names, config),
|
|
3302
|
+
"tests",
|
|
3303
|
+
`${names.componentFileName}.test.tsx`,
|
|
3304
|
+
);
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
function uploadUiComponentIndexFile(
|
|
3308
|
+
names: UploadNames,
|
|
3309
|
+
config: ResolvedBeignetConfig,
|
|
3310
|
+
): GeneratedFile {
|
|
3311
|
+
return {
|
|
3312
|
+
path: path.join(uploadFeatureDir(names, config), "components", "index.ts"),
|
|
3313
|
+
content: `export { ${names.componentExportName} } from "./${names.componentFileName}";\n`,
|
|
3314
|
+
};
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
function uploadUiFiles(
|
|
3318
|
+
names: UploadNames,
|
|
3319
|
+
config: ResolvedBeignetConfig,
|
|
3320
|
+
): GeneratedFile[] {
|
|
3321
|
+
return [
|
|
3322
|
+
{
|
|
3323
|
+
path: uploadUiClientFilePath(names, config),
|
|
3324
|
+
content: uploadUiClientFile(names, config),
|
|
3325
|
+
},
|
|
3326
|
+
{
|
|
3327
|
+
path: uploadUiComponentFilePath(names, config),
|
|
3328
|
+
content: uploadUiComponentFile(names, config),
|
|
3329
|
+
},
|
|
3330
|
+
{
|
|
3331
|
+
path: uploadUiComponentTestFilePath(names, config),
|
|
3332
|
+
content: uploadUiComponentTestFile(names, config),
|
|
3333
|
+
},
|
|
2958
3334
|
];
|
|
2959
3335
|
}
|
|
2960
3336
|
|
|
@@ -3447,7 +3823,7 @@ function schemasFile(
|
|
|
3447
3823
|
|
|
3448
3824
|
export const ${names.singularPascal}Schema = z.object({
|
|
3449
3825
|
id: z.string().uuid(),
|
|
3450
|
-
${options.
|
|
3826
|
+
${options.tenantScoped ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
|
|
3451
3827
|
version: z.number().int().min(1),
|
|
3452
3828
|
createdAt: z.string().datetime(),
|
|
3453
3829
|
updatedAt: z.string().datetime(),
|
|
@@ -3547,7 +3923,7 @@ export const List${names.pluralPascal}OutputSchema = z.object({
|
|
|
3547
3923
|
});
|
|
3548
3924
|
|
|
3549
3925
|
export const Create${names.singularPascal}InputSchema = z.object({
|
|
3550
|
-
name: z.string().min(1).max(120),
|
|
3926
|
+
name: z.string().trim().min(1).max(120),
|
|
3551
3927
|
});
|
|
3552
3928
|
${
|
|
3553
3929
|
mode === "resource"
|
|
@@ -3557,7 +3933,7 @@ export const ${names.singularPascal}IdInputSchema = z.object({
|
|
|
3557
3933
|
});
|
|
3558
3934
|
|
|
3559
3935
|
export const Update${names.singularPascal}BodySchema = z.object({
|
|
3560
|
-
name: z.string().min(1).max(120),
|
|
3936
|
+
name: z.string().trim().min(1).max(120),
|
|
3561
3937
|
version: z.number().int().min(1),
|
|
3562
3938
|
});
|
|
3563
3939
|
|
|
@@ -3603,7 +3979,7 @@ function listUseCaseFile(
|
|
|
3603
3979
|
|
|
3604
3980
|
return `import "@beignet/core/server-only";
|
|
3605
3981
|
import { normalizeCursorPage } from "@beignet/core/pagination";
|
|
3606
|
-
${options.
|
|
3982
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3607
3983
|
import {
|
|
3608
3984
|
decode${names.singularPascal}Cursor,
|
|
3609
3985
|
List${names.pluralPascal}InputSchema,
|
|
@@ -3615,7 +3991,7 @@ export const list${names.pluralPascal}UseCase = useCase
|
|
|
3615
3991
|
.input(List${names.pluralPascal}InputSchema)
|
|
3616
3992
|
.output(List${names.pluralPascal}OutputSchema)
|
|
3617
3993
|
.run(async ({ ctx, input }) => {
|
|
3618
|
-
${options.
|
|
3994
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const page = normalizeCursorPage(input, {
|
|
3619
3995
|
defaultLimit: 20,
|
|
3620
3996
|
maxLimit: 100,
|
|
3621
3997
|
});
|
|
@@ -3626,7 +4002,7 @@ ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const
|
|
|
3626
4002
|
name: input.name,
|
|
3627
4003
|
sortBy: input.sortBy,
|
|
3628
4004
|
sortDirection: input.sortDirection,
|
|
3629
|
-
}${options.
|
|
4005
|
+
}${options.tenantScoped ? ", scope" : ""});
|
|
3630
4006
|
});
|
|
3631
4007
|
`;
|
|
3632
4008
|
}
|
|
@@ -3642,7 +4018,7 @@ function createUseCaseFile(
|
|
|
3642
4018
|
);
|
|
3643
4019
|
|
|
3644
4020
|
return `import "@beignet/core/server-only";
|
|
3645
|
-
${options.
|
|
4021
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3646
4022
|
import {
|
|
3647
4023
|
Create${names.singularPascal}InputSchema,
|
|
3648
4024
|
${names.singularPascal}Schema,
|
|
@@ -3654,7 +4030,7 @@ export const create${names.singularPascal}UseCase = useCase
|
|
|
3654
4030
|
.input(Create${names.singularPascal}InputSchema)
|
|
3655
4031
|
.output(${names.singularPascal}Schema)
|
|
3656
4032
|
.run(async ({ ctx, input }) => {
|
|
3657
|
-
${options.
|
|
4033
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""}${options.authorization ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.create");\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.create(input${options.tenantScoped ? ", scope" : ""});
|
|
3658
4034
|
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Created, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
|
|
3659
4035
|
return ${names.singularCamel};
|
|
3660
4036
|
});
|
|
@@ -3672,7 +4048,7 @@ function getUseCaseFile(
|
|
|
3672
4048
|
);
|
|
3673
4049
|
|
|
3674
4050
|
return `import "@beignet/core/server-only";
|
|
3675
|
-
${options.
|
|
4051
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
3676
4052
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3677
4053
|
import {
|
|
3678
4054
|
${names.singularPascal}IdInputSchema,
|
|
@@ -3684,7 +4060,7 @@ export const get${names.singularPascal}UseCase = useCase
|
|
|
3684
4060
|
.input(${names.singularPascal}IdInputSchema)
|
|
3685
4061
|
.output(${names.singularPascal}Schema)
|
|
3686
4062
|
.run(async ({ ctx, input }) => {
|
|
3687
|
-
${options.
|
|
4063
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
|
|
3688
4064
|
if (!${names.singularCamel}) {
|
|
3689
4065
|
throw appError("${names.singularPascal}NotFound", {
|
|
3690
4066
|
details: { id: input.id },
|
|
@@ -3707,7 +4083,7 @@ function updateUseCaseFile(
|
|
|
3707
4083
|
);
|
|
3708
4084
|
|
|
3709
4085
|
return `import "@beignet/core/server-only";
|
|
3710
|
-
${options.
|
|
4086
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
3711
4087
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3712
4088
|
${options.events ? `import { ${names.singularPascal}Updated } from "../domain/events";\n` : ""}import {
|
|
3713
4089
|
Update${names.singularPascal}InputSchema,
|
|
@@ -3719,13 +4095,13 @@ export const update${names.singularPascal}UseCase = useCase
|
|
|
3719
4095
|
.input(Update${names.singularPascal}InputSchema)
|
|
3720
4096
|
.output(${names.singularPascal}Schema)
|
|
3721
4097
|
.run(async ({ ctx, input }) => {
|
|
3722
|
-
${options.
|
|
4098
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
|
|
3723
4099
|
if (!existing) {
|
|
3724
4100
|
throw appError("${names.singularPascal}NotFound", {
|
|
3725
4101
|
details: { id: input.id },
|
|
3726
4102
|
});
|
|
3727
4103
|
}
|
|
3728
|
-
${options.
|
|
4104
|
+
${options.authorization ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
|
|
3729
4105
|
throw appError("${names.singularPascal}Conflict", {
|
|
3730
4106
|
details: { id: input.id, expectedVersion: existing.version },
|
|
3731
4107
|
});
|
|
@@ -3733,7 +4109,7 @@ ${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", ex
|
|
|
3733
4109
|
|
|
3734
4110
|
const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.update({
|
|
3735
4111
|
...input,
|
|
3736
|
-
}${options.
|
|
4112
|
+
}${options.tenantScoped ? ", scope" : ""});
|
|
3737
4113
|
if (!${names.singularCamel}) {
|
|
3738
4114
|
throw appError("${names.singularPascal}Conflict", {
|
|
3739
4115
|
details: { id: input.id, expectedVersion: existing.version },
|
|
@@ -3757,7 +4133,7 @@ function deleteUseCaseFile(
|
|
|
3757
4133
|
|
|
3758
4134
|
return `import "@beignet/core/server-only";
|
|
3759
4135
|
import { z } from "zod";
|
|
3760
|
-
${options.
|
|
4136
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
3761
4137
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3762
4138
|
${options.events ? `import { ${names.singularPascal}Deleted } from "../domain/events";\n` : ""}import { ${names.singularPascal}IdInputSchema } from "../schemas";
|
|
3763
4139
|
|
|
@@ -3766,7 +4142,7 @@ export const delete${names.singularPascal}UseCase = useCase
|
|
|
3766
4142
|
.input(${names.singularPascal}IdInputSchema)
|
|
3767
4143
|
.output(z.void())
|
|
3768
4144
|
.run(async ({ ctx, input }) => {
|
|
3769
|
-
${options.
|
|
4145
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""}${options.authorization ? `\t\tconst existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});\n\t\tif (!existing) {\n\t\t\tthrow appError("${names.singularPascal}NotFound", {\n\t\t\t\tdetails: { id: input.id },\n\t\t\t});\n\t\t}\n\t\tawait ctx.gate.authorize("${names.pluralCamel}.delete", existing);\n\n` : ""} const deleted = await ctx.ports.${names.pluralCamel}.delete(input.id${options.tenantScoped ? ", scope" : ""});
|
|
3770
4146
|
if (!deleted) {
|
|
3771
4147
|
throw appError("${names.singularPascal}NotFound", {
|
|
3772
4148
|
details: { id: input.id },
|
|
@@ -3806,7 +4182,7 @@ function repositoryPortFile(
|
|
|
3806
4182
|
PageResult,
|
|
3807
4183
|
SortDirection,
|
|
3808
4184
|
} from "@beignet/core/pagination";
|
|
3809
|
-
${options.
|
|
4185
|
+
${options.tenantScoped ? `import type { TenantScope } from "@beignet/core/tenancy";\n` : ""}import type {
|
|
3810
4186
|
Create${names.singularPascal}Input,
|
|
3811
4187
|
${names.singularPascal}Cursor,
|
|
3812
4188
|
${names.singularPascal}SortBy,
|
|
@@ -3823,9 +4199,9 @@ export type List${names.pluralPascal}Query = {
|
|
|
3823
4199
|
};
|
|
3824
4200
|
|
|
3825
4201
|
export interface ${names.singularPascal}Repository {
|
|
3826
|
-
list(query: List${names.pluralPascal}Query${options.
|
|
3827
|
-
create(input: Create${names.singularPascal}Input${options.
|
|
3828
|
-
${mode === "resource" ? ` findById(id: string${options.
|
|
4202
|
+
list(query: List${names.pluralPascal}Query${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<List${names.pluralPascal}Result>;
|
|
4203
|
+
create(input: Create${names.singularPascal}Input${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal}>;
|
|
4204
|
+
${mode === "resource" ? ` findById(id: string${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal} | null>;\n update(input: Update${names.singularPascal}Input${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal} | null>;\n delete(id: string${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<boolean>;\n` : ""}}
|
|
3829
4205
|
`;
|
|
3830
4206
|
}
|
|
3831
4207
|
|
|
@@ -3859,14 +4235,14 @@ function inMemoryRepositoryFile(
|
|
|
3859
4235
|
? " || existing.deletedAt !== null"
|
|
3860
4236
|
: "";
|
|
3861
4237
|
const deleteImplementation = options.softDelete
|
|
3862
|
-
? `${options.
|
|
4238
|
+
? `${options.tenantScoped ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.tenantId !== tenantId || existing.deletedAt !== null) return false;\n` : `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.deletedAt !== null) return false;\n`} const now = new Date().toISOString();
|
|
3863
4239
|
${names.pluralCamel}.set(id, { ...existing, deletedAt: now, updatedAt: now });
|
|
3864
4240
|
return true;`
|
|
3865
|
-
: `${options.
|
|
4241
|
+
: `${options.tenantScoped ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (existing?.tenantId !== tenantId) return false;\n` : ""} return ${names.pluralCamel}.delete(id);`;
|
|
3866
4242
|
|
|
3867
4243
|
return `import "@beignet/core/server-only";
|
|
3868
4244
|
import { cursorPageResult } from "@beignet/core/pagination";
|
|
3869
|
-
${options.
|
|
4245
|
+
${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
3870
4246
|
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
3871
4247
|
import type {
|
|
3872
4248
|
Create${names.singularPascal}Input,
|
|
@@ -3876,7 +4252,7 @@ ${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.s
|
|
|
3876
4252
|
${recordType}function to${names.singularPascal}(${names.singularCamel}: ${mapValueType}): ${names.singularPascal} {
|
|
3877
4253
|
return {
|
|
3878
4254
|
id: ${names.singularCamel}.id,
|
|
3879
|
-
${options.
|
|
4255
|
+
${options.tenantScoped ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
|
|
3880
4256
|
version: ${names.singularCamel}.version,
|
|
3881
4257
|
createdAt: ${names.singularCamel}.createdAt,
|
|
3882
4258
|
updatedAt: ${names.singularCamel}.updatedAt,
|
|
@@ -3934,10 +4310,10 @@ export function createInMemory${names.singularPascal}Repository(
|
|
|
3934
4310
|
);
|
|
3935
4311
|
|
|
3936
4312
|
return {
|
|
3937
|
-
async list(query${options.
|
|
4313
|
+
async list(query${options.tenantScoped ? ", scope" : ""}) {
|
|
3938
4314
|
const name = query.name?.toLocaleLowerCase();
|
|
3939
|
-
${options.
|
|
3940
|
-
${activeFilter}${options.
|
|
4315
|
+
${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values())
|
|
4316
|
+
${activeFilter}${options.tenantScoped ? `\t\t\t\t.filter((${names.singularCamel}) => ${names.singularCamel}.tenantId === tenantId)\n` : ""} .filter((${names.singularCamel}) => !name || ${names.singularCamel}.name.toLocaleLowerCase().includes(name))
|
|
3941
4317
|
.sort((left, right) => compare${names.pluralPascal}(left, right, query))
|
|
3942
4318
|
.filter((${names.singularCamel}) => isAfter${names.singularPascal}Cursor(${names.singularCamel}, query));
|
|
3943
4319
|
const pageItems = all${names.pluralPascal}.slice(0, query.page.limit);
|
|
@@ -3952,11 +4328,11 @@ ${activeFilter}${options.tenant ? `\t\t\t\t.filter((${names.singularCamel}) => $
|
|
|
3952
4328
|
nextCursor,
|
|
3953
4329
|
);
|
|
3954
4330
|
},
|
|
3955
|
-
async create(input${options.
|
|
4331
|
+
async create(input${options.tenantScoped ? ", scope" : ""}) {
|
|
3956
4332
|
const now = new Date().toISOString();
|
|
3957
|
-
${options.
|
|
4333
|
+
${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel}: ${names.singularPascal} = {
|
|
3958
4334
|
id: crypto.randomUUID(),
|
|
3959
|
-
${options.
|
|
4335
|
+
${options.tenantScoped ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
|
|
3960
4336
|
version: 1,
|
|
3961
4337
|
createdAt: now,
|
|
3962
4338
|
updatedAt: now,
|
|
@@ -3966,7 +4342,7 @@ ${options.tenant ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
|
|
|
3966
4342
|
${newDeletedAtField} });
|
|
3967
4343
|
return ${names.singularCamel};
|
|
3968
4344
|
},
|
|
3969
|
-
${mode === "resource" ? ` async findById(id: string${options.
|
|
4345
|
+
${mode === "resource" ? ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {\n${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel} = ${names.pluralCamel}.get(id) ?? null;\n${options.tenantScoped ? `\t\t\tif (${names.singularCamel}?.tenantId !== tenantId) return null;\n` : ""}${findDeletedGuard} return ${names.singularCamel} ? to${names.singularPascal}(${names.singularCamel}) : null;\n },\n async update(input${options.tenantScoped ? ", scope" : ""}) {\n${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const existing = ${names.pluralCamel}.get(input.id);\n if (!existing${options.tenantScoped ? " || existing.tenantId !== tenantId" : ""}${updateDeletedGuard} || existing.version !== input.version) return null;\n\n const next: ${mapValueType} = {\n ...existing,\n name: input.name,\n version: existing.version + 1,\n updatedAt: new Date().toISOString(),\n };\n ${names.pluralCamel}.set(input.id, next);\n return to${names.singularPascal}(next);\n },\n async delete(id: string${options.tenantScoped ? ", scope" : ""}) {\n${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""}${deleteImplementation}\n },\n` : ""}
|
|
3970
4346
|
};
|
|
3971
4347
|
}
|
|
3972
4348
|
`;
|
|
@@ -3981,7 +4357,7 @@ function drizzleSchemaFile(
|
|
|
3981
4357
|
|
|
3982
4358
|
export const ${names.pluralCamel} = ${dialect.tableFunction}("${names.pluralKebab.replaceAll("-", "_")}", {
|
|
3983
4359
|
id: ${dialect.idColumn("id")}.primaryKey(),
|
|
3984
|
-
${options.
|
|
4360
|
+
${options.tenantScoped ? `\ttenantId: ${dialect.idColumn("tenant_id")}.notNull(),\n` : ""} name: text("name").notNull(),
|
|
3985
4361
|
version: ${dialect.integerColumn("version")}.notNull(),
|
|
3986
4362
|
createdAt: ${dialect.timestampColumn("created_at")}.notNull(),
|
|
3987
4363
|
updatedAt: ${dialect.timestampColumn("updated_at")}.notNull(),
|
|
@@ -4091,20 +4467,20 @@ function drizzleRepositoryFile(
|
|
|
4091
4467
|
const schemaPath = drizzleSchemaIndexPath(config);
|
|
4092
4468
|
const findWhere = drizzleResourceWhere(names, options, [
|
|
4093
4469
|
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
4094
|
-
...(options.
|
|
4470
|
+
...(options.tenantScoped
|
|
4095
4471
|
? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
|
|
4096
4472
|
: []),
|
|
4097
4473
|
]);
|
|
4098
4474
|
const updateWhere = drizzleResourceWhere(names, options, [
|
|
4099
4475
|
`eq(schema.${names.pluralCamel}.id, input.id)`,
|
|
4100
4476
|
`eq(schema.${names.pluralCamel}.version, input.version)`,
|
|
4101
|
-
...(options.
|
|
4477
|
+
...(options.tenantScoped
|
|
4102
4478
|
? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
|
|
4103
4479
|
: []),
|
|
4104
4480
|
]);
|
|
4105
4481
|
const deleteWhere = drizzleResourceWhere(names, options, [
|
|
4106
4482
|
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
4107
|
-
...(options.
|
|
4483
|
+
...(options.tenantScoped
|
|
4108
4484
|
? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
|
|
4109
4485
|
: []),
|
|
4110
4486
|
]);
|
|
@@ -4122,7 +4498,7 @@ function drizzleRepositoryFile(
|
|
|
4122
4498
|
].filter((name): name is string => Boolean(name));
|
|
4123
4499
|
// After a version-guarded update succeeds, re-select by identity only —
|
|
4124
4500
|
// matching what `.returning()` yields on the dialects that support it.
|
|
4125
|
-
const updateReselectWhere = options.
|
|
4501
|
+
const updateReselectWhere = options.tenantScoped
|
|
4126
4502
|
? `and(eq(schema.${names.pluralCamel}.id, input.id), eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)))`
|
|
4127
4503
|
: `eq(schema.${names.pluralCamel}.id, input.id)`;
|
|
4128
4504
|
// drizzle-orm/mysql2 has no `.returning(...)`; mutations resolve to a
|
|
@@ -4145,13 +4521,13 @@ function affectedRows(result: unknown): number {
|
|
|
4145
4521
|
`
|
|
4146
4522
|
: "";
|
|
4147
4523
|
const createMethod = dialect.supportsReturning
|
|
4148
|
-
? ` async create(input${options.
|
|
4524
|
+
? ` async create(input${options.tenantScoped ? ", scope" : ""}) {
|
|
4149
4525
|
const now = new Date().toISOString();
|
|
4150
4526
|
const [row] = await db
|
|
4151
4527
|
.insert(schema.${names.pluralCamel})
|
|
4152
4528
|
.values({
|
|
4153
4529
|
id: crypto.randomUUID(),
|
|
4154
|
-
${options.
|
|
4530
|
+
${options.tenantScoped ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
|
|
4155
4531
|
version: 1,
|
|
4156
4532
|
createdAt: now,
|
|
4157
4533
|
updatedAt: now,
|
|
@@ -4165,11 +4541,11 @@ ${options.tenant ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name:
|
|
|
4165
4541
|
return to${names.singularPascal}(row);
|
|
4166
4542
|
},
|
|
4167
4543
|
`
|
|
4168
|
-
: ` async create(input${options.
|
|
4544
|
+
: ` async create(input${options.tenantScoped ? ", scope" : ""}) {
|
|
4169
4545
|
const now = new Date().toISOString();
|
|
4170
4546
|
const ${names.singularCamel}: ${names.singularPascal} = {
|
|
4171
4547
|
id: crypto.randomUUID(),
|
|
4172
|
-
${options.
|
|
4548
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
|
|
4173
4549
|
version: 1,
|
|
4174
4550
|
createdAt: now,
|
|
4175
4551
|
updatedAt: now,
|
|
@@ -4183,7 +4559,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
|
|
|
4183
4559
|
mode !== "resource"
|
|
4184
4560
|
? ""
|
|
4185
4561
|
: dialect.supportsReturning
|
|
4186
|
-
? ` async findById(id: string${options.
|
|
4562
|
+
? ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
4187
4563
|
const [row] = await db
|
|
4188
4564
|
.select()
|
|
4189
4565
|
.from(schema.${names.pluralCamel})
|
|
@@ -4192,7 +4568,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
|
|
|
4192
4568
|
|
|
4193
4569
|
return row ? to${names.singularPascal}(row) : null;
|
|
4194
4570
|
},
|
|
4195
|
-
async update(input${options.
|
|
4571
|
+
async update(input${options.tenantScoped ? ", scope" : ""}) {
|
|
4196
4572
|
const [row] = await db
|
|
4197
4573
|
.update(schema.${names.pluralCamel})
|
|
4198
4574
|
.set({
|
|
@@ -4205,12 +4581,12 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
|
|
|
4205
4581
|
|
|
4206
4582
|
return row ? to${names.singularPascal}(row) : null;
|
|
4207
4583
|
},
|
|
4208
|
-
async delete(id: string${options.
|
|
4584
|
+
async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
4209
4585
|
${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst rows = await db\n\t\t\t\t.update(schema.${names.pluralCamel})\n\t\t\t\t.set({ deletedAt: now, updatedAt: now })\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n` : `\t\t\tconst rows = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n`}
|
|
4210
4586
|
return rows.length > 0;
|
|
4211
4587
|
},
|
|
4212
4588
|
`
|
|
4213
|
-
: ` async findById(id: string${options.
|
|
4589
|
+
: ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
4214
4590
|
const [row] = await db
|
|
4215
4591
|
.select()
|
|
4216
4592
|
.from(schema.${names.pluralCamel})
|
|
@@ -4219,7 +4595,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
|
|
|
4219
4595
|
|
|
4220
4596
|
return row ? to${names.singularPascal}(row) : null;
|
|
4221
4597
|
},
|
|
4222
|
-
async update(input${options.
|
|
4598
|
+
async update(input${options.tenantScoped ? ", scope" : ""}) {
|
|
4223
4599
|
const result = await db
|
|
4224
4600
|
.update(schema.${names.pluralCamel})
|
|
4225
4601
|
.set({
|
|
@@ -4239,7 +4615,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
|
|
|
4239
4615
|
|
|
4240
4616
|
return row ? to${names.singularPascal}(row) : null;
|
|
4241
4617
|
},
|
|
4242
|
-
async delete(id: string${options.
|
|
4618
|
+
async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
4243
4619
|
${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst result = await db\n\t\t\t\t.update(schema.${names.pluralCamel})\n\t\t\t\t.set({ deletedAt: now, updatedAt: now })\n\t\t\t\t.where(${deleteWhere});\n` : `\t\t\tconst result = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere});\n`}
|
|
4244
4620
|
return affectedRows(result) > 0;
|
|
4245
4621
|
},
|
|
@@ -4247,7 +4623,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
|
|
|
4247
4623
|
|
|
4248
4624
|
return `import "@beignet/core/server-only";
|
|
4249
4625
|
import { cursorPageResult } from "@beignet/core/pagination";
|
|
4250
|
-
${options.
|
|
4626
|
+
${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
|
|
4251
4627
|
import { ${drizzleImports.join(", ")} } from "drizzle-orm";
|
|
4252
4628
|
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
4253
4629
|
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
@@ -4262,7 +4638,7 @@ type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelec
|
|
|
4262
4638
|
function to${names.singularPascal}(row: ${names.singularPascal}Row): ${names.singularPascal} {
|
|
4263
4639
|
return {
|
|
4264
4640
|
id: row.id,
|
|
4265
|
-
${options.
|
|
4641
|
+
${options.tenantScoped ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
|
|
4266
4642
|
version: row.version,
|
|
4267
4643
|
createdAt: row.createdAt,
|
|
4268
4644
|
updatedAt: row.updatedAt,
|
|
@@ -4294,9 +4670,9 @@ function ${names.singularCamel}CursorFilter(
|
|
|
4294
4670
|
|
|
4295
4671
|
function ${names.singularCamel}ListWhere(
|
|
4296
4672
|
query: List${names.pluralPascal}Query,
|
|
4297
|
-
${options.
|
|
4673
|
+
${options.tenantScoped ? `\tscope: Parameters<${names.singularPascal}Repository["list"]>[1],\n` : ""}): SQL<unknown> | undefined {
|
|
4298
4674
|
const filters: SQL<unknown>[] = [
|
|
4299
|
-
${options.
|
|
4675
|
+
${options.tenantScoped ? `\t\teq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
|
|
4300
4676
|
const cursor = ${names.singularCamel}CursorFilter(query);
|
|
4301
4677
|
|
|
4302
4678
|
if (query.name) {
|
|
@@ -4330,8 +4706,8 @@ export function createDrizzle${names.singularPascal}Repository(
|
|
|
4330
4706
|
db: ${dialect.dbTypeName}<typeof schema>,
|
|
4331
4707
|
): ${names.singularPascal}Repository {
|
|
4332
4708
|
return {
|
|
4333
|
-
async list(query${options.
|
|
4334
|
-
const where = ${names.singularCamel}ListWhere(query${options.
|
|
4709
|
+
async list(query${options.tenantScoped ? ", scope" : ""}) {
|
|
4710
|
+
const where = ${names.singularCamel}ListWhere(query${options.tenantScoped ? ", scope" : ""});
|
|
4335
4711
|
const rows = await db
|
|
4336
4712
|
.select()
|
|
4337
4713
|
.from(schema.${names.pluralCamel})
|
|
@@ -4393,7 +4769,7 @@ export const list${names.pluralPascal} = ${names.pluralCamel}
|
|
|
4393
4769
|
export const create${names.singularPascal} = ${names.pluralCamel}
|
|
4394
4770
|
.post("/api/${names.pluralKebab}")
|
|
4395
4771
|
.body(Create${names.singularPascal}InputSchema)
|
|
4396
|
-
${options.
|
|
4772
|
+
${options.authorization ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
|
|
4397
4773
|
201: ${names.singularPascal}Schema,
|
|
4398
4774
|
});
|
|
4399
4775
|
${
|
|
@@ -4411,7 +4787,7 @@ export const update${names.singularPascal} = ${names.pluralCamel}
|
|
|
4411
4787
|
.patch("/api/${names.pluralKebab}/:id")
|
|
4412
4788
|
.pathParams(${names.singularPascal}IdInputSchema)
|
|
4413
4789
|
.body(Update${names.singularPascal}BodySchema)
|
|
4414
|
-
${options.
|
|
4790
|
+
${options.authorization ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.update" },\n\t})\n` : ""} .errors({${options.authorization ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound, ${names.singularPascal}Conflict: errors.${names.singularPascal}Conflict })
|
|
4415
4791
|
.responses({
|
|
4416
4792
|
200: ${names.singularPascal}Schema,
|
|
4417
4793
|
});
|
|
@@ -4419,7 +4795,7 @@ ${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability
|
|
|
4419
4795
|
export const delete${names.singularPascal} = ${names.pluralCamel}
|
|
4420
4796
|
.delete("/api/${names.pluralKebab}/:id")
|
|
4421
4797
|
.pathParams(${names.singularPascal}IdInputSchema)
|
|
4422
|
-
${options.
|
|
4798
|
+
${options.authorization ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.delete" },\n\t})\n` : ""} .errors({${options.authorization ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound })
|
|
4423
4799
|
.responses({
|
|
4424
4800
|
204: null,
|
|
4425
4801
|
});
|
|
@@ -4624,7 +5000,7 @@ function isAuthenticated(ctx: AuthorizationContext) {
|
|
|
4624
5000
|
|
|
4625
5001
|
function canWrite(ctx: AuthorizationContext) {
|
|
4626
5002
|
if (!isAuthenticated(ctx)) return deny("You must be signed in.");
|
|
4627
|
-
${options.
|
|
5003
|
+
${options.tenantScoped ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
|
|
4628
5004
|
}
|
|
4629
5005
|
|
|
4630
5006
|
export const ${names.singularCamel}Policy = definePolicy({
|
|
@@ -4634,13 +5010,13 @@ export const ${names.singularCamel}Policy = definePolicy({
|
|
|
4634
5010
|
"${names.pluralCamel}.update": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
4635
5011
|
const decision = canWrite(ctx);
|
|
4636
5012
|
if (decision !== true) return decision;
|
|
4637
|
-
${options.
|
|
5013
|
+
${options.tenantScoped ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot update a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
|
|
4638
5014
|
return true;
|
|
4639
5015
|
},
|
|
4640
5016
|
"${names.pluralCamel}.delete": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
4641
5017
|
const decision = canWrite(ctx);
|
|
4642
5018
|
if (decision !== true) return decision;
|
|
4643
|
-
${options.
|
|
5019
|
+
${options.tenantScoped ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot delete a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
|
|
4644
5020
|
return true;
|
|
4645
5021
|
},
|
|
4646
5022
|
"${names.pluralCamel}.manage": (_ctx: AuthorizationContext) =>
|
|
@@ -4692,7 +5068,7 @@ function create${names.singularPascal}(): ${names.singularPascal} {
|
|
|
4692
5068
|
const now = new Date().toISOString();
|
|
4693
5069
|
return {
|
|
4694
5070
|
id: "00000000-0000-4000-8000-000000000001",
|
|
4695
|
-
${options.
|
|
5071
|
+
${options.tenantScoped ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
|
|
4696
5072
|
version: 1,
|
|
4697
5073
|
createdAt: now,
|
|
4698
5074
|
updatedAt: now,
|
|
@@ -4707,7 +5083,7 @@ describe("${names.singularCamel}Policy", () => {
|
|
|
4707
5083
|
const ${names.singularCamel} = create${names.singularPascal}();
|
|
4708
5084
|
const ctx = {
|
|
4709
5085
|
actor: { type: "user" as const, id: "user_test" },
|
|
4710
|
-
${options.
|
|
5086
|
+
${options.tenantScoped ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
|
|
4711
5087
|
|
|
4712
5088
|
await expect(
|
|
4713
5089
|
tester.assertMatrix([
|
|
@@ -4933,8 +5309,33 @@ ${names.timezone ? `\t\ttimezone: "${names.timezone}",\n` : ""}\t\tpayload: ${na
|
|
|
4933
5309
|
`;
|
|
4934
5310
|
}
|
|
4935
5311
|
|
|
5312
|
+
function uploadManifestFile(names: UploadNames): string {
|
|
5313
|
+
return `import type {
|
|
5314
|
+
\tUploadFileConstraints,
|
|
5315
|
+
\tUploadManifestEntry,
|
|
5316
|
+
} from "@beignet/core/uploads";
|
|
5317
|
+
|
|
5318
|
+
export const ${names.fileConstraintsExportName} = {
|
|
5319
|
+
\tcontentTypes: ["application/pdf", "text/plain"],
|
|
5320
|
+
\tmaxSizeBytes: 5 * 1024 * 1024,
|
|
5321
|
+
\tmaxFiles: 1,
|
|
5322
|
+
\tvisibility: "private",
|
|
5323
|
+
\tcacheControl: "private, max-age=0",
|
|
5324
|
+
} satisfies UploadFileConstraints;
|
|
5325
|
+
|
|
5326
|
+
export const ${names.manifestExportName} = [
|
|
5327
|
+
\t{
|
|
5328
|
+
\t\tname: "${names.uploadName}",
|
|
5329
|
+
\t\tdescription: "${names.artifact.pascal} upload",
|
|
5330
|
+
\t\tfile: ${names.fileConstraintsExportName},
|
|
5331
|
+
\t},
|
|
5332
|
+
] satisfies readonly UploadManifestEntry[];
|
|
5333
|
+
`;
|
|
5334
|
+
}
|
|
5335
|
+
|
|
4936
5336
|
function uploadFile(names: UploadNames, config: ResolvedBeignetConfig): string {
|
|
4937
5337
|
return `import { z } from "zod";
|
|
5338
|
+
import { ${names.fileConstraintsExportName} } from "${relativeModule(uploadFilePath(names, config), uploadManifestFilePath(names, config))}";
|
|
4938
5339
|
import { defineUpload } from "${aliasModule(config.paths.uploadsBuilder)}";
|
|
4939
5340
|
|
|
4940
5341
|
export const ${names.metadataSchemaName} = z.object({
|
|
@@ -4945,13 +5346,7 @@ export type ${names.metadataTypeName} = z.infer<typeof ${names.metadataSchemaNam
|
|
|
4945
5346
|
|
|
4946
5347
|
export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
|
|
4947
5348
|
\tmetadata: ${names.metadataSchemaName},
|
|
4948
|
-
\tfile: {
|
|
4949
|
-
\t\tcontentTypes: ["application/pdf", "text/plain"],
|
|
4950
|
-
\t\tmaxSizeBytes: 5 * 1024 * 1024,
|
|
4951
|
-
\t\tmaxFiles: 1,
|
|
4952
|
-
\t\tvisibility: "private",
|
|
4953
|
-
\t\tcacheControl: "private, max-age=0",
|
|
4954
|
-
\t},
|
|
5349
|
+
\tfile: ${names.fileConstraintsExportName},
|
|
4955
5350
|
\tauthorize({ ctx }) {
|
|
4956
5351
|
\t\treturn ctx.actor.type === "user";
|
|
4957
5352
|
\t},
|
|
@@ -4982,6 +5377,148 @@ export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
|
|
|
4982
5377
|
`;
|
|
4983
5378
|
}
|
|
4984
5379
|
|
|
5380
|
+
function uploadUiClientFile(
|
|
5381
|
+
names: UploadNames,
|
|
5382
|
+
config: ResolvedBeignetConfig,
|
|
5383
|
+
): string {
|
|
5384
|
+
const clientPath = uploadUiClientFilePath(names, config);
|
|
5385
|
+
const registryPath = path.join(
|
|
5386
|
+
featureArtifactDir(names, "uploads", config),
|
|
5387
|
+
"index.ts",
|
|
5388
|
+
);
|
|
5389
|
+
|
|
5390
|
+
return `import { createUploadClient } from "@beignet/core/uploads/client";
|
|
5391
|
+
import { createReactUploads } from "@beignet/react-uploads";
|
|
5392
|
+
import { ${names.manifestExportName} } from "${relativeModule(clientPath, uploadManifestFilePath(names, config))}";
|
|
5393
|
+
import type { ${names.featureSingularCamel}Uploads } from "${relativeModule(clientPath, registryPath)}";
|
|
5394
|
+
|
|
5395
|
+
const ${names.artifact.camel}UploadClient = createUploadClient<typeof ${names.featureSingularCamel}Uploads>({
|
|
5396
|
+
\tbaseUrl: "/api/uploads",
|
|
5397
|
+
\tmanifest: ${names.manifestExportName},
|
|
5398
|
+
});
|
|
5399
|
+
|
|
5400
|
+
export const ${names.reactUploadsExportName} = createReactUploads({
|
|
5401
|
+
\tuploads: ${names.artifact.camel}UploadClient,
|
|
5402
|
+
});
|
|
5403
|
+
`;
|
|
5404
|
+
}
|
|
5405
|
+
|
|
5406
|
+
function uploadUiComponentFile(
|
|
5407
|
+
names: UploadNames,
|
|
5408
|
+
config: ResolvedBeignetConfig,
|
|
5409
|
+
): string {
|
|
5410
|
+
const componentPath = uploadUiComponentFilePath(names, config);
|
|
5411
|
+
|
|
5412
|
+
return `"use client";
|
|
5413
|
+
|
|
5414
|
+
import { useId } from "react";
|
|
5415
|
+
import { ${names.reactUploadsExportName} } from "${relativeModule(componentPath, uploadUiClientFilePath(names, config))}";
|
|
5416
|
+
|
|
5417
|
+
export type ${names.componentExportName}Props = {
|
|
5418
|
+
\tresourceId: string;
|
|
5419
|
+
\tdisabled?: boolean;
|
|
5420
|
+
\tonComplete?(objectKeys: readonly string[]): void | Promise<void>;
|
|
5421
|
+
};
|
|
5422
|
+
|
|
5423
|
+
export function ${names.componentExportName}({
|
|
5424
|
+
\tresourceId,
|
|
5425
|
+
\tdisabled = false,
|
|
5426
|
+
\tonComplete,
|
|
5427
|
+
}: ${names.componentExportName}Props) {
|
|
5428
|
+
\tconst inputId = useId();
|
|
5429
|
+
\tconst upload = ${names.reactUploadsExportName}.useUpload("${names.uploadName}", {
|
|
5430
|
+
\t\tonSuccess({ result }) {
|
|
5431
|
+
\t\t\treturn onComplete?.(result.objectKeys);
|
|
5432
|
+
\t\t},
|
|
5433
|
+
\t});
|
|
5434
|
+
\tconst maxSizeMb = upload.constraints?.maxSizeBytes
|
|
5435
|
+
\t\t? Math.round(upload.constraints.maxSizeBytes / 1024 / 1024)
|
|
5436
|
+
\t\t: undefined;
|
|
5437
|
+
|
|
5438
|
+
\treturn (
|
|
5439
|
+
\t\t<section className="${names.artifact.kebab}-uploader">
|
|
5440
|
+
\t\t\t<label htmlFor={inputId}>${names.artifact.pascal}</label>
|
|
5441
|
+
\t\t\t<input
|
|
5442
|
+
\t\t\t\tid={inputId}
|
|
5443
|
+
\t\t\t\ttype="file"
|
|
5444
|
+
\t\t\t\taccept={upload.accept}
|
|
5445
|
+
\t\t\t\tdisabled={disabled || upload.isUploading}
|
|
5446
|
+
\t\t\t\tonChange={(event) => {
|
|
5447
|
+
\t\t\t\t\tconst file = event.currentTarget.files?.[0];
|
|
5448
|
+
\t\t\t\t\tevent.currentTarget.value = "";
|
|
5449
|
+
\t\t\t\t\tif (!file) return;
|
|
5450
|
+
|
|
5451
|
+
\t\t\t\t\tupload.uploadFile(file, {
|
|
5452
|
+
\t\t\t\t\t\tmetadata: { resourceId },
|
|
5453
|
+
\t\t\t\t\t});
|
|
5454
|
+
\t\t\t\t}}
|
|
5455
|
+
\t\t\t/>
|
|
5456
|
+
\t\t\t<p>
|
|
5457
|
+
\t\t\t\tPDF or plain text{maxSizeMb ? <> up to {maxSizeMb} MB</> : null}.
|
|
5458
|
+
\t\t\t</p>
|
|
5459
|
+
|
|
5460
|
+
\t\t\t{upload.files[0] ? <p>{upload.files[0].fileName}</p> : null}
|
|
5461
|
+
\t\t\t{upload.isUploading ? (
|
|
5462
|
+
\t\t\t\t<div aria-live="polite">
|
|
5463
|
+
\t\t\t\t\t<progress
|
|
5464
|
+
\t\t\t\t\t\taria-label="Upload progress"
|
|
5465
|
+
\t\t\t\t\t\tmax={100}
|
|
5466
|
+
\t\t\t\t\t\tvalue={upload.progress}
|
|
5467
|
+
\t\t\t\t\t/>
|
|
5468
|
+
\t\t\t\t\t<span>{Math.round(upload.progress)}%</span>
|
|
5469
|
+
\t\t\t\t\t<button type="button" onClick={upload.abort}>
|
|
5470
|
+
\t\t\t\t\t\tCancel upload
|
|
5471
|
+
\t\t\t\t\t</button>
|
|
5472
|
+
\t\t\t\t</div>
|
|
5473
|
+
\t\t\t) : null}
|
|
5474
|
+
\t\t\t{upload.isError ? (
|
|
5475
|
+
\t\t\t\t<p role="alert">{uploadErrorMessage(upload.error)}</p>
|
|
5476
|
+
\t\t\t) : null}
|
|
5477
|
+
\t\t\t{upload.isSuccess ? <p role="status">Upload complete.</p> : null}
|
|
5478
|
+
\t\t\t{upload.isError || upload.isSuccess ? (
|
|
5479
|
+
\t\t\t\t<button type="button" onClick={upload.reset}>
|
|
5480
|
+
\t\t\t\t\tReset upload
|
|
5481
|
+
\t\t\t\t</button>
|
|
5482
|
+
\t\t\t) : null}
|
|
5483
|
+
\t\t</section>
|
|
5484
|
+
\t);
|
|
5485
|
+
}
|
|
5486
|
+
|
|
5487
|
+
function uploadErrorMessage(error: unknown): string {
|
|
5488
|
+
\treturn error instanceof Error ? error.message : "Upload failed.";
|
|
5489
|
+
}
|
|
5490
|
+
`;
|
|
5491
|
+
}
|
|
5492
|
+
|
|
5493
|
+
function uploadUiComponentTestFile(
|
|
5494
|
+
names: UploadNames,
|
|
5495
|
+
config: ResolvedBeignetConfig,
|
|
5496
|
+
): string {
|
|
5497
|
+
const testPath = uploadUiComponentTestFilePath(names, config);
|
|
5498
|
+
const testHelperPath = path.join(
|
|
5499
|
+
path.dirname(config.paths.useCaseBuilder),
|
|
5500
|
+
"beignet-test.ts",
|
|
5501
|
+
);
|
|
5502
|
+
|
|
5503
|
+
return `import { renderToStaticMarkup } from "react-dom/server";
|
|
5504
|
+
import { describe, expect, test } from "${relativeModule(testPath, testHelperPath)}";
|
|
5505
|
+
import { ${names.manifestExportName} } from "${relativeModule(testPath, uploadManifestFilePath(names, config))}";
|
|
5506
|
+
import { ${names.componentExportName} } from "${relativeModule(testPath, uploadUiComponentFilePath(names, config))}";
|
|
5507
|
+
|
|
5508
|
+
describe("${names.componentExportName}", () => {
|
|
5509
|
+
\ttest("renders the generated upload constraints", () => {
|
|
5510
|
+
\t\tconst markup = renderToStaticMarkup(
|
|
5511
|
+
\t\t\t<${names.componentExportName} resourceId="resource-1" />,
|
|
5512
|
+
\t\t);
|
|
5513
|
+
|
|
5514
|
+
\t\texpect(${names.manifestExportName}[0]?.file.maxSizeBytes).toBe(5 * 1024 * 1024);
|
|
5515
|
+
\t\texpect(markup).toContain('accept="application/pdf,text/plain"');
|
|
5516
|
+
\t\texpect(markup).toContain("PDF or plain text");
|
|
5517
|
+
\t});
|
|
5518
|
+
});
|
|
5519
|
+
`;
|
|
5520
|
+
}
|
|
5521
|
+
|
|
4985
5522
|
function uploadRouteFile(
|
|
4986
5523
|
names: UploadNames,
|
|
4987
5524
|
config: ResolvedBeignetConfig,
|
|
@@ -5101,63 +5638,70 @@ function featureUiComponentFile(
|
|
|
5101
5638
|
|
|
5102
5639
|
return `"use client";
|
|
5103
5640
|
|
|
5104
|
-
import {
|
|
5641
|
+
import { rootFormError } from "@beignet/react-hook-form";
|
|
5105
5642
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5106
|
-
import {
|
|
5643
|
+
import { rhf } from "${aliasModule(clientFormsPath(config))}";
|
|
5644
|
+
import { create${names.singularPascal} } from "${relativeModule(componentPath, resourceContractFilePath(names, config))}";
|
|
5107
5645
|
import {
|
|
5108
5646
|
\tcreate${names.singularPascal}MutationOptions,
|
|
5109
5647
|
\tinvalidate${names.pluralPascal},
|
|
5110
5648
|
\tlist${names.pluralPascal}QueryOptions,
|
|
5111
5649
|
} from "${relativeModule(componentPath, clientQueriesPath)}";
|
|
5112
5650
|
|
|
5651
|
+
const create${names.singularPascal}Form = rhf(create${names.singularPascal});
|
|
5652
|
+
|
|
5113
5653
|
export function ${names.componentExportName}() {
|
|
5114
|
-
\tconst [name, setName] = useState("");
|
|
5115
5654
|
\tconst queryClient = useQueryClient();
|
|
5116
5655
|
\tconst ${names.pluralCamel}Query = useQuery(list${names.pluralPascal}QueryOptions());
|
|
5656
|
+
\tconst form = create${names.singularPascal}Form.useForm({
|
|
5657
|
+
\t\tdefaultValues: { name: "" },
|
|
5658
|
+
\t});
|
|
5659
|
+
\tconst name = form.watch("name");
|
|
5117
5660
|
\tconst create${names.singularPascal}Mutation = useMutation({
|
|
5118
5661
|
\t\t...create${names.singularPascal}MutationOptions(),
|
|
5119
5662
|
\t\tonSuccess: async () => {
|
|
5120
|
-
\t\t\
|
|
5663
|
+
\t\t\tform.reset({ name: "" });
|
|
5121
5664
|
\t\t\tawait invalidate${names.pluralPascal}(queryClient);
|
|
5122
5665
|
\t\t},
|
|
5666
|
+
\t\tonError: (error) => {
|
|
5667
|
+
\t\t\tform.setError(
|
|
5668
|
+
\t\t\t\t"root",
|
|
5669
|
+
\t\t\t\trootFormError(error, "Could not create ${names.singularKebab}."),
|
|
5670
|
+
\t\t\t);
|
|
5671
|
+
\t\t},
|
|
5123
5672
|
\t});
|
|
5124
5673
|
|
|
5125
5674
|
\treturn (
|
|
5126
5675
|
\t\t<section className="${names.pluralKebab}-panel">
|
|
5127
5676
|
\t\t\t<form
|
|
5128
5677
|
\t\t\t\tclassName="${names.pluralKebab}-composer"
|
|
5129
|
-
\t\t\t\tonSubmit={(
|
|
5130
|
-
\t\t\t\t\
|
|
5131
|
-
\t\t\t\t\tconst trimmedName = name.trim();
|
|
5132
|
-
\t\t\t\t\tif (!trimmedName) return;
|
|
5678
|
+
\t\t\t\tonSubmit={form.handleSubmit((values) => {
|
|
5679
|
+
\t\t\t\t\tform.clearErrors("root");
|
|
5133
5680
|
\t\t\t\t\tcreate${names.singularPascal}Mutation.reset();
|
|
5134
5681
|
\t\t\t\t\tcreate${names.singularPascal}Mutation.mutate({
|
|
5135
|
-
\t\t\t\t\t\tbody:
|
|
5682
|
+
\t\t\t\t\t\tbody: values,
|
|
5136
5683
|
\t\t\t\t\t});
|
|
5137
|
-
\t\t\t\t}}
|
|
5684
|
+
\t\t\t\t})}
|
|
5138
5685
|
\t\t\t>
|
|
5139
5686
|
\t\t\t\t<label htmlFor="${names.singularKebab}-name">New ${names.singularKebab}</label>
|
|
5140
5687
|
\t\t\t\t<div className="${names.pluralKebab}-composer-row">
|
|
5141
5688
|
\t\t\t\t\t<input
|
|
5142
5689
|
\t\t\t\t\t\tid="${names.singularKebab}-name"
|
|
5143
|
-
\t\t\t\t\t\
|
|
5144
|
-
\t\t\t\t\t\tonChange={(event) => setName(event.currentTarget.value)}
|
|
5690
|
+
\t\t\t\t\t\t{...form.register("name")}
|
|
5145
5691
|
\t\t\t\t\t\tplaceholder="Name"
|
|
5146
5692
|
\t\t\t\t\t/>
|
|
5147
5693
|
\t\t\t\t\t<button
|
|
5148
5694
|
\t\t\t\t\t\ttype="submit"
|
|
5149
|
-
\t\t\t\t\t\tdisabled={!name
|
|
5695
|
+
\t\t\t\t\t\tdisabled={!name?.trim() || create${names.singularPascal}Mutation.isPending}
|
|
5150
5696
|
\t\t\t\t\t>
|
|
5151
5697
|
\t\t\t\t\t\t{create${names.singularPascal}Mutation.isPending ? "Creating" : "Create"}
|
|
5152
5698
|
\t\t\t\t\t</button>
|
|
5153
5699
|
\t\t\t\t</div>
|
|
5154
|
-
\t\t\t\t{
|
|
5155
|
-
\t\t\t\t\t<p role="alert">
|
|
5156
|
-
\t\t\t\t
|
|
5157
|
-
\t\t\t\t
|
|
5158
|
-
\t\t\t\t\t
|
|
5159
|
-
\t\t\t\t\t\t)}
|
|
5160
|
-
\t\t\t\t\t</p>
|
|
5700
|
+
\t\t\t\t{form.formState.errors.name ? (
|
|
5701
|
+
\t\t\t\t\t<p role="alert">{form.formState.errors.name.message}</p>
|
|
5702
|
+
\t\t\t\t) : null}
|
|
5703
|
+
\t\t\t\t{form.formState.errors.root ? (
|
|
5704
|
+
\t\t\t\t\t<p role="alert">{form.formState.errors.root.message}</p>
|
|
5161
5705
|
\t\t\t\t) : null}
|
|
5162
5706
|
\t\t\t</form>
|
|
5163
5707
|
|
|
@@ -5306,7 +5850,7 @@ ${mode === "resource" ? `import { isAppError } from "@beignet/core/errors";\n` :
|
|
|
5306
5850
|
import { createTestApp } from "@beignet/web/testing";
|
|
5307
5851
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
5308
5852
|
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
5309
|
-
import { ${options.
|
|
5853
|
+
import { ${options.authorization ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenantScoped ? ", createTestTenant" : ""} } from "@beignet/core/testing";
|
|
5310
5854
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
5311
5855
|
import { initialPorts } from "${aliasModule(config.paths.portWiring)}";
|
|
5312
5856
|
import { appContext } from "${aliasModule(serverContextPath)}";
|
|
@@ -5326,40 +5870,40 @@ describe("${names.pluralCamel} resource", () => {
|
|
|
5326
5870
|
const seed${names.pluralPascal} = [
|
|
5327
5871
|
{
|
|
5328
5872
|
id: "00000000-0000-4000-8000-000000000101",
|
|
5329
|
-
${options.
|
|
5873
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
|
|
5330
5874
|
version: 1,
|
|
5331
5875
|
createdAt: "2024-01-01T00:00:00.000Z",
|
|
5332
5876
|
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
5333
5877
|
},
|
|
5334
5878
|
{
|
|
5335
5879
|
id: "00000000-0000-4000-8000-000000000102",
|
|
5336
|
-
${options.
|
|
5880
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
|
|
5337
5881
|
version: 1,
|
|
5338
5882
|
createdAt: "2024-01-02T00:00:00.000Z",
|
|
5339
5883
|
updatedAt: "2024-01-02T00:00:00.000Z",
|
|
5340
5884
|
},
|
|
5341
5885
|
{
|
|
5342
5886
|
id: "00000000-0000-4000-8000-000000000103",
|
|
5343
|
-
${options.
|
|
5887
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
|
|
5344
5888
|
version: 1,
|
|
5345
5889
|
createdAt: "2024-01-03T00:00:00.000Z",
|
|
5346
5890
|
updatedAt: "2024-01-03T00:00:00.000Z",
|
|
5347
5891
|
},
|
|
5348
|
-
${options.
|
|
5892
|
+
${options.tenantScoped ? `\t\t\t{\n\t\t\t\tid: "00000000-0000-4000-8000-000000000104",\n\t\t\t\ttenantId: "tenant_other",\n\t\t\t\tname: "Other Tenant ${names.singularPascal}",\n\t\t\t\tversion: 1,\n\t\t\t\tcreatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t\tupdatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t},\n` : ""} ];
|
|
5349
5893
|
const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository(seed${names.pluralPascal});
|
|
5350
5894
|
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
5351
5895
|
base: initialPorts,
|
|
5352
5896
|
overrides: {
|
|
5353
5897
|
${names.pluralCamel},
|
|
5354
5898
|
${
|
|
5355
|
-
options.
|
|
5899
|
+
options.authorization || options.tenantScoped
|
|
5356
5900
|
? `\t\t\t\tauth: {
|
|
5357
5901
|
getSession: async () => ({
|
|
5358
|
-
user: { id: "user_test", name: "Test User"${options.
|
|
5359
|
-
${options.
|
|
5902
|
+
user: { id: "user_test", name: "Test User"${options.tenantScoped ? `, tenantId: "tenant_test"` : ""} },
|
|
5903
|
+
${options.tenantScoped ? `\t\t\t\t\t\tsession: { tenantId: "tenant_test" },\n` : ""}
|
|
5360
5904
|
}),
|
|
5361
5905
|
},
|
|
5362
|
-
${options.
|
|
5906
|
+
${options.authorization ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
|
|
5363
5907
|
`
|
|
5364
5908
|
: `\t\t\t\tauth: {
|
|
5365
5909
|
getSession: async () => null,
|
|
@@ -5375,8 +5919,8 @@ ${options.auth ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
|
|
|
5375
5919
|
});
|
|
5376
5920
|
const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
|
|
5377
5921
|
ports: testFixture.ports,
|
|
5378
|
-
actor: ${options.
|
|
5379
|
-
tenant: ${options.
|
|
5922
|
+
actor: ${options.authorization ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
|
|
5923
|
+
tenant: ${options.tenantScoped ? `createTestTenant("tenant_test")` : "null"},
|
|
5380
5924
|
});
|
|
5381
5925
|
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
5382
5926
|
|