@beignet/cli 0.0.15 → 0.0.17
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 +20 -0
- package/README.md +21 -16
- package/dist/choices.d.ts +1 -1
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +8 -0
- package/dist/choices.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -2
- package/dist/index.js.map +1 -1
- package/dist/inspect.js +1 -1
- package/dist/inspect.js.map +1 -1
- package/dist/make.d.ts +14 -0
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +620 -15
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +12 -4
- package/dist/mcp.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +9 -6
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/shared.js +2 -2
- package/dist/templates/shared.js.map +1 -1
- package/dist/templates/shell.d.ts.map +1 -1
- package/dist/templates/shell.js +2 -2
- package/dist/templates/shell.js.map +1 -1
- package/package.json +2 -2
- package/src/choices.ts +8 -0
- package/src/index.ts +35 -2
- package/src/inspect.ts +1 -1
- package/src/make.ts +874 -16
- package/src/mcp.ts +12 -3
- package/src/templates/agents.ts +9 -6
- package/src/templates/shared.ts +2 -2
- package/src/templates/shell.ts +2 -1
package/src/make.ts
CHANGED
|
@@ -37,10 +37,14 @@ export { makeFeatureAddonChoices } from "./choices.js";
|
|
|
37
37
|
|
|
38
38
|
type NormalizedMakeFeatureAddon =
|
|
39
39
|
| "policy"
|
|
40
|
+
| "factory"
|
|
40
41
|
| "task"
|
|
41
42
|
| "event"
|
|
42
43
|
| "job"
|
|
44
|
+
| "listener"
|
|
43
45
|
| "notification"
|
|
46
|
+
| "schedule"
|
|
47
|
+
| "seed"
|
|
44
48
|
| "ui"
|
|
45
49
|
| "upload";
|
|
46
50
|
|
|
@@ -126,6 +130,7 @@ type MakeFactoryOptions = {
|
|
|
126
130
|
force?: boolean;
|
|
127
131
|
dryRun?: boolean;
|
|
128
132
|
config?: BeignetConfig;
|
|
133
|
+
skipPersistenceAssert?: boolean;
|
|
129
134
|
};
|
|
130
135
|
|
|
131
136
|
type MakeSeedOptions = {
|
|
@@ -134,6 +139,7 @@ type MakeSeedOptions = {
|
|
|
134
139
|
force?: boolean;
|
|
135
140
|
dryRun?: boolean;
|
|
136
141
|
config?: BeignetConfig;
|
|
142
|
+
skipPersistenceAssert?: boolean;
|
|
137
143
|
};
|
|
138
144
|
|
|
139
145
|
type MakeNotificationOptions = {
|
|
@@ -151,6 +157,7 @@ type MakeListenerOptions = {
|
|
|
151
157
|
force?: boolean;
|
|
152
158
|
dryRun?: boolean;
|
|
153
159
|
config?: BeignetConfig;
|
|
160
|
+
skipEventAssert?: boolean;
|
|
154
161
|
};
|
|
155
162
|
|
|
156
163
|
type MakeScheduleOptions = {
|
|
@@ -172,6 +179,13 @@ type MakeUploadOptions = {
|
|
|
172
179
|
config?: BeignetConfig;
|
|
173
180
|
};
|
|
174
181
|
|
|
182
|
+
type MakeOutboxOptions = {
|
|
183
|
+
cwd?: string;
|
|
184
|
+
force?: boolean;
|
|
185
|
+
dryRun?: boolean;
|
|
186
|
+
config?: BeignetConfig;
|
|
187
|
+
};
|
|
188
|
+
|
|
175
189
|
type MakePaymentsOptions = {
|
|
176
190
|
cwd?: string;
|
|
177
191
|
force?: boolean;
|
|
@@ -258,6 +272,10 @@ export type MakeScheduleResult = MakeResult;
|
|
|
258
272
|
* Result returned by `beignet make upload`.
|
|
259
273
|
*/
|
|
260
274
|
export type MakeUploadResult = MakeResult;
|
|
275
|
+
/**
|
|
276
|
+
* Result returned by `beignet make outbox`.
|
|
277
|
+
*/
|
|
278
|
+
export type MakeOutboxResult = MakeResult;
|
|
261
279
|
/**
|
|
262
280
|
* Result returned by `beignet make payments`.
|
|
263
281
|
*/
|
|
@@ -742,6 +760,13 @@ async function makeFeatureAddon(
|
|
|
742
760
|
if (addon === "policy") {
|
|
743
761
|
return makePolicy({ ...shared, name: feature });
|
|
744
762
|
}
|
|
763
|
+
if (addon === "factory") {
|
|
764
|
+
return makeFactory({
|
|
765
|
+
...shared,
|
|
766
|
+
name: `${feature}/${singularize(feature)}`,
|
|
767
|
+
skipPersistenceAssert: true,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
745
770
|
if (addon === "task") {
|
|
746
771
|
return makeTask({ ...shared, name: `${feature}/backfill` });
|
|
747
772
|
}
|
|
@@ -751,9 +776,27 @@ async function makeFeatureAddon(
|
|
|
751
776
|
if (addon === "job") {
|
|
752
777
|
return makeJob({ ...shared, name: `${feature}/process` });
|
|
753
778
|
}
|
|
779
|
+
if (addon === "listener") {
|
|
780
|
+
return makeListener({
|
|
781
|
+
...shared,
|
|
782
|
+
name: `${feature}/handle-created`,
|
|
783
|
+
event: `${feature}/created`,
|
|
784
|
+
skipEventAssert: true,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
754
787
|
if (addon === "notification") {
|
|
755
788
|
return makeNotification({ ...shared, name: `${feature}/created` });
|
|
756
789
|
}
|
|
790
|
+
if (addon === "schedule") {
|
|
791
|
+
return makeSchedule({ ...shared, name: `${feature}/daily-summary` });
|
|
792
|
+
}
|
|
793
|
+
if (addon === "seed") {
|
|
794
|
+
return makeSeed({
|
|
795
|
+
...shared,
|
|
796
|
+
name: `${feature}/demo-${feature}`,
|
|
797
|
+
skipPersistenceAssert: true,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
757
800
|
if (addon === "ui") {
|
|
758
801
|
return makeFeatureUi({ ...shared, name: feature });
|
|
759
802
|
}
|
|
@@ -767,12 +810,21 @@ function normalizeMakeFeatureAddons(
|
|
|
767
810
|
const normalized = new Set<NormalizedMakeFeatureAddon>();
|
|
768
811
|
|
|
769
812
|
for (const addon of addons) {
|
|
770
|
-
if (addon === "
|
|
771
|
-
else if (addon === "
|
|
813
|
+
if (addon === "factories") normalized.add("factory");
|
|
814
|
+
else if (addon === "events") normalized.add("event");
|
|
815
|
+
else if (addon === "listeners") {
|
|
816
|
+
normalized.add("event");
|
|
817
|
+
normalized.add("listener");
|
|
818
|
+
} else if (addon === "tasks") normalized.add("task");
|
|
772
819
|
else if (addon === "jobs") normalized.add("job");
|
|
773
820
|
else if (addon === "notifications") normalized.add("notification");
|
|
821
|
+
else if (addon === "schedules") normalized.add("schedule");
|
|
822
|
+
else if (addon === "seeds") normalized.add("seed");
|
|
774
823
|
else if (addon === "uploads") normalized.add("upload");
|
|
775
|
-
else
|
|
824
|
+
else if (addon === "listener") {
|
|
825
|
+
normalized.add("event");
|
|
826
|
+
normalized.add("listener");
|
|
827
|
+
} else normalized.add(addon);
|
|
776
828
|
}
|
|
777
829
|
|
|
778
830
|
return featureAddonOrder.filter((addon) => normalized.has(addon));
|
|
@@ -780,10 +832,14 @@ function normalizeMakeFeatureAddons(
|
|
|
780
832
|
|
|
781
833
|
const featureAddonOrder: readonly NormalizedMakeFeatureAddon[] = [
|
|
782
834
|
"policy",
|
|
835
|
+
"factory",
|
|
783
836
|
"task",
|
|
784
837
|
"event",
|
|
785
838
|
"job",
|
|
839
|
+
"listener",
|
|
786
840
|
"notification",
|
|
841
|
+
"schedule",
|
|
842
|
+
"seed",
|
|
787
843
|
"ui",
|
|
788
844
|
"upload",
|
|
789
845
|
];
|
|
@@ -798,6 +854,22 @@ function mergeMakeResults(left: MakeResult, right: MakeResult): MakeResult {
|
|
|
798
854
|
};
|
|
799
855
|
}
|
|
800
856
|
|
|
857
|
+
function mergeMakeResultInto(target: MakeResult, source: MakeResult): void {
|
|
858
|
+
target.files = uniqueStrings([...target.files, ...source.files]);
|
|
859
|
+
target.createdFiles = uniqueStrings([
|
|
860
|
+
...target.createdFiles,
|
|
861
|
+
...source.createdFiles,
|
|
862
|
+
]);
|
|
863
|
+
target.updatedFiles = uniqueStrings([
|
|
864
|
+
...target.updatedFiles,
|
|
865
|
+
...source.updatedFiles,
|
|
866
|
+
]);
|
|
867
|
+
target.skippedFiles = uniqueStrings([
|
|
868
|
+
...target.skippedFiles,
|
|
869
|
+
...source.skippedFiles,
|
|
870
|
+
]);
|
|
871
|
+
}
|
|
872
|
+
|
|
801
873
|
function uniqueStrings(values: readonly string[]): string[] {
|
|
802
874
|
return [...new Set(values)];
|
|
803
875
|
}
|
|
@@ -1086,6 +1158,8 @@ export async function makeEvent(
|
|
|
1086
1158
|
: await loadBeignetConfig(targetDir);
|
|
1087
1159
|
|
|
1088
1160
|
await assertFeatureArtifactApp(targetDir, config, "event");
|
|
1161
|
+
await assertServerRuntimeApp(targetDir, config, "event");
|
|
1162
|
+
await updateOutboxPortWiring(targetDir, config, { dryRun: true });
|
|
1089
1163
|
|
|
1090
1164
|
// Events are published through ctx.ports.eventBus, so the generator wires
|
|
1091
1165
|
// the port and an in-memory dev provider when the app has no bus yet. Plan
|
|
@@ -1095,6 +1169,9 @@ export async function makeEvent(
|
|
|
1095
1169
|
await wirePortProviders(targetDir, config, eventBusWirings, {
|
|
1096
1170
|
dryRun: true,
|
|
1097
1171
|
});
|
|
1172
|
+
const outboxDrainPlan = await planOutboxDrainWiring(targetDir, config, {
|
|
1173
|
+
force: Boolean(options.force),
|
|
1174
|
+
});
|
|
1098
1175
|
|
|
1099
1176
|
const result = await makeFeatureArtifact({
|
|
1100
1177
|
targetDir,
|
|
@@ -1112,6 +1189,12 @@ export async function makeEvent(
|
|
|
1112
1189
|
await applyOutboxRegistryUpdate(result, targetDir, names, "events", config, {
|
|
1113
1190
|
dryRun: Boolean(options.dryRun),
|
|
1114
1191
|
});
|
|
1192
|
+
await applyOutboxDrainWiring(result, targetDir, outboxDrainPlan, {
|
|
1193
|
+
dryRun: Boolean(options.dryRun),
|
|
1194
|
+
});
|
|
1195
|
+
await applyOutboxPortWiring(result, targetDir, config, {
|
|
1196
|
+
dryRun: Boolean(options.dryRun),
|
|
1197
|
+
});
|
|
1115
1198
|
|
|
1116
1199
|
await applyPortProviderWiring(result, targetDir, config, eventBusWirings, {
|
|
1117
1200
|
dryRun: Boolean(options.dryRun),
|
|
@@ -1128,6 +1211,11 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
|
|
|
1128
1211
|
: await loadBeignetConfig(targetDir);
|
|
1129
1212
|
|
|
1130
1213
|
await assertFeatureArtifactApp(targetDir, config, "job");
|
|
1214
|
+
await assertServerRuntimeApp(targetDir, config, "job");
|
|
1215
|
+
await updateOutboxPortWiring(targetDir, config, { dryRun: true });
|
|
1216
|
+
const outboxDrainPlan = await planOutboxDrainWiring(targetDir, config, {
|
|
1217
|
+
force: Boolean(options.force),
|
|
1218
|
+
});
|
|
1131
1219
|
|
|
1132
1220
|
const result = await makeFeatureArtifact({
|
|
1133
1221
|
targetDir,
|
|
@@ -1148,6 +1236,12 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
|
|
|
1148
1236
|
await applyOutboxRegistryUpdate(result, targetDir, names, "jobs", config, {
|
|
1149
1237
|
dryRun: Boolean(options.dryRun),
|
|
1150
1238
|
});
|
|
1239
|
+
await applyOutboxDrainWiring(result, targetDir, outboxDrainPlan, {
|
|
1240
|
+
dryRun: Boolean(options.dryRun),
|
|
1241
|
+
});
|
|
1242
|
+
await applyOutboxPortWiring(result, targetDir, config, {
|
|
1243
|
+
dryRun: Boolean(options.dryRun),
|
|
1244
|
+
});
|
|
1151
1245
|
|
|
1152
1246
|
return result;
|
|
1153
1247
|
}
|
|
@@ -1204,7 +1298,9 @@ export async function makeFactory(
|
|
|
1204
1298
|
? resolveConfig(options.config)
|
|
1205
1299
|
: await loadBeignetConfig(targetDir);
|
|
1206
1300
|
|
|
1207
|
-
|
|
1301
|
+
if (!options.skipPersistenceAssert) {
|
|
1302
|
+
await assertPersistenceArtifactApp(targetDir, names, config, "factory");
|
|
1303
|
+
}
|
|
1208
1304
|
|
|
1209
1305
|
return makeFeatureArtifact({
|
|
1210
1306
|
targetDir,
|
|
@@ -1229,9 +1325,11 @@ export async function makeSeed(
|
|
|
1229
1325
|
? resolveConfig(options.config)
|
|
1230
1326
|
: await loadBeignetConfig(targetDir);
|
|
1231
1327
|
|
|
1232
|
-
|
|
1328
|
+
if (!options.skipPersistenceAssert) {
|
|
1329
|
+
await assertPersistenceArtifactApp(targetDir, names, config, "seed");
|
|
1330
|
+
}
|
|
1233
1331
|
|
|
1234
|
-
|
|
1332
|
+
const result = await makeFeatureArtifact({
|
|
1235
1333
|
targetDir,
|
|
1236
1334
|
config,
|
|
1237
1335
|
force: Boolean(options.force),
|
|
@@ -1243,6 +1341,57 @@ export async function makeSeed(
|
|
|
1243
1341
|
"@beignet/core": beignetDependencyVersion,
|
|
1244
1342
|
},
|
|
1245
1343
|
});
|
|
1344
|
+
|
|
1345
|
+
const factoryNamesForSeed = defaultFactoryNamesForSeed(names);
|
|
1346
|
+
if (
|
|
1347
|
+
!(await fileExists(
|
|
1348
|
+
path.join(targetDir, factoryFilePath(factoryNamesForSeed, config)),
|
|
1349
|
+
))
|
|
1350
|
+
) {
|
|
1351
|
+
const factoryResult = await makeFeatureArtifact({
|
|
1352
|
+
targetDir,
|
|
1353
|
+
config,
|
|
1354
|
+
force: Boolean(options.force),
|
|
1355
|
+
dryRun: Boolean(options.dryRun),
|
|
1356
|
+
name: `${factoryNamesForSeed.feature.kebab}/${factoryNamesForSeed.artifact.kebab}`,
|
|
1357
|
+
files: factoryFiles(factoryNamesForSeed, config),
|
|
1358
|
+
index: featureArtifactIndexFile("factory", factoryNamesForSeed, config),
|
|
1359
|
+
dependencies: {
|
|
1360
|
+
"@beignet/core": beignetDependencyVersion,
|
|
1361
|
+
},
|
|
1362
|
+
});
|
|
1363
|
+
mergeMakeResultInto(result, factoryResult);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const seedEntrypointResult = await updateSeedEntrypoint(
|
|
1367
|
+
targetDir,
|
|
1368
|
+
names,
|
|
1369
|
+
config,
|
|
1370
|
+
{ dryRun: Boolean(options.dryRun) },
|
|
1371
|
+
);
|
|
1372
|
+
if (seedEntrypointResult === "created")
|
|
1373
|
+
result.createdFiles.push(seedEntrypointPath(config));
|
|
1374
|
+
if (seedEntrypointResult === "updated")
|
|
1375
|
+
result.updatedFiles.push(seedEntrypointPath(config));
|
|
1376
|
+
if (seedEntrypointResult === "skipped")
|
|
1377
|
+
result.skippedFiles.push(seedEntrypointPath(config));
|
|
1378
|
+
if (!result.files.includes(seedEntrypointPath(config))) {
|
|
1379
|
+
result.files.push(seedEntrypointPath(config));
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
if (
|
|
1383
|
+
await updatePackageScripts(
|
|
1384
|
+
targetDir,
|
|
1385
|
+
{ "db:seed": `bun ${seedEntrypointPath(config)}` },
|
|
1386
|
+
{ dryRun: Boolean(options.dryRun) },
|
|
1387
|
+
)
|
|
1388
|
+
) {
|
|
1389
|
+
result.updatedFiles.push("package.json");
|
|
1390
|
+
if (!result.files.includes("package.json"))
|
|
1391
|
+
result.files.push("package.json");
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
return result;
|
|
1246
1395
|
}
|
|
1247
1396
|
|
|
1248
1397
|
export async function makeNotification(
|
|
@@ -1309,7 +1458,9 @@ export async function makeListener(
|
|
|
1309
1458
|
: await loadBeignetConfig(targetDir);
|
|
1310
1459
|
|
|
1311
1460
|
await assertFeatureArtifactApp(targetDir, config, "listener");
|
|
1312
|
-
|
|
1461
|
+
if (!options.skipEventAssert) {
|
|
1462
|
+
await assertListenerEventExists(targetDir, names.event, config);
|
|
1463
|
+
}
|
|
1313
1464
|
|
|
1314
1465
|
return makeFeatureArtifact({
|
|
1315
1466
|
targetDir,
|
|
@@ -1518,6 +1669,253 @@ async function applyCronSecretWiring(
|
|
|
1518
1669
|
};
|
|
1519
1670
|
}
|
|
1520
1671
|
|
|
1672
|
+
type OutboxDrainWiringPlan = {
|
|
1673
|
+
route: PlannedGeneratedFile;
|
|
1674
|
+
cronSecret: CronSecretWiringPlan;
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
async function planOutboxDrainWiring(
|
|
1678
|
+
targetDir: string,
|
|
1679
|
+
config: ResolvedBeignetConfig,
|
|
1680
|
+
options: { force: boolean },
|
|
1681
|
+
): Promise<OutboxDrainWiringPlan | undefined> {
|
|
1682
|
+
if (await fileExists(path.join(targetDir, outboxDrainRoutePath(config)))) {
|
|
1683
|
+
return undefined;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
const route = await planGeneratedFile(
|
|
1687
|
+
targetDir,
|
|
1688
|
+
{
|
|
1689
|
+
path: outboxDrainRoutePath(config),
|
|
1690
|
+
content: outboxDrainRouteFile(config),
|
|
1691
|
+
},
|
|
1692
|
+
{ force: options.force },
|
|
1693
|
+
);
|
|
1694
|
+
|
|
1695
|
+
return {
|
|
1696
|
+
route,
|
|
1697
|
+
cronSecret: await planCronSecretWiring(targetDir),
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
async function updateOutboxPortWiring(
|
|
1702
|
+
targetDir: string,
|
|
1703
|
+
config: ResolvedBeignetConfig,
|
|
1704
|
+
options: { dryRun: boolean },
|
|
1705
|
+
): Promise<string[]> {
|
|
1706
|
+
const updated = new Set<string>();
|
|
1707
|
+
|
|
1708
|
+
if (await updateOutboxPorts(targetDir, config, options)) {
|
|
1709
|
+
updated.add(config.paths.ports);
|
|
1710
|
+
}
|
|
1711
|
+
if (await updateOutboxInfrastructurePorts(targetDir, config, options)) {
|
|
1712
|
+
updated.add(config.paths.infrastructurePorts);
|
|
1713
|
+
}
|
|
1714
|
+
if (await updateOutboxDatabaseProvider(targetDir, config, options)) {
|
|
1715
|
+
updated.add(path.join(infrastructureDir(config), "db/provider.ts"));
|
|
1716
|
+
}
|
|
1717
|
+
if (await updateOutboxRepositoriesReturnType(targetDir, config, options)) {
|
|
1718
|
+
updated.add(drizzleRepositoriesPath(config));
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
return [...updated];
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
async function applyOutboxPortWiring(
|
|
1725
|
+
result: MakeResult,
|
|
1726
|
+
targetDir: string,
|
|
1727
|
+
config: ResolvedBeignetConfig,
|
|
1728
|
+
options: { dryRun: boolean },
|
|
1729
|
+
): Promise<void> {
|
|
1730
|
+
const updatedFiles = await updateOutboxPortWiring(targetDir, config, options);
|
|
1731
|
+
for (const file of updatedFiles) {
|
|
1732
|
+
if (!result.updatedFiles.includes(file)) result.updatedFiles.push(file);
|
|
1733
|
+
if (!result.files.includes(file)) result.files.push(file);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
async function updateOutboxPorts(
|
|
1738
|
+
targetDir: string,
|
|
1739
|
+
config: ResolvedBeignetConfig,
|
|
1740
|
+
options: { dryRun: boolean },
|
|
1741
|
+
): Promise<boolean> {
|
|
1742
|
+
const filePath = path.join(targetDir, config.paths.ports);
|
|
1743
|
+
const original = await readFile(filePath, "utf8");
|
|
1744
|
+
let next = addNamedTypeImport(original, "OutboxPort", "@beignet/core/outbox");
|
|
1745
|
+
if (!typeHasTopLevelProperty(next, "AppPorts", "outbox")) {
|
|
1746
|
+
next = insertTypeProperty(
|
|
1747
|
+
next,
|
|
1748
|
+
"AppPorts",
|
|
1749
|
+
"outbox: OutboxPort;",
|
|
1750
|
+
`Could not find AppPorts in ${config.paths.ports}. Add outbox: OutboxPort; manually, or restore the generated ports file before running make outbox.`,
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
if (!typeHasTopLevelProperty(next, "AppTransactionPorts", "outbox")) {
|
|
1754
|
+
next = insertTypeProperty(
|
|
1755
|
+
next,
|
|
1756
|
+
"AppTransactionPorts",
|
|
1757
|
+
"outbox: OutboxPort;",
|
|
1758
|
+
`Could not find AppTransactionPorts in ${config.paths.ports}. Add outbox: OutboxPort; manually, or restore the generated ports file before running make outbox.`,
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
if (next === original) return false;
|
|
1763
|
+
if (!options.dryRun) await writeFile(filePath, next);
|
|
1764
|
+
return true;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
async function updateOutboxInfrastructurePorts(
|
|
1768
|
+
targetDir: string,
|
|
1769
|
+
config: ResolvedBeignetConfig,
|
|
1770
|
+
options: { dryRun: boolean },
|
|
1771
|
+
): Promise<boolean> {
|
|
1772
|
+
const filePath = path.join(targetDir, config.paths.infrastructurePorts);
|
|
1773
|
+
const original = await readFile(filePath, "utf8");
|
|
1774
|
+
const next = appendDeferredPortKey(original, "outbox");
|
|
1775
|
+
|
|
1776
|
+
if (next === original) return false;
|
|
1777
|
+
if (!options.dryRun) await writeFile(filePath, next);
|
|
1778
|
+
return true;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
async function updateOutboxDatabaseProvider(
|
|
1782
|
+
targetDir: string,
|
|
1783
|
+
config: ResolvedBeignetConfig,
|
|
1784
|
+
options: { dryRun: boolean },
|
|
1785
|
+
): Promise<boolean> {
|
|
1786
|
+
const file = path.join(infrastructureDir(config), "db/provider.ts");
|
|
1787
|
+
const filePath = path.join(targetDir, file);
|
|
1788
|
+
if (!(await fileExists(filePath))) return false;
|
|
1789
|
+
|
|
1790
|
+
const database = await detectResourceDatabase(targetDir, config);
|
|
1791
|
+
const factoryName = drizzleOutboxPortFactoryName(database);
|
|
1792
|
+
const original = await readFile(filePath, "utf8");
|
|
1793
|
+
let next = addNamedImport(
|
|
1794
|
+
original,
|
|
1795
|
+
factoryName,
|
|
1796
|
+
`@beignet/provider-db-drizzle/${database}`,
|
|
1797
|
+
);
|
|
1798
|
+
next = appendPickStringLiteralMember(next, "AppPorts", "outbox");
|
|
1799
|
+
|
|
1800
|
+
if (!/\bconst\s+outbox\s*=/.test(next)) {
|
|
1801
|
+
const beforeOutboxConst = next;
|
|
1802
|
+
next = beforeOutboxConst.replace(
|
|
1803
|
+
/(\n\s*const idempotency = createDrizzle[A-Za-z]+IdempotencyPort\(dbPort\.db\);)/,
|
|
1804
|
+
`$1\n\t\tconst outbox = ${factoryName}(dbPort.db);`,
|
|
1805
|
+
);
|
|
1806
|
+
if (next === beforeOutboxConst) {
|
|
1807
|
+
throw new Error(
|
|
1808
|
+
`Could not find the idempotency port in ${file}. Add const outbox = ${factoryName}(dbPort.db) manually, or restore the generated database provider before running make outbox.`,
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
if (!/\boutbox\s*,/.test(next)) {
|
|
1814
|
+
const withOutboxPort = next.replace(
|
|
1815
|
+
/(\n\s*idempotency,\n)/,
|
|
1816
|
+
`$1\t\t\toutbox,\n`,
|
|
1817
|
+
);
|
|
1818
|
+
if (withOutboxPort === next) {
|
|
1819
|
+
throw new Error(
|
|
1820
|
+
`Could not find idempotency in providedPorts in ${file}. Add outbox to providedPorts manually, or restore the generated database provider before running make outbox.`,
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
next = withOutboxPort;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
if (!new RegExp(`outbox:\\s*${factoryName}\\(tx\\)`).test(next)) {
|
|
1827
|
+
const withTransactionOutbox = next.replace(
|
|
1828
|
+
/(\n\s*idempotency: createDrizzle[A-Za-z]+IdempotencyPort\(tx\),\n)/,
|
|
1829
|
+
`$1\t\t\t\t\toutbox: ${factoryName}(tx),\n`,
|
|
1830
|
+
);
|
|
1831
|
+
if (withTransactionOutbox === next) {
|
|
1832
|
+
throw new Error(
|
|
1833
|
+
`Could not find transaction idempotency wiring in ${file}. Add outbox: ${factoryName}(tx) manually, or restore the generated database provider before running make outbox.`,
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
next = withTransactionOutbox;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
if (next === original) return false;
|
|
1840
|
+
if (!options.dryRun) await writeFile(filePath, next);
|
|
1841
|
+
return true;
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
async function updateOutboxRepositoriesReturnType(
|
|
1845
|
+
targetDir: string,
|
|
1846
|
+
config: ResolvedBeignetConfig,
|
|
1847
|
+
options: { dryRun: boolean },
|
|
1848
|
+
): Promise<boolean> {
|
|
1849
|
+
const file = drizzleRepositoriesPath(config);
|
|
1850
|
+
const filePath = path.join(targetDir, file);
|
|
1851
|
+
if (!(await fileExists(filePath))) return false;
|
|
1852
|
+
|
|
1853
|
+
const original = await readFile(filePath, "utf8");
|
|
1854
|
+
const next = original.replace(
|
|
1855
|
+
/Omit<AppTransactionPorts,\s*"idempotency"\s*>/g,
|
|
1856
|
+
'Omit<AppTransactionPorts, "idempotency" | "outbox">',
|
|
1857
|
+
);
|
|
1858
|
+
|
|
1859
|
+
if (next === original) return false;
|
|
1860
|
+
if (!options.dryRun) await writeFile(filePath, next);
|
|
1861
|
+
return true;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
function drizzleOutboxPortFactoryName(database: DatabaseName): string {
|
|
1865
|
+
if (database === "postgres") return "createDrizzlePostgresOutboxPort";
|
|
1866
|
+
if (database === "mysql") return "createDrizzleMysqlOutboxPort";
|
|
1867
|
+
return "createDrizzleSqliteOutboxPort";
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
async function applyOutboxDrainWiring(
|
|
1871
|
+
result: MakeResult,
|
|
1872
|
+
targetDir: string,
|
|
1873
|
+
plan: OutboxDrainWiringPlan | undefined,
|
|
1874
|
+
options: { dryRun: boolean },
|
|
1875
|
+
): Promise<void> {
|
|
1876
|
+
if (!plan) return;
|
|
1877
|
+
|
|
1878
|
+
if (!options.dryRun) {
|
|
1879
|
+
await writePlannedGeneratedFile(targetDir, plan.route);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
if (plan.route.result === "created") {
|
|
1883
|
+
result.createdFiles.push(plan.route.path);
|
|
1884
|
+
}
|
|
1885
|
+
if (plan.route.result === "updated") {
|
|
1886
|
+
result.updatedFiles.push(plan.route.path);
|
|
1887
|
+
}
|
|
1888
|
+
if (plan.route.result === "skipped") {
|
|
1889
|
+
result.skippedFiles.push(plan.route.path);
|
|
1890
|
+
}
|
|
1891
|
+
if (!result.files.includes(plan.route.path)) {
|
|
1892
|
+
result.files.push(plan.route.path);
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
const cronSecretResult = await applyCronSecretWiring(
|
|
1896
|
+
targetDir,
|
|
1897
|
+
plan.cronSecret,
|
|
1898
|
+
options,
|
|
1899
|
+
);
|
|
1900
|
+
result.createdFiles = uniqueStrings([
|
|
1901
|
+
...result.createdFiles,
|
|
1902
|
+
...cronSecretResult.createdFiles,
|
|
1903
|
+
]);
|
|
1904
|
+
result.updatedFiles = uniqueStrings([
|
|
1905
|
+
...result.updatedFiles,
|
|
1906
|
+
...cronSecretResult.updatedFiles,
|
|
1907
|
+
]);
|
|
1908
|
+
result.skippedFiles = uniqueStrings([
|
|
1909
|
+
...result.skippedFiles,
|
|
1910
|
+
...cronSecretResult.skippedFiles,
|
|
1911
|
+
]);
|
|
1912
|
+
result.files = uniqueStrings([
|
|
1913
|
+
...result.files,
|
|
1914
|
+
...plan.cronSecret.files.map((file) => file.path),
|
|
1915
|
+
...plan.cronSecret.skippedFiles,
|
|
1916
|
+
]);
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1521
1919
|
/**
|
|
1522
1920
|
* A provider-backed app port a generator depends on.
|
|
1523
1921
|
*
|
|
@@ -1588,6 +1986,18 @@ function paymentsPortWiring(command: string): PortProviderWiring {
|
|
|
1588
1986
|
};
|
|
1589
1987
|
}
|
|
1590
1988
|
|
|
1989
|
+
function storagePortWiring(command: string): PortProviderWiring {
|
|
1990
|
+
return {
|
|
1991
|
+
command,
|
|
1992
|
+
portKey: "storage",
|
|
1993
|
+
portType: "StoragePort",
|
|
1994
|
+
portTypeModule: "@beignet/core/ports",
|
|
1995
|
+
providerFactory: "createLocalStorageProvider",
|
|
1996
|
+
providerModule: "@beignet/provider-storage-local",
|
|
1997
|
+
dependency: "@beignet/provider-storage-local",
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1591
2001
|
function providersFilePath(config: ResolvedBeignetConfig): string {
|
|
1592
2002
|
return `${directoryPath(path.dirname(config.paths.server))}/providers.ts`;
|
|
1593
2003
|
}
|
|
@@ -1770,7 +2180,16 @@ export async function makeUpload(
|
|
|
1770
2180
|
|
|
1771
2181
|
await assertFeatureArtifactApp(targetDir, config, "upload");
|
|
1772
2182
|
|
|
1773
|
-
|
|
2183
|
+
await wirePortProviders(
|
|
2184
|
+
targetDir,
|
|
2185
|
+
config,
|
|
2186
|
+
[storagePortWiring("make upload")],
|
|
2187
|
+
{
|
|
2188
|
+
dryRun: true,
|
|
2189
|
+
},
|
|
2190
|
+
);
|
|
2191
|
+
|
|
2192
|
+
const result = await makeFeatureArtifact({
|
|
1774
2193
|
targetDir,
|
|
1775
2194
|
config,
|
|
1776
2195
|
force: Boolean(options.force),
|
|
@@ -1782,6 +2201,95 @@ export async function makeUpload(
|
|
|
1782
2201
|
"@beignet/core": beignetDependencyVersion,
|
|
1783
2202
|
},
|
|
1784
2203
|
});
|
|
2204
|
+
|
|
2205
|
+
await applyPortProviderWiring(
|
|
2206
|
+
result,
|
|
2207
|
+
targetDir,
|
|
2208
|
+
config,
|
|
2209
|
+
[storagePortWiring("make upload")],
|
|
2210
|
+
{ dryRun: Boolean(options.dryRun) },
|
|
2211
|
+
);
|
|
2212
|
+
|
|
2213
|
+
const uploadRouteResult = await updateUploadRoute(targetDir, names, config, {
|
|
2214
|
+
dryRun: Boolean(options.dryRun),
|
|
2215
|
+
});
|
|
2216
|
+
if (uploadRouteResult === "created")
|
|
2217
|
+
result.createdFiles.push(uploadRoutePath(config));
|
|
2218
|
+
if (uploadRouteResult === "updated")
|
|
2219
|
+
result.updatedFiles.push(uploadRoutePath(config));
|
|
2220
|
+
if (uploadRouteResult === "skipped")
|
|
2221
|
+
result.skippedFiles.push(uploadRoutePath(config));
|
|
2222
|
+
if (!result.files.includes(uploadRoutePath(config))) {
|
|
2223
|
+
result.files.push(uploadRoutePath(config));
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
return result;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
export async function makeOutbox(
|
|
2230
|
+
options: MakeOutboxOptions = {},
|
|
2231
|
+
): Promise<MakeOutboxResult> {
|
|
2232
|
+
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
2233
|
+
const config = options.config
|
|
2234
|
+
? resolveConfig(options.config)
|
|
2235
|
+
: await loadBeignetConfig(targetDir);
|
|
2236
|
+
|
|
2237
|
+
await assertServerRuntimeApp(targetDir, config, "outbox");
|
|
2238
|
+
await updateOutboxPortWiring(targetDir, config, { dryRun: true });
|
|
2239
|
+
|
|
2240
|
+
const cronSecretPlan = await planCronSecretWiring(targetDir);
|
|
2241
|
+
const files = outboxFiles(config);
|
|
2242
|
+
const plannedFiles = await planGeneratedFiles(targetDir, files, {
|
|
2243
|
+
force: Boolean(options.force),
|
|
2244
|
+
});
|
|
2245
|
+
const createdFiles = plannedFiles
|
|
2246
|
+
.filter((file) => file.result === "created")
|
|
2247
|
+
.map((file) => file.path);
|
|
2248
|
+
const updatedFiles = plannedFiles
|
|
2249
|
+
.filter((file) => file.result === "updated")
|
|
2250
|
+
.map((file) => file.path);
|
|
2251
|
+
const skippedFiles = plannedFiles
|
|
2252
|
+
.filter((file) => file.result === "skipped")
|
|
2253
|
+
.map((file) => file.path);
|
|
2254
|
+
|
|
2255
|
+
if (!options.dryRun) {
|
|
2256
|
+
for (const file of plannedFiles) {
|
|
2257
|
+
await writePlannedGeneratedFile(targetDir, file);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
const cronSecretResult = await applyCronSecretWiring(
|
|
2262
|
+
targetDir,
|
|
2263
|
+
cronSecretPlan,
|
|
2264
|
+
{ dryRun: Boolean(options.dryRun) },
|
|
2265
|
+
);
|
|
2266
|
+
createdFiles.push(...cronSecretResult.createdFiles);
|
|
2267
|
+
updatedFiles.push(...cronSecretResult.updatedFiles);
|
|
2268
|
+
skippedFiles.push(...cronSecretResult.skippedFiles);
|
|
2269
|
+
updatedFiles.push(
|
|
2270
|
+
...(await updateOutboxPortWiring(targetDir, config, {
|
|
2271
|
+
dryRun: Boolean(options.dryRun),
|
|
2272
|
+
})),
|
|
2273
|
+
);
|
|
2274
|
+
|
|
2275
|
+
return {
|
|
2276
|
+
schemaVersion: 1,
|
|
2277
|
+
name: "outbox",
|
|
2278
|
+
targetDir,
|
|
2279
|
+
dryRun: Boolean(options.dryRun),
|
|
2280
|
+
files: [
|
|
2281
|
+
...files.map((file) => file.path),
|
|
2282
|
+
...cronSecretPlan.files.map((file) => file.path),
|
|
2283
|
+
...cronSecretPlan.skippedFiles,
|
|
2284
|
+
config.paths.ports,
|
|
2285
|
+
config.paths.infrastructurePorts,
|
|
2286
|
+
path.join(infrastructureDir(config), "db/provider.ts"),
|
|
2287
|
+
drizzleRepositoriesPath(config),
|
|
2288
|
+
],
|
|
2289
|
+
createdFiles: uniqueStrings(createdFiles),
|
|
2290
|
+
updatedFiles: uniqueStrings(updatedFiles),
|
|
2291
|
+
skippedFiles: uniqueStrings(skippedFiles),
|
|
2292
|
+
};
|
|
1785
2293
|
}
|
|
1786
2294
|
|
|
1787
2295
|
async function makeFeatureUi(
|
|
@@ -2017,6 +2525,28 @@ async function assertFeatureUiApp(
|
|
|
2017
2525
|
}
|
|
2018
2526
|
}
|
|
2019
2527
|
|
|
2528
|
+
async function assertServerRuntimeApp(
|
|
2529
|
+
targetDir: string,
|
|
2530
|
+
config: ResolvedBeignetConfig,
|
|
2531
|
+
artifact: string,
|
|
2532
|
+
): Promise<void> {
|
|
2533
|
+
const requiredFiles = [
|
|
2534
|
+
config.paths.appContext,
|
|
2535
|
+
config.paths.server,
|
|
2536
|
+
config.paths.infrastructurePorts,
|
|
2537
|
+
];
|
|
2538
|
+
|
|
2539
|
+
for (const file of requiredFiles) {
|
|
2540
|
+
try {
|
|
2541
|
+
await stat(path.join(targetDir, file));
|
|
2542
|
+
} catch {
|
|
2543
|
+
throw new Error(
|
|
2544
|
+
`beignet make ${artifact} expects a standard Beignet app with a server runtime. Missing ${file}.`,
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2020
2550
|
async function assertPersistenceArtifactApp(
|
|
2021
2551
|
targetDir: string,
|
|
2022
2552
|
names: FactoryNames | SeedNames,
|
|
@@ -2268,6 +2798,7 @@ async function makeFeatureArtifact(options: {
|
|
|
2268
2798
|
}
|
|
2269
2799
|
|
|
2270
2800
|
type PackageJsonLike = {
|
|
2801
|
+
scripts?: Record<string, string>;
|
|
2271
2802
|
dependencies?: Record<string, string>;
|
|
2272
2803
|
devDependencies?: Record<string, string>;
|
|
2273
2804
|
};
|
|
@@ -2292,6 +2823,26 @@ async function updatePackageDependencies(
|
|
|
2292
2823
|
return true;
|
|
2293
2824
|
}
|
|
2294
2825
|
|
|
2826
|
+
async function updatePackageScripts(
|
|
2827
|
+
targetDir: string,
|
|
2828
|
+
scripts: Record<string, string>,
|
|
2829
|
+
options: { dryRun: boolean },
|
|
2830
|
+
): Promise<boolean> {
|
|
2831
|
+
const filePath = path.join(targetDir, "package.json");
|
|
2832
|
+
const original = await readFile(filePath, "utf8");
|
|
2833
|
+
const packageJson = JSON.parse(original) as PackageJsonLike;
|
|
2834
|
+
packageJson.scripts = packageJson.scripts ?? {};
|
|
2835
|
+
|
|
2836
|
+
for (const [name, command] of Object.entries(scripts)) {
|
|
2837
|
+
packageJson.scripts[name] ??= command;
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
|
|
2841
|
+
if (next === original) return false;
|
|
2842
|
+
if (!options.dryRun) await writeFile(filePath, next);
|
|
2843
|
+
return true;
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2295
2846
|
function beignetDependencyVersion(packageJson: PackageJsonLike): string {
|
|
2296
2847
|
return (
|
|
2297
2848
|
packageJson.dependencies?.["@beignet/core"] ??
|
|
@@ -3670,6 +4221,15 @@ function typeBodySource(source: string, typeName: string): string | undefined {
|
|
|
3670
4221
|
return source.slice(openBrace + 1, closeBrace);
|
|
3671
4222
|
}
|
|
3672
4223
|
|
|
4224
|
+
function typeHasTopLevelProperty(
|
|
4225
|
+
source: string,
|
|
4226
|
+
typeName: string,
|
|
4227
|
+
propertyName: string,
|
|
4228
|
+
): boolean {
|
|
4229
|
+
const body = typeBodySource(source, typeName);
|
|
4230
|
+
return body !== undefined && hasTopLevelObjectProperty(body, propertyName);
|
|
4231
|
+
}
|
|
4232
|
+
|
|
3673
4233
|
function appendUnionTypeMember(
|
|
3674
4234
|
source: string,
|
|
3675
4235
|
typeName: string,
|
|
@@ -3976,6 +4536,45 @@ function addNamedTypeImport(
|
|
|
3976
4536
|
)}`;
|
|
3977
4537
|
}
|
|
3978
4538
|
|
|
4539
|
+
function addNamedImport(
|
|
4540
|
+
source: string,
|
|
4541
|
+
name: string,
|
|
4542
|
+
moduleName: string,
|
|
4543
|
+
): string {
|
|
4544
|
+
const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4545
|
+
const importPattern = new RegExp(
|
|
4546
|
+
`import \\{([^}]*)\\} from "${escapedModule}";`,
|
|
4547
|
+
);
|
|
4548
|
+
const match = importPattern.exec(source);
|
|
4549
|
+
if (!match) {
|
|
4550
|
+
return insertAfterImports(
|
|
4551
|
+
source,
|
|
4552
|
+
`import { ${name} } from "${moduleName}";`,
|
|
4553
|
+
);
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
const names = match[1]
|
|
4557
|
+
.split(",")
|
|
4558
|
+
.map((entry) => entry.trim())
|
|
4559
|
+
.filter(Boolean);
|
|
4560
|
+
if (names.includes(name)) return source;
|
|
4561
|
+
|
|
4562
|
+
const insertIndex = names.findIndex(
|
|
4563
|
+
(existing) => existing.localeCompare(name) > 0,
|
|
4564
|
+
);
|
|
4565
|
+
if (insertIndex === -1) names.push(name);
|
|
4566
|
+
else names.splice(insertIndex, 0, name);
|
|
4567
|
+
|
|
4568
|
+
const indent = match[1].match(/\n([\t ]+)/)?.[1] ?? "\t";
|
|
4569
|
+
const replacement = match[1].includes("\n")
|
|
4570
|
+
? `import {\n${indent}${names.join(`,\n${indent}`)},\n} from "${moduleName}";`
|
|
4571
|
+
: `import { ${names.join(", ")} } from "${moduleName}";`;
|
|
4572
|
+
|
|
4573
|
+
return `${source.slice(0, match.index)}${replacement}${source.slice(
|
|
4574
|
+
match.index + match[0].length,
|
|
4575
|
+
)}`;
|
|
4576
|
+
}
|
|
4577
|
+
|
|
3979
4578
|
function topLevelBoundObjectRange(
|
|
3980
4579
|
source: string,
|
|
3981
4580
|
openBrace: number,
|
|
@@ -4454,6 +5053,10 @@ function seedNames(input: string): SeedNames {
|
|
|
4454
5053
|
};
|
|
4455
5054
|
}
|
|
4456
5055
|
|
|
5056
|
+
function defaultFactoryNamesForSeed(names: SeedNames): FactoryNames {
|
|
5057
|
+
return factoryNames(`${names.feature.kebab}/${names.featureSingularKebab}`);
|
|
5058
|
+
}
|
|
5059
|
+
|
|
4457
5060
|
function notificationNames(input: string): NotificationNames {
|
|
4458
5061
|
const names = featureArtifactNames(input, "Notification");
|
|
4459
5062
|
const notificationExportName = `${names.featureSingularPascal}${names.artifact.pascal}Notification`;
|
|
@@ -5065,6 +5668,19 @@ function uploadFiles(
|
|
|
5065
5668
|
];
|
|
5066
5669
|
}
|
|
5067
5670
|
|
|
5671
|
+
function outboxFiles(config: ResolvedBeignetConfig): GeneratedFile[] {
|
|
5672
|
+
return [
|
|
5673
|
+
{
|
|
5674
|
+
path: config.paths.outbox,
|
|
5675
|
+
content: outboxRegistryFile(config),
|
|
5676
|
+
},
|
|
5677
|
+
{
|
|
5678
|
+
path: outboxDrainRoutePath(config),
|
|
5679
|
+
content: outboxDrainRouteFile(config),
|
|
5680
|
+
},
|
|
5681
|
+
];
|
|
5682
|
+
}
|
|
5683
|
+
|
|
5068
5684
|
function portFilePath(names: PortNames, config: ResolvedBeignetConfig): string {
|
|
5069
5685
|
return path.join(path.dirname(config.paths.ports), `${names.kebab}.ts`);
|
|
5070
5686
|
}
|
|
@@ -5470,6 +6086,24 @@ function uploadFilePath(
|
|
|
5470
6086
|
);
|
|
5471
6087
|
}
|
|
5472
6088
|
|
|
6089
|
+
function seedEntrypointPath(config: ResolvedBeignetConfig): string {
|
|
6090
|
+
return path.join(infrastructureDir(config), "db", "seed.ts");
|
|
6091
|
+
}
|
|
6092
|
+
|
|
6093
|
+
function uploadRoutePath(config: ResolvedBeignetConfig): string {
|
|
6094
|
+
return path.join(
|
|
6095
|
+
config.paths.routes,
|
|
6096
|
+
"uploads",
|
|
6097
|
+
"[uploadName]",
|
|
6098
|
+
"[action]",
|
|
6099
|
+
"route.ts",
|
|
6100
|
+
);
|
|
6101
|
+
}
|
|
6102
|
+
|
|
6103
|
+
function outboxDrainRoutePath(config: ResolvedBeignetConfig): string {
|
|
6104
|
+
return path.join(config.paths.routes, "cron", "outbox", "drain", "route.ts");
|
|
6105
|
+
}
|
|
6106
|
+
|
|
5473
6107
|
function scheduleRouteFilePath(
|
|
5474
6108
|
names: ScheduleNames,
|
|
5475
6109
|
config: ResolvedBeignetConfig,
|
|
@@ -5909,13 +6543,98 @@ export async function stopScheduleContext(): Promise<void> {
|
|
|
5909
6543
|
return "updated";
|
|
5910
6544
|
}
|
|
5911
6545
|
|
|
6546
|
+
async function updateSeedEntrypoint(
|
|
6547
|
+
targetDir: string,
|
|
6548
|
+
names: SeedNames,
|
|
6549
|
+
config: ResolvedBeignetConfig,
|
|
6550
|
+
options: { dryRun: boolean },
|
|
6551
|
+
): Promise<WriteGeneratedFileResult> {
|
|
6552
|
+
const file = seedEntrypointPath(config);
|
|
6553
|
+
const destination = path.join(targetDir, file);
|
|
6554
|
+
const existing = await readOptionalFile(destination);
|
|
6555
|
+
const registryName = `${names.featureSingularCamel}Seeds`;
|
|
6556
|
+
const importLine = `import { ${registryName} } from "${aliasModule(
|
|
6557
|
+
path.join(featureArtifactDir(names, "seeds", config), "index.ts"),
|
|
6558
|
+
)}";`;
|
|
6559
|
+
const registryEntry = `...${registryName}`;
|
|
6560
|
+
|
|
6561
|
+
if (existing === undefined) {
|
|
6562
|
+
const content = seedEntrypointFile(names, config);
|
|
6563
|
+
if (!options.dryRun) {
|
|
6564
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
6565
|
+
await writeFile(destination, content);
|
|
6566
|
+
}
|
|
6567
|
+
return "created";
|
|
6568
|
+
}
|
|
6569
|
+
|
|
6570
|
+
let next = existing;
|
|
6571
|
+
if (!next.includes(importLine)) {
|
|
6572
|
+
next = insertAfterImports(next, importLine);
|
|
6573
|
+
}
|
|
6574
|
+
|
|
6575
|
+
const registry = arrayInitializerInfo(next, "seeds");
|
|
6576
|
+
if (!registry) {
|
|
6577
|
+
next = `${next.trimEnd()}\n\nconst seeds = [${registryEntry}] as const;\n`;
|
|
6578
|
+
} else if (!identifiersFromArrayExpression(registry.text).has(registryName)) {
|
|
6579
|
+
const nextArray = appendToArrayExpression(registry.text, [registryEntry]);
|
|
6580
|
+
next = `${next.slice(0, registry.start)}${nextArray}${next.slice(
|
|
6581
|
+
registry.end,
|
|
6582
|
+
)}`;
|
|
6583
|
+
}
|
|
6584
|
+
|
|
6585
|
+
if (next === existing) return "skipped";
|
|
6586
|
+
if (!options.dryRun) await writeFile(destination, next);
|
|
6587
|
+
return "updated";
|
|
6588
|
+
}
|
|
6589
|
+
|
|
6590
|
+
async function updateUploadRoute(
|
|
6591
|
+
targetDir: string,
|
|
6592
|
+
names: UploadNames,
|
|
6593
|
+
config: ResolvedBeignetConfig,
|
|
6594
|
+
options: { dryRun: boolean },
|
|
6595
|
+
): Promise<WriteGeneratedFileResult> {
|
|
6596
|
+
const file = uploadRoutePath(config);
|
|
6597
|
+
const destination = path.join(targetDir, file);
|
|
6598
|
+
const existing = await readOptionalFile(destination);
|
|
6599
|
+
const registryName = `${names.featureSingularCamel}Uploads`;
|
|
6600
|
+
const registryKey = names.featureSingularCamel;
|
|
6601
|
+
const importLine = `import { ${registryName} } from "${aliasModule(
|
|
6602
|
+
path.join(featureArtifactDir(names, "uploads", config), "index.ts"),
|
|
6603
|
+
)}";`;
|
|
6604
|
+
|
|
6605
|
+
if (existing === undefined) {
|
|
6606
|
+
const content = uploadRouteFile(names, config);
|
|
6607
|
+
if (!options.dryRun) {
|
|
6608
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
6609
|
+
await writeFile(destination, content);
|
|
6610
|
+
}
|
|
6611
|
+
return "created";
|
|
6612
|
+
}
|
|
6613
|
+
|
|
6614
|
+
let next = existing;
|
|
6615
|
+
if (!next.includes(importLine)) {
|
|
6616
|
+
next = insertAfterImports(next, importLine);
|
|
6617
|
+
}
|
|
6618
|
+
|
|
6619
|
+
const registry = defineUploadsInitializerInfo(next, "uploadRegistry");
|
|
6620
|
+
if (!registry) {
|
|
6621
|
+
return "skipped";
|
|
6622
|
+
}
|
|
6623
|
+
if (!new RegExp(`\\b${registryKey}\\s*:`).test(registry.text)) {
|
|
6624
|
+
next = `${next.slice(0, registry.end)}\t${registryKey}: ${registryName},\n${next.slice(
|
|
6625
|
+
registry.end,
|
|
6626
|
+
)}`;
|
|
6627
|
+
}
|
|
6628
|
+
|
|
6629
|
+
if (next === existing) return "skipped";
|
|
6630
|
+
if (!options.dryRun) await writeFile(destination, next);
|
|
6631
|
+
return "updated";
|
|
6632
|
+
}
|
|
6633
|
+
|
|
5912
6634
|
/**
|
|
5913
6635
|
* Append a feature event or job registry to `server/outbox.ts`.
|
|
5914
|
-
*
|
|
5915
|
-
*
|
|
5916
|
-
* `undefined` when the app has no outbox module. It also bails to "skipped"
|
|
5917
|
-
* instead of writing when the `defineOutboxRegistry({...})` anchor is missing
|
|
5918
|
-
* or the matching array is not editable.
|
|
6636
|
+
* Creates the registry when missing so event/job generators produce a complete
|
|
6637
|
+
* drainable path instead of leaving durable delivery as manual wiring.
|
|
5919
6638
|
*/
|
|
5920
6639
|
async function updateOutboxRegistry(
|
|
5921
6640
|
targetDir: string,
|
|
@@ -5923,10 +6642,17 @@ async function updateOutboxRegistry(
|
|
|
5923
6642
|
kind: "events" | "jobs",
|
|
5924
6643
|
config: ResolvedBeignetConfig,
|
|
5925
6644
|
options: { dryRun: boolean },
|
|
5926
|
-
): Promise<WriteGeneratedFileResult
|
|
6645
|
+
): Promise<WriteGeneratedFileResult> {
|
|
5927
6646
|
const destination = path.join(targetDir, config.paths.outbox);
|
|
5928
6647
|
const existing = await readOptionalFile(destination);
|
|
5929
|
-
if (existing === undefined)
|
|
6648
|
+
if (existing === undefined) {
|
|
6649
|
+
const content = outboxRegistryFile(config, { names, kind });
|
|
6650
|
+
if (!options.dryRun) {
|
|
6651
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
6652
|
+
await writeFile(destination, content);
|
|
6653
|
+
}
|
|
6654
|
+
return "created";
|
|
6655
|
+
}
|
|
5930
6656
|
|
|
5931
6657
|
const featureIndexPath = path.join(
|
|
5932
6658
|
featureArtifactDir(names, kind, config),
|
|
@@ -5976,8 +6702,9 @@ async function applyOutboxRegistryUpdate(
|
|
|
5976
6702
|
config,
|
|
5977
6703
|
options,
|
|
5978
6704
|
);
|
|
5979
|
-
if (registryResult === undefined) return;
|
|
5980
6705
|
|
|
6706
|
+
if (registryResult === "created")
|
|
6707
|
+
result.createdFiles.push(config.paths.outbox);
|
|
5981
6708
|
if (registryResult === "updated")
|
|
5982
6709
|
result.updatedFiles.push(config.paths.outbox);
|
|
5983
6710
|
if (registryResult === "skipped")
|
|
@@ -7488,6 +8215,137 @@ export const ${names.uploadExportName} = defineUpload<
|
|
|
7488
8215
|
`;
|
|
7489
8216
|
}
|
|
7490
8217
|
|
|
8218
|
+
function seedEntrypointFile(
|
|
8219
|
+
names: SeedNames,
|
|
8220
|
+
config: ResolvedBeignetConfig,
|
|
8221
|
+
): string {
|
|
8222
|
+
const registryName = `${names.featureSingularCamel}Seeds`;
|
|
8223
|
+
|
|
8224
|
+
return `import { createServiceActor } from "@beignet/core/ports";
|
|
8225
|
+
import { runSeeds } from "@beignet/core/testing";
|
|
8226
|
+
import { ${registryName} } from "${aliasModule(path.join(featureArtifactDir(names, "seeds", config), "index.ts"))}";
|
|
8227
|
+
import { server } from "${aliasModule(config.paths.server)}";
|
|
8228
|
+
|
|
8229
|
+
const seeds = [...${registryName}] as const;
|
|
8230
|
+
|
|
8231
|
+
const ctx = await server.createServiceContext({
|
|
8232
|
+
\tactor: createServiceActor("beignet-seed"),
|
|
8233
|
+
});
|
|
8234
|
+
|
|
8235
|
+
try {
|
|
8236
|
+
\tawait runSeeds({ ctx, seeds });
|
|
8237
|
+
} finally {
|
|
8238
|
+
\tawait server.stop();
|
|
8239
|
+
}
|
|
8240
|
+
`;
|
|
8241
|
+
}
|
|
8242
|
+
|
|
8243
|
+
function uploadRouteFile(
|
|
8244
|
+
names: UploadNames,
|
|
8245
|
+
config: ResolvedBeignetConfig,
|
|
8246
|
+
): string {
|
|
8247
|
+
const registryName = `${names.featureSingularCamel}Uploads`;
|
|
8248
|
+
|
|
8249
|
+
return `import {
|
|
8250
|
+
\tcreateUploadRouter,
|
|
8251
|
+
\tdefineUploads,
|
|
8252
|
+
\tuploadsFromRegistry,
|
|
8253
|
+
} from "@beignet/core/uploads";
|
|
8254
|
+
import { createUploadRoute } from "@beignet/next";
|
|
8255
|
+
import { ${registryName} } from "${aliasModule(path.join(featureArtifactDir(names, "uploads", config), "index.ts"))}";
|
|
8256
|
+
import { server } from "${aliasModule(config.paths.server)}";
|
|
8257
|
+
|
|
8258
|
+
const uploadRegistry = defineUploads({
|
|
8259
|
+
\t${names.featureSingularCamel}: ${registryName},
|
|
8260
|
+
});
|
|
8261
|
+
|
|
8262
|
+
const uploadRouter = createUploadRouter({
|
|
8263
|
+
\tuploads: uploadsFromRegistry(uploadRegistry),
|
|
8264
|
+
\tctx: () => server.createContextFromNext(),
|
|
8265
|
+
\tstorage: server.ports.storage,
|
|
8266
|
+
});
|
|
8267
|
+
|
|
8268
|
+
export const { POST } = createUploadRoute(uploadRouter);
|
|
8269
|
+
`;
|
|
8270
|
+
}
|
|
8271
|
+
|
|
8272
|
+
function outboxRegistryFile(
|
|
8273
|
+
config: ResolvedBeignetConfig,
|
|
8274
|
+
registration?: {
|
|
8275
|
+
names: EventNames | JobNames;
|
|
8276
|
+
kind: "events" | "jobs";
|
|
8277
|
+
},
|
|
8278
|
+
): string {
|
|
8279
|
+
const featureRegistry = registration
|
|
8280
|
+
? outboxFeatureRegistry(registration.names, registration.kind, config)
|
|
8281
|
+
: undefined;
|
|
8282
|
+
const eventEntry =
|
|
8283
|
+
featureRegistry?.kind === "events" ? `...${featureRegistry.name}` : "";
|
|
8284
|
+
const jobEntry =
|
|
8285
|
+
featureRegistry?.kind === "jobs" ? `...${featureRegistry.name}` : "";
|
|
8286
|
+
const featureImport = featureRegistry
|
|
8287
|
+
? `${featureRegistry.importLine}\n`
|
|
8288
|
+
: "";
|
|
8289
|
+
|
|
8290
|
+
return `import { defineOutboxRegistry } from "@beignet/core/outbox";
|
|
8291
|
+
import { createServiceActor } from "@beignet/core/ports";
|
|
8292
|
+
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
8293
|
+
${featureImport}import { server } from "${relativeModule(config.paths.outbox, config.paths.server)}";
|
|
8294
|
+
|
|
8295
|
+
export const outboxRegistry = defineOutboxRegistry({
|
|
8296
|
+
\tevents: [${eventEntry}],
|
|
8297
|
+
\tjobs: [${jobEntry}],
|
|
8298
|
+
});
|
|
8299
|
+
|
|
8300
|
+
export async function createOutboxDrainContext(): Promise<AppContext> {
|
|
8301
|
+
\treturn server.createServiceContext({
|
|
8302
|
+
\t\tactor: createServiceActor("beignet-outbox"),
|
|
8303
|
+
\t});
|
|
8304
|
+
}
|
|
8305
|
+
|
|
8306
|
+
export async function stopOutboxDrainContext(): Promise<void> {
|
|
8307
|
+
\tawait server.stop();
|
|
8308
|
+
}
|
|
8309
|
+
`;
|
|
8310
|
+
}
|
|
8311
|
+
|
|
8312
|
+
function outboxDrainRouteFile(config: ResolvedBeignetConfig): string {
|
|
8313
|
+
return `import { createOutboxDrainRoute } from "@beignet/next";
|
|
8314
|
+
import { env } from "@/lib/env";
|
|
8315
|
+
import { server } from "${aliasModule(config.paths.server)}";
|
|
8316
|
+
import { outboxRegistry } from "${aliasModule(config.paths.outbox)}";
|
|
8317
|
+
|
|
8318
|
+
export const runtime = "nodejs";
|
|
8319
|
+
|
|
8320
|
+
export const { GET, POST } = createOutboxDrainRoute({
|
|
8321
|
+
\tserver,
|
|
8322
|
+
\tregistry: outboxRegistry,
|
|
8323
|
+
\tsecret: env.CRON_SECRET,
|
|
8324
|
+
\tbatchSize: 100,
|
|
8325
|
+
});
|
|
8326
|
+
`;
|
|
8327
|
+
}
|
|
8328
|
+
|
|
8329
|
+
function outboxFeatureRegistry(
|
|
8330
|
+
names: EventNames | JobNames,
|
|
8331
|
+
kind: "events" | "jobs",
|
|
8332
|
+
config: ResolvedBeignetConfig,
|
|
8333
|
+
): { kind: "events" | "jobs"; name: string; importLine: string } {
|
|
8334
|
+
const featureIndexPath = path.join(
|
|
8335
|
+
featureArtifactDir(names, kind, config),
|
|
8336
|
+
"index.ts",
|
|
8337
|
+
);
|
|
8338
|
+
const name = `${names.featureSingularCamel}${
|
|
8339
|
+
kind === "events" ? "Events" : "Jobs"
|
|
8340
|
+
}`;
|
|
8341
|
+
|
|
8342
|
+
return {
|
|
8343
|
+
kind,
|
|
8344
|
+
name,
|
|
8345
|
+
importLine: `import { ${name} } from "${aliasModule(featureIndexPath)}";`,
|
|
8346
|
+
};
|
|
8347
|
+
}
|
|
8348
|
+
|
|
7491
8349
|
function billingSchemasFile(): string {
|
|
7492
8350
|
return `import { z } from "zod";
|
|
7493
8351
|
|