@beignet/cli 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +144 -55
- package/dist/config.d.ts +31 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -0
- package/dist/config.js.map +1 -1
- package/dist/create.d.ts +9 -0
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +5 -0
- package/dist/create.js.map +1 -1
- package/dist/db.d.ts +36 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +111 -0
- package/dist/db.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +243 -2
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +24 -0
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +288 -4
- package/dist/inspect.js.map +1 -1
- package/dist/lint.d.ts +12 -0
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +30 -5
- package/dist/lint.js.map +1 -1
- package/dist/make.d.ts +91 -0
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +602 -72
- package/dist/make.js.map +1 -1
- package/dist/templates.d.ts +33 -0
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +1396 -112
- package/dist/templates.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +31 -0
- package/src/create.ts +11 -0
- package/src/db.ts +166 -0
- package/src/index.ts +320 -0
- package/src/inspect.ts +497 -3
- package/src/lint.ts +43 -9
- package/src/make.ts +917 -80
- package/src/templates.ts +1425 -111
package/dist/make.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { directoryPath, loadBeignetConfig, normalizePath, resolveConfig, } from "./config.js";
|
|
4
|
+
export const makeFeatureAddonChoices = [
|
|
5
|
+
"policy",
|
|
6
|
+
"event",
|
|
7
|
+
"events",
|
|
8
|
+
"job",
|
|
9
|
+
"jobs",
|
|
10
|
+
"notification",
|
|
11
|
+
"notifications",
|
|
12
|
+
"upload",
|
|
13
|
+
"uploads",
|
|
14
|
+
];
|
|
4
15
|
export async function makeResource(options) {
|
|
5
16
|
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
6
17
|
const names = resourceNames(options.name);
|
|
@@ -47,6 +58,91 @@ export async function makeResource(options) {
|
|
|
47
58
|
skippedFiles,
|
|
48
59
|
};
|
|
49
60
|
}
|
|
61
|
+
export async function makeFeature(options) {
|
|
62
|
+
const addons = normalizeMakeFeatureAddons(options.with ?? []);
|
|
63
|
+
if (addons.length === 0) {
|
|
64
|
+
return makeFeatureResource(options);
|
|
65
|
+
}
|
|
66
|
+
if (options.dryRun) {
|
|
67
|
+
return makeFeatureSlice(options, addons);
|
|
68
|
+
}
|
|
69
|
+
await makeFeatureSlice({ ...options, dryRun: true }, addons);
|
|
70
|
+
return makeFeatureSlice(options, addons);
|
|
71
|
+
}
|
|
72
|
+
async function makeFeatureResource(options) {
|
|
73
|
+
try {
|
|
74
|
+
return await makeResource(options);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error instanceof Error && error.message.includes("make resource")) {
|
|
78
|
+
throw new Error(error.message.replaceAll("make resource", "make feature"));
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function makeFeatureSlice(options, addons) {
|
|
84
|
+
let result = await makeFeatureResource(options);
|
|
85
|
+
for (const addon of addons) {
|
|
86
|
+
result = mergeMakeResults(result, await makeFeatureAddon(addon, result.name, options));
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
async function makeFeatureAddon(addon, feature, options) {
|
|
91
|
+
const shared = {
|
|
92
|
+
cwd: options.cwd,
|
|
93
|
+
force: options.force,
|
|
94
|
+
dryRun: options.dryRun,
|
|
95
|
+
config: options.config,
|
|
96
|
+
};
|
|
97
|
+
if (addon === "policy") {
|
|
98
|
+
return makePolicy({ ...shared, name: feature });
|
|
99
|
+
}
|
|
100
|
+
if (addon === "event") {
|
|
101
|
+
return makeEvent({ ...shared, name: `${feature}/created` });
|
|
102
|
+
}
|
|
103
|
+
if (addon === "job") {
|
|
104
|
+
return makeJob({ ...shared, name: `${feature}/process` });
|
|
105
|
+
}
|
|
106
|
+
if (addon === "notification") {
|
|
107
|
+
return makeNotification({ ...shared, name: `${feature}/created` });
|
|
108
|
+
}
|
|
109
|
+
return makeUpload({ ...shared, name: `${feature}/attachment` });
|
|
110
|
+
}
|
|
111
|
+
function normalizeMakeFeatureAddons(addons) {
|
|
112
|
+
const normalized = new Set();
|
|
113
|
+
for (const addon of addons) {
|
|
114
|
+
if (addon === "events")
|
|
115
|
+
normalized.add("event");
|
|
116
|
+
else if (addon === "jobs")
|
|
117
|
+
normalized.add("job");
|
|
118
|
+
else if (addon === "notifications")
|
|
119
|
+
normalized.add("notification");
|
|
120
|
+
else if (addon === "uploads")
|
|
121
|
+
normalized.add("upload");
|
|
122
|
+
else
|
|
123
|
+
normalized.add(addon);
|
|
124
|
+
}
|
|
125
|
+
return featureAddonOrder.filter((addon) => normalized.has(addon));
|
|
126
|
+
}
|
|
127
|
+
const featureAddonOrder = [
|
|
128
|
+
"policy",
|
|
129
|
+
"event",
|
|
130
|
+
"job",
|
|
131
|
+
"notification",
|
|
132
|
+
"upload",
|
|
133
|
+
];
|
|
134
|
+
function mergeMakeResults(left, right) {
|
|
135
|
+
return {
|
|
136
|
+
...left,
|
|
137
|
+
files: uniqueStrings([...left.files, ...right.files]),
|
|
138
|
+
createdFiles: uniqueStrings([...left.createdFiles, ...right.createdFiles]),
|
|
139
|
+
updatedFiles: uniqueStrings([...left.updatedFiles, ...right.updatedFiles]),
|
|
140
|
+
skippedFiles: uniqueStrings([...left.skippedFiles, ...right.skippedFiles]),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function uniqueStrings(values) {
|
|
144
|
+
return [...new Set(values)];
|
|
145
|
+
}
|
|
50
146
|
export async function makeContract(options) {
|
|
51
147
|
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
52
148
|
const names = resourceNames(options.name);
|
|
@@ -145,7 +241,9 @@ export async function makeTest(options) {
|
|
|
145
241
|
if (result === "skipped")
|
|
146
242
|
skippedFiles.push(file.path);
|
|
147
243
|
}
|
|
148
|
-
if (await updatePackageJson(targetDir, {
|
|
244
|
+
if (await updatePackageJson(targetDir, {
|
|
245
|
+
dryRun: Boolean(options.dryRun),
|
|
246
|
+
})) {
|
|
149
247
|
updatedFiles.push("package.json");
|
|
150
248
|
}
|
|
151
249
|
return {
|
|
@@ -320,6 +418,66 @@ export async function makeJob(options) {
|
|
|
320
418
|
},
|
|
321
419
|
});
|
|
322
420
|
}
|
|
421
|
+
export async function makeFactory(options) {
|
|
422
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
423
|
+
const names = factoryNames(options.name);
|
|
424
|
+
const config = options.config
|
|
425
|
+
? resolveConfig(options.config)
|
|
426
|
+
: await loadBeignetConfig(targetDir);
|
|
427
|
+
await assertPersistenceArtifactApp(targetDir, names, config, "factory");
|
|
428
|
+
return makeFeatureArtifact({
|
|
429
|
+
targetDir,
|
|
430
|
+
config,
|
|
431
|
+
force: Boolean(options.force),
|
|
432
|
+
dryRun: Boolean(options.dryRun),
|
|
433
|
+
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
434
|
+
files: factoryFiles(names, config),
|
|
435
|
+
index: featureArtifactIndexFile("factory", names, config),
|
|
436
|
+
dependencies: {
|
|
437
|
+
"@beignet/core": beignetDependencyVersion,
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
export async function makeSeed(options) {
|
|
442
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
443
|
+
const names = seedNames(options.name);
|
|
444
|
+
const config = options.config
|
|
445
|
+
? resolveConfig(options.config)
|
|
446
|
+
: await loadBeignetConfig(targetDir);
|
|
447
|
+
await assertPersistenceArtifactApp(targetDir, names, config, "seed");
|
|
448
|
+
return makeFeatureArtifact({
|
|
449
|
+
targetDir,
|
|
450
|
+
config,
|
|
451
|
+
force: Boolean(options.force),
|
|
452
|
+
dryRun: Boolean(options.dryRun),
|
|
453
|
+
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
454
|
+
files: seedFiles(names, config),
|
|
455
|
+
index: featureArtifactIndexFile("seed", names, config),
|
|
456
|
+
dependencies: {
|
|
457
|
+
"@beignet/core": beignetDependencyVersion,
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
export async function makeNotification(options) {
|
|
462
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
463
|
+
const names = notificationNames(options.name);
|
|
464
|
+
const config = options.config
|
|
465
|
+
? resolveConfig(options.config)
|
|
466
|
+
: await loadBeignetConfig(targetDir);
|
|
467
|
+
await assertFeatureArtifactApp(targetDir, config, "notification");
|
|
468
|
+
return makeFeatureArtifact({
|
|
469
|
+
targetDir,
|
|
470
|
+
config,
|
|
471
|
+
force: Boolean(options.force),
|
|
472
|
+
dryRun: Boolean(options.dryRun),
|
|
473
|
+
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
474
|
+
files: notificationFiles(names, config),
|
|
475
|
+
index: featureArtifactIndexFile("notification", names, config),
|
|
476
|
+
dependencies: {
|
|
477
|
+
"@beignet/core": beignetDependencyVersion,
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
}
|
|
323
481
|
export async function makeListener(options) {
|
|
324
482
|
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
325
483
|
const names = listenerNames(options.name, options.event);
|
|
@@ -376,6 +534,26 @@ export async function makeSchedule(options) {
|
|
|
376
534
|
},
|
|
377
535
|
});
|
|
378
536
|
}
|
|
537
|
+
export async function makeUpload(options) {
|
|
538
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
539
|
+
const names = uploadNames(options.name);
|
|
540
|
+
const config = options.config
|
|
541
|
+
? resolveConfig(options.config)
|
|
542
|
+
: await loadBeignetConfig(targetDir);
|
|
543
|
+
await assertFeatureArtifactApp(targetDir, config, "upload");
|
|
544
|
+
return makeFeatureArtifact({
|
|
545
|
+
targetDir,
|
|
546
|
+
config,
|
|
547
|
+
force: Boolean(options.force),
|
|
548
|
+
dryRun: Boolean(options.dryRun),
|
|
549
|
+
name: `${names.feature.kebab}/${names.artifact.kebab}`,
|
|
550
|
+
files: uploadFiles(names, config),
|
|
551
|
+
index: featureArtifactIndexFile("upload", names, config),
|
|
552
|
+
dependencies: {
|
|
553
|
+
"@beignet/core": beignetDependencyVersion,
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
}
|
|
379
557
|
async function assertStandardApp(targetDir, config) {
|
|
380
558
|
const requiredFiles = [
|
|
381
559
|
config.paths.appContext,
|
|
@@ -468,6 +646,21 @@ async function assertFeatureArtifactApp(targetDir, config, artifact, options = {
|
|
|
468
646
|
}
|
|
469
647
|
}
|
|
470
648
|
}
|
|
649
|
+
async function assertPersistenceArtifactApp(targetDir, names, config, artifact) {
|
|
650
|
+
const requiredFiles = [
|
|
651
|
+
config.paths.appContext,
|
|
652
|
+
config.paths.ports,
|
|
653
|
+
resourcePortFilePath(featureResourceNames(names), config),
|
|
654
|
+
];
|
|
655
|
+
for (const file of requiredFiles) {
|
|
656
|
+
try {
|
|
657
|
+
await stat(path.join(targetDir, file));
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
throw new Error(`beignet make ${artifact} expects a feature with a repository port. Missing ${file}. Run beignet make feature ${names.feature.kebab} first, or add the repository port before generating ${artifact}s.`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
471
664
|
async function writeGeneratedFile(targetDir, file, options) {
|
|
472
665
|
const plannedFile = await planGeneratedFile(targetDir, file, {
|
|
473
666
|
force: options.force,
|
|
@@ -587,12 +780,17 @@ async function updateResourceWiring(targetDir, names, config, persistenceOrOptio
|
|
|
587
780
|
? maybeOptions
|
|
588
781
|
: persistenceOrOptions;
|
|
589
782
|
const updated = new Set();
|
|
590
|
-
if (await updatePackageJson(targetDir,
|
|
783
|
+
if (await updatePackageJson(targetDir, {
|
|
784
|
+
...options,
|
|
785
|
+
webTestingDependency: true,
|
|
786
|
+
})) {
|
|
591
787
|
updated.add("package.json");
|
|
788
|
+
}
|
|
592
789
|
if (await updatePortsIndex(targetDir, names, config, options)) {
|
|
593
790
|
updated.add(config.paths.ports);
|
|
594
791
|
}
|
|
595
|
-
if (
|
|
792
|
+
if (persistence === "memory" &&
|
|
793
|
+
(await updateInfrastructurePorts(targetDir, names, config, options))) {
|
|
596
794
|
updated.add(config.paths.infrastructurePorts);
|
|
597
795
|
}
|
|
598
796
|
if (persistence === "drizzle" &&
|
|
@@ -620,9 +818,14 @@ async function updatePackageJson(targetDir, options) {
|
|
|
620
818
|
const original = await readFile(filePath, "utf8");
|
|
621
819
|
const packageJson = JSON.parse(original);
|
|
622
820
|
packageJson.scripts = packageJson.scripts ?? {};
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
821
|
+
packageJson.scripts.test ??= "bun test";
|
|
822
|
+
if (options.webTestingDependency) {
|
|
823
|
+
packageJson.devDependencies = packageJson.devDependencies ?? {};
|
|
824
|
+
packageJson.devDependencies["@beignet/web"] ??=
|
|
825
|
+
packageJson.dependencies?.["@beignet/core"] ??
|
|
826
|
+
packageJson.dependencies?.["@beignet/next"] ??
|
|
827
|
+
"*";
|
|
828
|
+
}
|
|
626
829
|
const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
|
|
627
830
|
if (!options.dryRun)
|
|
628
831
|
await writeFile(filePath, next);
|
|
@@ -1383,6 +1586,9 @@ function resourceNames(input) {
|
|
|
1383
1586
|
]);
|
|
1384
1587
|
return names;
|
|
1385
1588
|
}
|
|
1589
|
+
function featureResourceNames(names) {
|
|
1590
|
+
return resourceNames(names.feature.kebab);
|
|
1591
|
+
}
|
|
1386
1592
|
function useCaseNames(input) {
|
|
1387
1593
|
const parts = input
|
|
1388
1594
|
.trim()
|
|
@@ -1497,6 +1703,40 @@ function jobNames(input) {
|
|
|
1497
1703
|
payloadTypeName: `${jobExportName}Payload`,
|
|
1498
1704
|
};
|
|
1499
1705
|
}
|
|
1706
|
+
function factoryNames(input) {
|
|
1707
|
+
const names = featureArtifactNames(input, "Factory");
|
|
1708
|
+
const factoryExportName = `${names.artifact.camel}Factory`;
|
|
1709
|
+
assertGeneratedIdentifiers(input, "Factory name", [factoryExportName]);
|
|
1710
|
+
return {
|
|
1711
|
+
...names,
|
|
1712
|
+
factoryName: names.stableName,
|
|
1713
|
+
factoryExportName,
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
function seedNames(input) {
|
|
1717
|
+
const names = featureArtifactNames(input, "Seed");
|
|
1718
|
+
const seedExportName = `${names.artifact.camel}Seed`;
|
|
1719
|
+
assertGeneratedIdentifiers(input, "Seed name", [seedExportName]);
|
|
1720
|
+
return {
|
|
1721
|
+
...names,
|
|
1722
|
+
seedName: names.stableName,
|
|
1723
|
+
seedExportName,
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
function notificationNames(input) {
|
|
1727
|
+
const names = featureArtifactNames(input, "Notification");
|
|
1728
|
+
const notificationExportName = `${names.featureSingularPascal}${names.artifact.pascal}Notification`;
|
|
1729
|
+
assertGeneratedIdentifiers(input, "Notification name", [
|
|
1730
|
+
notificationExportName,
|
|
1731
|
+
]);
|
|
1732
|
+
return {
|
|
1733
|
+
...names,
|
|
1734
|
+
notificationName: names.stableName,
|
|
1735
|
+
notificationExportName,
|
|
1736
|
+
payloadSchemaName: `${notificationExportName}PayloadSchema`,
|
|
1737
|
+
payloadTypeName: `${notificationExportName}Payload`,
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1500
1740
|
function listenerNames(input, eventInput) {
|
|
1501
1741
|
const names = featureArtifactNames(input, "Listener");
|
|
1502
1742
|
const listenerExportName = names.artifact.camel;
|
|
@@ -1522,6 +1762,18 @@ function scheduleNames(input, options) {
|
|
|
1522
1762
|
timezone: options.timezone?.trim() || undefined,
|
|
1523
1763
|
};
|
|
1524
1764
|
}
|
|
1765
|
+
function uploadNames(input) {
|
|
1766
|
+
const names = featureArtifactNames(input, "Upload");
|
|
1767
|
+
const uploadExportName = `${names.artifact.pascal}Upload`;
|
|
1768
|
+
assertGeneratedIdentifiers(input, "Upload name", [uploadExportName]);
|
|
1769
|
+
return {
|
|
1770
|
+
...names,
|
|
1771
|
+
uploadName: names.stableName,
|
|
1772
|
+
uploadExportName,
|
|
1773
|
+
metadataSchemaName: `${uploadExportName}MetadataSchema`,
|
|
1774
|
+
metadataTypeName: `${uploadExportName}Metadata`,
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1525
1777
|
function normalizeEventReference(input) {
|
|
1526
1778
|
const trimmed = input.trim();
|
|
1527
1779
|
if (trimmed.includes("/"))
|
|
@@ -1767,6 +2019,30 @@ function jobFiles(names, config) {
|
|
|
1767
2019
|
},
|
|
1768
2020
|
];
|
|
1769
2021
|
}
|
|
2022
|
+
function notificationFiles(names, config) {
|
|
2023
|
+
return [
|
|
2024
|
+
{
|
|
2025
|
+
path: notificationFilePath(names, config),
|
|
2026
|
+
content: notificationFile(names, config),
|
|
2027
|
+
},
|
|
2028
|
+
];
|
|
2029
|
+
}
|
|
2030
|
+
function factoryFiles(names, config) {
|
|
2031
|
+
return [
|
|
2032
|
+
{
|
|
2033
|
+
path: factoryFilePath(names, config),
|
|
2034
|
+
content: factoryFile(names, config),
|
|
2035
|
+
},
|
|
2036
|
+
];
|
|
2037
|
+
}
|
|
2038
|
+
function seedFiles(names, config) {
|
|
2039
|
+
return [
|
|
2040
|
+
{
|
|
2041
|
+
path: seedFilePath(names, config),
|
|
2042
|
+
content: seedFile(names, config),
|
|
2043
|
+
},
|
|
2044
|
+
];
|
|
2045
|
+
}
|
|
1770
2046
|
function listenerFiles(names, config) {
|
|
1771
2047
|
return [
|
|
1772
2048
|
{
|
|
@@ -1790,6 +2066,14 @@ function scheduleFiles(names, config, options) {
|
|
|
1790
2066
|
}
|
|
1791
2067
|
return files;
|
|
1792
2068
|
}
|
|
2069
|
+
function uploadFiles(names, config) {
|
|
2070
|
+
return [
|
|
2071
|
+
{
|
|
2072
|
+
path: uploadFilePath(names, config),
|
|
2073
|
+
content: uploadFile(names, config),
|
|
2074
|
+
},
|
|
2075
|
+
];
|
|
2076
|
+
}
|
|
1793
2077
|
function portFilePath(names, config) {
|
|
1794
2078
|
return path.join(path.dirname(config.paths.ports), `${names.kebab}.ts`);
|
|
1795
2079
|
}
|
|
@@ -1882,6 +2166,9 @@ function featureArtifactDir(names, kind, config) {
|
|
|
1882
2166
|
if (kind === "events") {
|
|
1883
2167
|
return path.join(featureDir, "domain", "events");
|
|
1884
2168
|
}
|
|
2169
|
+
if (kind === "factories") {
|
|
2170
|
+
return path.join(featureDir, "tests", "factories");
|
|
2171
|
+
}
|
|
1885
2172
|
return path.join(featureDir, kind);
|
|
1886
2173
|
}
|
|
1887
2174
|
function eventFilePath(names, config) {
|
|
@@ -1890,12 +2177,24 @@ function eventFilePath(names, config) {
|
|
|
1890
2177
|
function jobFilePath(names, config) {
|
|
1891
2178
|
return path.join(featureArtifactDir(names, "jobs", config), `${names.artifact.kebab}.ts`);
|
|
1892
2179
|
}
|
|
2180
|
+
function factoryFilePath(names, config) {
|
|
2181
|
+
return path.join(featureArtifactDir(names, "factories", config), `${names.artifact.kebab}.ts`);
|
|
2182
|
+
}
|
|
2183
|
+
function seedFilePath(names, config) {
|
|
2184
|
+
return path.join(featureArtifactDir(names, "seeds", config), `${names.artifact.kebab}.ts`);
|
|
2185
|
+
}
|
|
2186
|
+
function notificationFilePath(names, config) {
|
|
2187
|
+
return path.join(featureArtifactDir(names, "notifications", config), `${names.artifact.kebab}.ts`);
|
|
2188
|
+
}
|
|
1893
2189
|
function listenerFilePath(names, config) {
|
|
1894
2190
|
return path.join(featureArtifactDir(names, "listeners", config), `${names.artifact.kebab}.ts`);
|
|
1895
2191
|
}
|
|
1896
2192
|
function scheduleFilePath(names, config) {
|
|
1897
2193
|
return path.join(featureArtifactDir(names, "schedules", config), `${names.artifact.kebab}.ts`);
|
|
1898
2194
|
}
|
|
2195
|
+
function uploadFilePath(names, config) {
|
|
2196
|
+
return path.join(featureArtifactDir(names, "uploads", config), `${names.artifact.kebab}.ts`);
|
|
2197
|
+
}
|
|
1899
2198
|
function scheduleRouteFilePath(names, config) {
|
|
1900
2199
|
return path.join(config.paths.routes, "cron", names.feature.kebab, names.artifact.kebab, "route.ts");
|
|
1901
2200
|
}
|
|
@@ -1932,28 +2231,54 @@ function featureArtifactIndexFile(kind, names, config) {
|
|
|
1932
2231
|
? eventFilePath(names, config)
|
|
1933
2232
|
: kind === "job"
|
|
1934
2233
|
? jobFilePath(names, config)
|
|
1935
|
-
: kind === "
|
|
1936
|
-
?
|
|
1937
|
-
:
|
|
2234
|
+
: kind === "factory"
|
|
2235
|
+
? factoryFilePath(names, config)
|
|
2236
|
+
: kind === "seed"
|
|
2237
|
+
? seedFilePath(names, config)
|
|
2238
|
+
: kind === "notification"
|
|
2239
|
+
? notificationFilePath(names, config)
|
|
2240
|
+
: kind === "listener"
|
|
2241
|
+
? listenerFilePath(names, config)
|
|
2242
|
+
: kind === "schedule"
|
|
2243
|
+
? scheduleFilePath(names, config)
|
|
2244
|
+
: uploadFilePath(names, config);
|
|
1938
2245
|
const registrySuffix = kind === "event"
|
|
1939
2246
|
? "Events"
|
|
1940
2247
|
: kind === "job"
|
|
1941
2248
|
? "Jobs"
|
|
1942
|
-
: kind === "
|
|
1943
|
-
? "
|
|
1944
|
-
: "
|
|
2249
|
+
: kind === "factory"
|
|
2250
|
+
? "Factories"
|
|
2251
|
+
: kind === "seed"
|
|
2252
|
+
? "Seeds"
|
|
2253
|
+
: kind === "notification"
|
|
2254
|
+
? "Notifications"
|
|
2255
|
+
: kind === "listener"
|
|
2256
|
+
? "Listeners"
|
|
2257
|
+
: kind === "schedule"
|
|
2258
|
+
? "Schedules"
|
|
2259
|
+
: "Uploads";
|
|
1945
2260
|
const importName = kind === "event"
|
|
1946
2261
|
? names.eventExportName
|
|
1947
2262
|
: kind === "job"
|
|
1948
2263
|
? names.jobExportName
|
|
1949
|
-
: kind === "
|
|
1950
|
-
? names.
|
|
1951
|
-
:
|
|
2264
|
+
: kind === "factory"
|
|
2265
|
+
? names.factoryExportName
|
|
2266
|
+
: kind === "seed"
|
|
2267
|
+
? names.seedExportName
|
|
2268
|
+
: kind === "notification"
|
|
2269
|
+
? names.notificationExportName
|
|
2270
|
+
: kind === "listener"
|
|
2271
|
+
? names.listenerExportName
|
|
2272
|
+
: kind === "schedule"
|
|
2273
|
+
? names.scheduleExportName
|
|
2274
|
+
: names.uploadExportName;
|
|
1952
2275
|
return {
|
|
1953
2276
|
path: path.join(path.dirname(artifactPath), "index.ts"),
|
|
2277
|
+
kind,
|
|
1954
2278
|
importName,
|
|
1955
2279
|
artifactPath,
|
|
1956
2280
|
registryName: `${names.featureSingularCamel}${registrySuffix}`,
|
|
2281
|
+
registryEntryName: names.artifact.camel,
|
|
1957
2282
|
};
|
|
1958
2283
|
}
|
|
1959
2284
|
async function updateFeatureArtifactIndex(targetDir, file, options) {
|
|
@@ -1961,9 +2286,20 @@ async function updateFeatureArtifactIndex(targetDir, file, options) {
|
|
|
1961
2286
|
const existing = await readOptionalFile(destination);
|
|
1962
2287
|
const specifier = relativeModule(file.path, file.artifactPath);
|
|
1963
2288
|
const importLine = `import { ${file.importName} } from "${specifier}";`;
|
|
2289
|
+
const defineUploadsImport = `import { defineUploads } from "@beignet/core/uploads";`;
|
|
1964
2290
|
const exportLine = `export { ${file.importName} } from "${specifier}";`;
|
|
1965
2291
|
if (existing === undefined) {
|
|
1966
|
-
const content =
|
|
2292
|
+
const content = file.kind === "upload"
|
|
2293
|
+
? `${defineUploadsImport}
|
|
2294
|
+
${importLine}
|
|
2295
|
+
|
|
2296
|
+
${exportLine}
|
|
2297
|
+
|
|
2298
|
+
export const ${file.registryName} = defineUploads({
|
|
2299
|
+
\t${file.registryEntryName}: ${file.importName},
|
|
2300
|
+
});
|
|
2301
|
+
`
|
|
2302
|
+
: `${importLine}
|
|
1967
2303
|
|
|
1968
2304
|
${exportLine}
|
|
1969
2305
|
|
|
@@ -1985,13 +2321,43 @@ export const ${file.registryName} = [${file.importName}] as const;
|
|
|
1985
2321
|
? `${next.slice(0, registryExport.index).trimEnd()}\n${exportLine}\n\n${next.slice(registryExport.index)}`
|
|
1986
2322
|
: `${next.trimEnd()}\n${exportLine}\n`;
|
|
1987
2323
|
}
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
2324
|
+
if (file.kind === "upload") {
|
|
2325
|
+
const registry = defineUploadsInitializerInfo(next, file.registryName);
|
|
2326
|
+
const arrayRegistry = arrayInitializerInfo(next, file.registryName);
|
|
2327
|
+
if (registry) {
|
|
2328
|
+
if (!next.includes(defineUploadsImport)) {
|
|
2329
|
+
next = insertAfterImports(next, defineUploadsImport);
|
|
2330
|
+
}
|
|
2331
|
+
if (!registry.text.includes(`${file.registryEntryName}:`)) {
|
|
2332
|
+
next = `${next.slice(0, registry.end)}\t${file.registryEntryName}: ${file.importName},\n${next.slice(registry.end)}`;
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
else if (arrayRegistry) {
|
|
2336
|
+
if (!identifiersFromArrayExpression(arrayRegistry.text).has(file.importName)) {
|
|
2337
|
+
const nextArray = appendToArrayExpression(arrayRegistry.text, [
|
|
2338
|
+
file.importName,
|
|
2339
|
+
]);
|
|
2340
|
+
next = `${next.slice(0, arrayRegistry.start)}${nextArray}${next.slice(arrayRegistry.end)}`;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
else {
|
|
2344
|
+
if (!next.includes(defineUploadsImport)) {
|
|
2345
|
+
next = insertAfterImports(next, defineUploadsImport);
|
|
2346
|
+
}
|
|
2347
|
+
next = `${next.trimEnd()}\n\nexport const ${file.registryName} = defineUploads({\n\t${file.registryEntryName}: ${file.importName},\n});\n`;
|
|
2348
|
+
}
|
|
1991
2349
|
}
|
|
1992
|
-
else
|
|
1993
|
-
const
|
|
1994
|
-
|
|
2350
|
+
else {
|
|
2351
|
+
const registry = arrayInitializerInfo(next, file.registryName);
|
|
2352
|
+
if (!registry) {
|
|
2353
|
+
next = `${next.trimEnd()}\n\nexport const ${file.registryName} = [${file.importName}] as const;\n`;
|
|
2354
|
+
}
|
|
2355
|
+
else if (!identifiersFromArrayExpression(registry.text).has(file.importName)) {
|
|
2356
|
+
const nextArray = appendToArrayExpression(registry.text, [
|
|
2357
|
+
file.importName,
|
|
2358
|
+
]);
|
|
2359
|
+
next = `${next.slice(0, registry.start)}${nextArray}${next.slice(registry.end)}`;
|
|
2360
|
+
}
|
|
1995
2361
|
}
|
|
1996
2362
|
if (next === existing)
|
|
1997
2363
|
return "skipped";
|
|
@@ -2015,6 +2381,22 @@ function arrayInitializerInfo(source, constName) {
|
|
|
2015
2381
|
end: closeBracket + 1,
|
|
2016
2382
|
};
|
|
2017
2383
|
}
|
|
2384
|
+
function defineUploadsInitializerInfo(source, constName) {
|
|
2385
|
+
const match = new RegExp(`\\bconst\\s+${constName}\\s*=\\s*defineUploads\\s*\\(`).exec(source);
|
|
2386
|
+
if (!match)
|
|
2387
|
+
return undefined;
|
|
2388
|
+
const openBrace = source.indexOf("{", match.index);
|
|
2389
|
+
if (openBrace === -1)
|
|
2390
|
+
return undefined;
|
|
2391
|
+
const closeBrace = matchingDelimiterIndex(source, openBrace, "{", "}");
|
|
2392
|
+
if (closeBrace === -1)
|
|
2393
|
+
return undefined;
|
|
2394
|
+
return {
|
|
2395
|
+
text: source.slice(openBrace, closeBrace + 1),
|
|
2396
|
+
start: openBrace,
|
|
2397
|
+
end: closeBrace,
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2018
2400
|
function schemasFile(names) {
|
|
2019
2401
|
return `import { z } from "zod";
|
|
2020
2402
|
|
|
@@ -2030,10 +2412,14 @@ export const List${names.pluralPascal}InputSchema = z.object({
|
|
|
2030
2412
|
});
|
|
2031
2413
|
|
|
2032
2414
|
export const List${names.pluralPascal}OutputSchema = z.object({
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2415
|
+
items: z.array(${names.singularPascal}Schema),
|
|
2416
|
+
page: z.object({
|
|
2417
|
+
kind: z.literal("offset"),
|
|
2418
|
+
limit: z.number().int().min(1),
|
|
2419
|
+
offset: z.number().int().min(0),
|
|
2420
|
+
total: z.number().int().min(0),
|
|
2421
|
+
hasMore: z.boolean(),
|
|
2422
|
+
}),
|
|
2037
2423
|
});
|
|
2038
2424
|
|
|
2039
2425
|
export const Create${names.singularPascal}InputSchema = z.object({
|
|
@@ -2051,7 +2437,8 @@ export type List${names.pluralPascal}Input = z.infer<
|
|
|
2051
2437
|
}
|
|
2052
2438
|
function listUseCaseFile(names, config) {
|
|
2053
2439
|
const filePath = path.join(resourceUseCaseDir(names, config), `list-${names.pluralKebab}.ts`);
|
|
2054
|
-
return `import {
|
|
2440
|
+
return `import { normalizeOffsetPage } from "@beignet/core/pagination";
|
|
2441
|
+
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2055
2442
|
import {
|
|
2056
2443
|
List${names.pluralPascal}InputSchema,
|
|
2057
2444
|
List${names.pluralPascal}OutputSchema,
|
|
@@ -2062,13 +2449,12 @@ export const list${names.pluralPascal}UseCase = useCase
|
|
|
2062
2449
|
.input(List${names.pluralPascal}InputSchema)
|
|
2063
2450
|
.output(List${names.pluralPascal}OutputSchema)
|
|
2064
2451
|
.run(async ({ ctx, input }) => {
|
|
2065
|
-
const
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
};
|
|
2452
|
+
const page = normalizeOffsetPage(input, {
|
|
2453
|
+
defaultLimit: 20,
|
|
2454
|
+
maxLimit: 100,
|
|
2455
|
+
});
|
|
2456
|
+
|
|
2457
|
+
return ctx.ports.${names.pluralCamel}.list(page);
|
|
2072
2458
|
});
|
|
2073
2459
|
`;
|
|
2074
2460
|
}
|
|
@@ -2103,28 +2489,29 @@ export {
|
|
|
2103
2489
|
}
|
|
2104
2490
|
function repositoryPortFile(names, config) {
|
|
2105
2491
|
return `import type {
|
|
2492
|
+
OffsetPage,
|
|
2493
|
+
OffsetPageInfo,
|
|
2494
|
+
PageResult,
|
|
2495
|
+
} from "@beignet/core/pagination";
|
|
2496
|
+
import type {
|
|
2106
2497
|
Create${names.singularPascal}Input,
|
|
2107
|
-
List${names.pluralPascal}Input,
|
|
2108
2498
|
${names.singularPascal},
|
|
2109
2499
|
} from "${aliasModule(path.join(resourceUseCaseDir(names, config), "schemas.ts"))}";
|
|
2110
2500
|
|
|
2111
|
-
export type List${names.pluralPascal}Result = {
|
|
2112
|
-
${names.pluralCamel}: ${names.singularPascal}[];
|
|
2113
|
-
total: number;
|
|
2114
|
-
};
|
|
2501
|
+
export type List${names.pluralPascal}Result = PageResult<${names.singularPascal}, OffsetPageInfo>;
|
|
2115
2502
|
|
|
2116
2503
|
export interface ${names.singularPascal}Repository {
|
|
2117
|
-
list(
|
|
2504
|
+
list(page: OffsetPage): Promise<List${names.pluralPascal}Result>;
|
|
2118
2505
|
create(input: Create${names.singularPascal}Input): Promise<${names.singularPascal}>;
|
|
2119
2506
|
}
|
|
2120
2507
|
`;
|
|
2121
2508
|
}
|
|
2122
2509
|
function inMemoryRepositoryFile(names, config) {
|
|
2123
2510
|
const repositoryPortPath = resourcePortFilePath(names, config);
|
|
2124
|
-
return `import
|
|
2511
|
+
return `import { offsetPageResult } from "@beignet/core/pagination";
|
|
2512
|
+
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
2125
2513
|
import type {
|
|
2126
2514
|
Create${names.singularPascal}Input,
|
|
2127
|
-
List${names.pluralPascal}Input,
|
|
2128
2515
|
${names.singularPascal},
|
|
2129
2516
|
} from "${aliasModule(path.join(resourceUseCaseDir(names, config), "schemas.ts"))}";
|
|
2130
2517
|
|
|
@@ -2134,18 +2521,16 @@ export function createInMemory${names.singularPascal}Repository(
|
|
|
2134
2521
|
const ${names.pluralCamel} = new Map(seed.map((${names.singularCamel}) => [${names.singularCamel}.id, ${names.singularCamel}]));
|
|
2135
2522
|
|
|
2136
2523
|
return {
|
|
2137
|
-
async list(
|
|
2524
|
+
async list(page) {
|
|
2138
2525
|
const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values()).sort(
|
|
2139
2526
|
(left, right) => right.createdAt.localeCompare(left.createdAt),
|
|
2140
2527
|
);
|
|
2141
2528
|
|
|
2142
|
-
return
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
total: all${names.pluralPascal}.length,
|
|
2148
|
-
};
|
|
2529
|
+
return offsetPageResult(
|
|
2530
|
+
all${names.pluralPascal}.slice(page.offset, page.offset + page.limit),
|
|
2531
|
+
page,
|
|
2532
|
+
all${names.pluralPascal}.length,
|
|
2533
|
+
);
|
|
2149
2534
|
},
|
|
2150
2535
|
async create(input: Create${names.singularPascal}Input) {
|
|
2151
2536
|
const ${names.singularCamel}: ${names.singularPascal} = {
|
|
@@ -2173,12 +2558,12 @@ export const ${names.pluralCamel} = sqliteTable("${names.pluralKebab.replaceAll(
|
|
|
2173
2558
|
function drizzleRepositoryFile(names, config) {
|
|
2174
2559
|
const repositoryPortPath = resourcePortFilePath(names, config);
|
|
2175
2560
|
const schemaPath = drizzleSchemaIndexPath(config);
|
|
2176
|
-
return `import
|
|
2561
|
+
return `import { offsetPageResult } from "@beignet/core/pagination";
|
|
2562
|
+
import type { DrizzleTursoDatabase } from "@beignet/provider-drizzle-turso";
|
|
2177
2563
|
import { count, desc } from "drizzle-orm";
|
|
2178
2564
|
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
2179
2565
|
import type {
|
|
2180
2566
|
Create${names.singularPascal}Input,
|
|
2181
|
-
List${names.pluralPascal}Input,
|
|
2182
2567
|
${names.singularPascal},
|
|
2183
2568
|
} from "${aliasModule(path.join(resourceUseCaseDir(names, config), "schemas.ts"))}";
|
|
2184
2569
|
import * as schema from "${aliasModule(schemaPath)}";
|
|
@@ -2197,21 +2582,18 @@ export function createDrizzle${names.singularPascal}Repository(
|
|
|
2197
2582
|
db: DrizzleTursoDatabase<typeof schema>,
|
|
2198
2583
|
): ${names.singularPascal}Repository {
|
|
2199
2584
|
return {
|
|
2200
|
-
async list(
|
|
2585
|
+
async list(page) {
|
|
2201
2586
|
const rows = await db
|
|
2202
2587
|
.select()
|
|
2203
2588
|
.from(schema.${names.pluralCamel})
|
|
2204
2589
|
.orderBy(desc(schema.${names.pluralCamel}.createdAt))
|
|
2205
|
-
.limit(
|
|
2206
|
-
.offset(
|
|
2590
|
+
.limit(page.limit)
|
|
2591
|
+
.offset(page.offset);
|
|
2207
2592
|
const [{ total }] = await db
|
|
2208
2593
|
.select({ total: count() })
|
|
2209
2594
|
.from(schema.${names.pluralCamel});
|
|
2210
2595
|
|
|
2211
|
-
return {
|
|
2212
|
-
${names.pluralCamel}: rows.map(to${names.singularPascal}),
|
|
2213
|
-
total,
|
|
2214
|
-
};
|
|
2596
|
+
return offsetPageResult(rows.map(to${names.singularPascal}), page, total);
|
|
2215
2597
|
},
|
|
2216
2598
|
async create(input: Create${names.singularPascal}Input) {
|
|
2217
2599
|
const [row] = await db
|
|
@@ -2299,10 +2681,14 @@ export const list${names.pluralPascal} = ${names.pluralCamel}
|
|
|
2299
2681
|
)
|
|
2300
2682
|
.responses({
|
|
2301
2683
|
200: z.object({
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2684
|
+
items: z.array(${names.singularCamel}Schema),
|
|
2685
|
+
page: z.object({
|
|
2686
|
+
kind: z.literal("offset"),
|
|
2687
|
+
limit: z.number().int().min(1),
|
|
2688
|
+
offset: z.number().int().min(0),
|
|
2689
|
+
total: z.number().int().min(0),
|
|
2690
|
+
hasMore: z.boolean(),
|
|
2691
|
+
}),
|
|
2306
2692
|
}),
|
|
2307
2693
|
});
|
|
2308
2694
|
|
|
@@ -2349,7 +2735,7 @@ describe("${names.exportName}", () => {
|
|
|
2349
2735
|
...appPorts,
|
|
2350
2736
|
uow: createNoopUnitOfWork(
|
|
2351
2737
|
() => appPorts as unknown as AppContext["ports"],
|
|
2352
|
-
) as AppContext["ports"]["uow"],
|
|
2738
|
+
) as unknown as AppContext["ports"]["uow"],
|
|
2353
2739
|
devtools: createInMemoryDevtools(),
|
|
2354
2740
|
storage: createMemoryStorage(),
|
|
2355
2741
|
} as AppContext["ports"];
|
|
@@ -2437,7 +2823,7 @@ export const ${names.eventExportName} = defineEvent("${names.eventName}", {
|
|
|
2437
2823
|
`;
|
|
2438
2824
|
}
|
|
2439
2825
|
function jobFile(names, config) {
|
|
2440
|
-
return `import { createJobHandlers } from "@beignet/core/jobs";
|
|
2826
|
+
return `import { createJobHandlers, retry } from "@beignet/core/jobs";
|
|
2441
2827
|
import { z } from "zod";
|
|
2442
2828
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
2443
2829
|
|
|
@@ -2451,9 +2837,11 @@ export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}
|
|
|
2451
2837
|
|
|
2452
2838
|
export const ${names.jobExportName} = jobs.defineJob("${names.jobName}", {
|
|
2453
2839
|
\tpayload: ${names.payloadSchemaName},
|
|
2454
|
-
\tretry: {
|
|
2840
|
+
\tretry: retry.exponential({
|
|
2455
2841
|
\t\tattempts: 3,
|
|
2456
|
-
\t
|
|
2842
|
+
\t\tinitialDelay: "1s",
|
|
2843
|
+
\t\tmaxDelay: "1m",
|
|
2844
|
+
\t}),
|
|
2457
2845
|
\tasync handle({ payload, ctx }) {
|
|
2458
2846
|
\t\tctx.ports.logger.info("Job handled", {
|
|
2459
2847
|
\t\t\tjobName: "${names.jobName}",
|
|
@@ -2463,6 +2851,67 @@ export const ${names.jobExportName} = jobs.defineJob("${names.jobName}", {
|
|
|
2463
2851
|
});
|
|
2464
2852
|
`;
|
|
2465
2853
|
}
|
|
2854
|
+
function factoryFile(names, config) {
|
|
2855
|
+
const resource = featureResourceNames(names);
|
|
2856
|
+
return `import { defineFactory } from "@beignet/core/testing";
|
|
2857
|
+
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
2858
|
+
|
|
2859
|
+
export const ${names.factoryExportName} = defineFactory("${names.factoryName}", {
|
|
2860
|
+
\tdefaults: ({ sequence }) => ({
|
|
2861
|
+
\t\tname: "${resource.singularPascal} " + sequence,
|
|
2862
|
+
\t}),
|
|
2863
|
+
\tpersist: (ctx: AppContext, input) => ctx.ports.${resource.pluralCamel}.create(input),
|
|
2864
|
+
});
|
|
2865
|
+
`;
|
|
2866
|
+
}
|
|
2867
|
+
function seedFile(names, config) {
|
|
2868
|
+
const resource = featureResourceNames(names);
|
|
2869
|
+
return `import { defineSeed } from "@beignet/core/testing";
|
|
2870
|
+
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
2871
|
+
|
|
2872
|
+
export const ${names.seedExportName} = defineSeed("${names.seedName}", {
|
|
2873
|
+
\tdescription: "Creates starter ${names.feature.kebab} data.",
|
|
2874
|
+
\trun: async (ctx: AppContext) => {
|
|
2875
|
+
\t\tawait ctx.ports.${resource.pluralCamel}.create({
|
|
2876
|
+
\t\t\tname: "Demo ${resource.singularPascal}",
|
|
2877
|
+
\t\t});
|
|
2878
|
+
\t},
|
|
2879
|
+
});
|
|
2880
|
+
`;
|
|
2881
|
+
}
|
|
2882
|
+
function notificationFile(names, config) {
|
|
2883
|
+
return `import {
|
|
2884
|
+
\tcreateNotificationHandlers,
|
|
2885
|
+
\tdefineMailNotificationChannel,
|
|
2886
|
+
} from "@beignet/core/notifications";
|
|
2887
|
+
import { z } from "zod";
|
|
2888
|
+
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
2889
|
+
|
|
2890
|
+
const notifications = createNotificationHandlers<AppContext>();
|
|
2891
|
+
|
|
2892
|
+
export const ${names.payloadSchemaName} = z.object({
|
|
2893
|
+
\tid: z.string().uuid(),
|
|
2894
|
+
\tsubject: z.string(),
|
|
2895
|
+
\trecipientEmail: z.string().email(),
|
|
2896
|
+
});
|
|
2897
|
+
|
|
2898
|
+
export type ${names.payloadTypeName} = z.infer<typeof ${names.payloadSchemaName}>;
|
|
2899
|
+
|
|
2900
|
+
export const ${names.notificationExportName} = notifications.defineNotification(
|
|
2901
|
+
\t"${names.notificationName}",
|
|
2902
|
+
\t{
|
|
2903
|
+
\t\tpayload: ${names.payloadSchemaName},
|
|
2904
|
+
\t\tchannels: {
|
|
2905
|
+
\t\t\temail: defineMailNotificationChannel(({ payload }) => ({
|
|
2906
|
+
\t\t\t\tto: payload.recipientEmail,
|
|
2907
|
+
\t\t\t\tsubject: payload.subject,
|
|
2908
|
+
\t\t\t\ttext: \`${names.notificationName} notification for \${payload.id}.\`,
|
|
2909
|
+
\t\t\t})),
|
|
2910
|
+
\t\t},
|
|
2911
|
+
\t},
|
|
2912
|
+
);
|
|
2913
|
+
`;
|
|
2914
|
+
}
|
|
2466
2915
|
function listenerFile(names, config) {
|
|
2467
2916
|
const filePath = listenerFilePath(names, config);
|
|
2468
2917
|
return `import { createEventHandlers } from "@beignet/core/events";
|
|
@@ -2517,6 +2966,60 @@ ${names.timezone ? `\t\ttimezone: "${names.timezone}",\n` : ""}\t\tpayload: ${na
|
|
|
2517
2966
|
);
|
|
2518
2967
|
`;
|
|
2519
2968
|
}
|
|
2969
|
+
function uploadFile(names, config) {
|
|
2970
|
+
return `import { defineUpload } from "@beignet/core/uploads";
|
|
2971
|
+
import { z } from "zod";
|
|
2972
|
+
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
2973
|
+
|
|
2974
|
+
export const ${names.metadataSchemaName} = z.object({
|
|
2975
|
+
\tresourceId: z.string().min(1),
|
|
2976
|
+
});
|
|
2977
|
+
|
|
2978
|
+
export type ${names.metadataTypeName} = z.infer<typeof ${names.metadataSchemaName}>;
|
|
2979
|
+
|
|
2980
|
+
export const ${names.uploadExportName} = defineUpload<
|
|
2981
|
+
\t"${names.uploadName}",
|
|
2982
|
+
\ttypeof ${names.metadataSchemaName},
|
|
2983
|
+
\tAppContext,
|
|
2984
|
+
\t{ objectKeys: string[] }
|
|
2985
|
+
>("${names.uploadName}", {
|
|
2986
|
+
\tmetadata: ${names.metadataSchemaName},
|
|
2987
|
+
\tfile: {
|
|
2988
|
+
\t\tcontentTypes: ["application/pdf", "text/plain"],
|
|
2989
|
+
\t\tmaxSizeBytes: 5 * 1024 * 1024,
|
|
2990
|
+
\t\tmaxFiles: 1,
|
|
2991
|
+
\t\tvisibility: "private",
|
|
2992
|
+
\t\tcacheControl: "private, max-age=0",
|
|
2993
|
+
\t},
|
|
2994
|
+
\tauthorize({ ctx }) {
|
|
2995
|
+
\t\treturn ctx.actor.type === "user";
|
|
2996
|
+
\t},
|
|
2997
|
+
\tkey({ ctx, metadata, uploadId, file }) {
|
|
2998
|
+
\t\tconst tenantId = ctx.tenant?.id ?? "default";
|
|
2999
|
+
\t\tconst extension = file.name.includes(".") ? file.name.split(".").pop() : undefined;
|
|
3000
|
+
\t\tconst suffix = extension ? \`.\${extension}\` : "";
|
|
3001
|
+
\t\treturn "${names.feature.kebab}/" + tenantId + "/" + metadata.resourceId + "/" + uploadId + suffix;
|
|
3002
|
+
\t},
|
|
3003
|
+
\tstorageMetadata({ ctx, metadata }) {
|
|
3004
|
+
\t\treturn {
|
|
3005
|
+
\t\t\ttenantId: ctx.tenant?.id ?? "default",
|
|
3006
|
+
\t\t\tresourceId: metadata.resourceId,
|
|
3007
|
+
\t\t};
|
|
3008
|
+
\t},
|
|
3009
|
+
\tasync onComplete({ ctx, metadata, files }) {
|
|
3010
|
+
\t\tctx.ports.logger.info("Upload completed", {
|
|
3011
|
+
\t\t\tuploadName: "${names.uploadName}",
|
|
3012
|
+
\t\t\tresourceId: metadata.resourceId,
|
|
3013
|
+
\t\t\tobjectKeys: files.map((file) => file.key),
|
|
3014
|
+
\t\t});
|
|
3015
|
+
|
|
3016
|
+
\t\treturn {
|
|
3017
|
+
\t\t\tobjectKeys: files.map((file) => file.key),
|
|
3018
|
+
\t\t};
|
|
3019
|
+
\t},
|
|
3020
|
+
});
|
|
3021
|
+
`;
|
|
3022
|
+
}
|
|
2520
3023
|
function scheduleRouteFile(names, config) {
|
|
2521
3024
|
return `import { createInlineScheduleRunner } from "@beignet/core/schedules";
|
|
2522
3025
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
@@ -2665,11 +3168,15 @@ function testFile(names, config) {
|
|
|
2665
3168
|
const repositoryPath = path.join(path.dirname(config.paths.infrastructurePorts), names.pluralKebab, `in-memory-${names.singularKebab}-repository.ts`);
|
|
2666
3169
|
return `import { describe, expect, it } from "bun:test";
|
|
2667
3170
|
import { createUseCaseTester } from "@beignet/core/application";
|
|
3171
|
+
import { defineRoutes } from "@beignet/web";
|
|
3172
|
+
import { createTestApp } from "@beignet/web/testing";
|
|
2668
3173
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
2669
3174
|
import { createAnonymousActor, createMemoryStorage, createNoopUnitOfWork } from "@beignet/core/ports";
|
|
2670
3175
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
2671
3176
|
import { appPorts } from "${aliasModule(config.paths.infrastructurePorts)}";
|
|
3177
|
+
import { create${names.singularPascal}, list${names.pluralPascal} } from "${aliasModule(resourceContractFilePath(names, config))}";
|
|
2672
3178
|
import { createInMemory${names.singularPascal}Repository } from "${aliasModule(repositoryPath)}";
|
|
3179
|
+
import { ${names.singularCamel}Routes } from "${aliasModule(path.join(resourceFeatureDir(names, config), "routes.ts"))}";
|
|
2673
3180
|
import {
|
|
2674
3181
|
create${names.singularPascal}UseCase,
|
|
2675
3182
|
list${names.pluralPascal}UseCase,
|
|
@@ -2684,18 +3191,19 @@ describe("${names.pluralCamel} resource", () => {
|
|
|
2684
3191
|
uow: createNoopUnitOfWork(() => ({
|
|
2685
3192
|
...(appPorts as unknown as AppContext["ports"]),
|
|
2686
3193
|
${names.pluralCamel},
|
|
2687
|
-
})) as AppContext["ports"]["uow"],
|
|
3194
|
+
})) as unknown as AppContext["ports"]["uow"],
|
|
2688
3195
|
devtools: createInMemoryDevtools(),
|
|
2689
3196
|
storage: createMemoryStorage(),
|
|
2690
3197
|
} as AppContext["ports"];
|
|
2691
3198
|
const actor = createAnonymousActor();
|
|
2692
|
-
const
|
|
3199
|
+
const createTestContext = () => ({
|
|
2693
3200
|
requestId: "test-request",
|
|
2694
3201
|
actor,
|
|
2695
3202
|
auth: null,
|
|
2696
3203
|
gate: testPorts.gate.bind({ actor, auth: null }),
|
|
2697
3204
|
ports: testPorts,
|
|
2698
|
-
})
|
|
3205
|
+
});
|
|
3206
|
+
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
2699
3207
|
|
|
2700
3208
|
const ctx = await tester.ctx();
|
|
2701
3209
|
const created = await tester.run(
|
|
@@ -2710,8 +3218,30 @@ describe("${names.pluralCamel} resource", () => {
|
|
|
2710
3218
|
);
|
|
2711
3219
|
|
|
2712
3220
|
expect(created.name).toBe("First ${names.singularPascal}");
|
|
2713
|
-
expect(result.total).toBe(1);
|
|
2714
|
-
expect(result
|
|
3221
|
+
expect(result.page.total).toBe(1);
|
|
3222
|
+
expect(result.items).toEqual([created]);
|
|
3223
|
+
|
|
3224
|
+
const app = await createTestApp<AppContext, AppContext["ports"]>({
|
|
3225
|
+
ports: testPorts,
|
|
3226
|
+
routes: defineRoutes<AppContext>([${names.singularCamel}Routes]),
|
|
3227
|
+
createContext: async () => createTestContext(),
|
|
3228
|
+
mapUnhandledError: () => ({
|
|
3229
|
+
status: 500,
|
|
3230
|
+
body: { code: "INTERNAL_SERVER_ERROR", message: "Internal server error" },
|
|
3231
|
+
}),
|
|
3232
|
+
});
|
|
3233
|
+
|
|
3234
|
+
const createdViaRoute = await app.request(create${names.singularPascal}, {
|
|
3235
|
+
body: { name: "Second ${names.singularPascal}" },
|
|
3236
|
+
});
|
|
3237
|
+
const listedViaRoute = await app.request(list${names.pluralPascal}, {
|
|
3238
|
+
query: { limit: 20, offset: 0 },
|
|
3239
|
+
});
|
|
3240
|
+
await app.stop();
|
|
3241
|
+
|
|
3242
|
+
expect(createdViaRoute.name).toBe("Second ${names.singularPascal}");
|
|
3243
|
+
expect(listedViaRoute.page.total).toBe(2);
|
|
3244
|
+
expect(listedViaRoute.items).toContainEqual(createdViaRoute);
|
|
2715
3245
|
});
|
|
2716
3246
|
});
|
|
2717
3247
|
`;
|