@beignet/cli 0.0.38 → 0.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +120 -96
- 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/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 +54 -1
- 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.js +3 -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 +455 -185
- 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/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 +13 -8
- package/src/choices.ts +38 -88
- package/src/create-prompts.ts +20 -21
- package/src/create.ts +54 -36
- package/src/index.ts +193 -65
- package/src/inspect.ts +101 -1
- package/src/lib.ts +1 -1
- package/src/make/inbox.ts +3 -3
- package/src/make/payments.ts +3 -3
- package/src/make/shared.ts +16 -10
- package/src/make.ts +578 -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/shared.ts +10 -10
- 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),
|
|
@@ -1043,7 +1047,7 @@ export async function makeEvent(
|
|
|
1043
1047
|
config,
|
|
1044
1048
|
force: Boolean(options.force),
|
|
1045
1049
|
dryRun: Boolean(options.dryRun),
|
|
1046
|
-
name:
|
|
1050
|
+
name: names.stableName,
|
|
1047
1051
|
files: eventFiles(names, config),
|
|
1048
1052
|
index: featureArtifactIndexFile("event", names, config),
|
|
1049
1053
|
dependencies: {
|
|
@@ -1090,7 +1094,7 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
|
|
|
1090
1094
|
config,
|
|
1091
1095
|
force: Boolean(options.force),
|
|
1092
1096
|
dryRun: Boolean(options.dryRun),
|
|
1093
|
-
name:
|
|
1097
|
+
name: names.stableName,
|
|
1094
1098
|
files: [
|
|
1095
1099
|
...(await missingCapabilityBuilderFiles(targetDir, "jobs", config)),
|
|
1096
1100
|
...jobFiles(names, config),
|
|
@@ -1130,7 +1134,7 @@ export async function makeTask(
|
|
|
1130
1134
|
config,
|
|
1131
1135
|
force: Boolean(options.force),
|
|
1132
1136
|
dryRun: Boolean(options.dryRun),
|
|
1133
|
-
name:
|
|
1137
|
+
name: names.stableName,
|
|
1134
1138
|
files: [
|
|
1135
1139
|
...(await missingCapabilityBuilderFiles(targetDir, "tasks", config)),
|
|
1136
1140
|
...taskFiles(names, config),
|
|
@@ -1175,7 +1179,7 @@ export async function makeFactory(
|
|
|
1175
1179
|
config,
|
|
1176
1180
|
force: Boolean(options.force),
|
|
1177
1181
|
dryRun: Boolean(options.dryRun),
|
|
1178
|
-
name:
|
|
1182
|
+
name: names.stableName,
|
|
1179
1183
|
files: factoryFiles(names, config),
|
|
1180
1184
|
index: featureArtifactIndexFile("factory", names, config),
|
|
1181
1185
|
dependencies: {
|
|
@@ -1202,7 +1206,7 @@ export async function makeSeed(
|
|
|
1202
1206
|
config,
|
|
1203
1207
|
force: Boolean(options.force),
|
|
1204
1208
|
dryRun: Boolean(options.dryRun),
|
|
1205
|
-
name:
|
|
1209
|
+
name: names.stableName,
|
|
1206
1210
|
files: seedFiles(names, config),
|
|
1207
1211
|
index: featureArtifactIndexFile("seed", names, config),
|
|
1208
1212
|
dependencies: {
|
|
@@ -1221,7 +1225,7 @@ export async function makeSeed(
|
|
|
1221
1225
|
config,
|
|
1222
1226
|
force: Boolean(options.force),
|
|
1223
1227
|
dryRun: Boolean(options.dryRun),
|
|
1224
|
-
name:
|
|
1228
|
+
name: factoryNamesForSeed.stableName,
|
|
1225
1229
|
files: factoryFiles(factoryNamesForSeed, config),
|
|
1226
1230
|
index: featureArtifactIndexFile("factory", factoryNamesForSeed, config),
|
|
1227
1231
|
dependencies: {
|
|
@@ -1277,7 +1281,7 @@ export async function makeNotification(
|
|
|
1277
1281
|
// notifications uses ctx.ports.notifications, so the generator wires both
|
|
1278
1282
|
// ports with dev-default providers. Each port is wired independently and
|
|
1279
1283
|
// skipped when the app already has it, for example when the resend
|
|
1280
|
-
//
|
|
1284
|
+
// provider contributed the mailer. Plan the wiring before writing any
|
|
1281
1285
|
// files so an anchor miss aborts the generator without leaving partial
|
|
1282
1286
|
// output.
|
|
1283
1287
|
const notificationWirings = notificationPortWirings("make notification");
|
|
@@ -1290,7 +1294,7 @@ export async function makeNotification(
|
|
|
1290
1294
|
config,
|
|
1291
1295
|
force: Boolean(options.force),
|
|
1292
1296
|
dryRun: Boolean(options.dryRun),
|
|
1293
|
-
name:
|
|
1297
|
+
name: names.stableName,
|
|
1294
1298
|
files: [
|
|
1295
1299
|
...(await missingCapabilityBuilderFiles(
|
|
1296
1300
|
targetDir,
|
|
@@ -1341,7 +1345,7 @@ export async function makeListener(
|
|
|
1341
1345
|
config,
|
|
1342
1346
|
force: Boolean(options.force),
|
|
1343
1347
|
dryRun: Boolean(options.dryRun),
|
|
1344
|
-
name:
|
|
1348
|
+
name: names.stableName,
|
|
1345
1349
|
files: [
|
|
1346
1350
|
...(await missingCapabilityBuilderFiles(targetDir, "listeners", config)),
|
|
1347
1351
|
...listenerFiles(names, config),
|
|
@@ -1445,7 +1449,7 @@ async function assertListenerEventExists(
|
|
|
1445
1449
|
await stat(path.join(targetDir, file));
|
|
1446
1450
|
} catch {
|
|
1447
1451
|
throw new Error(
|
|
1448
|
-
`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.`,
|
|
1449
1453
|
);
|
|
1450
1454
|
}
|
|
1451
1455
|
}
|
|
@@ -1515,7 +1519,7 @@ export async function makeSchedule(
|
|
|
1515
1519
|
config,
|
|
1516
1520
|
force: Boolean(options.force),
|
|
1517
1521
|
dryRun: Boolean(options.dryRun),
|
|
1518
|
-
name:
|
|
1522
|
+
name: names.stableName,
|
|
1519
1523
|
files,
|
|
1520
1524
|
index: featureArtifactIndexFile("schedule", names, config),
|
|
1521
1525
|
dependencies: {
|
|
@@ -2065,6 +2069,9 @@ export async function makeUpload(
|
|
|
2065
2069
|
: await loadBeignetConfig(targetDir);
|
|
2066
2070
|
|
|
2067
2071
|
await assertFeatureArtifactApp(targetDir, config, "upload");
|
|
2072
|
+
if (options.ui) {
|
|
2073
|
+
await assertUploadUiApp(targetDir, config);
|
|
2074
|
+
}
|
|
2068
2075
|
|
|
2069
2076
|
await wirePortProviders(
|
|
2070
2077
|
targetDir,
|
|
@@ -2080,10 +2087,10 @@ export async function makeUpload(
|
|
|
2080
2087
|
config,
|
|
2081
2088
|
force: Boolean(options.force),
|
|
2082
2089
|
dryRun: Boolean(options.dryRun),
|
|
2083
|
-
name:
|
|
2090
|
+
name: names.stableName,
|
|
2084
2091
|
files: [
|
|
2085
2092
|
...(await missingCapabilityBuilderFiles(targetDir, "uploads", config)),
|
|
2086
|
-
...uploadFiles(names, config),
|
|
2093
|
+
...uploadFiles(names, config, { ui: Boolean(options.ui) }),
|
|
2087
2094
|
],
|
|
2088
2095
|
index: featureArtifactIndexFile("upload", names, config),
|
|
2089
2096
|
dependencies: {
|
|
@@ -2112,6 +2119,44 @@ export async function makeUpload(
|
|
|
2112
2119
|
result.files.push(uploadRoutePath(config));
|
|
2113
2120
|
}
|
|
2114
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
|
+
|
|
2115
2160
|
return result;
|
|
2116
2161
|
}
|
|
2117
2162
|
|
|
@@ -2195,7 +2240,10 @@ async function makeFeatureUi(
|
|
|
2195
2240
|
|
|
2196
2241
|
await assertFeatureUiApp(targetDir, config);
|
|
2197
2242
|
|
|
2198
|
-
const componentFiles =
|
|
2243
|
+
const componentFiles = [
|
|
2244
|
+
...(await missingFeatureUiFormsFiles(targetDir, config)),
|
|
2245
|
+
...featureUiComponentFiles(names, config),
|
|
2246
|
+
];
|
|
2199
2247
|
const indexFile = featureUiIndexFile(names, config);
|
|
2200
2248
|
const plannedFiles = await planGeneratedFiles(targetDir, componentFiles, {
|
|
2201
2249
|
force: Boolean(options.force),
|
|
@@ -2227,8 +2275,15 @@ async function makeFeatureUi(
|
|
|
2227
2275
|
await updatePackageDependencies(
|
|
2228
2276
|
targetDir,
|
|
2229
2277
|
{
|
|
2278
|
+
"@beignet/react-hook-form": beignetDependencyVersion,
|
|
2230
2279
|
"@beignet/react-query": beignetDependencyVersion,
|
|
2280
|
+
"@hookform/resolvers": (packageJson) =>
|
|
2281
|
+
packageJson.dependencies?.["@hookform/resolvers"] ??
|
|
2282
|
+
externalVersions.hookformResolvers,
|
|
2231
2283
|
"@tanstack/react-query": tanstackReactQueryDependencyVersion,
|
|
2284
|
+
"react-hook-form": (packageJson) =>
|
|
2285
|
+
packageJson.dependencies?.["react-hook-form"] ??
|
|
2286
|
+
externalVersions.reactHookForm,
|
|
2232
2287
|
},
|
|
2233
2288
|
{ dryRun: Boolean(options.dryRun) },
|
|
2234
2289
|
)
|
|
@@ -2370,7 +2425,7 @@ async function updateResourceWiring(
|
|
|
2370
2425
|
updated.add(config.paths.portWiring);
|
|
2371
2426
|
}
|
|
2372
2427
|
if (
|
|
2373
|
-
generationOptions.
|
|
2428
|
+
generationOptions.authorization &&
|
|
2374
2429
|
(await updateInfrastructureGatePolicies(targetDir, names, config, options))
|
|
2375
2430
|
) {
|
|
2376
2431
|
updated.add(config.paths.portWiring);
|
|
@@ -2432,13 +2487,12 @@ function typeHasTopLevelProperty(
|
|
|
2432
2487
|
function useCaseNames(input: string): UseCaseNames {
|
|
2433
2488
|
const parts = input
|
|
2434
2489
|
.trim()
|
|
2435
|
-
.split("
|
|
2436
|
-
.map((part) => part.trim())
|
|
2437
|
-
.filter(Boolean);
|
|
2490
|
+
.split(".")
|
|
2491
|
+
.map((part) => part.trim());
|
|
2438
2492
|
|
|
2439
|
-
if (parts.length !== 2) {
|
|
2493
|
+
if (parts.length !== 2 || parts.some((part) => part.length === 0)) {
|
|
2440
2494
|
throw new Error(
|
|
2441
|
-
"Use case name must use feature
|
|
2495
|
+
"Use case name must use feature.action format, for example projects.archive-project.",
|
|
2442
2496
|
);
|
|
2443
2497
|
}
|
|
2444
2498
|
|
|
@@ -2478,7 +2532,7 @@ function policyNames(input: string): PolicyNames {
|
|
|
2478
2532
|
}
|
|
2479
2533
|
|
|
2480
2534
|
function eventNames(input: string): EventNames {
|
|
2481
|
-
const names = featureArtifactNames(
|
|
2535
|
+
const names = featureArtifactNames(input, "Event");
|
|
2482
2536
|
const eventExportName = `${names.featureSingularPascal}${names.artifact.pascal}`;
|
|
2483
2537
|
|
|
2484
2538
|
assertGeneratedIdentifiers(input, "Event name", [eventExportName]);
|
|
@@ -2536,7 +2590,7 @@ function factoryNames(input: string): FactoryNames {
|
|
|
2536
2590
|
}
|
|
2537
2591
|
|
|
2538
2592
|
function defaultFactoryNamesForSeed(names: SeedNames): FactoryNames {
|
|
2539
|
-
return factoryNames(`${names.feature.kebab}
|
|
2593
|
+
return factoryNames(`${names.feature.kebab}.${names.featureSingularKebab}`);
|
|
2540
2594
|
}
|
|
2541
2595
|
|
|
2542
2596
|
function notificationNames(input: string): NotificationNames {
|
|
@@ -2593,8 +2647,12 @@ function scheduleNames(
|
|
|
2593
2647
|
function uploadNames(input: string): UploadNames {
|
|
2594
2648
|
const names = featureArtifactNames(input, "Upload");
|
|
2595
2649
|
const uploadExportName = `${names.artifact.pascal}Upload`;
|
|
2650
|
+
const componentExportName = `${names.artifact.pascal}Uploader`;
|
|
2596
2651
|
|
|
2597
|
-
assertGeneratedIdentifiers(input, "Upload name", [
|
|
2652
|
+
assertGeneratedIdentifiers(input, "Upload name", [
|
|
2653
|
+
uploadExportName,
|
|
2654
|
+
componentExportName,
|
|
2655
|
+
]);
|
|
2598
2656
|
|
|
2599
2657
|
return {
|
|
2600
2658
|
...names,
|
|
@@ -2602,19 +2660,16 @@ function uploadNames(input: string): UploadNames {
|
|
|
2602
2660
|
uploadExportName,
|
|
2603
2661
|
metadataSchemaName: `${uploadExportName}MetadataSchema`,
|
|
2604
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`,
|
|
2605
2670
|
};
|
|
2606
2671
|
}
|
|
2607
2672
|
|
|
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
2673
|
function inferUseCaseKind(action: string): "command" | "query" {
|
|
2619
2674
|
if (/^(count|find|get|list|search)-/.test(`${action}-`)) {
|
|
2620
2675
|
return "query";
|
|
@@ -2628,8 +2683,8 @@ function resourceFiles(
|
|
|
2628
2683
|
persistence: ResourcePersistence = "memory",
|
|
2629
2684
|
mode: ResourceGenerationMode = "feature",
|
|
2630
2685
|
options: ResourceGenerationOptions = {
|
|
2631
|
-
|
|
2632
|
-
|
|
2686
|
+
authorization: false,
|
|
2687
|
+
tenantScoped: false,
|
|
2633
2688
|
events: false,
|
|
2634
2689
|
softDelete: false,
|
|
2635
2690
|
},
|
|
@@ -2684,7 +2739,7 @@ function resourceFiles(
|
|
|
2684
2739
|
},
|
|
2685
2740
|
]
|
|
2686
2741
|
: []),
|
|
2687
|
-
...(mode === "resource" && options.
|
|
2742
|
+
...(mode === "resource" && options.authorization
|
|
2688
2743
|
? [
|
|
2689
2744
|
{
|
|
2690
2745
|
path: path.join(featureDir, "tests/policy.test.ts"),
|
|
@@ -2754,6 +2809,99 @@ function featureUiComponentFiles(
|
|
|
2754
2809
|
];
|
|
2755
2810
|
}
|
|
2756
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
|
+
|
|
2757
2905
|
function featureUiIndexFile(
|
|
2758
2906
|
names: FeatureUiNames,
|
|
2759
2907
|
config: ResolvedBeignetConfig,
|
|
@@ -2949,12 +3097,98 @@ function scheduleFiles(
|
|
|
2949
3097
|
function uploadFiles(
|
|
2950
3098
|
names: UploadNames,
|
|
2951
3099
|
config: ResolvedBeignetConfig,
|
|
3100
|
+
options: { ui: boolean },
|
|
2952
3101
|
): GeneratedFile[] {
|
|
2953
3102
|
return [
|
|
3103
|
+
{
|
|
3104
|
+
path: uploadManifestFilePath(names, config),
|
|
3105
|
+
content: uploadManifestFile(names),
|
|
3106
|
+
},
|
|
2954
3107
|
{
|
|
2955
3108
|
path: uploadFilePath(names, config),
|
|
2956
3109
|
content: uploadFile(names, config),
|
|
2957
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
|
+
},
|
|
2958
3192
|
];
|
|
2959
3193
|
}
|
|
2960
3194
|
|
|
@@ -3447,7 +3681,7 @@ function schemasFile(
|
|
|
3447
3681
|
|
|
3448
3682
|
export const ${names.singularPascal}Schema = z.object({
|
|
3449
3683
|
id: z.string().uuid(),
|
|
3450
|
-
${options.
|
|
3684
|
+
${options.tenantScoped ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
|
|
3451
3685
|
version: z.number().int().min(1),
|
|
3452
3686
|
createdAt: z.string().datetime(),
|
|
3453
3687
|
updatedAt: z.string().datetime(),
|
|
@@ -3547,7 +3781,7 @@ export const List${names.pluralPascal}OutputSchema = z.object({
|
|
|
3547
3781
|
});
|
|
3548
3782
|
|
|
3549
3783
|
export const Create${names.singularPascal}InputSchema = z.object({
|
|
3550
|
-
name: z.string().min(1).max(120),
|
|
3784
|
+
name: z.string().trim().min(1).max(120),
|
|
3551
3785
|
});
|
|
3552
3786
|
${
|
|
3553
3787
|
mode === "resource"
|
|
@@ -3557,7 +3791,7 @@ export const ${names.singularPascal}IdInputSchema = z.object({
|
|
|
3557
3791
|
});
|
|
3558
3792
|
|
|
3559
3793
|
export const Update${names.singularPascal}BodySchema = z.object({
|
|
3560
|
-
name: z.string().min(1).max(120),
|
|
3794
|
+
name: z.string().trim().min(1).max(120),
|
|
3561
3795
|
version: z.number().int().min(1),
|
|
3562
3796
|
});
|
|
3563
3797
|
|
|
@@ -3603,7 +3837,7 @@ function listUseCaseFile(
|
|
|
3603
3837
|
|
|
3604
3838
|
return `import "@beignet/core/server-only";
|
|
3605
3839
|
import { normalizeCursorPage } from "@beignet/core/pagination";
|
|
3606
|
-
${options.
|
|
3840
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3607
3841
|
import {
|
|
3608
3842
|
decode${names.singularPascal}Cursor,
|
|
3609
3843
|
List${names.pluralPascal}InputSchema,
|
|
@@ -3615,7 +3849,7 @@ export const list${names.pluralPascal}UseCase = useCase
|
|
|
3615
3849
|
.input(List${names.pluralPascal}InputSchema)
|
|
3616
3850
|
.output(List${names.pluralPascal}OutputSchema)
|
|
3617
3851
|
.run(async ({ ctx, input }) => {
|
|
3618
|
-
${options.
|
|
3852
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const page = normalizeCursorPage(input, {
|
|
3619
3853
|
defaultLimit: 20,
|
|
3620
3854
|
maxLimit: 100,
|
|
3621
3855
|
});
|
|
@@ -3626,7 +3860,7 @@ ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const
|
|
|
3626
3860
|
name: input.name,
|
|
3627
3861
|
sortBy: input.sortBy,
|
|
3628
3862
|
sortDirection: input.sortDirection,
|
|
3629
|
-
}${options.
|
|
3863
|
+
}${options.tenantScoped ? ", scope" : ""});
|
|
3630
3864
|
});
|
|
3631
3865
|
`;
|
|
3632
3866
|
}
|
|
@@ -3642,7 +3876,7 @@ function createUseCaseFile(
|
|
|
3642
3876
|
);
|
|
3643
3877
|
|
|
3644
3878
|
return `import "@beignet/core/server-only";
|
|
3645
|
-
${options.
|
|
3879
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3646
3880
|
import {
|
|
3647
3881
|
Create${names.singularPascal}InputSchema,
|
|
3648
3882
|
${names.singularPascal}Schema,
|
|
@@ -3654,7 +3888,7 @@ export const create${names.singularPascal}UseCase = useCase
|
|
|
3654
3888
|
.input(Create${names.singularPascal}InputSchema)
|
|
3655
3889
|
.output(${names.singularPascal}Schema)
|
|
3656
3890
|
.run(async ({ ctx, input }) => {
|
|
3657
|
-
${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" : ""});
|
|
3658
3892
|
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Created, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
|
|
3659
3893
|
return ${names.singularCamel};
|
|
3660
3894
|
});
|
|
@@ -3672,7 +3906,7 @@ function getUseCaseFile(
|
|
|
3672
3906
|
);
|
|
3673
3907
|
|
|
3674
3908
|
return `import "@beignet/core/server-only";
|
|
3675
|
-
${options.
|
|
3909
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
3676
3910
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3677
3911
|
import {
|
|
3678
3912
|
${names.singularPascal}IdInputSchema,
|
|
@@ -3684,7 +3918,7 @@ export const get${names.singularPascal}UseCase = useCase
|
|
|
3684
3918
|
.input(${names.singularPascal}IdInputSchema)
|
|
3685
3919
|
.output(${names.singularPascal}Schema)
|
|
3686
3920
|
.run(async ({ ctx, input }) => {
|
|
3687
|
-
${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" : ""});
|
|
3688
3922
|
if (!${names.singularCamel}) {
|
|
3689
3923
|
throw appError("${names.singularPascal}NotFound", {
|
|
3690
3924
|
details: { id: input.id },
|
|
@@ -3707,7 +3941,7 @@ function updateUseCaseFile(
|
|
|
3707
3941
|
);
|
|
3708
3942
|
|
|
3709
3943
|
return `import "@beignet/core/server-only";
|
|
3710
|
-
${options.
|
|
3944
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
3711
3945
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3712
3946
|
${options.events ? `import { ${names.singularPascal}Updated } from "../domain/events";\n` : ""}import {
|
|
3713
3947
|
Update${names.singularPascal}InputSchema,
|
|
@@ -3719,13 +3953,13 @@ export const update${names.singularPascal}UseCase = useCase
|
|
|
3719
3953
|
.input(Update${names.singularPascal}InputSchema)
|
|
3720
3954
|
.output(${names.singularPascal}Schema)
|
|
3721
3955
|
.run(async ({ ctx, input }) => {
|
|
3722
|
-
${options.
|
|
3956
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
|
|
3723
3957
|
if (!existing) {
|
|
3724
3958
|
throw appError("${names.singularPascal}NotFound", {
|
|
3725
3959
|
details: { id: input.id },
|
|
3726
3960
|
});
|
|
3727
3961
|
}
|
|
3728
|
-
${options.
|
|
3962
|
+
${options.authorization ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
|
|
3729
3963
|
throw appError("${names.singularPascal}Conflict", {
|
|
3730
3964
|
details: { id: input.id, expectedVersion: existing.version },
|
|
3731
3965
|
});
|
|
@@ -3733,7 +3967,7 @@ ${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", ex
|
|
|
3733
3967
|
|
|
3734
3968
|
const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.update({
|
|
3735
3969
|
...input,
|
|
3736
|
-
}${options.
|
|
3970
|
+
}${options.tenantScoped ? ", scope" : ""});
|
|
3737
3971
|
if (!${names.singularCamel}) {
|
|
3738
3972
|
throw appError("${names.singularPascal}Conflict", {
|
|
3739
3973
|
details: { id: input.id, expectedVersion: existing.version },
|
|
@@ -3757,7 +3991,7 @@ function deleteUseCaseFile(
|
|
|
3757
3991
|
|
|
3758
3992
|
return `import "@beignet/core/server-only";
|
|
3759
3993
|
import { z } from "zod";
|
|
3760
|
-
${options.
|
|
3994
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
3761
3995
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
3762
3996
|
${options.events ? `import { ${names.singularPascal}Deleted } from "../domain/events";\n` : ""}import { ${names.singularPascal}IdInputSchema } from "../schemas";
|
|
3763
3997
|
|
|
@@ -3766,7 +4000,7 @@ export const delete${names.singularPascal}UseCase = useCase
|
|
|
3766
4000
|
.input(${names.singularPascal}IdInputSchema)
|
|
3767
4001
|
.output(z.void())
|
|
3768
4002
|
.run(async ({ ctx, input }) => {
|
|
3769
|
-
${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" : ""});
|
|
3770
4004
|
if (!deleted) {
|
|
3771
4005
|
throw appError("${names.singularPascal}NotFound", {
|
|
3772
4006
|
details: { id: input.id },
|
|
@@ -3806,7 +4040,7 @@ function repositoryPortFile(
|
|
|
3806
4040
|
PageResult,
|
|
3807
4041
|
SortDirection,
|
|
3808
4042
|
} from "@beignet/core/pagination";
|
|
3809
|
-
${options.
|
|
4043
|
+
${options.tenantScoped ? `import type { TenantScope } from "@beignet/core/tenancy";\n` : ""}import type {
|
|
3810
4044
|
Create${names.singularPascal}Input,
|
|
3811
4045
|
${names.singularPascal}Cursor,
|
|
3812
4046
|
${names.singularPascal}SortBy,
|
|
@@ -3823,9 +4057,9 @@ export type List${names.pluralPascal}Query = {
|
|
|
3823
4057
|
};
|
|
3824
4058
|
|
|
3825
4059
|
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.
|
|
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` : ""}}
|
|
3829
4063
|
`;
|
|
3830
4064
|
}
|
|
3831
4065
|
|
|
@@ -3859,14 +4093,14 @@ function inMemoryRepositoryFile(
|
|
|
3859
4093
|
? " || existing.deletedAt !== null"
|
|
3860
4094
|
: "";
|
|
3861
4095
|
const deleteImplementation = options.softDelete
|
|
3862
|
-
? `${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();
|
|
3863
4097
|
${names.pluralCamel}.set(id, { ...existing, deletedAt: now, updatedAt: now });
|
|
3864
4098
|
return true;`
|
|
3865
|
-
: `${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);`;
|
|
3866
4100
|
|
|
3867
4101
|
return `import "@beignet/core/server-only";
|
|
3868
4102
|
import { cursorPageResult } from "@beignet/core/pagination";
|
|
3869
|
-
${options.
|
|
4103
|
+
${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
3870
4104
|
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
3871
4105
|
import type {
|
|
3872
4106
|
Create${names.singularPascal}Input,
|
|
@@ -3876,7 +4110,7 @@ ${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.s
|
|
|
3876
4110
|
${recordType}function to${names.singularPascal}(${names.singularCamel}: ${mapValueType}): ${names.singularPascal} {
|
|
3877
4111
|
return {
|
|
3878
4112
|
id: ${names.singularCamel}.id,
|
|
3879
|
-
${options.
|
|
4113
|
+
${options.tenantScoped ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
|
|
3880
4114
|
version: ${names.singularCamel}.version,
|
|
3881
4115
|
createdAt: ${names.singularCamel}.createdAt,
|
|
3882
4116
|
updatedAt: ${names.singularCamel}.updatedAt,
|
|
@@ -3934,10 +4168,10 @@ export function createInMemory${names.singularPascal}Repository(
|
|
|
3934
4168
|
);
|
|
3935
4169
|
|
|
3936
4170
|
return {
|
|
3937
|
-
async list(query${options.
|
|
4171
|
+
async list(query${options.tenantScoped ? ", scope" : ""}) {
|
|
3938
4172
|
const name = query.name?.toLocaleLowerCase();
|
|
3939
|
-
${options.
|
|
3940
|
-
${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))
|
|
3941
4175
|
.sort((left, right) => compare${names.pluralPascal}(left, right, query))
|
|
3942
4176
|
.filter((${names.singularCamel}) => isAfter${names.singularPascal}Cursor(${names.singularCamel}, query));
|
|
3943
4177
|
const pageItems = all${names.pluralPascal}.slice(0, query.page.limit);
|
|
@@ -3952,11 +4186,11 @@ ${activeFilter}${options.tenant ? `\t\t\t\t.filter((${names.singularCamel}) => $
|
|
|
3952
4186
|
nextCursor,
|
|
3953
4187
|
);
|
|
3954
4188
|
},
|
|
3955
|
-
async create(input${options.
|
|
4189
|
+
async create(input${options.tenantScoped ? ", scope" : ""}) {
|
|
3956
4190
|
const now = new Date().toISOString();
|
|
3957
|
-
${options.
|
|
4191
|
+
${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel}: ${names.singularPascal} = {
|
|
3958
4192
|
id: crypto.randomUUID(),
|
|
3959
|
-
${options.
|
|
4193
|
+
${options.tenantScoped ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
|
|
3960
4194
|
version: 1,
|
|
3961
4195
|
createdAt: now,
|
|
3962
4196
|
updatedAt: now,
|
|
@@ -3966,7 +4200,7 @@ ${options.tenant ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
|
|
|
3966
4200
|
${newDeletedAtField} });
|
|
3967
4201
|
return ${names.singularCamel};
|
|
3968
4202
|
},
|
|
3969
|
-
${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` : ""}
|
|
3970
4204
|
};
|
|
3971
4205
|
}
|
|
3972
4206
|
`;
|
|
@@ -3981,7 +4215,7 @@ function drizzleSchemaFile(
|
|
|
3981
4215
|
|
|
3982
4216
|
export const ${names.pluralCamel} = ${dialect.tableFunction}("${names.pluralKebab.replaceAll("-", "_")}", {
|
|
3983
4217
|
id: ${dialect.idColumn("id")}.primaryKey(),
|
|
3984
|
-
${options.
|
|
4218
|
+
${options.tenantScoped ? `\ttenantId: ${dialect.idColumn("tenant_id")}.notNull(),\n` : ""} name: text("name").notNull(),
|
|
3985
4219
|
version: ${dialect.integerColumn("version")}.notNull(),
|
|
3986
4220
|
createdAt: ${dialect.timestampColumn("created_at")}.notNull(),
|
|
3987
4221
|
updatedAt: ${dialect.timestampColumn("updated_at")}.notNull(),
|
|
@@ -4091,20 +4325,20 @@ function drizzleRepositoryFile(
|
|
|
4091
4325
|
const schemaPath = drizzleSchemaIndexPath(config);
|
|
4092
4326
|
const findWhere = drizzleResourceWhere(names, options, [
|
|
4093
4327
|
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
4094
|
-
...(options.
|
|
4328
|
+
...(options.tenantScoped
|
|
4095
4329
|
? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
|
|
4096
4330
|
: []),
|
|
4097
4331
|
]);
|
|
4098
4332
|
const updateWhere = drizzleResourceWhere(names, options, [
|
|
4099
4333
|
`eq(schema.${names.pluralCamel}.id, input.id)`,
|
|
4100
4334
|
`eq(schema.${names.pluralCamel}.version, input.version)`,
|
|
4101
|
-
...(options.
|
|
4335
|
+
...(options.tenantScoped
|
|
4102
4336
|
? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
|
|
4103
4337
|
: []),
|
|
4104
4338
|
]);
|
|
4105
4339
|
const deleteWhere = drizzleResourceWhere(names, options, [
|
|
4106
4340
|
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
4107
|
-
...(options.
|
|
4341
|
+
...(options.tenantScoped
|
|
4108
4342
|
? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
|
|
4109
4343
|
: []),
|
|
4110
4344
|
]);
|
|
@@ -4122,7 +4356,7 @@ function drizzleRepositoryFile(
|
|
|
4122
4356
|
].filter((name): name is string => Boolean(name));
|
|
4123
4357
|
// After a version-guarded update succeeds, re-select by identity only —
|
|
4124
4358
|
// matching what `.returning()` yields on the dialects that support it.
|
|
4125
|
-
const updateReselectWhere = options.
|
|
4359
|
+
const updateReselectWhere = options.tenantScoped
|
|
4126
4360
|
? `and(eq(schema.${names.pluralCamel}.id, input.id), eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)))`
|
|
4127
4361
|
: `eq(schema.${names.pluralCamel}.id, input.id)`;
|
|
4128
4362
|
// drizzle-orm/mysql2 has no `.returning(...)`; mutations resolve to a
|
|
@@ -4145,13 +4379,13 @@ function affectedRows(result: unknown): number {
|
|
|
4145
4379
|
`
|
|
4146
4380
|
: "";
|
|
4147
4381
|
const createMethod = dialect.supportsReturning
|
|
4148
|
-
? ` async create(input${options.
|
|
4382
|
+
? ` async create(input${options.tenantScoped ? ", scope" : ""}) {
|
|
4149
4383
|
const now = new Date().toISOString();
|
|
4150
4384
|
const [row] = await db
|
|
4151
4385
|
.insert(schema.${names.pluralCamel})
|
|
4152
4386
|
.values({
|
|
4153
4387
|
id: crypto.randomUUID(),
|
|
4154
|
-
${options.
|
|
4388
|
+
${options.tenantScoped ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
|
|
4155
4389
|
version: 1,
|
|
4156
4390
|
createdAt: now,
|
|
4157
4391
|
updatedAt: now,
|
|
@@ -4165,11 +4399,11 @@ ${options.tenant ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name:
|
|
|
4165
4399
|
return to${names.singularPascal}(row);
|
|
4166
4400
|
},
|
|
4167
4401
|
`
|
|
4168
|
-
: ` async create(input${options.
|
|
4402
|
+
: ` async create(input${options.tenantScoped ? ", scope" : ""}) {
|
|
4169
4403
|
const now = new Date().toISOString();
|
|
4170
4404
|
const ${names.singularCamel}: ${names.singularPascal} = {
|
|
4171
4405
|
id: crypto.randomUUID(),
|
|
4172
|
-
${options.
|
|
4406
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
|
|
4173
4407
|
version: 1,
|
|
4174
4408
|
createdAt: now,
|
|
4175
4409
|
updatedAt: now,
|
|
@@ -4183,7 +4417,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
|
|
|
4183
4417
|
mode !== "resource"
|
|
4184
4418
|
? ""
|
|
4185
4419
|
: dialect.supportsReturning
|
|
4186
|
-
? ` async findById(id: string${options.
|
|
4420
|
+
? ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
4187
4421
|
const [row] = await db
|
|
4188
4422
|
.select()
|
|
4189
4423
|
.from(schema.${names.pluralCamel})
|
|
@@ -4192,7 +4426,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
|
|
|
4192
4426
|
|
|
4193
4427
|
return row ? to${names.singularPascal}(row) : null;
|
|
4194
4428
|
},
|
|
4195
|
-
async update(input${options.
|
|
4429
|
+
async update(input${options.tenantScoped ? ", scope" : ""}) {
|
|
4196
4430
|
const [row] = await db
|
|
4197
4431
|
.update(schema.${names.pluralCamel})
|
|
4198
4432
|
.set({
|
|
@@ -4205,12 +4439,12 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
|
|
|
4205
4439
|
|
|
4206
4440
|
return row ? to${names.singularPascal}(row) : null;
|
|
4207
4441
|
},
|
|
4208
|
-
async delete(id: string${options.
|
|
4442
|
+
async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
4209
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`}
|
|
4210
4444
|
return rows.length > 0;
|
|
4211
4445
|
},
|
|
4212
4446
|
`
|
|
4213
|
-
: ` async findById(id: string${options.
|
|
4447
|
+
: ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
4214
4448
|
const [row] = await db
|
|
4215
4449
|
.select()
|
|
4216
4450
|
.from(schema.${names.pluralCamel})
|
|
@@ -4219,7 +4453,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
|
|
|
4219
4453
|
|
|
4220
4454
|
return row ? to${names.singularPascal}(row) : null;
|
|
4221
4455
|
},
|
|
4222
|
-
async update(input${options.
|
|
4456
|
+
async update(input${options.tenantScoped ? ", scope" : ""}) {
|
|
4223
4457
|
const result = await db
|
|
4224
4458
|
.update(schema.${names.pluralCamel})
|
|
4225
4459
|
.set({
|
|
@@ -4239,7 +4473,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
|
|
|
4239
4473
|
|
|
4240
4474
|
return row ? to${names.singularPascal}(row) : null;
|
|
4241
4475
|
},
|
|
4242
|
-
async delete(id: string${options.
|
|
4476
|
+
async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
4243
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`}
|
|
4244
4478
|
return affectedRows(result) > 0;
|
|
4245
4479
|
},
|
|
@@ -4247,7 +4481,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
|
|
|
4247
4481
|
|
|
4248
4482
|
return `import "@beignet/core/server-only";
|
|
4249
4483
|
import { cursorPageResult } from "@beignet/core/pagination";
|
|
4250
|
-
${options.
|
|
4484
|
+
${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
|
|
4251
4485
|
import { ${drizzleImports.join(", ")} } from "drizzle-orm";
|
|
4252
4486
|
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
4253
4487
|
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
@@ -4262,7 +4496,7 @@ type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelec
|
|
|
4262
4496
|
function to${names.singularPascal}(row: ${names.singularPascal}Row): ${names.singularPascal} {
|
|
4263
4497
|
return {
|
|
4264
4498
|
id: row.id,
|
|
4265
|
-
${options.
|
|
4499
|
+
${options.tenantScoped ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
|
|
4266
4500
|
version: row.version,
|
|
4267
4501
|
createdAt: row.createdAt,
|
|
4268
4502
|
updatedAt: row.updatedAt,
|
|
@@ -4294,9 +4528,9 @@ function ${names.singularCamel}CursorFilter(
|
|
|
4294
4528
|
|
|
4295
4529
|
function ${names.singularCamel}ListWhere(
|
|
4296
4530
|
query: List${names.pluralPascal}Query,
|
|
4297
|
-
${options.
|
|
4531
|
+
${options.tenantScoped ? `\tscope: Parameters<${names.singularPascal}Repository["list"]>[1],\n` : ""}): SQL<unknown> | undefined {
|
|
4298
4532
|
const filters: SQL<unknown>[] = [
|
|
4299
|
-
${options.
|
|
4533
|
+
${options.tenantScoped ? `\t\teq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
|
|
4300
4534
|
const cursor = ${names.singularCamel}CursorFilter(query);
|
|
4301
4535
|
|
|
4302
4536
|
if (query.name) {
|
|
@@ -4330,8 +4564,8 @@ export function createDrizzle${names.singularPascal}Repository(
|
|
|
4330
4564
|
db: ${dialect.dbTypeName}<typeof schema>,
|
|
4331
4565
|
): ${names.singularPascal}Repository {
|
|
4332
4566
|
return {
|
|
4333
|
-
async list(query${options.
|
|
4334
|
-
const where = ${names.singularCamel}ListWhere(query${options.
|
|
4567
|
+
async list(query${options.tenantScoped ? ", scope" : ""}) {
|
|
4568
|
+
const where = ${names.singularCamel}ListWhere(query${options.tenantScoped ? ", scope" : ""});
|
|
4335
4569
|
const rows = await db
|
|
4336
4570
|
.select()
|
|
4337
4571
|
.from(schema.${names.pluralCamel})
|
|
@@ -4393,7 +4627,7 @@ export const list${names.pluralPascal} = ${names.pluralCamel}
|
|
|
4393
4627
|
export const create${names.singularPascal} = ${names.pluralCamel}
|
|
4394
4628
|
.post("/api/${names.pluralKebab}")
|
|
4395
4629
|
.body(Create${names.singularPascal}InputSchema)
|
|
4396
|
-
${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({
|
|
4397
4631
|
201: ${names.singularPascal}Schema,
|
|
4398
4632
|
});
|
|
4399
4633
|
${
|
|
@@ -4411,7 +4645,7 @@ export const update${names.singularPascal} = ${names.pluralCamel}
|
|
|
4411
4645
|
.patch("/api/${names.pluralKebab}/:id")
|
|
4412
4646
|
.pathParams(${names.singularPascal}IdInputSchema)
|
|
4413
4647
|
.body(Update${names.singularPascal}BodySchema)
|
|
4414
|
-
${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 })
|
|
4415
4649
|
.responses({
|
|
4416
4650
|
200: ${names.singularPascal}Schema,
|
|
4417
4651
|
});
|
|
@@ -4419,7 +4653,7 @@ ${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability
|
|
|
4419
4653
|
export const delete${names.singularPascal} = ${names.pluralCamel}
|
|
4420
4654
|
.delete("/api/${names.pluralKebab}/:id")
|
|
4421
4655
|
.pathParams(${names.singularPascal}IdInputSchema)
|
|
4422
|
-
${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 })
|
|
4423
4657
|
.responses({
|
|
4424
4658
|
204: null,
|
|
4425
4659
|
});
|
|
@@ -4624,7 +4858,7 @@ function isAuthenticated(ctx: AuthorizationContext) {
|
|
|
4624
4858
|
|
|
4625
4859
|
function canWrite(ctx: AuthorizationContext) {
|
|
4626
4860
|
if (!isAuthenticated(ctx)) return deny("You must be signed in.");
|
|
4627
|
-
${options.
|
|
4861
|
+
${options.tenantScoped ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
|
|
4628
4862
|
}
|
|
4629
4863
|
|
|
4630
4864
|
export const ${names.singularCamel}Policy = definePolicy({
|
|
@@ -4634,13 +4868,13 @@ export const ${names.singularCamel}Policy = definePolicy({
|
|
|
4634
4868
|
"${names.pluralCamel}.update": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
4635
4869
|
const decision = canWrite(ctx);
|
|
4636
4870
|
if (decision !== true) return decision;
|
|
4637
|
-
${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` : ""}
|
|
4638
4872
|
return true;
|
|
4639
4873
|
},
|
|
4640
4874
|
"${names.pluralCamel}.delete": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
4641
4875
|
const decision = canWrite(ctx);
|
|
4642
4876
|
if (decision !== true) return decision;
|
|
4643
|
-
${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` : ""}
|
|
4644
4878
|
return true;
|
|
4645
4879
|
},
|
|
4646
4880
|
"${names.pluralCamel}.manage": (_ctx: AuthorizationContext) =>
|
|
@@ -4692,7 +4926,7 @@ function create${names.singularPascal}(): ${names.singularPascal} {
|
|
|
4692
4926
|
const now = new Date().toISOString();
|
|
4693
4927
|
return {
|
|
4694
4928
|
id: "00000000-0000-4000-8000-000000000001",
|
|
4695
|
-
${options.
|
|
4929
|
+
${options.tenantScoped ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
|
|
4696
4930
|
version: 1,
|
|
4697
4931
|
createdAt: now,
|
|
4698
4932
|
updatedAt: now,
|
|
@@ -4707,7 +4941,7 @@ describe("${names.singularCamel}Policy", () => {
|
|
|
4707
4941
|
const ${names.singularCamel} = create${names.singularPascal}();
|
|
4708
4942
|
const ctx = {
|
|
4709
4943
|
actor: { type: "user" as const, id: "user_test" },
|
|
4710
|
-
${options.
|
|
4944
|
+
${options.tenantScoped ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
|
|
4711
4945
|
|
|
4712
4946
|
await expect(
|
|
4713
4947
|
tester.assertMatrix([
|
|
@@ -4933,8 +5167,33 @@ ${names.timezone ? `\t\ttimezone: "${names.timezone}",\n` : ""}\t\tpayload: ${na
|
|
|
4933
5167
|
`;
|
|
4934
5168
|
}
|
|
4935
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
|
+
|
|
4936
5194
|
function uploadFile(names: UploadNames, config: ResolvedBeignetConfig): string {
|
|
4937
5195
|
return `import { z } from "zod";
|
|
5196
|
+
import { ${names.fileConstraintsExportName} } from "${relativeModule(uploadFilePath(names, config), uploadManifestFilePath(names, config))}";
|
|
4938
5197
|
import { defineUpload } from "${aliasModule(config.paths.uploadsBuilder)}";
|
|
4939
5198
|
|
|
4940
5199
|
export const ${names.metadataSchemaName} = z.object({
|
|
@@ -4945,13 +5204,7 @@ export type ${names.metadataTypeName} = z.infer<typeof ${names.metadataSchemaNam
|
|
|
4945
5204
|
|
|
4946
5205
|
export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
|
|
4947
5206
|
\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},
|
|
5207
|
+
\tfile: ${names.fileConstraintsExportName},
|
|
4955
5208
|
\tauthorize({ ctx }) {
|
|
4956
5209
|
\t\treturn ctx.actor.type === "user";
|
|
4957
5210
|
\t},
|
|
@@ -4982,6 +5235,148 @@ export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
|
|
|
4982
5235
|
`;
|
|
4983
5236
|
}
|
|
4984
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
|
+
|
|
4985
5380
|
function uploadRouteFile(
|
|
4986
5381
|
names: UploadNames,
|
|
4987
5382
|
config: ResolvedBeignetConfig,
|
|
@@ -5101,63 +5496,70 @@ function featureUiComponentFile(
|
|
|
5101
5496
|
|
|
5102
5497
|
return `"use client";
|
|
5103
5498
|
|
|
5104
|
-
import {
|
|
5499
|
+
import { rootFormError } from "@beignet/react-hook-form";
|
|
5105
5500
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5106
|
-
import {
|
|
5501
|
+
import { rhf } from "${aliasModule(clientFormsPath(config))}";
|
|
5502
|
+
import { create${names.singularPascal} } from "${relativeModule(componentPath, resourceContractFilePath(names, config))}";
|
|
5107
5503
|
import {
|
|
5108
5504
|
\tcreate${names.singularPascal}MutationOptions,
|
|
5109
5505
|
\tinvalidate${names.pluralPascal},
|
|
5110
5506
|
\tlist${names.pluralPascal}QueryOptions,
|
|
5111
5507
|
} from "${relativeModule(componentPath, clientQueriesPath)}";
|
|
5112
5508
|
|
|
5509
|
+
const create${names.singularPascal}Form = rhf(create${names.singularPascal});
|
|
5510
|
+
|
|
5113
5511
|
export function ${names.componentExportName}() {
|
|
5114
|
-
\tconst [name, setName] = useState("");
|
|
5115
5512
|
\tconst queryClient = useQueryClient();
|
|
5116
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");
|
|
5117
5518
|
\tconst create${names.singularPascal}Mutation = useMutation({
|
|
5118
5519
|
\t\t...create${names.singularPascal}MutationOptions(),
|
|
5119
5520
|
\t\tonSuccess: async () => {
|
|
5120
|
-
\t\t\
|
|
5521
|
+
\t\t\tform.reset({ name: "" });
|
|
5121
5522
|
\t\t\tawait invalidate${names.pluralPascal}(queryClient);
|
|
5122
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},
|
|
5123
5530
|
\t});
|
|
5124
5531
|
|
|
5125
5532
|
\treturn (
|
|
5126
5533
|
\t\t<section className="${names.pluralKebab}-panel">
|
|
5127
5534
|
\t\t\t<form
|
|
5128
5535
|
\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;
|
|
5536
|
+
\t\t\t\tonSubmit={form.handleSubmit((values) => {
|
|
5537
|
+
\t\t\t\t\tform.clearErrors("root");
|
|
5133
5538
|
\t\t\t\t\tcreate${names.singularPascal}Mutation.reset();
|
|
5134
5539
|
\t\t\t\t\tcreate${names.singularPascal}Mutation.mutate({
|
|
5135
|
-
\t\t\t\t\t\tbody:
|
|
5540
|
+
\t\t\t\t\t\tbody: values,
|
|
5136
5541
|
\t\t\t\t\t});
|
|
5137
|
-
\t\t\t\t}}
|
|
5542
|
+
\t\t\t\t})}
|
|
5138
5543
|
\t\t\t>
|
|
5139
5544
|
\t\t\t\t<label htmlFor="${names.singularKebab}-name">New ${names.singularKebab}</label>
|
|
5140
5545
|
\t\t\t\t<div className="${names.pluralKebab}-composer-row">
|
|
5141
5546
|
\t\t\t\t\t<input
|
|
5142
5547
|
\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)}
|
|
5548
|
+
\t\t\t\t\t\t{...form.register("name")}
|
|
5145
5549
|
\t\t\t\t\t\tplaceholder="Name"
|
|
5146
5550
|
\t\t\t\t\t/>
|
|
5147
5551
|
\t\t\t\t\t<button
|
|
5148
5552
|
\t\t\t\t\t\ttype="submit"
|
|
5149
|
-
\t\t\t\t\t\tdisabled={!name
|
|
5553
|
+
\t\t\t\t\t\tdisabled={!name?.trim() || create${names.singularPascal}Mutation.isPending}
|
|
5150
5554
|
\t\t\t\t\t>
|
|
5151
5555
|
\t\t\t\t\t\t{create${names.singularPascal}Mutation.isPending ? "Creating" : "Create"}
|
|
5152
5556
|
\t\t\t\t\t</button>
|
|
5153
5557
|
\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>
|
|
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>
|
|
5161
5563
|
\t\t\t\t) : null}
|
|
5162
5564
|
\t\t\t</form>
|
|
5163
5565
|
|
|
@@ -5306,7 +5708,7 @@ ${mode === "resource" ? `import { isAppError } from "@beignet/core/errors";\n` :
|
|
|
5306
5708
|
import { createTestApp } from "@beignet/web/testing";
|
|
5307
5709
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
5308
5710
|
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
5309
|
-
import { ${options.
|
|
5711
|
+
import { ${options.authorization ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenantScoped ? ", createTestTenant" : ""} } from "@beignet/core/testing";
|
|
5310
5712
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
5311
5713
|
import { initialPorts } from "${aliasModule(config.paths.portWiring)}";
|
|
5312
5714
|
import { appContext } from "${aliasModule(serverContextPath)}";
|
|
@@ -5326,40 +5728,40 @@ describe("${names.pluralCamel} resource", () => {
|
|
|
5326
5728
|
const seed${names.pluralPascal} = [
|
|
5327
5729
|
{
|
|
5328
5730
|
id: "00000000-0000-4000-8000-000000000101",
|
|
5329
|
-
${options.
|
|
5731
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
|
|
5330
5732
|
version: 1,
|
|
5331
5733
|
createdAt: "2024-01-01T00:00:00.000Z",
|
|
5332
5734
|
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
5333
5735
|
},
|
|
5334
5736
|
{
|
|
5335
5737
|
id: "00000000-0000-4000-8000-000000000102",
|
|
5336
|
-
${options.
|
|
5738
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
|
|
5337
5739
|
version: 1,
|
|
5338
5740
|
createdAt: "2024-01-02T00:00:00.000Z",
|
|
5339
5741
|
updatedAt: "2024-01-02T00:00:00.000Z",
|
|
5340
5742
|
},
|
|
5341
5743
|
{
|
|
5342
5744
|
id: "00000000-0000-4000-8000-000000000103",
|
|
5343
|
-
${options.
|
|
5745
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
|
|
5344
5746
|
version: 1,
|
|
5345
5747
|
createdAt: "2024-01-03T00:00:00.000Z",
|
|
5346
5748
|
updatedAt: "2024-01-03T00:00:00.000Z",
|
|
5347
5749
|
},
|
|
5348
|
-
${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` : ""} ];
|
|
5349
5751
|
const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository(seed${names.pluralPascal});
|
|
5350
5752
|
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
5351
5753
|
base: initialPorts,
|
|
5352
5754
|
overrides: {
|
|
5353
5755
|
${names.pluralCamel},
|
|
5354
5756
|
${
|
|
5355
|
-
options.
|
|
5757
|
+
options.authorization || options.tenantScoped
|
|
5356
5758
|
? `\t\t\t\tauth: {
|
|
5357
5759
|
getSession: async () => ({
|
|
5358
|
-
user: { id: "user_test", name: "Test User"${options.
|
|
5359
|
-
${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` : ""}
|
|
5360
5762
|
}),
|
|
5361
5763
|
},
|
|
5362
|
-
${options.
|
|
5764
|
+
${options.authorization ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
|
|
5363
5765
|
`
|
|
5364
5766
|
: `\t\t\t\tauth: {
|
|
5365
5767
|
getSession: async () => null,
|
|
@@ -5375,8 +5777,8 @@ ${options.auth ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
|
|
|
5375
5777
|
});
|
|
5376
5778
|
const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
|
|
5377
5779
|
ports: testFixture.ports,
|
|
5378
|
-
actor: ${options.
|
|
5379
|
-
tenant: ${options.
|
|
5780
|
+
actor: ${options.authorization ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
|
|
5781
|
+
tenant: ${options.tenantScoped ? `createTestTenant("tenant_test")` : "null"},
|
|
5380
5782
|
});
|
|
5381
5783
|
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
5382
5784
|
|