@beignet/cli 0.0.38 → 0.0.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +154 -102
- package/dist/choices.d.ts +17 -21
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +21 -67
- package/dist/choices.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -2
- package/dist/config.js.map +1 -1
- package/dist/create-prompts.d.ts +5 -6
- package/dist/create-prompts.d.ts.map +1 -1
- package/dist/create-prompts.js +15 -15
- package/dist/create-prompts.js.map +1 -1
- package/dist/create.d.ts +11 -2
- package/dist/create.d.ts.map +1 -1
- package/dist/create.js +38 -28
- package/dist/create.js.map +1 -1
- package/dist/framework.d.ts +3 -0
- package/dist/framework.d.ts.map +1 -0
- package/dist/framework.js +6 -0
- package/dist/framework.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +149 -47
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +4 -2
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +174 -34
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +1 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/make/inbox.js +3 -3
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.d.ts.map +1 -1
- package/dist/make/payments.js +5 -3
- package/dist/make/payments.js.map +1 -1
- package/dist/make/shared.d.ts +10 -3
- package/dist/make/shared.d.ts.map +1 -1
- package/dist/make/shared.js +7 -8
- package/dist/make/shared.js.map +1 -1
- package/dist/make.d.ts +3 -2
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +549 -186
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +15 -11
- package/dist/mcp.js.map +1 -1
- package/dist/provider-add.d.ts +21 -3
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +101 -65
- package/dist/provider-add.js.map +1 -1
- package/dist/task.js +1 -1
- package/dist/task.js.map +1 -1
- package/dist/templates/base.d.ts +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +15 -15
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/index.d.ts +2 -2
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +5 -5
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +17 -13
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shadcn.js +1 -1
- package/dist/templates/shared.d.ts +4 -4
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +6 -6
- package/dist/templates/shared.js.map +1 -1
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +29 -9
- package/src/choices.ts +38 -88
- package/src/config.ts +7 -3
- package/src/create-prompts.ts +20 -21
- package/src/create.ts +54 -36
- package/src/framework.ts +13 -0
- package/src/index.ts +193 -65
- package/src/inspect.ts +290 -39
- package/src/lib.ts +1 -1
- package/src/make/inbox.ts +3 -3
- package/src/make/payments.ts +10 -3
- package/src/make/shared.ts +16 -10
- package/src/make.ts +720 -176
- package/src/mcp.ts +20 -13
- package/src/provider-add.ts +159 -79
- package/src/task.ts +1 -1
- package/src/templates/base.ts +16 -16
- package/src/templates/index.ts +6 -6
- package/src/templates/server.ts +17 -13
- package/src/templates/shadcn.ts +1 -1
- package/src/templates/shared.ts +10 -10
- package/src/test-helpers/generated-app.ts +2 -1
package/dist/make.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import { clientFormsPath, clientIndexPath, loadBeignetConfig, resolveConfig, } from "./config.js";
|
|
6
|
+
import { assertNextFramework } from "./framework.js";
|
|
5
7
|
import { adapterFilePath, addNamedImport, addNamedTypeImport, aliasModule, appendDeferredPortKey, appendPickStringLiteralMember, applyPortProviderWiring, assertAdapterApp, assertFeatureArtifactApp, assertFeatureUiApp, assertGeneratedIdentifiers, assertPersistenceArtifactApp, assertPolicyApp, assertPortApp, assertServerRuntimeApp, assertStandardApp, assertTestApp, assertUseCaseApp, beignetDependencyVersion, constantCase, createListenersRegistrySource, defineUploadsInitializerInfo, detectResourceDatabase, detectResourcePersistence, drizzleDialects, drizzleRepositoriesPath, drizzleResourceRepositoryFilePath, drizzleSchemaIndexPath, eventBusPortWiring, eventFilePath, factoryFilePath, featureArtifactDir, featureArtifactIndexFile, featureArtifactNames, featureResourceNames, featureUiClientQueriesFilePath, featureUiComponentFilePath, featureUiIndexFilePath, featureUiNames, fileExists, hasTopLevelObjectProperty, identifierNames, infrastructureDir, insertTypeProperty, jobFilePath, listenerFilePath, listenersFilePath, mergeMakeResultInto, mergeMakeResults, missingCapabilityBuilderFiles, notificationFilePath, notificationPortWirings, planGeneratedFile, planGeneratedFiles, portFilePath, portNames, providersFilePath, readOptionalFile, relativeModule, resourceContractFilePath, resourceFeatureDir, resourceNames, resourcePortFilePath, resourceSharedErrorsPath, scheduleFilePath, seedEntrypointPath, seedFilePath, seedNames, singularize, storagePortWiring, tanstackReactQueryDependencyVersion, taskFilePath, typeBodySource, uniqueStrings, updateDrizzleRepositories, updateDrizzleSchemaIndex, updateFeatureArtifactIndex, updateInfrastructureAdapterWiring, updateInfrastructureDeferredPorts, updateInfrastructureGatePolicies, updateInfrastructurePortStub, updateInfrastructurePorts, updateListenersRegistrySource, updateOpenApiRoute, updatePackageDependencies, updatePackageJson, updatePackageScripts, updatePortIndex, updatePortsIndex, updateSeedEntrypoint, updateServerRoutes, updateServerTransactionPorts, updateSharedErrors, uploadFilePath, useCaseFeatureDir, useCaseFilePath, usesFeatureOwnedPolicies, usesFeatureOwnedTests, wireListenersProviderSource, wirePortProviders, writeGeneratedFile, writePlannedGeneratedFile, } from "./make/shared.js";
|
|
6
|
-
import { appendToArrayExpression, appendToOutboxRegistryArray, arrayInitializerInfo, identifiersFromArrayExpression, insertAfterImports, } from "./registry-edits.js";
|
|
8
|
+
import { appendToArrayExpression, appendToNamedArray, appendToOutboxRegistryArray, arrayInitializerInfo, identifiersFromArrayExpression, insertAfterImports, } from "./registry-edits.js";
|
|
9
|
+
import { externalVersions } from "./templates/shared.js";
|
|
7
10
|
import { testSupportTemplateFiles } from "./templates/testing.js";
|
|
8
11
|
export { makeFeatureAddonChoices, makeFeatureRecipeChoices, } from "./choices.js";
|
|
9
12
|
export * from "./make/inbox.js";
|
|
@@ -17,7 +20,7 @@ export async function makeFeature(options) {
|
|
|
17
20
|
const addons = normalizeMakeFeatureAddons([
|
|
18
21
|
...makeFeatureRecipeAddons(options.recipe),
|
|
19
22
|
...(options.with ?? []),
|
|
20
|
-
...(options.events ? ["
|
|
23
|
+
...(options.events ? ["event"] : []),
|
|
21
24
|
]);
|
|
22
25
|
const featureOptions = {
|
|
23
26
|
...options,
|
|
@@ -122,30 +125,36 @@ async function makeResourceSlice(options, mode) {
|
|
|
122
125
|
function resourceGenerationOptions(options, mode) {
|
|
123
126
|
if (mode === "feature") {
|
|
124
127
|
return {
|
|
125
|
-
|
|
126
|
-
|
|
128
|
+
authorization: false,
|
|
129
|
+
tenantScoped: false,
|
|
127
130
|
events: Boolean(options.events),
|
|
128
131
|
softDelete: false,
|
|
129
132
|
};
|
|
130
133
|
}
|
|
131
134
|
if (mode !== "resource") {
|
|
132
|
-
return {
|
|
135
|
+
return {
|
|
136
|
+
authorization: false,
|
|
137
|
+
tenantScoped: false,
|
|
138
|
+
events: false,
|
|
139
|
+
softDelete: false,
|
|
140
|
+
};
|
|
133
141
|
}
|
|
134
142
|
return {
|
|
135
|
-
|
|
136
|
-
|
|
143
|
+
authorization: Boolean(options.authorization),
|
|
144
|
+
tenantScoped: Boolean(options.tenantScoped),
|
|
137
145
|
events: Boolean(options.events),
|
|
138
146
|
softDelete: Boolean(options.softDelete),
|
|
139
147
|
};
|
|
140
148
|
}
|
|
141
149
|
async function makeFeatureSlice(options, addons) {
|
|
142
150
|
let result = await makeFeatureResource(options);
|
|
151
|
+
const includeUploadUi = addons.includes("ui") && addons.includes("upload");
|
|
143
152
|
for (const addon of addons) {
|
|
144
|
-
result = mergeMakeResults(result, await makeFeatureAddon(addon, result.name, options));
|
|
153
|
+
result = mergeMakeResults(result, await makeFeatureAddon(addon, result.name, options, includeUploadUi));
|
|
145
154
|
}
|
|
146
155
|
return result;
|
|
147
156
|
}
|
|
148
|
-
async function makeFeatureAddon(addon, feature, options) {
|
|
157
|
+
async function makeFeatureAddon(addon, feature, options, includeUploadUi) {
|
|
149
158
|
const shared = {
|
|
150
159
|
cwd: options.cwd,
|
|
151
160
|
force: options.force,
|
|
@@ -158,69 +167,53 @@ async function makeFeatureAddon(addon, feature, options) {
|
|
|
158
167
|
if (addon === "factory") {
|
|
159
168
|
return makeFactory({
|
|
160
169
|
...shared,
|
|
161
|
-
name: `${feature}
|
|
170
|
+
name: `${feature}.${singularize(feature)}`,
|
|
162
171
|
skipPersistenceAssert: true,
|
|
163
172
|
});
|
|
164
173
|
}
|
|
165
174
|
if (addon === "task") {
|
|
166
|
-
return makeTask({ ...shared, name: `${feature}
|
|
175
|
+
return makeTask({ ...shared, name: `${feature}.backfill` });
|
|
167
176
|
}
|
|
168
177
|
if (addon === "event") {
|
|
169
|
-
return makeEvent({ ...shared, name: `${feature}
|
|
178
|
+
return makeEvent({ ...shared, name: `${feature}.created` });
|
|
170
179
|
}
|
|
171
180
|
if (addon === "job") {
|
|
172
|
-
return makeJob({ ...shared, name: `${feature}
|
|
181
|
+
return makeJob({ ...shared, name: `${feature}.process` });
|
|
173
182
|
}
|
|
174
183
|
if (addon === "listener") {
|
|
175
184
|
return makeListener({
|
|
176
185
|
...shared,
|
|
177
|
-
name: `${feature}
|
|
178
|
-
event: `${feature}
|
|
186
|
+
name: `${feature}.handle-created`,
|
|
187
|
+
event: `${feature}.created`,
|
|
179
188
|
skipEventAssert: true,
|
|
180
189
|
});
|
|
181
190
|
}
|
|
182
191
|
if (addon === "notification") {
|
|
183
|
-
return makeNotification({ ...shared, name: `${feature}
|
|
192
|
+
return makeNotification({ ...shared, name: `${feature}.created` });
|
|
184
193
|
}
|
|
185
194
|
if (addon === "schedule") {
|
|
186
|
-
return makeSchedule({ ...shared, name: `${feature}
|
|
195
|
+
return makeSchedule({ ...shared, name: `${feature}.daily-summary` });
|
|
187
196
|
}
|
|
188
197
|
if (addon === "seed") {
|
|
189
198
|
return makeSeed({
|
|
190
199
|
...shared,
|
|
191
|
-
name: `${feature}
|
|
200
|
+
name: `${feature}.demo-${feature}`,
|
|
192
201
|
skipPersistenceAssert: true,
|
|
193
202
|
});
|
|
194
203
|
}
|
|
195
204
|
if (addon === "ui") {
|
|
196
205
|
return makeFeatureUi({ ...shared, name: feature });
|
|
197
206
|
}
|
|
198
|
-
return makeUpload({
|
|
207
|
+
return makeUpload({
|
|
208
|
+
...shared,
|
|
209
|
+
name: `${feature}.attachment`,
|
|
210
|
+
ui: includeUploadUi,
|
|
211
|
+
});
|
|
199
212
|
}
|
|
200
213
|
function normalizeMakeFeatureAddons(addons) {
|
|
201
214
|
const normalized = new Set();
|
|
202
215
|
for (const addon of addons) {
|
|
203
|
-
if (addon === "
|
|
204
|
-
normalized.add("factory");
|
|
205
|
-
else if (addon === "events")
|
|
206
|
-
normalized.add("event");
|
|
207
|
-
else if (addon === "listeners") {
|
|
208
|
-
normalized.add("event");
|
|
209
|
-
normalized.add("listener");
|
|
210
|
-
}
|
|
211
|
-
else if (addon === "tasks")
|
|
212
|
-
normalized.add("task");
|
|
213
|
-
else if (addon === "jobs")
|
|
214
|
-
normalized.add("job");
|
|
215
|
-
else if (addon === "notifications")
|
|
216
|
-
normalized.add("notification");
|
|
217
|
-
else if (addon === "schedules")
|
|
218
|
-
normalized.add("schedule");
|
|
219
|
-
else if (addon === "seeds")
|
|
220
|
-
normalized.add("seed");
|
|
221
|
-
else if (addon === "uploads")
|
|
222
|
-
normalized.add("upload");
|
|
223
|
-
else if (addon === "listener") {
|
|
216
|
+
if (addon === "listener") {
|
|
224
217
|
normalized.add("event");
|
|
225
218
|
normalized.add("listener");
|
|
226
219
|
}
|
|
@@ -233,16 +226,16 @@ function makeFeatureRecipeAddons(recipe) {
|
|
|
233
226
|
if (recipe === "full-slice") {
|
|
234
227
|
return [
|
|
235
228
|
"policy",
|
|
236
|
-
"
|
|
237
|
-
"
|
|
238
|
-
"
|
|
239
|
-
"
|
|
240
|
-
"
|
|
241
|
-
"
|
|
242
|
-
"
|
|
243
|
-
"
|
|
229
|
+
"factory",
|
|
230
|
+
"seed",
|
|
231
|
+
"task",
|
|
232
|
+
"event",
|
|
233
|
+
"listener",
|
|
234
|
+
"job",
|
|
235
|
+
"notification",
|
|
236
|
+
"schedule",
|
|
244
237
|
"ui",
|
|
245
|
-
"
|
|
238
|
+
"upload",
|
|
246
239
|
];
|
|
247
240
|
}
|
|
248
241
|
return [];
|
|
@@ -328,7 +321,7 @@ export async function makeUseCase(options) {
|
|
|
328
321
|
skippedFiles.push(indexFile.path);
|
|
329
322
|
return {
|
|
330
323
|
schemaVersion: 1,
|
|
331
|
-
name: `${names.feature.kebab}
|
|
324
|
+
name: `${names.feature.kebab}.${names.action.kebab}`,
|
|
332
325
|
targetDir,
|
|
333
326
|
dryRun: Boolean(options.dryRun),
|
|
334
327
|
files: [...generatedFiles.map((file) => file.path), indexFile.path],
|
|
@@ -367,7 +360,7 @@ export async function makeTest(options) {
|
|
|
367
360
|
}
|
|
368
361
|
return {
|
|
369
362
|
schemaVersion: 1,
|
|
370
|
-
name: `${names.feature.kebab}
|
|
363
|
+
name: `${names.feature.kebab}.${names.action.kebab}`,
|
|
371
364
|
targetDir,
|
|
372
365
|
dryRun: Boolean(options.dryRun),
|
|
373
366
|
files: generatedFiles.map((file) => file.path),
|
|
@@ -529,7 +522,7 @@ export async function makeEvent(options) {
|
|
|
529
522
|
config,
|
|
530
523
|
force: Boolean(options.force),
|
|
531
524
|
dryRun: Boolean(options.dryRun),
|
|
532
|
-
name:
|
|
525
|
+
name: names.stableName,
|
|
533
526
|
files: eventFiles(names, config),
|
|
534
527
|
index: featureArtifactIndexFile("event", names, config),
|
|
535
528
|
dependencies: {
|
|
@@ -570,7 +563,7 @@ export async function makeJob(options) {
|
|
|
570
563
|
config,
|
|
571
564
|
force: Boolean(options.force),
|
|
572
565
|
dryRun: Boolean(options.dryRun),
|
|
573
|
-
name:
|
|
566
|
+
name: names.stableName,
|
|
574
567
|
files: [
|
|
575
568
|
...(await missingCapabilityBuilderFiles(targetDir, "jobs", config)),
|
|
576
569
|
...jobFiles(names, config),
|
|
@@ -603,7 +596,7 @@ export async function makeTask(options) {
|
|
|
603
596
|
config,
|
|
604
597
|
force: Boolean(options.force),
|
|
605
598
|
dryRun: Boolean(options.dryRun),
|
|
606
|
-
name:
|
|
599
|
+
name: names.stableName,
|
|
607
600
|
files: [
|
|
608
601
|
...(await missingCapabilityBuilderFiles(targetDir, "tasks", config)),
|
|
609
602
|
...taskFiles(names, config),
|
|
@@ -641,7 +634,7 @@ export async function makeFactory(options) {
|
|
|
641
634
|
config,
|
|
642
635
|
force: Boolean(options.force),
|
|
643
636
|
dryRun: Boolean(options.dryRun),
|
|
644
|
-
name:
|
|
637
|
+
name: names.stableName,
|
|
645
638
|
files: factoryFiles(names, config),
|
|
646
639
|
index: featureArtifactIndexFile("factory", names, config),
|
|
647
640
|
dependencies: {
|
|
@@ -663,7 +656,7 @@ export async function makeSeed(options) {
|
|
|
663
656
|
config,
|
|
664
657
|
force: Boolean(options.force),
|
|
665
658
|
dryRun: Boolean(options.dryRun),
|
|
666
|
-
name:
|
|
659
|
+
name: names.stableName,
|
|
667
660
|
files: seedFiles(names, config),
|
|
668
661
|
index: featureArtifactIndexFile("seed", names, config),
|
|
669
662
|
dependencies: {
|
|
@@ -677,7 +670,7 @@ export async function makeSeed(options) {
|
|
|
677
670
|
config,
|
|
678
671
|
force: Boolean(options.force),
|
|
679
672
|
dryRun: Boolean(options.dryRun),
|
|
680
|
-
name:
|
|
673
|
+
name: factoryNamesForSeed.stableName,
|
|
681
674
|
files: factoryFiles(factoryNamesForSeed, config),
|
|
682
675
|
index: featureArtifactIndexFile("factory", factoryNamesForSeed, config),
|
|
683
676
|
dependencies: {
|
|
@@ -714,7 +707,7 @@ export async function makeNotification(options) {
|
|
|
714
707
|
// notifications uses ctx.ports.notifications, so the generator wires both
|
|
715
708
|
// ports with dev-default providers. Each port is wired independently and
|
|
716
709
|
// skipped when the app already has it, for example when the resend
|
|
717
|
-
//
|
|
710
|
+
// provider contributed the mailer. Plan the wiring before writing any
|
|
718
711
|
// files so an anchor miss aborts the generator without leaving partial
|
|
719
712
|
// output.
|
|
720
713
|
const notificationWirings = notificationPortWirings("make notification");
|
|
@@ -726,7 +719,7 @@ export async function makeNotification(options) {
|
|
|
726
719
|
config,
|
|
727
720
|
force: Boolean(options.force),
|
|
728
721
|
dryRun: Boolean(options.dryRun),
|
|
729
|
-
name:
|
|
722
|
+
name: names.stableName,
|
|
730
723
|
files: [
|
|
731
724
|
...(await missingCapabilityBuilderFiles(targetDir, "notifications", config)),
|
|
732
725
|
...notificationFiles(names, config),
|
|
@@ -760,7 +753,7 @@ export async function makeListener(options) {
|
|
|
760
753
|
config,
|
|
761
754
|
force: Boolean(options.force),
|
|
762
755
|
dryRun: Boolean(options.dryRun),
|
|
763
|
-
name:
|
|
756
|
+
name: names.stableName,
|
|
764
757
|
files: [
|
|
765
758
|
...(await missingCapabilityBuilderFiles(targetDir, "listeners", config)),
|
|
766
759
|
...listenerFiles(names, config),
|
|
@@ -832,7 +825,7 @@ async function assertListenerEventExists(targetDir, names, config) {
|
|
|
832
825
|
await stat(path.join(targetDir, file));
|
|
833
826
|
}
|
|
834
827
|
catch {
|
|
835
|
-
throw new Error(`beignet make listener expected event ${names.eventName} at ${file}. Run beignet make event ${names.
|
|
828
|
+
throw new Error(`beignet make listener expected event ${names.eventName} at ${file}. Run beignet make event ${names.eventName} first, or pass an event that exists.`);
|
|
836
829
|
}
|
|
837
830
|
}
|
|
838
831
|
async function updateListenerProviderWiring(targetDir, names, config, options) {
|
|
@@ -867,6 +860,9 @@ export async function makeSchedule(options) {
|
|
|
867
860
|
const config = options.config
|
|
868
861
|
? resolveConfig(options.config)
|
|
869
862
|
: await loadBeignetConfig(targetDir);
|
|
863
|
+
if (options.route) {
|
|
864
|
+
assertNextFramework(config, "beignet make schedule --route", "a Next.js App Router cron handler");
|
|
865
|
+
}
|
|
870
866
|
await assertFeatureArtifactApp(targetDir, config, "schedule", {
|
|
871
867
|
requireServer: Boolean(options.route),
|
|
872
868
|
});
|
|
@@ -884,7 +880,7 @@ export async function makeSchedule(options) {
|
|
|
884
880
|
config,
|
|
885
881
|
force: Boolean(options.force),
|
|
886
882
|
dryRun: Boolean(options.dryRun),
|
|
887
|
-
name:
|
|
883
|
+
name: names.stableName,
|
|
888
884
|
files,
|
|
889
885
|
index: featureArtifactIndexFile("schedule", names, config),
|
|
890
886
|
dependencies: {
|
|
@@ -1014,6 +1010,9 @@ async function updateOutboxPortWiring(targetDir, config, options) {
|
|
|
1014
1010
|
if (await updateOutboxDatabaseProvider(targetDir, config, options)) {
|
|
1015
1011
|
changes.updatedFiles.push(path.join(infrastructureDir(config), "db/provider.ts"));
|
|
1016
1012
|
}
|
|
1013
|
+
if (await updateOutboxDrainProvider(targetDir, config, options)) {
|
|
1014
|
+
changes.updatedFiles.push(providersFilePath(config));
|
|
1015
|
+
}
|
|
1017
1016
|
if (await updateOutboxRepositoriesReturnType(targetDir, config, options)) {
|
|
1018
1017
|
changes.updatedFiles.push(drizzleRepositoriesPath(config));
|
|
1019
1018
|
}
|
|
@@ -1091,6 +1090,25 @@ async function updateOutboxInfrastructurePorts(targetDir, config, options) {
|
|
|
1091
1090
|
await writeFile(filePath, next);
|
|
1092
1091
|
return true;
|
|
1093
1092
|
}
|
|
1093
|
+
async function projectSupportsNextAfter(targetDir) {
|
|
1094
|
+
const packageJson = JSON.parse(await readFile(path.join(targetDir, "package.json"), "utf8"));
|
|
1095
|
+
const version = packageJson.dependencies?.next ?? packageJson.devDependencies?.next;
|
|
1096
|
+
if (!version)
|
|
1097
|
+
return false;
|
|
1098
|
+
return version.split("||").every((range) => {
|
|
1099
|
+
const lowerBound = /^\s*(\^|~|>=|>|=|<=|<)?\s*v?(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?([+-][0-9A-Za-z.-]+)?(?=\s|$)/.exec(range);
|
|
1100
|
+
if (!lowerBound)
|
|
1101
|
+
return false;
|
|
1102
|
+
const operator = lowerBound[1];
|
|
1103
|
+
if (operator === "<" || operator === "<=")
|
|
1104
|
+
return false;
|
|
1105
|
+
if (lowerBound[5]?.startsWith("-"))
|
|
1106
|
+
return false;
|
|
1107
|
+
const major = Number(lowerBound[2]);
|
|
1108
|
+
const minor = /^\d+$/.test(lowerBound[3] ?? "") ? Number(lowerBound[3]) : 0;
|
|
1109
|
+
return major > 15 || (major === 15 && minor >= 1);
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1094
1112
|
async function updateOutboxDatabaseProvider(targetDir, config, options) {
|
|
1095
1113
|
const file = path.join(infrastructureDir(config), "db/provider.ts");
|
|
1096
1114
|
const filePath = path.join(targetDir, file);
|
|
@@ -1145,6 +1163,68 @@ async function updateOutboxDatabaseProvider(targetDir, config, options) {
|
|
|
1145
1163
|
await writeFile(filePath, next);
|
|
1146
1164
|
return true;
|
|
1147
1165
|
}
|
|
1166
|
+
async function updateOutboxDrainProvider(targetDir, config, options) {
|
|
1167
|
+
if (!(await projectSupportsNextAfter(targetDir)))
|
|
1168
|
+
return false;
|
|
1169
|
+
const file = providersFilePath(config);
|
|
1170
|
+
const filePath = path.join(targetDir, file);
|
|
1171
|
+
const original = await readOptionalFile(filePath);
|
|
1172
|
+
if (original === undefined) {
|
|
1173
|
+
throw new Error(`Could not find the generated providers file ${file}. Register the outbox drain trigger manually, or restore the generated providers file before running make outbox.`);
|
|
1174
|
+
}
|
|
1175
|
+
let next = addNamedImport(original, "createObservedUnitOfWork", "@beignet/core/ports");
|
|
1176
|
+
next = addNamedImport(next, "createProvider", "@beignet/core/providers");
|
|
1177
|
+
next = addNamedImport(next, "createNextOutboxDrainTrigger", "@beignet/next");
|
|
1178
|
+
next = addNamedImport(next, "after", "next/server");
|
|
1179
|
+
next = addNamedTypeImport(next, "AppContext", aliasModule(config.paths.appContext));
|
|
1180
|
+
next = addNamedTypeImport(next, "AppPorts", aliasModule(config.paths.ports));
|
|
1181
|
+
next = addNamedTypeImport(next, "AppServiceContextInput", aliasModule(path.join(path.dirname(config.paths.server), "context.ts")));
|
|
1182
|
+
const providerName = "outboxDrainProvider";
|
|
1183
|
+
if (!new RegExp(`\\bconst\\s+${providerName}\\b`).test(next)) {
|
|
1184
|
+
const outboxModule = aliasModule(config.paths.outbox);
|
|
1185
|
+
const providerDefinition = `const ${providerName} = createProvider<
|
|
1186
|
+
\tPick<AppPorts, "uow">,
|
|
1187
|
+
\tAppContext,
|
|
1188
|
+
\tAppServiceContextInput
|
|
1189
|
+
>()({
|
|
1190
|
+
\tname: "outbox-drain-trigger",
|
|
1191
|
+
\tsetup({ ports, createServiceContext }): { ports: Pick<AppPorts, "uow"> } {
|
|
1192
|
+
\t\tconst triggerOutboxDrain: () => void = createNextOutboxDrainTrigger({
|
|
1193
|
+
\t\t\tdefer: after,
|
|
1194
|
+
\t\t\tcreateContext: () => createServiceContext(undefined),
|
|
1195
|
+
\t\t\tregistry: async () => (await import("${outboxModule}")).outboxRegistry,
|
|
1196
|
+
\t\t\tbatchSize: 100,
|
|
1197
|
+
\t\t});
|
|
1198
|
+
|
|
1199
|
+
\t\treturn {
|
|
1200
|
+
\t\t\tports: {
|
|
1201
|
+
\t\t\t\tuow: createObservedUnitOfWork({
|
|
1202
|
+
\t\t\t\t\tunitOfWork: ports.uow,
|
|
1203
|
+
\t\t\t\t\tafterCommit: triggerOutboxDrain,
|
|
1204
|
+
\t\t\t\t}),
|
|
1205
|
+
\t\t\t},
|
|
1206
|
+
\t\t};
|
|
1207
|
+
\t},
|
|
1208
|
+
});
|
|
1209
|
+
`;
|
|
1210
|
+
const withProvider = next.replace(/\nexport const providers = \[/, `\n${providerDefinition}\nexport const providers = [`);
|
|
1211
|
+
if (withProvider === next) {
|
|
1212
|
+
throw new Error(`Could not find the exported providers array in ${file}. Register outboxDrainProvider manually, or restore the generated providers file before running make outbox.`);
|
|
1213
|
+
}
|
|
1214
|
+
next = withProvider;
|
|
1215
|
+
}
|
|
1216
|
+
const appended = appendToNamedArray(next, "providers", providerName);
|
|
1217
|
+
if (appended.kind === "missing") {
|
|
1218
|
+
throw new Error(`Could not find the exported providers array in ${file}. Register outboxDrainProvider manually, or restore the generated providers file before running make outbox.`);
|
|
1219
|
+
}
|
|
1220
|
+
if (appended.kind === "updated")
|
|
1221
|
+
next = appended.source;
|
|
1222
|
+
if (next === original)
|
|
1223
|
+
return false;
|
|
1224
|
+
if (!options.dryRun)
|
|
1225
|
+
await writeFile(filePath, next);
|
|
1226
|
+
return true;
|
|
1227
|
+
}
|
|
1148
1228
|
async function updateOutboxRepositoriesReturnType(targetDir, config, options) {
|
|
1149
1229
|
const file = drizzleRepositoriesPath(config);
|
|
1150
1230
|
const filePath = path.join(targetDir, file);
|
|
@@ -1251,7 +1331,11 @@ export async function makeUpload(options) {
|
|
|
1251
1331
|
const config = options.config
|
|
1252
1332
|
? resolveConfig(options.config)
|
|
1253
1333
|
: await loadBeignetConfig(targetDir);
|
|
1334
|
+
assertNextFramework(config, "beignet make upload", "a Next.js App Router upload handler");
|
|
1254
1335
|
await assertFeatureArtifactApp(targetDir, config, "upload");
|
|
1336
|
+
if (options.ui) {
|
|
1337
|
+
await assertUploadUiApp(targetDir, config);
|
|
1338
|
+
}
|
|
1255
1339
|
await wirePortProviders(targetDir, config, [storagePortWiring("make upload")], {
|
|
1256
1340
|
dryRun: true,
|
|
1257
1341
|
});
|
|
@@ -1260,10 +1344,10 @@ export async function makeUpload(options) {
|
|
|
1260
1344
|
config,
|
|
1261
1345
|
force: Boolean(options.force),
|
|
1262
1346
|
dryRun: Boolean(options.dryRun),
|
|
1263
|
-
name:
|
|
1347
|
+
name: names.stableName,
|
|
1264
1348
|
files: [
|
|
1265
1349
|
...(await missingCapabilityBuilderFiles(targetDir, "uploads", config)),
|
|
1266
|
-
...uploadFiles(names, config),
|
|
1350
|
+
...uploadFiles(names, config, { ui: Boolean(options.ui) }),
|
|
1267
1351
|
],
|
|
1268
1352
|
index: featureArtifactIndexFile("upload", names, config),
|
|
1269
1353
|
dependencies: {
|
|
@@ -1283,6 +1367,31 @@ export async function makeUpload(options) {
|
|
|
1283
1367
|
if (!result.files.includes(uploadRoutePath(config))) {
|
|
1284
1368
|
result.files.push(uploadRoutePath(config));
|
|
1285
1369
|
}
|
|
1370
|
+
if (options.ui) {
|
|
1371
|
+
const componentIndex = uploadUiComponentIndexFile(names, config);
|
|
1372
|
+
const componentIndexResult = await updateFeatureUiIndex(targetDir, componentIndex, { dryRun: Boolean(options.dryRun) });
|
|
1373
|
+
if (componentIndexResult === "created") {
|
|
1374
|
+
result.createdFiles.push(componentIndex.path);
|
|
1375
|
+
}
|
|
1376
|
+
if (componentIndexResult === "updated") {
|
|
1377
|
+
result.updatedFiles.push(componentIndex.path);
|
|
1378
|
+
}
|
|
1379
|
+
if (componentIndexResult === "skipped") {
|
|
1380
|
+
result.skippedFiles.push(componentIndex.path);
|
|
1381
|
+
}
|
|
1382
|
+
if (!result.files.includes(componentIndex.path)) {
|
|
1383
|
+
result.files.push(componentIndex.path);
|
|
1384
|
+
}
|
|
1385
|
+
if (await updatePackageDependencies(targetDir, { "@beignet/react-uploads": beignetDependencyVersion }, { dryRun: Boolean(options.dryRun) })) {
|
|
1386
|
+
result.updatedFiles.push("package.json");
|
|
1387
|
+
if (!result.files.includes("package.json")) {
|
|
1388
|
+
result.files.push("package.json");
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
result.createdFiles = uniqueStrings(result.createdFiles);
|
|
1393
|
+
result.updatedFiles = uniqueStrings(result.updatedFiles);
|
|
1394
|
+
result.skippedFiles = uniqueStrings(result.skippedFiles);
|
|
1286
1395
|
return result;
|
|
1287
1396
|
}
|
|
1288
1397
|
export async function makeOutbox(options = {}) {
|
|
@@ -1290,6 +1399,7 @@ export async function makeOutbox(options = {}) {
|
|
|
1290
1399
|
const config = options.config
|
|
1291
1400
|
? resolveConfig(options.config)
|
|
1292
1401
|
: await loadBeignetConfig(targetDir);
|
|
1402
|
+
assertNextFramework(config, "beignet make outbox", "a Next.js App Router recovery drain handler");
|
|
1293
1403
|
await assertServerRuntimeApp(targetDir, config, "outbox");
|
|
1294
1404
|
await updateOutboxPortWiring(targetDir, config, { dryRun: true });
|
|
1295
1405
|
const cronSecretPlan = await planCronSecretWiring(targetDir);
|
|
@@ -1333,6 +1443,9 @@ export async function makeOutbox(options = {}) {
|
|
|
1333
1443
|
config.paths.ports,
|
|
1334
1444
|
config.paths.portWiring,
|
|
1335
1445
|
path.join(infrastructureDir(config), "db/provider.ts"),
|
|
1446
|
+
...((await projectSupportsNextAfter(targetDir))
|
|
1447
|
+
? [providersFilePath(config)]
|
|
1448
|
+
: []),
|
|
1336
1449
|
drizzleRepositoriesPath(config),
|
|
1337
1450
|
outboxDrizzleSchemaFilePath(config),
|
|
1338
1451
|
drizzleSchemaIndexPath(config),
|
|
@@ -1349,7 +1462,10 @@ async function makeFeatureUi(options) {
|
|
|
1349
1462
|
? resolveConfig(options.config)
|
|
1350
1463
|
: await loadBeignetConfig(targetDir);
|
|
1351
1464
|
await assertFeatureUiApp(targetDir, config);
|
|
1352
|
-
const componentFiles =
|
|
1465
|
+
const componentFiles = [
|
|
1466
|
+
...(await missingFeatureUiFormsFiles(targetDir, config)),
|
|
1467
|
+
...featureUiComponentFiles(names, config),
|
|
1468
|
+
];
|
|
1353
1469
|
const indexFile = featureUiIndexFile(names, config);
|
|
1354
1470
|
const plannedFiles = await planGeneratedFiles(targetDir, componentFiles, {
|
|
1355
1471
|
force: Boolean(options.force),
|
|
@@ -1378,8 +1494,13 @@ async function makeFeatureUi(options) {
|
|
|
1378
1494
|
if (indexResult === "skipped")
|
|
1379
1495
|
skippedFiles.push(indexFile.path);
|
|
1380
1496
|
if (await updatePackageDependencies(targetDir, {
|
|
1497
|
+
"@beignet/react-hook-form": beignetDependencyVersion,
|
|
1381
1498
|
"@beignet/react-query": beignetDependencyVersion,
|
|
1499
|
+
"@hookform/resolvers": (packageJson) => packageJson.dependencies?.["@hookform/resolvers"] ??
|
|
1500
|
+
externalVersions.hookformResolvers,
|
|
1382
1501
|
"@tanstack/react-query": tanstackReactQueryDependencyVersion,
|
|
1502
|
+
"react-hook-form": (packageJson) => packageJson.dependencies?.["react-hook-form"] ??
|
|
1503
|
+
externalVersions.reactHookForm,
|
|
1383
1504
|
}, { dryRun: Boolean(options.dryRun) })) {
|
|
1384
1505
|
updatedFiles.push("package.json");
|
|
1385
1506
|
}
|
|
@@ -1467,7 +1588,7 @@ async function updateResourceWiring(targetDir, names, config, persistenceOrOptio
|
|
|
1467
1588
|
(await updateInfrastructurePorts(targetDir, names, config, options))) {
|
|
1468
1589
|
updated.add(config.paths.portWiring);
|
|
1469
1590
|
}
|
|
1470
|
-
if (generationOptions.
|
|
1591
|
+
if (generationOptions.authorization &&
|
|
1471
1592
|
(await updateInfrastructureGatePolicies(targetDir, names, config, options))) {
|
|
1472
1593
|
updated.add(config.paths.portWiring);
|
|
1473
1594
|
}
|
|
@@ -1505,11 +1626,10 @@ function typeHasTopLevelProperty(source, typeName, propertyName) {
|
|
|
1505
1626
|
function useCaseNames(input) {
|
|
1506
1627
|
const parts = input
|
|
1507
1628
|
.trim()
|
|
1508
|
-
.split("
|
|
1509
|
-
.map((part) => part.trim())
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
throw new Error("Use case name must use feature/action format, for example projects/archive-project.");
|
|
1629
|
+
.split(".")
|
|
1630
|
+
.map((part) => part.trim());
|
|
1631
|
+
if (parts.length !== 2 || parts.some((part) => part.length === 0)) {
|
|
1632
|
+
throw new Error("Use case name must use feature.action format, for example projects.archive-project.");
|
|
1513
1633
|
}
|
|
1514
1634
|
const feature = identifierNames(parts[0], "Use case feature");
|
|
1515
1635
|
const action = identifierNames(parts[1], "Use case action");
|
|
@@ -1541,7 +1661,7 @@ function policyNames(input) {
|
|
|
1541
1661
|
return resolvedNames;
|
|
1542
1662
|
}
|
|
1543
1663
|
function eventNames(input) {
|
|
1544
|
-
const names = featureArtifactNames(
|
|
1664
|
+
const names = featureArtifactNames(input, "Event");
|
|
1545
1665
|
const eventExportName = `${names.featureSingularPascal}${names.artifact.pascal}`;
|
|
1546
1666
|
assertGeneratedIdentifiers(input, "Event name", [eventExportName]);
|
|
1547
1667
|
return {
|
|
@@ -1587,7 +1707,7 @@ function factoryNames(input) {
|
|
|
1587
1707
|
};
|
|
1588
1708
|
}
|
|
1589
1709
|
function defaultFactoryNamesForSeed(names) {
|
|
1590
|
-
return factoryNames(`${names.feature.kebab}
|
|
1710
|
+
return factoryNames(`${names.feature.kebab}.${names.featureSingularKebab}`);
|
|
1591
1711
|
}
|
|
1592
1712
|
function notificationNames(input) {
|
|
1593
1713
|
const names = featureArtifactNames(input, "Notification");
|
|
@@ -1631,24 +1751,26 @@ function scheduleNames(input, options) {
|
|
|
1631
1751
|
function uploadNames(input) {
|
|
1632
1752
|
const names = featureArtifactNames(input, "Upload");
|
|
1633
1753
|
const uploadExportName = `${names.artifact.pascal}Upload`;
|
|
1634
|
-
|
|
1754
|
+
const componentExportName = `${names.artifact.pascal}Uploader`;
|
|
1755
|
+
assertGeneratedIdentifiers(input, "Upload name", [
|
|
1756
|
+
uploadExportName,
|
|
1757
|
+
componentExportName,
|
|
1758
|
+
]);
|
|
1635
1759
|
return {
|
|
1636
1760
|
...names,
|
|
1637
1761
|
uploadName: names.stableName,
|
|
1638
1762
|
uploadExportName,
|
|
1639
1763
|
metadataSchemaName: `${uploadExportName}MetadataSchema`,
|
|
1640
1764
|
metadataTypeName: `${uploadExportName}Metadata`,
|
|
1765
|
+
fileConstraintsExportName: `${names.artifact.camel}UploadFile`,
|
|
1766
|
+
manifestExportName: `${names.artifact.camel}UploadManifest`,
|
|
1767
|
+
manifestFileName: `${names.artifact.kebab}-upload-manifest`,
|
|
1768
|
+
reactUploadsExportName: `${names.artifact.camel}ReactUploads`,
|
|
1769
|
+
clientFileName: `${names.artifact.kebab}-upload`,
|
|
1770
|
+
componentExportName,
|
|
1771
|
+
componentFileName: `${names.artifact.kebab}-uploader`,
|
|
1641
1772
|
};
|
|
1642
1773
|
}
|
|
1643
|
-
function normalizeEventReference(input) {
|
|
1644
|
-
const trimmed = input.trim();
|
|
1645
|
-
if (trimmed.includes("/"))
|
|
1646
|
-
return trimmed;
|
|
1647
|
-
const dotIndex = trimmed.indexOf(".");
|
|
1648
|
-
if (dotIndex === -1)
|
|
1649
|
-
return trimmed;
|
|
1650
|
-
return `${trimmed.slice(0, dotIndex)}/${trimmed.slice(dotIndex + 1)}`;
|
|
1651
|
-
}
|
|
1652
1774
|
function inferUseCaseKind(action) {
|
|
1653
1775
|
if (/^(count|find|get|list|search)-/.test(`${action}-`)) {
|
|
1654
1776
|
return "query";
|
|
@@ -1656,8 +1778,8 @@ function inferUseCaseKind(action) {
|
|
|
1656
1778
|
return "command";
|
|
1657
1779
|
}
|
|
1658
1780
|
function resourceFiles(names, config, persistence = "memory", mode = "feature", options = {
|
|
1659
|
-
|
|
1660
|
-
|
|
1781
|
+
authorization: false,
|
|
1782
|
+
tenantScoped: false,
|
|
1661
1783
|
events: false,
|
|
1662
1784
|
softDelete: false,
|
|
1663
1785
|
}, database = "sqlite") {
|
|
@@ -1710,7 +1832,7 @@ function resourceFiles(names, config, persistence = "memory", mode = "feature",
|
|
|
1710
1832
|
},
|
|
1711
1833
|
]
|
|
1712
1834
|
: []),
|
|
1713
|
-
...(mode === "resource" && options.
|
|
1835
|
+
...(mode === "resource" && options.authorization
|
|
1714
1836
|
? [
|
|
1715
1837
|
{
|
|
1716
1838
|
path: path.join(featureDir, "tests/policy.test.ts"),
|
|
@@ -1765,6 +1887,59 @@ function featureUiComponentFiles(names, config) {
|
|
|
1765
1887
|
},
|
|
1766
1888
|
];
|
|
1767
1889
|
}
|
|
1890
|
+
async function missingFeatureUiFormsFiles(targetDir, config) {
|
|
1891
|
+
const formsPath = clientFormsPath(config);
|
|
1892
|
+
const existing = await readOptionalFile(path.join(targetDir, formsPath));
|
|
1893
|
+
if (existing === undefined) {
|
|
1894
|
+
return [
|
|
1895
|
+
{
|
|
1896
|
+
path: formsPath,
|
|
1897
|
+
content: `import { createReactHookForm } from "@beignet/react-hook-form";
|
|
1898
|
+
|
|
1899
|
+
export const rhf = createReactHookForm();
|
|
1900
|
+
`,
|
|
1901
|
+
},
|
|
1902
|
+
];
|
|
1903
|
+
}
|
|
1904
|
+
if (!sourceExportsValue(existing, "rhf")) {
|
|
1905
|
+
throw new Error(`beignet make feature --with ui expects ${formsPath} to export rhf. Add \`export const rhf = createReactHookForm();\` from @beignet/react-hook-form, or move the existing helper before generating UI.`);
|
|
1906
|
+
}
|
|
1907
|
+
return [];
|
|
1908
|
+
}
|
|
1909
|
+
async function assertUploadUiApp(targetDir, config) {
|
|
1910
|
+
const clientPath = clientIndexPath(config);
|
|
1911
|
+
const clientSource = await readOptionalFile(path.join(targetDir, clientPath));
|
|
1912
|
+
if (!clientSource || !sourceExportsValue(clientSource, "rq")) {
|
|
1913
|
+
throw new Error(`beignet make upload --ui expects a full-stack Beignet app with ${clientPath} exporting rq. API-only apps stay backend-only; omit --ui or add the full-stack client setup first.`);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
function sourceExportsValue(source, name) {
|
|
1917
|
+
const sourceFile = ts.createSourceFile("client-forms.ts", source, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
|
|
1918
|
+
for (const statement of sourceFile.statements) {
|
|
1919
|
+
const exported = ts.canHaveModifiers(statement) &&
|
|
1920
|
+
ts
|
|
1921
|
+
.getModifiers(statement)
|
|
1922
|
+
?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
1923
|
+
if (exported && ts.isVariableStatement(statement)) {
|
|
1924
|
+
if (statement.declarationList.declarations.some((declaration) => ts.isIdentifier(declaration.name) && declaration.name.text === name)) {
|
|
1925
|
+
return true;
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
if (exported &&
|
|
1929
|
+
ts.isFunctionDeclaration(statement) &&
|
|
1930
|
+
statement.name?.text === name) {
|
|
1931
|
+
return true;
|
|
1932
|
+
}
|
|
1933
|
+
if (ts.isExportDeclaration(statement) &&
|
|
1934
|
+
!statement.isTypeOnly &&
|
|
1935
|
+
statement.exportClause &&
|
|
1936
|
+
ts.isNamedExports(statement.exportClause) &&
|
|
1937
|
+
statement.exportClause.elements.some((element) => !element.isTypeOnly && element.name.text === name)) {
|
|
1938
|
+
return true;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
return false;
|
|
1942
|
+
}
|
|
1768
1943
|
function featureUiIndexFile(names, config) {
|
|
1769
1944
|
return {
|
|
1770
1945
|
path: featureUiIndexFilePath(names, config),
|
|
@@ -1893,12 +2068,54 @@ function scheduleFiles(names, config, options) {
|
|
|
1893
2068
|
}
|
|
1894
2069
|
return files;
|
|
1895
2070
|
}
|
|
1896
|
-
function uploadFiles(names, config) {
|
|
2071
|
+
function uploadFiles(names, config, options) {
|
|
1897
2072
|
return [
|
|
2073
|
+
{
|
|
2074
|
+
path: uploadManifestFilePath(names, config),
|
|
2075
|
+
content: uploadManifestFile(names),
|
|
2076
|
+
},
|
|
1898
2077
|
{
|
|
1899
2078
|
path: uploadFilePath(names, config),
|
|
1900
2079
|
content: uploadFile(names, config),
|
|
1901
2080
|
},
|
|
2081
|
+
...(options.ui ? uploadUiFiles(names, config) : []),
|
|
2082
|
+
];
|
|
2083
|
+
}
|
|
2084
|
+
function uploadFeatureDir(names, config) {
|
|
2085
|
+
return path.join(config.paths.features, names.feature.kebab);
|
|
2086
|
+
}
|
|
2087
|
+
function uploadManifestFilePath(names, config) {
|
|
2088
|
+
return path.join(uploadFeatureDir(names, config), `${names.manifestFileName}.ts`);
|
|
2089
|
+
}
|
|
2090
|
+
function uploadUiClientFilePath(names, config) {
|
|
2091
|
+
return path.join(uploadFeatureDir(names, config), "client", `${names.clientFileName}.ts`);
|
|
2092
|
+
}
|
|
2093
|
+
function uploadUiComponentFilePath(names, config) {
|
|
2094
|
+
return path.join(uploadFeatureDir(names, config), "components", `${names.componentFileName}.tsx`);
|
|
2095
|
+
}
|
|
2096
|
+
function uploadUiComponentTestFilePath(names, config) {
|
|
2097
|
+
return path.join(uploadFeatureDir(names, config), "tests", `${names.componentFileName}.test.tsx`);
|
|
2098
|
+
}
|
|
2099
|
+
function uploadUiComponentIndexFile(names, config) {
|
|
2100
|
+
return {
|
|
2101
|
+
path: path.join(uploadFeatureDir(names, config), "components", "index.ts"),
|
|
2102
|
+
content: `export { ${names.componentExportName} } from "./${names.componentFileName}";\n`,
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
function uploadUiFiles(names, config) {
|
|
2106
|
+
return [
|
|
2107
|
+
{
|
|
2108
|
+
path: uploadUiClientFilePath(names, config),
|
|
2109
|
+
content: uploadUiClientFile(names, config),
|
|
2110
|
+
},
|
|
2111
|
+
{
|
|
2112
|
+
path: uploadUiComponentFilePath(names, config),
|
|
2113
|
+
content: uploadUiComponentFile(names, config),
|
|
2114
|
+
},
|
|
2115
|
+
{
|
|
2116
|
+
path: uploadUiComponentTestFilePath(names, config),
|
|
2117
|
+
content: uploadUiComponentTestFile(names, config),
|
|
2118
|
+
},
|
|
1902
2119
|
];
|
|
1903
2120
|
}
|
|
1904
2121
|
function outboxFiles(config) {
|
|
@@ -2204,7 +2421,7 @@ function schemasFile(names, mode, options) {
|
|
|
2204
2421
|
|
|
2205
2422
|
export const ${names.singularPascal}Schema = z.object({
|
|
2206
2423
|
id: z.string().uuid(),
|
|
2207
|
-
${options.
|
|
2424
|
+
${options.tenantScoped ? `\ttenantId: z.string().min(1),\n` : ""} name: z.string().min(1),
|
|
2208
2425
|
version: z.number().int().min(1),
|
|
2209
2426
|
createdAt: z.string().datetime(),
|
|
2210
2427
|
updatedAt: z.string().datetime(),
|
|
@@ -2304,7 +2521,7 @@ export const List${names.pluralPascal}OutputSchema = z.object({
|
|
|
2304
2521
|
});
|
|
2305
2522
|
|
|
2306
2523
|
export const Create${names.singularPascal}InputSchema = z.object({
|
|
2307
|
-
name: z.string().min(1).max(120),
|
|
2524
|
+
name: z.string().trim().min(1).max(120),
|
|
2308
2525
|
});
|
|
2309
2526
|
${mode === "resource"
|
|
2310
2527
|
? `
|
|
@@ -2313,7 +2530,7 @@ export const ${names.singularPascal}IdInputSchema = z.object({
|
|
|
2313
2530
|
});
|
|
2314
2531
|
|
|
2315
2532
|
export const Update${names.singularPascal}BodySchema = z.object({
|
|
2316
|
-
name: z.string().min(1).max(120),
|
|
2533
|
+
name: z.string().trim().min(1).max(120),
|
|
2317
2534
|
version: z.number().int().min(1),
|
|
2318
2535
|
});
|
|
2319
2536
|
|
|
@@ -2347,7 +2564,7 @@ function listUseCaseFile(names, config, options) {
|
|
|
2347
2564
|
const filePath = path.join(resourceUseCaseDir(names, config), `list-${names.pluralKebab}.ts`);
|
|
2348
2565
|
return `import "@beignet/core/server-only";
|
|
2349
2566
|
import { normalizeCursorPage } from "@beignet/core/pagination";
|
|
2350
|
-
${options.
|
|
2567
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2351
2568
|
import {
|
|
2352
2569
|
decode${names.singularPascal}Cursor,
|
|
2353
2570
|
List${names.pluralPascal}InputSchema,
|
|
@@ -2359,7 +2576,7 @@ export const list${names.pluralPascal}UseCase = useCase
|
|
|
2359
2576
|
.input(List${names.pluralPascal}InputSchema)
|
|
2360
2577
|
.output(List${names.pluralPascal}OutputSchema)
|
|
2361
2578
|
.run(async ({ ctx, input }) => {
|
|
2362
|
-
${options.
|
|
2579
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const page = normalizeCursorPage(input, {
|
|
2363
2580
|
defaultLimit: 20,
|
|
2364
2581
|
maxLimit: 100,
|
|
2365
2582
|
});
|
|
@@ -2370,14 +2587,14 @@ ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const
|
|
|
2370
2587
|
name: input.name,
|
|
2371
2588
|
sortBy: input.sortBy,
|
|
2372
2589
|
sortDirection: input.sortDirection,
|
|
2373
|
-
}${options.
|
|
2590
|
+
}${options.tenantScoped ? ", scope" : ""});
|
|
2374
2591
|
});
|
|
2375
2592
|
`;
|
|
2376
2593
|
}
|
|
2377
2594
|
function createUseCaseFile(names, config, options) {
|
|
2378
2595
|
const filePath = path.join(resourceUseCaseDir(names, config), `create-${names.singularKebab}.ts`);
|
|
2379
2596
|
return `import "@beignet/core/server-only";
|
|
2380
|
-
${options.
|
|
2597
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2381
2598
|
import {
|
|
2382
2599
|
Create${names.singularPascal}InputSchema,
|
|
2383
2600
|
${names.singularPascal}Schema,
|
|
@@ -2389,7 +2606,7 @@ export const create${names.singularPascal}UseCase = useCase
|
|
|
2389
2606
|
.input(Create${names.singularPascal}InputSchema)
|
|
2390
2607
|
.output(${names.singularPascal}Schema)
|
|
2391
2608
|
.run(async ({ ctx, input }) => {
|
|
2392
|
-
${options.
|
|
2609
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""}${options.authorization ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.create");\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.create(input${options.tenantScoped ? ", scope" : ""});
|
|
2393
2610
|
${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPascal}Created, {\n\t\t\tid: ${names.singularCamel}.id,\n\t\t});\n` : ""}
|
|
2394
2611
|
return ${names.singularCamel};
|
|
2395
2612
|
});
|
|
@@ -2398,7 +2615,7 @@ ${options.events ? `\n\t\tawait ctx.ports.eventBus.publish(${names.singularPasca
|
|
|
2398
2615
|
function getUseCaseFile(names, config, options) {
|
|
2399
2616
|
const filePath = path.join(resourceUseCaseDir(names, config), `get-${names.singularKebab}.ts`);
|
|
2400
2617
|
return `import "@beignet/core/server-only";
|
|
2401
|
-
${options.
|
|
2618
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
2402
2619
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2403
2620
|
import {
|
|
2404
2621
|
${names.singularPascal}IdInputSchema,
|
|
@@ -2410,7 +2627,7 @@ export const get${names.singularPascal}UseCase = useCase
|
|
|
2410
2627
|
.input(${names.singularPascal}IdInputSchema)
|
|
2411
2628
|
.output(${names.singularPascal}Schema)
|
|
2412
2629
|
.run(async ({ ctx, input }) => {
|
|
2413
|
-
${options.
|
|
2630
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
|
|
2414
2631
|
if (!${names.singularCamel}) {
|
|
2415
2632
|
throw appError("${names.singularPascal}NotFound", {
|
|
2416
2633
|
details: { id: input.id },
|
|
@@ -2424,7 +2641,7 @@ ${options.tenant ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const
|
|
|
2424
2641
|
function updateUseCaseFile(names, config, options) {
|
|
2425
2642
|
const filePath = path.join(resourceUseCaseDir(names, config), `update-${names.singularKebab}.ts`);
|
|
2426
2643
|
return `import "@beignet/core/server-only";
|
|
2427
|
-
${options.
|
|
2644
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
2428
2645
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2429
2646
|
${options.events ? `import { ${names.singularPascal}Updated } from "../domain/events";\n` : ""}import {
|
|
2430
2647
|
Update${names.singularPascal}InputSchema,
|
|
@@ -2436,13 +2653,13 @@ export const update${names.singularPascal}UseCase = useCase
|
|
|
2436
2653
|
.input(Update${names.singularPascal}InputSchema)
|
|
2437
2654
|
.output(${names.singularPascal}Schema)
|
|
2438
2655
|
.run(async ({ ctx, input }) => {
|
|
2439
|
-
${options.
|
|
2656
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""} const existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});
|
|
2440
2657
|
if (!existing) {
|
|
2441
2658
|
throw appError("${names.singularPascal}NotFound", {
|
|
2442
2659
|
details: { id: input.id },
|
|
2443
2660
|
});
|
|
2444
2661
|
}
|
|
2445
|
-
${options.
|
|
2662
|
+
${options.authorization ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", existing);\n\n` : ""} if (existing.version !== input.version) {
|
|
2446
2663
|
throw appError("${names.singularPascal}Conflict", {
|
|
2447
2664
|
details: { id: input.id, expectedVersion: existing.version },
|
|
2448
2665
|
});
|
|
@@ -2450,7 +2667,7 @@ ${options.auth ? `\t\tawait ctx.gate.authorize("${names.pluralCamel}.update", ex
|
|
|
2450
2667
|
|
|
2451
2668
|
const ${names.singularCamel} = await ctx.ports.${names.pluralCamel}.update({
|
|
2452
2669
|
...input,
|
|
2453
|
-
}${options.
|
|
2670
|
+
}${options.tenantScoped ? ", scope" : ""});
|
|
2454
2671
|
if (!${names.singularCamel}) {
|
|
2455
2672
|
throw appError("${names.singularPascal}Conflict", {
|
|
2456
2673
|
details: { id: input.id, expectedVersion: existing.version },
|
|
@@ -2465,7 +2682,7 @@ function deleteUseCaseFile(names, config, options) {
|
|
|
2465
2682
|
const filePath = path.join(resourceUseCaseDir(names, config), `delete-${names.singularKebab}.ts`);
|
|
2466
2683
|
return `import "@beignet/core/server-only";
|
|
2467
2684
|
import { z } from "zod";
|
|
2468
|
-
${options.
|
|
2685
|
+
${options.tenantScoped ? `import { requireTenantScope } from "@beignet/core/tenancy";\n` : ""}import { appError } from "${relativeModule(filePath, resourceSharedErrorsPath(config))}";
|
|
2469
2686
|
import { useCase } from "${relativeModule(filePath, config.paths.useCaseBuilder)}";
|
|
2470
2687
|
${options.events ? `import { ${names.singularPascal}Deleted } from "../domain/events";\n` : ""}import { ${names.singularPascal}IdInputSchema } from "../schemas";
|
|
2471
2688
|
|
|
@@ -2474,7 +2691,7 @@ export const delete${names.singularPascal}UseCase = useCase
|
|
|
2474
2691
|
.input(${names.singularPascal}IdInputSchema)
|
|
2475
2692
|
.output(z.void())
|
|
2476
2693
|
.run(async ({ ctx, input }) => {
|
|
2477
|
-
${options.
|
|
2694
|
+
${options.tenantScoped ? `\t\tconst scope = requireTenantScope(ctx);\n\n` : ""}${options.authorization ? `\t\tconst existing = await ctx.ports.${names.pluralCamel}.findById(input.id${options.tenantScoped ? ", scope" : ""});\n\t\tif (!existing) {\n\t\t\tthrow appError("${names.singularPascal}NotFound", {\n\t\t\t\tdetails: { id: input.id },\n\t\t\t});\n\t\t}\n\t\tawait ctx.gate.authorize("${names.pluralCamel}.delete", existing);\n\n` : ""} const deleted = await ctx.ports.${names.pluralCamel}.delete(input.id${options.tenantScoped ? ", scope" : ""});
|
|
2478
2695
|
if (!deleted) {
|
|
2479
2696
|
throw appError("${names.singularPascal}NotFound", {
|
|
2480
2697
|
details: { id: input.id },
|
|
@@ -2504,7 +2721,7 @@ function repositoryPortFile(names, config, mode, options) {
|
|
|
2504
2721
|
PageResult,
|
|
2505
2722
|
SortDirection,
|
|
2506
2723
|
} from "@beignet/core/pagination";
|
|
2507
|
-
${options.
|
|
2724
|
+
${options.tenantScoped ? `import type { TenantScope } from "@beignet/core/tenancy";\n` : ""}import type {
|
|
2508
2725
|
Create${names.singularPascal}Input,
|
|
2509
2726
|
${names.singularPascal}Cursor,
|
|
2510
2727
|
${names.singularPascal}SortBy,
|
|
@@ -2521,9 +2738,9 @@ export type List${names.pluralPascal}Query = {
|
|
|
2521
2738
|
};
|
|
2522
2739
|
|
|
2523
2740
|
export interface ${names.singularPascal}Repository {
|
|
2524
|
-
list(query: List${names.pluralPascal}Query${options.
|
|
2525
|
-
create(input: Create${names.singularPascal}Input${options.
|
|
2526
|
-
${mode === "resource" ? ` findById(id: string${options.
|
|
2741
|
+
list(query: List${names.pluralPascal}Query${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<List${names.pluralPascal}Result>;
|
|
2742
|
+
create(input: Create${names.singularPascal}Input${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal}>;
|
|
2743
|
+
${mode === "resource" ? ` findById(id: string${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal} | null>;\n update(input: Update${names.singularPascal}Input${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<${names.singularPascal} | null>;\n delete(id: string${options.tenantScoped ? ", scope: TenantScope" : ""}): Promise<boolean>;\n` : ""}}
|
|
2527
2744
|
`;
|
|
2528
2745
|
}
|
|
2529
2746
|
function inMemoryRepositoryFile(names, config, mode, options) {
|
|
@@ -2551,13 +2768,13 @@ function inMemoryRepositoryFile(names, config, mode, options) {
|
|
|
2551
2768
|
? " || existing.deletedAt !== null"
|
|
2552
2769
|
: "";
|
|
2553
2770
|
const deleteImplementation = options.softDelete
|
|
2554
|
-
? `${options.
|
|
2771
|
+
? `${options.tenantScoped ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.tenantId !== tenantId || existing.deletedAt !== null) return false;\n` : `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (!existing || existing.deletedAt !== null) return false;\n`} const now = new Date().toISOString();
|
|
2555
2772
|
${names.pluralCamel}.set(id, { ...existing, deletedAt: now, updatedAt: now });
|
|
2556
2773
|
return true;`
|
|
2557
|
-
: `${options.
|
|
2774
|
+
: `${options.tenantScoped ? `\t\t\tconst existing = ${names.pluralCamel}.get(id);\n\t\t\tif (existing?.tenantId !== tenantId) return false;\n` : ""} return ${names.pluralCamel}.delete(id);`;
|
|
2558
2775
|
return `import "@beignet/core/server-only";
|
|
2559
2776
|
import { cursorPageResult } from "@beignet/core/pagination";
|
|
2560
|
-
${options.
|
|
2777
|
+
${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
2561
2778
|
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
2562
2779
|
import type {
|
|
2563
2780
|
Create${names.singularPascal}Input,
|
|
@@ -2567,7 +2784,7 @@ ${mode === "resource" ? ` Update${names.singularPascal}Input,\n` : ""} ${names.s
|
|
|
2567
2784
|
${recordType}function to${names.singularPascal}(${names.singularCamel}: ${mapValueType}): ${names.singularPascal} {
|
|
2568
2785
|
return {
|
|
2569
2786
|
id: ${names.singularCamel}.id,
|
|
2570
|
-
${options.
|
|
2787
|
+
${options.tenantScoped ? `\t\ttenantId: ${names.singularCamel}.tenantId,\n` : ""} name: ${names.singularCamel}.name,
|
|
2571
2788
|
version: ${names.singularCamel}.version,
|
|
2572
2789
|
createdAt: ${names.singularCamel}.createdAt,
|
|
2573
2790
|
updatedAt: ${names.singularCamel}.updatedAt,
|
|
@@ -2625,10 +2842,10 @@ export function createInMemory${names.singularPascal}Repository(
|
|
|
2625
2842
|
);
|
|
2626
2843
|
|
|
2627
2844
|
return {
|
|
2628
|
-
async list(query${options.
|
|
2845
|
+
async list(query${options.tenantScoped ? ", scope" : ""}) {
|
|
2629
2846
|
const name = query.name?.toLocaleLowerCase();
|
|
2630
|
-
${options.
|
|
2631
|
-
${activeFilter}${options.
|
|
2847
|
+
${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const all${names.pluralPascal} = Array.from(${names.pluralCamel}.values())
|
|
2848
|
+
${activeFilter}${options.tenantScoped ? `\t\t\t\t.filter((${names.singularCamel}) => ${names.singularCamel}.tenantId === tenantId)\n` : ""} .filter((${names.singularCamel}) => !name || ${names.singularCamel}.name.toLocaleLowerCase().includes(name))
|
|
2632
2849
|
.sort((left, right) => compare${names.pluralPascal}(left, right, query))
|
|
2633
2850
|
.filter((${names.singularCamel}) => isAfter${names.singularPascal}Cursor(${names.singularCamel}, query));
|
|
2634
2851
|
const pageItems = all${names.pluralPascal}.slice(0, query.page.limit);
|
|
@@ -2643,11 +2860,11 @@ ${activeFilter}${options.tenant ? `\t\t\t\t.filter((${names.singularCamel}) => $
|
|
|
2643
2860
|
nextCursor,
|
|
2644
2861
|
);
|
|
2645
2862
|
},
|
|
2646
|
-
async create(input${options.
|
|
2863
|
+
async create(input${options.tenantScoped ? ", scope" : ""}) {
|
|
2647
2864
|
const now = new Date().toISOString();
|
|
2648
|
-
${options.
|
|
2865
|
+
${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel}: ${names.singularPascal} = {
|
|
2649
2866
|
id: crypto.randomUUID(),
|
|
2650
|
-
${options.
|
|
2867
|
+
${options.tenantScoped ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
|
|
2651
2868
|
version: 1,
|
|
2652
2869
|
createdAt: now,
|
|
2653
2870
|
updatedAt: now,
|
|
@@ -2657,7 +2874,7 @@ ${options.tenant ? `\t\t\t\ttenantId,\n` : ""} name: input.name,
|
|
|
2657
2874
|
${newDeletedAtField} });
|
|
2658
2875
|
return ${names.singularCamel};
|
|
2659
2876
|
},
|
|
2660
|
-
${mode === "resource" ? ` async findById(id: string${options.
|
|
2877
|
+
${mode === "resource" ? ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {\n${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const ${names.singularCamel} = ${names.pluralCamel}.get(id) ?? null;\n${options.tenantScoped ? `\t\t\tif (${names.singularCamel}?.tenantId !== tenantId) return null;\n` : ""}${findDeletedGuard} return ${names.singularCamel} ? to${names.singularPascal}(${names.singularCamel}) : null;\n },\n async update(input${options.tenantScoped ? ", scope" : ""}) {\n${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""} const existing = ${names.pluralCamel}.get(input.id);\n if (!existing${options.tenantScoped ? " || existing.tenantId !== tenantId" : ""}${updateDeletedGuard} || existing.version !== input.version) return null;\n\n const next: ${mapValueType} = {\n ...existing,\n name: input.name,\n version: existing.version + 1,\n updatedAt: new Date().toISOString(),\n };\n ${names.pluralCamel}.set(input.id, next);\n return to${names.singularPascal}(next);\n },\n async delete(id: string${options.tenantScoped ? ", scope" : ""}) {\n${options.tenantScoped ? `\t\t\tconst tenantId = tenantScopeId(scope);\n` : ""}${deleteImplementation}\n },\n` : ""}
|
|
2661
2878
|
};
|
|
2662
2879
|
}
|
|
2663
2880
|
`;
|
|
@@ -2667,7 +2884,7 @@ function drizzleSchemaFile(names, options, dialect) {
|
|
|
2667
2884
|
|
|
2668
2885
|
export const ${names.pluralCamel} = ${dialect.tableFunction}("${names.pluralKebab.replaceAll("-", "_")}", {
|
|
2669
2886
|
id: ${dialect.idColumn("id")}.primaryKey(),
|
|
2670
|
-
${options.
|
|
2887
|
+
${options.tenantScoped ? `\ttenantId: ${dialect.idColumn("tenant_id")}.notNull(),\n` : ""} name: text("name").notNull(),
|
|
2671
2888
|
version: ${dialect.integerColumn("version")}.notNull(),
|
|
2672
2889
|
createdAt: ${dialect.timestampColumn("created_at")}.notNull(),
|
|
2673
2890
|
updatedAt: ${dialect.timestampColumn("updated_at")}.notNull(),
|
|
@@ -2764,20 +2981,20 @@ function drizzleRepositoryFile(names, config, mode, options, dialect) {
|
|
|
2764
2981
|
const schemaPath = drizzleSchemaIndexPath(config);
|
|
2765
2982
|
const findWhere = drizzleResourceWhere(names, options, [
|
|
2766
2983
|
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
2767
|
-
...(options.
|
|
2984
|
+
...(options.tenantScoped
|
|
2768
2985
|
? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
|
|
2769
2986
|
: []),
|
|
2770
2987
|
]);
|
|
2771
2988
|
const updateWhere = drizzleResourceWhere(names, options, [
|
|
2772
2989
|
`eq(schema.${names.pluralCamel}.id, input.id)`,
|
|
2773
2990
|
`eq(schema.${names.pluralCamel}.version, input.version)`,
|
|
2774
|
-
...(options.
|
|
2991
|
+
...(options.tenantScoped
|
|
2775
2992
|
? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
|
|
2776
2993
|
: []),
|
|
2777
2994
|
]);
|
|
2778
2995
|
const deleteWhere = drizzleResourceWhere(names, options, [
|
|
2779
2996
|
`eq(schema.${names.pluralCamel}.id, id)`,
|
|
2780
|
-
...(options.
|
|
2997
|
+
...(options.tenantScoped
|
|
2781
2998
|
? [`eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope))`]
|
|
2782
2999
|
: []),
|
|
2783
3000
|
]);
|
|
@@ -2795,7 +3012,7 @@ function drizzleRepositoryFile(names, config, mode, options, dialect) {
|
|
|
2795
3012
|
].filter((name) => Boolean(name));
|
|
2796
3013
|
// After a version-guarded update succeeds, re-select by identity only —
|
|
2797
3014
|
// matching what `.returning()` yields on the dialects that support it.
|
|
2798
|
-
const updateReselectWhere = options.
|
|
3015
|
+
const updateReselectWhere = options.tenantScoped
|
|
2799
3016
|
? `and(eq(schema.${names.pluralCamel}.id, input.id), eq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)))`
|
|
2800
3017
|
: `eq(schema.${names.pluralCamel}.id, input.id)`;
|
|
2801
3018
|
// drizzle-orm/mysql2 has no `.returning(...)`; mutations resolve to a
|
|
@@ -2817,13 +3034,13 @@ function affectedRows(result: unknown): number {
|
|
|
2817
3034
|
`
|
|
2818
3035
|
: "";
|
|
2819
3036
|
const createMethod = dialect.supportsReturning
|
|
2820
|
-
? ` async create(input${options.
|
|
3037
|
+
? ` async create(input${options.tenantScoped ? ", scope" : ""}) {
|
|
2821
3038
|
const now = new Date().toISOString();
|
|
2822
3039
|
const [row] = await db
|
|
2823
3040
|
.insert(schema.${names.pluralCamel})
|
|
2824
3041
|
.values({
|
|
2825
3042
|
id: crypto.randomUUID(),
|
|
2826
|
-
${options.
|
|
3043
|
+
${options.tenantScoped ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
|
|
2827
3044
|
version: 1,
|
|
2828
3045
|
createdAt: now,
|
|
2829
3046
|
updatedAt: now,
|
|
@@ -2837,11 +3054,11 @@ ${options.tenant ? `\t\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name:
|
|
|
2837
3054
|
return to${names.singularPascal}(row);
|
|
2838
3055
|
},
|
|
2839
3056
|
`
|
|
2840
|
-
: ` async create(input${options.
|
|
3057
|
+
: ` async create(input${options.tenantScoped ? ", scope" : ""}) {
|
|
2841
3058
|
const now = new Date().toISOString();
|
|
2842
3059
|
const ${names.singularCamel}: ${names.singularPascal} = {
|
|
2843
3060
|
id: crypto.randomUUID(),
|
|
2844
|
-
${options.
|
|
3061
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: input.name,
|
|
2845
3062
|
version: 1,
|
|
2846
3063
|
createdAt: now,
|
|
2847
3064
|
updatedAt: now,
|
|
@@ -2854,7 +3071,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
|
|
|
2854
3071
|
const resourceMethods = mode !== "resource"
|
|
2855
3072
|
? ""
|
|
2856
3073
|
: dialect.supportsReturning
|
|
2857
|
-
? ` async findById(id: string${options.
|
|
3074
|
+
? ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
2858
3075
|
const [row] = await db
|
|
2859
3076
|
.select()
|
|
2860
3077
|
.from(schema.${names.pluralCamel})
|
|
@@ -2863,7 +3080,7 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
|
|
|
2863
3080
|
|
|
2864
3081
|
return row ? to${names.singularPascal}(row) : null;
|
|
2865
3082
|
},
|
|
2866
|
-
async update(input${options.
|
|
3083
|
+
async update(input${options.tenantScoped ? ", scope" : ""}) {
|
|
2867
3084
|
const [row] = await db
|
|
2868
3085
|
.update(schema.${names.pluralCamel})
|
|
2869
3086
|
.set({
|
|
@@ -2876,12 +3093,12 @@ ${options.tenant ? `\t\t\t\ttenantId: tenantScopeId(scope),\n` : ""} name: in
|
|
|
2876
3093
|
|
|
2877
3094
|
return row ? to${names.singularPascal}(row) : null;
|
|
2878
3095
|
},
|
|
2879
|
-
async delete(id: string${options.
|
|
3096
|
+
async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
2880
3097
|
${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst rows = await db\n\t\t\t\t.update(schema.${names.pluralCamel})\n\t\t\t\t.set({ deletedAt: now, updatedAt: now })\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n` : `\t\t\tconst rows = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere})\n\t\t\t\t.returning({ id: schema.${names.pluralCamel}.id });\n`}
|
|
2881
3098
|
return rows.length > 0;
|
|
2882
3099
|
},
|
|
2883
3100
|
`
|
|
2884
|
-
: ` async findById(id: string${options.
|
|
3101
|
+
: ` async findById(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
2885
3102
|
const [row] = await db
|
|
2886
3103
|
.select()
|
|
2887
3104
|
.from(schema.${names.pluralCamel})
|
|
@@ -2890,7 +3107,7 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
|
|
|
2890
3107
|
|
|
2891
3108
|
return row ? to${names.singularPascal}(row) : null;
|
|
2892
3109
|
},
|
|
2893
|
-
async update(input${options.
|
|
3110
|
+
async update(input${options.tenantScoped ? ", scope" : ""}) {
|
|
2894
3111
|
const result = await db
|
|
2895
3112
|
.update(schema.${names.pluralCamel})
|
|
2896
3113
|
.set({
|
|
@@ -2910,14 +3127,14 @@ ${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst
|
|
|
2910
3127
|
|
|
2911
3128
|
return row ? to${names.singularPascal}(row) : null;
|
|
2912
3129
|
},
|
|
2913
|
-
async delete(id: string${options.
|
|
3130
|
+
async delete(id: string${options.tenantScoped ? ", scope" : ""}) {
|
|
2914
3131
|
${options.softDelete ? `\t\t\tconst now = new Date().toISOString();\n\t\t\tconst result = await db\n\t\t\t\t.update(schema.${names.pluralCamel})\n\t\t\t\t.set({ deletedAt: now, updatedAt: now })\n\t\t\t\t.where(${deleteWhere});\n` : `\t\t\tconst result = await db\n\t\t\t\t.delete(schema.${names.pluralCamel})\n\t\t\t\t.where(${deleteWhere});\n`}
|
|
2915
3132
|
return affectedRows(result) > 0;
|
|
2916
3133
|
},
|
|
2917
3134
|
`;
|
|
2918
3135
|
return `import "@beignet/core/server-only";
|
|
2919
3136
|
import { cursorPageResult } from "@beignet/core/pagination";
|
|
2920
|
-
${options.
|
|
3137
|
+
${options.tenantScoped ? `import { tenantScopeId } from "@beignet/core/tenancy";\n` : ""}import type { ${dialect.dbTypeName} } from "@beignet/provider-db-drizzle/${dialect.subpath}";
|
|
2921
3138
|
import { ${drizzleImports.join(", ")} } from "drizzle-orm";
|
|
2922
3139
|
import type { ${names.singularPascal}Repository } from "${aliasModule(repositoryPortPath)}";
|
|
2923
3140
|
import { encode${names.singularPascal}Cursor } from "${aliasModule(resourceSchemaFilePath(names, config))}";
|
|
@@ -2932,7 +3149,7 @@ type ${names.singularPascal}Row = typeof schema.${names.pluralCamel}.$inferSelec
|
|
|
2932
3149
|
function to${names.singularPascal}(row: ${names.singularPascal}Row): ${names.singularPascal} {
|
|
2933
3150
|
return {
|
|
2934
3151
|
id: row.id,
|
|
2935
|
-
${options.
|
|
3152
|
+
${options.tenantScoped ? `\t\ttenantId: row.tenantId,\n` : ""} name: row.name,
|
|
2936
3153
|
version: row.version,
|
|
2937
3154
|
createdAt: row.createdAt,
|
|
2938
3155
|
updatedAt: row.updatedAt,
|
|
@@ -2964,9 +3181,9 @@ function ${names.singularCamel}CursorFilter(
|
|
|
2964
3181
|
|
|
2965
3182
|
function ${names.singularCamel}ListWhere(
|
|
2966
3183
|
query: List${names.pluralPascal}Query,
|
|
2967
|
-
${options.
|
|
3184
|
+
${options.tenantScoped ? `\tscope: Parameters<${names.singularPascal}Repository["list"]>[1],\n` : ""}): SQL<unknown> | undefined {
|
|
2968
3185
|
const filters: SQL<unknown>[] = [
|
|
2969
|
-
${options.
|
|
3186
|
+
${options.tenantScoped ? `\t\teq(schema.${names.pluralCamel}.tenantId, tenantScopeId(scope)),\n` : ""}${options.softDelete ? `\t\tisNull(schema.${names.pluralCamel}.deletedAt),\n` : ""} ];
|
|
2970
3187
|
const cursor = ${names.singularCamel}CursorFilter(query);
|
|
2971
3188
|
|
|
2972
3189
|
if (query.name) {
|
|
@@ -3000,8 +3217,8 @@ export function createDrizzle${names.singularPascal}Repository(
|
|
|
3000
3217
|
db: ${dialect.dbTypeName}<typeof schema>,
|
|
3001
3218
|
): ${names.singularPascal}Repository {
|
|
3002
3219
|
return {
|
|
3003
|
-
async list(query${options.
|
|
3004
|
-
const where = ${names.singularCamel}ListWhere(query${options.
|
|
3220
|
+
async list(query${options.tenantScoped ? ", scope" : ""}) {
|
|
3221
|
+
const where = ${names.singularCamel}ListWhere(query${options.tenantScoped ? ", scope" : ""});
|
|
3005
3222
|
const rows = await db
|
|
3006
3223
|
.select()
|
|
3007
3224
|
.from(schema.${names.pluralCamel})
|
|
@@ -3057,7 +3274,7 @@ export const list${names.pluralPascal} = ${names.pluralCamel}
|
|
|
3057
3274
|
export const create${names.singularPascal} = ${names.pluralCamel}
|
|
3058
3275
|
.post("/api/${names.pluralKebab}")
|
|
3059
3276
|
.body(Create${names.singularPascal}InputSchema)
|
|
3060
|
-
${options.
|
|
3277
|
+
${options.authorization ? ` .meta({\n auth: "required",\n authorization: { ability: "${names.pluralCamel}.create" },\n })\n .errors({ Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden })\n` : ""} .responses({
|
|
3061
3278
|
201: ${names.singularPascal}Schema,
|
|
3062
3279
|
});
|
|
3063
3280
|
${mode === "resource"
|
|
@@ -3074,7 +3291,7 @@ export const update${names.singularPascal} = ${names.pluralCamel}
|
|
|
3074
3291
|
.patch("/api/${names.pluralKebab}/:id")
|
|
3075
3292
|
.pathParams(${names.singularPascal}IdInputSchema)
|
|
3076
3293
|
.body(Update${names.singularPascal}BodySchema)
|
|
3077
|
-
${options.
|
|
3294
|
+
${options.authorization ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.update" },\n\t})\n` : ""} .errors({${options.authorization ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound, ${names.singularPascal}Conflict: errors.${names.singularPascal}Conflict })
|
|
3078
3295
|
.responses({
|
|
3079
3296
|
200: ${names.singularPascal}Schema,
|
|
3080
3297
|
});
|
|
@@ -3082,7 +3299,7 @@ ${options.auth ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability
|
|
|
3082
3299
|
export const delete${names.singularPascal} = ${names.pluralCamel}
|
|
3083
3300
|
.delete("/api/${names.pluralKebab}/:id")
|
|
3084
3301
|
.pathParams(${names.singularPascal}IdInputSchema)
|
|
3085
|
-
${options.
|
|
3302
|
+
${options.authorization ? `\t.meta({\n\t\tauth: "required",\n\t\tauthorization: { ability: "${names.pluralCamel}.delete" },\n\t})\n` : ""} .errors({${options.authorization ? " Unauthorized: errors.Unauthorized, Forbidden: errors.Forbidden," : ""} ${names.singularPascal}NotFound: errors.${names.singularPascal}NotFound })
|
|
3086
3303
|
.responses({
|
|
3087
3304
|
204: null,
|
|
3088
3305
|
});
|
|
@@ -3266,7 +3483,7 @@ function isAuthenticated(ctx: AuthorizationContext) {
|
|
|
3266
3483
|
|
|
3267
3484
|
function canWrite(ctx: AuthorizationContext) {
|
|
3268
3485
|
if (!isAuthenticated(ctx)) return deny("You must be signed in.");
|
|
3269
|
-
${options.
|
|
3486
|
+
${options.tenantScoped ? `\tif (!ctx.tenant?.id) return deny("A tenant is required.");\n` : ""} return true;
|
|
3270
3487
|
}
|
|
3271
3488
|
|
|
3272
3489
|
export const ${names.singularCamel}Policy = definePolicy({
|
|
@@ -3276,13 +3493,13 @@ export const ${names.singularCamel}Policy = definePolicy({
|
|
|
3276
3493
|
"${names.pluralCamel}.update": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
3277
3494
|
const decision = canWrite(ctx);
|
|
3278
3495
|
if (decision !== true) return decision;
|
|
3279
|
-
${options.
|
|
3496
|
+
${options.tenantScoped ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot update a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
|
|
3280
3497
|
return true;
|
|
3281
3498
|
},
|
|
3282
3499
|
"${names.pluralCamel}.delete": (ctx: AuthorizationContext, ${names.singularCamel}: ${names.singularPascal}PolicyResource) => {
|
|
3283
3500
|
const decision = canWrite(ctx);
|
|
3284
3501
|
if (decision !== true) return decision;
|
|
3285
|
-
${options.
|
|
3502
|
+
${options.tenantScoped ? `\t\tif (${names.singularCamel}.tenantId !== ctx.tenant?.id) {\n\t\t\treturn deny("You cannot delete a ${names.singularKebab} from another tenant.");\n\t\t}\n` : ""}
|
|
3286
3503
|
return true;
|
|
3287
3504
|
},
|
|
3288
3505
|
"${names.pluralCamel}.manage": (_ctx: AuthorizationContext) =>
|
|
@@ -3329,7 +3546,7 @@ function create${names.singularPascal}(): ${names.singularPascal} {
|
|
|
3329
3546
|
const now = new Date().toISOString();
|
|
3330
3547
|
return {
|
|
3331
3548
|
id: "00000000-0000-4000-8000-000000000001",
|
|
3332
|
-
${options.
|
|
3549
|
+
${options.tenantScoped ? `\t\ttenantId: "tenant_test",\n` : ""} name: "Test ${names.singularPascal}",
|
|
3333
3550
|
version: 1,
|
|
3334
3551
|
createdAt: now,
|
|
3335
3552
|
updatedAt: now,
|
|
@@ -3344,7 +3561,7 @@ describe("${names.singularCamel}Policy", () => {
|
|
|
3344
3561
|
const ${names.singularCamel} = create${names.singularPascal}();
|
|
3345
3562
|
const ctx = {
|
|
3346
3563
|
actor: { type: "user" as const, id: "user_test" },
|
|
3347
|
-
${options.
|
|
3564
|
+
${options.tenantScoped ? `\t\t\ttenant: { id: "tenant_test" },\n` : ""} };
|
|
3348
3565
|
|
|
3349
3566
|
await expect(
|
|
3350
3567
|
tester.assertMatrix([
|
|
@@ -3546,8 +3763,32 @@ ${names.timezone ? `\t\ttimezone: "${names.timezone}",\n` : ""}\t\tpayload: ${na
|
|
|
3546
3763
|
);
|
|
3547
3764
|
`;
|
|
3548
3765
|
}
|
|
3766
|
+
function uploadManifestFile(names) {
|
|
3767
|
+
return `import type {
|
|
3768
|
+
\tUploadFileConstraints,
|
|
3769
|
+
\tUploadManifestEntry,
|
|
3770
|
+
} from "@beignet/core/uploads";
|
|
3771
|
+
|
|
3772
|
+
export const ${names.fileConstraintsExportName} = {
|
|
3773
|
+
\tcontentTypes: ["application/pdf", "text/plain"],
|
|
3774
|
+
\tmaxSizeBytes: 5 * 1024 * 1024,
|
|
3775
|
+
\tmaxFiles: 1,
|
|
3776
|
+
\tvisibility: "private",
|
|
3777
|
+
\tcacheControl: "private, max-age=0",
|
|
3778
|
+
} satisfies UploadFileConstraints;
|
|
3779
|
+
|
|
3780
|
+
export const ${names.manifestExportName} = [
|
|
3781
|
+
\t{
|
|
3782
|
+
\t\tname: "${names.uploadName}",
|
|
3783
|
+
\t\tdescription: "${names.artifact.pascal} upload",
|
|
3784
|
+
\t\tfile: ${names.fileConstraintsExportName},
|
|
3785
|
+
\t},
|
|
3786
|
+
] satisfies readonly UploadManifestEntry[];
|
|
3787
|
+
`;
|
|
3788
|
+
}
|
|
3549
3789
|
function uploadFile(names, config) {
|
|
3550
3790
|
return `import { z } from "zod";
|
|
3791
|
+
import { ${names.fileConstraintsExportName} } from "${relativeModule(uploadFilePath(names, config), uploadManifestFilePath(names, config))}";
|
|
3551
3792
|
import { defineUpload } from "${aliasModule(config.paths.uploadsBuilder)}";
|
|
3552
3793
|
|
|
3553
3794
|
export const ${names.metadataSchemaName} = z.object({
|
|
@@ -3558,13 +3799,7 @@ export type ${names.metadataTypeName} = z.infer<typeof ${names.metadataSchemaNam
|
|
|
3558
3799
|
|
|
3559
3800
|
export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
|
|
3560
3801
|
\tmetadata: ${names.metadataSchemaName},
|
|
3561
|
-
\tfile: {
|
|
3562
|
-
\t\tcontentTypes: ["application/pdf", "text/plain"],
|
|
3563
|
-
\t\tmaxSizeBytes: 5 * 1024 * 1024,
|
|
3564
|
-
\t\tmaxFiles: 1,
|
|
3565
|
-
\t\tvisibility: "private",
|
|
3566
|
-
\t\tcacheControl: "private, max-age=0",
|
|
3567
|
-
\t},
|
|
3802
|
+
\tfile: ${names.fileConstraintsExportName},
|
|
3568
3803
|
\tauthorize({ ctx }) {
|
|
3569
3804
|
\t\treturn ctx.actor.type === "user";
|
|
3570
3805
|
\t},
|
|
@@ -3594,6 +3829,127 @@ export const ${names.uploadExportName} = defineUpload("${names.uploadName}", {
|
|
|
3594
3829
|
});
|
|
3595
3830
|
`;
|
|
3596
3831
|
}
|
|
3832
|
+
function uploadUiClientFile(names, config) {
|
|
3833
|
+
const clientPath = uploadUiClientFilePath(names, config);
|
|
3834
|
+
const registryPath = path.join(featureArtifactDir(names, "uploads", config), "index.ts");
|
|
3835
|
+
return `import { createUploadClient } from "@beignet/core/uploads/client";
|
|
3836
|
+
import { createReactUploads } from "@beignet/react-uploads";
|
|
3837
|
+
import { ${names.manifestExportName} } from "${relativeModule(clientPath, uploadManifestFilePath(names, config))}";
|
|
3838
|
+
import type { ${names.featureSingularCamel}Uploads } from "${relativeModule(clientPath, registryPath)}";
|
|
3839
|
+
|
|
3840
|
+
const ${names.artifact.camel}UploadClient = createUploadClient<typeof ${names.featureSingularCamel}Uploads>({
|
|
3841
|
+
\tbaseUrl: "/api/uploads",
|
|
3842
|
+
\tmanifest: ${names.manifestExportName},
|
|
3843
|
+
});
|
|
3844
|
+
|
|
3845
|
+
export const ${names.reactUploadsExportName} = createReactUploads({
|
|
3846
|
+
\tuploads: ${names.artifact.camel}UploadClient,
|
|
3847
|
+
});
|
|
3848
|
+
`;
|
|
3849
|
+
}
|
|
3850
|
+
function uploadUiComponentFile(names, config) {
|
|
3851
|
+
const componentPath = uploadUiComponentFilePath(names, config);
|
|
3852
|
+
return `"use client";
|
|
3853
|
+
|
|
3854
|
+
import { useId } from "react";
|
|
3855
|
+
import { ${names.reactUploadsExportName} } from "${relativeModule(componentPath, uploadUiClientFilePath(names, config))}";
|
|
3856
|
+
|
|
3857
|
+
export type ${names.componentExportName}Props = {
|
|
3858
|
+
\tresourceId: string;
|
|
3859
|
+
\tdisabled?: boolean;
|
|
3860
|
+
\tonComplete?(objectKeys: readonly string[]): void | Promise<void>;
|
|
3861
|
+
};
|
|
3862
|
+
|
|
3863
|
+
export function ${names.componentExportName}({
|
|
3864
|
+
\tresourceId,
|
|
3865
|
+
\tdisabled = false,
|
|
3866
|
+
\tonComplete,
|
|
3867
|
+
}: ${names.componentExportName}Props) {
|
|
3868
|
+
\tconst inputId = useId();
|
|
3869
|
+
\tconst upload = ${names.reactUploadsExportName}.useUpload("${names.uploadName}", {
|
|
3870
|
+
\t\tonSuccess({ result }) {
|
|
3871
|
+
\t\t\treturn onComplete?.(result.objectKeys);
|
|
3872
|
+
\t\t},
|
|
3873
|
+
\t});
|
|
3874
|
+
\tconst maxSizeMb = upload.constraints?.maxSizeBytes
|
|
3875
|
+
\t\t? Math.round(upload.constraints.maxSizeBytes / 1024 / 1024)
|
|
3876
|
+
\t\t: undefined;
|
|
3877
|
+
|
|
3878
|
+
\treturn (
|
|
3879
|
+
\t\t<section className="${names.artifact.kebab}-uploader">
|
|
3880
|
+
\t\t\t<label htmlFor={inputId}>${names.artifact.pascal}</label>
|
|
3881
|
+
\t\t\t<input
|
|
3882
|
+
\t\t\t\tid={inputId}
|
|
3883
|
+
\t\t\t\ttype="file"
|
|
3884
|
+
\t\t\t\taccept={upload.accept}
|
|
3885
|
+
\t\t\t\tdisabled={disabled || upload.isUploading}
|
|
3886
|
+
\t\t\t\tonChange={(event) => {
|
|
3887
|
+
\t\t\t\t\tconst file = event.currentTarget.files?.[0];
|
|
3888
|
+
\t\t\t\t\tevent.currentTarget.value = "";
|
|
3889
|
+
\t\t\t\t\tif (!file) return;
|
|
3890
|
+
|
|
3891
|
+
\t\t\t\t\tupload.uploadFile(file, {
|
|
3892
|
+
\t\t\t\t\t\tmetadata: { resourceId },
|
|
3893
|
+
\t\t\t\t\t});
|
|
3894
|
+
\t\t\t\t}}
|
|
3895
|
+
\t\t\t/>
|
|
3896
|
+
\t\t\t<p>
|
|
3897
|
+
\t\t\t\tPDF or plain text{maxSizeMb ? <> up to {maxSizeMb} MB</> : null}.
|
|
3898
|
+
\t\t\t</p>
|
|
3899
|
+
|
|
3900
|
+
\t\t\t{upload.files[0] ? <p>{upload.files[0].fileName}</p> : null}
|
|
3901
|
+
\t\t\t{upload.isUploading ? (
|
|
3902
|
+
\t\t\t\t<div aria-live="polite">
|
|
3903
|
+
\t\t\t\t\t<progress
|
|
3904
|
+
\t\t\t\t\t\taria-label="Upload progress"
|
|
3905
|
+
\t\t\t\t\t\tmax={100}
|
|
3906
|
+
\t\t\t\t\t\tvalue={upload.progress}
|
|
3907
|
+
\t\t\t\t\t/>
|
|
3908
|
+
\t\t\t\t\t<span>{Math.round(upload.progress)}%</span>
|
|
3909
|
+
\t\t\t\t\t<button type="button" onClick={upload.abort}>
|
|
3910
|
+
\t\t\t\t\t\tCancel upload
|
|
3911
|
+
\t\t\t\t\t</button>
|
|
3912
|
+
\t\t\t\t</div>
|
|
3913
|
+
\t\t\t) : null}
|
|
3914
|
+
\t\t\t{upload.isError ? (
|
|
3915
|
+
\t\t\t\t<p role="alert">{uploadErrorMessage(upload.error)}</p>
|
|
3916
|
+
\t\t\t) : null}
|
|
3917
|
+
\t\t\t{upload.isSuccess ? <p role="status">Upload complete.</p> : null}
|
|
3918
|
+
\t\t\t{upload.isError || upload.isSuccess ? (
|
|
3919
|
+
\t\t\t\t<button type="button" onClick={upload.reset}>
|
|
3920
|
+
\t\t\t\t\tReset upload
|
|
3921
|
+
\t\t\t\t</button>
|
|
3922
|
+
\t\t\t) : null}
|
|
3923
|
+
\t\t</section>
|
|
3924
|
+
\t);
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
function uploadErrorMessage(error: unknown): string {
|
|
3928
|
+
\treturn error instanceof Error ? error.message : "Upload failed.";
|
|
3929
|
+
}
|
|
3930
|
+
`;
|
|
3931
|
+
}
|
|
3932
|
+
function uploadUiComponentTestFile(names, config) {
|
|
3933
|
+
const testPath = uploadUiComponentTestFilePath(names, config);
|
|
3934
|
+
const testHelperPath = path.join(path.dirname(config.paths.useCaseBuilder), "beignet-test.ts");
|
|
3935
|
+
return `import { renderToStaticMarkup } from "react-dom/server";
|
|
3936
|
+
import { describe, expect, test } from "${relativeModule(testPath, testHelperPath)}";
|
|
3937
|
+
import { ${names.manifestExportName} } from "${relativeModule(testPath, uploadManifestFilePath(names, config))}";
|
|
3938
|
+
import { ${names.componentExportName} } from "${relativeModule(testPath, uploadUiComponentFilePath(names, config))}";
|
|
3939
|
+
|
|
3940
|
+
describe("${names.componentExportName}", () => {
|
|
3941
|
+
\ttest("renders the generated upload constraints", () => {
|
|
3942
|
+
\t\tconst markup = renderToStaticMarkup(
|
|
3943
|
+
\t\t\t<${names.componentExportName} resourceId="resource-1" />,
|
|
3944
|
+
\t\t);
|
|
3945
|
+
|
|
3946
|
+
\t\texpect(${names.manifestExportName}[0]?.file.maxSizeBytes).toBe(5 * 1024 * 1024);
|
|
3947
|
+
\t\texpect(markup).toContain('accept="application/pdf,text/plain"');
|
|
3948
|
+
\t\texpect(markup).toContain("PDF or plain text");
|
|
3949
|
+
\t});
|
|
3950
|
+
});
|
|
3951
|
+
`;
|
|
3952
|
+
}
|
|
3597
3953
|
function uploadRouteFile(names, config) {
|
|
3598
3954
|
const registryName = `${names.featureSingularCamel}Uploads`;
|
|
3599
3955
|
return `import {
|
|
@@ -3682,63 +4038,70 @@ function featureUiComponentFile(names, config) {
|
|
|
3682
4038
|
const clientQueriesPath = featureUiClientQueriesFilePath(names, config);
|
|
3683
4039
|
return `"use client";
|
|
3684
4040
|
|
|
3685
|
-
import {
|
|
4041
|
+
import { rootFormError } from "@beignet/react-hook-form";
|
|
3686
4042
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3687
|
-
import {
|
|
4043
|
+
import { rhf } from "${aliasModule(clientFormsPath(config))}";
|
|
4044
|
+
import { create${names.singularPascal} } from "${relativeModule(componentPath, resourceContractFilePath(names, config))}";
|
|
3688
4045
|
import {
|
|
3689
4046
|
\tcreate${names.singularPascal}MutationOptions,
|
|
3690
4047
|
\tinvalidate${names.pluralPascal},
|
|
3691
4048
|
\tlist${names.pluralPascal}QueryOptions,
|
|
3692
4049
|
} from "${relativeModule(componentPath, clientQueriesPath)}";
|
|
3693
4050
|
|
|
4051
|
+
const create${names.singularPascal}Form = rhf(create${names.singularPascal});
|
|
4052
|
+
|
|
3694
4053
|
export function ${names.componentExportName}() {
|
|
3695
|
-
\tconst [name, setName] = useState("");
|
|
3696
4054
|
\tconst queryClient = useQueryClient();
|
|
3697
4055
|
\tconst ${names.pluralCamel}Query = useQuery(list${names.pluralPascal}QueryOptions());
|
|
4056
|
+
\tconst form = create${names.singularPascal}Form.useForm({
|
|
4057
|
+
\t\tdefaultValues: { name: "" },
|
|
4058
|
+
\t});
|
|
4059
|
+
\tconst name = form.watch("name");
|
|
3698
4060
|
\tconst create${names.singularPascal}Mutation = useMutation({
|
|
3699
4061
|
\t\t...create${names.singularPascal}MutationOptions(),
|
|
3700
4062
|
\t\tonSuccess: async () => {
|
|
3701
|
-
\t\t\
|
|
4063
|
+
\t\t\tform.reset({ name: "" });
|
|
3702
4064
|
\t\t\tawait invalidate${names.pluralPascal}(queryClient);
|
|
3703
4065
|
\t\t},
|
|
4066
|
+
\t\tonError: (error) => {
|
|
4067
|
+
\t\t\tform.setError(
|
|
4068
|
+
\t\t\t\t"root",
|
|
4069
|
+
\t\t\t\trootFormError(error, "Could not create ${names.singularKebab}."),
|
|
4070
|
+
\t\t\t);
|
|
4071
|
+
\t\t},
|
|
3704
4072
|
\t});
|
|
3705
4073
|
|
|
3706
4074
|
\treturn (
|
|
3707
4075
|
\t\t<section className="${names.pluralKebab}-panel">
|
|
3708
4076
|
\t\t\t<form
|
|
3709
4077
|
\t\t\t\tclassName="${names.pluralKebab}-composer"
|
|
3710
|
-
\t\t\t\tonSubmit={(
|
|
3711
|
-
\t\t\t\t\
|
|
3712
|
-
\t\t\t\t\tconst trimmedName = name.trim();
|
|
3713
|
-
\t\t\t\t\tif (!trimmedName) return;
|
|
4078
|
+
\t\t\t\tonSubmit={form.handleSubmit((values) => {
|
|
4079
|
+
\t\t\t\t\tform.clearErrors("root");
|
|
3714
4080
|
\t\t\t\t\tcreate${names.singularPascal}Mutation.reset();
|
|
3715
4081
|
\t\t\t\t\tcreate${names.singularPascal}Mutation.mutate({
|
|
3716
|
-
\t\t\t\t\t\tbody:
|
|
4082
|
+
\t\t\t\t\t\tbody: values,
|
|
3717
4083
|
\t\t\t\t\t});
|
|
3718
|
-
\t\t\t\t}}
|
|
4084
|
+
\t\t\t\t})}
|
|
3719
4085
|
\t\t\t>
|
|
3720
4086
|
\t\t\t\t<label htmlFor="${names.singularKebab}-name">New ${names.singularKebab}</label>
|
|
3721
4087
|
\t\t\t\t<div className="${names.pluralKebab}-composer-row">
|
|
3722
4088
|
\t\t\t\t\t<input
|
|
3723
4089
|
\t\t\t\t\t\tid="${names.singularKebab}-name"
|
|
3724
|
-
\t\t\t\t\t\
|
|
3725
|
-
\t\t\t\t\t\tonChange={(event) => setName(event.currentTarget.value)}
|
|
4090
|
+
\t\t\t\t\t\t{...form.register("name")}
|
|
3726
4091
|
\t\t\t\t\t\tplaceholder="Name"
|
|
3727
4092
|
\t\t\t\t\t/>
|
|
3728
4093
|
\t\t\t\t\t<button
|
|
3729
4094
|
\t\t\t\t\t\ttype="submit"
|
|
3730
|
-
\t\t\t\t\t\tdisabled={!name
|
|
4095
|
+
\t\t\t\t\t\tdisabled={!name?.trim() || create${names.singularPascal}Mutation.isPending}
|
|
3731
4096
|
\t\t\t\t\t>
|
|
3732
4097
|
\t\t\t\t\t\t{create${names.singularPascal}Mutation.isPending ? "Creating" : "Create"}
|
|
3733
4098
|
\t\t\t\t\t</button>
|
|
3734
4099
|
\t\t\t\t</div>
|
|
3735
|
-
\t\t\t\t{
|
|
3736
|
-
\t\t\t\t\t<p role="alert">
|
|
3737
|
-
\t\t\t\t
|
|
3738
|
-
\t\t\t\t
|
|
3739
|
-
\t\t\t\t\t
|
|
3740
|
-
\t\t\t\t\t\t)}
|
|
3741
|
-
\t\t\t\t\t</p>
|
|
4100
|
+
\t\t\t\t{form.formState.errors.name ? (
|
|
4101
|
+
\t\t\t\t\t<p role="alert">{form.formState.errors.name.message}</p>
|
|
4102
|
+
\t\t\t\t) : null}
|
|
4103
|
+
\t\t\t\t{form.formState.errors.root ? (
|
|
4104
|
+
\t\t\t\t\t<p role="alert">{form.formState.errors.root.message}</p>
|
|
3742
4105
|
\t\t\t\t) : null}
|
|
3743
4106
|
\t\t\t</form>
|
|
3744
4107
|
|
|
@@ -3852,7 +4215,7 @@ ${mode === "resource" ? `import { isAppError } from "@beignet/core/errors";\n` :
|
|
|
3852
4215
|
import { createTestApp } from "@beignet/web/testing";
|
|
3853
4216
|
import { createInMemoryDevtools } from "@beignet/devtools";
|
|
3854
4217
|
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
|
|
3855
|
-
import { ${options.
|
|
4218
|
+
import { ${options.authorization ? "createTestUserActor" : "createTestAnonymousActor"}${options.tenantScoped ? ", createTestTenant" : ""} } from "@beignet/core/testing";
|
|
3856
4219
|
import type { AppContext } from "${aliasModule(config.paths.appContext)}";
|
|
3857
4220
|
import { initialPorts } from "${aliasModule(config.paths.portWiring)}";
|
|
3858
4221
|
import { appContext } from "${aliasModule(serverContextPath)}";
|
|
@@ -3872,39 +4235,39 @@ describe("${names.pluralCamel} resource", () => {
|
|
|
3872
4235
|
const seed${names.pluralPascal} = [
|
|
3873
4236
|
{
|
|
3874
4237
|
id: "00000000-0000-4000-8000-000000000101",
|
|
3875
|
-
${options.
|
|
4238
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Alpha ${names.singularPascal}",
|
|
3876
4239
|
version: 1,
|
|
3877
4240
|
createdAt: "2024-01-01T00:00:00.000Z",
|
|
3878
4241
|
updatedAt: "2024-01-01T00:00:00.000Z",
|
|
3879
4242
|
},
|
|
3880
4243
|
{
|
|
3881
4244
|
id: "00000000-0000-4000-8000-000000000102",
|
|
3882
|
-
${options.
|
|
4245
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Beta ${names.singularPascal}",
|
|
3883
4246
|
version: 1,
|
|
3884
4247
|
createdAt: "2024-01-02T00:00:00.000Z",
|
|
3885
4248
|
updatedAt: "2024-01-02T00:00:00.000Z",
|
|
3886
4249
|
},
|
|
3887
4250
|
{
|
|
3888
4251
|
id: "00000000-0000-4000-8000-000000000103",
|
|
3889
|
-
${options.
|
|
4252
|
+
${options.tenantScoped ? `\t\t\t\ttenantId: "tenant_test",\n` : ""} name: "Gamma ${names.singularPascal}",
|
|
3890
4253
|
version: 1,
|
|
3891
4254
|
createdAt: "2024-01-03T00:00:00.000Z",
|
|
3892
4255
|
updatedAt: "2024-01-03T00:00:00.000Z",
|
|
3893
4256
|
},
|
|
3894
|
-
${options.
|
|
4257
|
+
${options.tenantScoped ? `\t\t\t{\n\t\t\t\tid: "00000000-0000-4000-8000-000000000104",\n\t\t\t\ttenantId: "tenant_other",\n\t\t\t\tname: "Other Tenant ${names.singularPascal}",\n\t\t\t\tversion: 1,\n\t\t\t\tcreatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t\tupdatedAt: "2024-01-04T00:00:00.000Z",\n\t\t\t},\n` : ""} ];
|
|
3895
4258
|
const ${names.pluralCamel} = createInMemory${names.singularPascal}Repository(seed${names.pluralPascal});
|
|
3896
4259
|
const testFixture = createTestPorts<AppContext["ports"]>({
|
|
3897
4260
|
base: initialPorts,
|
|
3898
4261
|
overrides: {
|
|
3899
4262
|
${names.pluralCamel},
|
|
3900
|
-
${options.
|
|
4263
|
+
${options.authorization || options.tenantScoped
|
|
3901
4264
|
? `\t\t\t\tauth: {
|
|
3902
4265
|
getSession: async () => ({
|
|
3903
|
-
user: { id: "user_test", name: "Test User"${options.
|
|
3904
|
-
${options.
|
|
4266
|
+
user: { id: "user_test", name: "Test User"${options.tenantScoped ? `, tenantId: "tenant_test"` : ""} },
|
|
4267
|
+
${options.tenantScoped ? `\t\t\t\t\t\tsession: { tenantId: "tenant_test" },\n` : ""}
|
|
3905
4268
|
}),
|
|
3906
4269
|
},
|
|
3907
|
-
${options.
|
|
4270
|
+
${options.authorization ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
|
|
3908
4271
|
`
|
|
3909
4272
|
: `\t\t\t\tauth: {
|
|
3910
4273
|
getSession: async () => null,
|
|
@@ -3919,8 +4282,8 @@ ${options.auth ? `\t\t\t\tgate: initialPorts.gate,\n` : ""}
|
|
|
3919
4282
|
});
|
|
3920
4283
|
const createTestContext = createTestContextFactory<AppContext, AppContext["ports"]>({
|
|
3921
4284
|
ports: testFixture.ports,
|
|
3922
|
-
actor: ${options.
|
|
3923
|
-
tenant: ${options.
|
|
4285
|
+
actor: ${options.authorization ? `createTestUserActor("user_test")` : "createTestAnonymousActor()"},
|
|
4286
|
+
tenant: ${options.tenantScoped ? `createTestTenant("tenant_test")` : "null"},
|
|
3924
4287
|
});
|
|
3925
4288
|
const tester = createUseCaseTester<AppContext>(createTestContext);
|
|
3926
4289
|
|