@supacloud/cli 0.103.7 → 0.105.0
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/dist/index.js
CHANGED
|
@@ -9405,9 +9405,9 @@ var $pack = require_pack();
|
|
|
9405
9405
|
var FRONTEND_ARCHIVE_FORMAT = "tar.zst";
|
|
9406
9406
|
var FRONTEND_ARCHIVE_RELEASE_SCHEMA = "supacloud.frontend-release.v2";
|
|
9407
9407
|
var FRONTEND_ARCHIVE_CONTENT_TYPE = "application/vnd.supacloud.frontend.tar+zstd";
|
|
9408
|
-
var FRONTEND_ARCHIVE_MAX_BYTES =
|
|
9408
|
+
var FRONTEND_ARCHIVE_MAX_BYTES = 500 * 1024 * 1024;
|
|
9409
9409
|
var FRONTEND_ARCHIVE_MAX_FILES = 1e4;
|
|
9410
|
-
var FRONTEND_ARCHIVE_MAX_SOURCE_BYTES =
|
|
9410
|
+
var FRONTEND_ARCHIVE_MAX_SOURCE_BYTES = 500 * 1024 * 1024;
|
|
9411
9411
|
var FRONTEND_ARCHIVE_MAX_TAR_BYTES = FRONTEND_ARCHIVE_MAX_SOURCE_BYTES + FRONTEND_ARCHIVE_MAX_FILES * 2048 + 1024;
|
|
9412
9412
|
function assertFrontendArchivePath(path) {
|
|
9413
9413
|
if (!path || path.length > 1024 || Buffer.byteLength(path) > 4096 || path.startsWith("/") || path.includes("\\") || /[\u0000-\u001f\u007f]/u.test(path) || path.split("/").some((segment) => !segment || segment === "." || segment === "..") || /^[A-Za-z]:/u.test(path))
|
|
@@ -9996,6 +9996,7 @@ var reference = String2({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z][A-Za
|
|
|
9996
9996
|
var references = _Array_(reference, { uniqueItems: true });
|
|
9997
9997
|
var kind = Union([Literal("api"), Literal("jobs"), Literal("webhook"), Literal("function")]);
|
|
9998
9998
|
var isolation = Union([Literal("shared"), Literal("process")]);
|
|
9999
|
+
var mode = Union([Literal("required"), Literal("none")]);
|
|
9999
10000
|
var DeliveryFilePathSchema = String2({
|
|
10000
10001
|
maxLength: 512,
|
|
10001
10002
|
pattern: "^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*$"
|
|
@@ -10069,6 +10070,7 @@ var routeSchema = _Object_({
|
|
|
10069
10070
|
]),
|
|
10070
10071
|
path: String2(),
|
|
10071
10072
|
command: Optional(String2()),
|
|
10073
|
+
idempotency: Optional(mode),
|
|
10072
10074
|
reason: Union([Literal("default-http"), Literal("explicit-module")])
|
|
10073
10075
|
}, objectOptions);
|
|
10074
10076
|
var jobSchema = _Object_({
|
|
@@ -10213,7 +10215,8 @@ var DeliveryBuildManifestSchema = _Object_({
|
|
|
10213
10215
|
routes: _Array_(_Object_({
|
|
10214
10216
|
method: String2(),
|
|
10215
10217
|
path: String2(),
|
|
10216
|
-
target: String2()
|
|
10218
|
+
target: String2(),
|
|
10219
|
+
idempotency: Optional(Union([Literal("required"), Literal("none")]))
|
|
10217
10220
|
}, closed2)),
|
|
10218
10221
|
jobs: _Array_(_Object_({ name: String2(), target: String2() }, closed2))
|
|
10219
10222
|
}, closed2);
|
|
@@ -10275,7 +10278,12 @@ function parseDeliveryBuildManifest(value) {
|
|
|
10275
10278
|
throw new Error("Invalid delivery object inventory.");
|
|
10276
10279
|
}
|
|
10277
10280
|
}
|
|
10278
|
-
const routes = value.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
10281
|
+
const routes = value.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
10282
|
+
method: route.method,
|
|
10283
|
+
path: route.path,
|
|
10284
|
+
target: target.name,
|
|
10285
|
+
...route.idempotency === undefined ? {} : { idempotency: route.idempotency }
|
|
10286
|
+
})));
|
|
10279
10287
|
const jobs = value.plan.targets.flatMap((target) => target.jobs.map((job) => ({ name: job.name, target: target.name })));
|
|
10280
10288
|
if (canonical(value.routes) !== canonical(routes) || canonical(value.jobs) !== canonical(jobs)) {
|
|
10281
10289
|
throw new Error("Delivery mappings do not match planned ownership.");
|
|
@@ -10285,6 +10293,8 @@ function parseDeliveryBuildManifest(value) {
|
|
|
10285
10293
|
|
|
10286
10294
|
// node_modules/@supacloud/delivery/src/delivery-artifact.ts
|
|
10287
10295
|
var JSON_LIMIT = 1048576;
|
|
10296
|
+
var DELIVERY_ARTIFACT_MAX_FILE_BYTES = 500 * 1024 * 1024;
|
|
10297
|
+
var DELIVERY_ARTIFACT_MAX_TOTAL_BYTES = 500 * 1024 * 1024;
|
|
10288
10298
|
async function readDeliveryManifest(manifestPath) {
|
|
10289
10299
|
const selected = resolve4(manifestPath);
|
|
10290
10300
|
const root = await realpath2(dirname2(selected));
|
|
@@ -10298,7 +10308,7 @@ async function readDeliverySelection(manifestPath, target) {
|
|
|
10298
10308
|
return { root, object, planned };
|
|
10299
10309
|
}
|
|
10300
10310
|
async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
|
|
10301
|
-
if (object.files.length > 1024 || object.files.some((file) => file.bytes >
|
|
10311
|
+
if (object.files.length > 1024 || object.files.some((file) => file.bytes > DELIVERY_ARTIFACT_MAX_FILE_BYTES) || object.files.reduce((bytes, file) => bytes + file.bytes, 0) > DELIVERY_ARTIFACT_MAX_TOTAL_BYTES) {
|
|
10302
10312
|
throw new Error("Artifact inventory exceeds limits.");
|
|
10303
10313
|
}
|
|
10304
10314
|
const objectRoot = join2(root, "objects", object.objectId);
|
|
@@ -11420,7 +11430,7 @@ function buildDeliveryMigrationPlan(archive, inventory, ref) {
|
|
|
11420
11430
|
// node_modules/@supacloud/delivery/src/index.ts
|
|
11421
11431
|
async function readDeliveryExecutableArchive(manifestPath) {
|
|
11422
11432
|
const { root, manifest } = await readDeliveryManifest(manifestPath);
|
|
11423
|
-
if (manifest.objects.length === 0 || manifest.objects.length > 32 || manifest.objects.some((object) => object.entryKind === "compiled-module-factory") || manifest.objects.reduce((total, object) => total + object.files.reduce((bytes, file) => bytes + file.bytes, 0), 0) >
|
|
11433
|
+
if (manifest.objects.length === 0 || manifest.objects.length > 32 || manifest.objects.some((object) => object.entryKind === "compiled-module-factory") || manifest.objects.reduce((total, object) => total + object.files.reduce((bytes, file) => bytes + file.bytes, 0), 0) > DELIVERY_ARTIFACT_MAX_TOTAL_BYTES) {
|
|
11424
11434
|
throw new Error("Invalid executable delivery inventory.");
|
|
11425
11435
|
}
|
|
11426
11436
|
const objects = [];
|
|
@@ -21479,9 +21489,13 @@ function uniqueToolName(name, moduleName, used) {
|
|
|
21479
21489
|
}
|
|
21480
21490
|
function findRouteForCommand(manifest, commandClassName) {
|
|
21481
21491
|
for (const module of manifest.modules) {
|
|
21492
|
+
const command = module.commands.find((candidate) => candidate.className === commandClassName || candidate.name === commandClassName);
|
|
21493
|
+
if (!command)
|
|
21494
|
+
continue;
|
|
21482
21495
|
for (const controller of module.controllers) {
|
|
21483
21496
|
for (const route of controller.routes) {
|
|
21484
|
-
|
|
21497
|
+
const transition = module.featureSpec?.transitions.find((candidate) => candidate.route !== undefined && (candidate.command === command.className || candidate.command === command.name) && candidate.route === `${route.method} ${controller.path}${route.path}`);
|
|
21498
|
+
if (route.command !== command.className && route.command !== command.name && transition === undefined)
|
|
21485
21499
|
continue;
|
|
21486
21500
|
return {
|
|
21487
21501
|
httpMethod: route.method,
|
|
@@ -21574,25 +21588,25 @@ var starter_metadata_default = {
|
|
|
21574
21588
|
generatedBy: "supacloud/starter-metadata-v1",
|
|
21575
21589
|
packages: {
|
|
21576
21590
|
compiler: {
|
|
21577
|
-
version: "0.
|
|
21591
|
+
version: "0.49.0"
|
|
21578
21592
|
},
|
|
21579
21593
|
app: {
|
|
21580
|
-
version: "0.
|
|
21594
|
+
version: "0.30.0",
|
|
21581
21595
|
dependencies: {
|
|
21582
21596
|
rxjs: "7.8.2"
|
|
21583
21597
|
}
|
|
21584
21598
|
},
|
|
21585
21599
|
elysia: {
|
|
21586
|
-
version: "0.
|
|
21600
|
+
version: "0.35.0"
|
|
21587
21601
|
},
|
|
21588
21602
|
commands: {
|
|
21589
|
-
version: "0.
|
|
21603
|
+
version: "0.11.0"
|
|
21590
21604
|
},
|
|
21591
21605
|
contracts: {
|
|
21592
|
-
version: "0.
|
|
21606
|
+
version: "0.13.0"
|
|
21593
21607
|
},
|
|
21594
21608
|
db: {
|
|
21595
|
-
version: "0.
|
|
21609
|
+
version: "0.20.0",
|
|
21596
21610
|
peerDependencies: {
|
|
21597
21611
|
"drizzle-orm": "1.0.0-rc.5-5935859"
|
|
21598
21612
|
},
|
|
@@ -21601,7 +21615,7 @@ var starter_metadata_default = {
|
|
|
21601
21615
|
}
|
|
21602
21616
|
},
|
|
21603
21617
|
sdk: {
|
|
21604
|
-
version: "0.
|
|
21618
|
+
version: "0.45.0",
|
|
21605
21619
|
peerDependencies: {
|
|
21606
21620
|
"@supabase/supabase-js": "^2.117.3"
|
|
21607
21621
|
}
|
|
@@ -26246,8 +26260,12 @@ function featureScaffold(name, className, governance) {
|
|
|
26246
26260
|
export const ${className}Params = t.Object({
|
|
26247
26261
|
id: t.String({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9_-]+$" }),
|
|
26248
26262
|
});
|
|
26263
|
+
export const ${className}ReceiptParams = t.Object({
|
|
26264
|
+
id: t.String({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9_-]+$" }),
|
|
26265
|
+
operationId: t.String({ minLength: 1, maxLength: 512, pattern: "^[A-Za-z0-9._:-]+$" }),
|
|
26266
|
+
});
|
|
26249
26267
|
export const ${className}ListQuery = t.Object({
|
|
26250
|
-
state: t.Optional(t.
|
|
26268
|
+
state: t.Optional(t.Union([t.Literal("draft"), t.Literal("approved")])),
|
|
26251
26269
|
});
|
|
26252
26270
|
export const ${className}ApproveBody = t.Object({
|
|
26253
26271
|
expectedVersion: t.Integer({ minimum: 1 }),
|
|
@@ -26258,13 +26276,50 @@ export const ${className}Row = t.Object({
|
|
|
26258
26276
|
state: t.String(),
|
|
26259
26277
|
version: t.Integer({ minimum: 1 }),
|
|
26260
26278
|
});
|
|
26279
|
+
export const ${className}Receipt = t.Object({
|
|
26280
|
+
operationId: t.String({ minLength: 1, maxLength: 512 }),
|
|
26281
|
+
status: t.Union([
|
|
26282
|
+
t.Literal("pending"), t.Literal("confirmed"), t.Literal("failed"), t.Literal("unknown"),
|
|
26283
|
+
]),
|
|
26284
|
+
state: t.String(),
|
|
26285
|
+
version: t.Integer({ minimum: 1 }),
|
|
26286
|
+
});
|
|
26287
|
+
export const ${className}JobStatus = t.Object({
|
|
26288
|
+
operationId: t.String({ minLength: 1, maxLength: 512 }),
|
|
26289
|
+
status: t.Union([
|
|
26290
|
+
t.Literal("queued"), t.Literal("running"), t.Literal("completed"),
|
|
26291
|
+
t.Literal("failed"), t.Literal("cancelled"), t.Literal("unknown"),
|
|
26292
|
+
]),
|
|
26293
|
+
errorCode: t.Optional(t.String({ minLength: 1, maxLength: 128 })),
|
|
26294
|
+
});
|
|
26295
|
+
/** Query tags are explicit cache contracts; use them with @supacloud/query. */
|
|
26296
|
+
export const ${className}QueryTags = {
|
|
26297
|
+
list: ${JSON.stringify(`${name}.list`)},
|
|
26298
|
+
detail: ${JSON.stringify(`${name}.detail`)},
|
|
26299
|
+
receipt: ${JSON.stringify(`${name}.receipt`)},
|
|
26300
|
+
} as const;
|
|
26301
|
+
export const ${className}ApproveInvalidateTags = [
|
|
26302
|
+
${className}QueryTags.list,
|
|
26303
|
+
${className}QueryTags.detail,
|
|
26304
|
+
${className}QueryTags.receipt,
|
|
26305
|
+
] as const;
|
|
26261
26306
|
export const ${className}ListResponse = t.Array(${className}Row);
|
|
26307
|
+
export const ${className}RefreshInput = t.Object({
|
|
26308
|
+
id: t.String({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9_-]+$" }),
|
|
26309
|
+
version: t.Integer({ minimum: 1 }),
|
|
26310
|
+
});
|
|
26262
26311
|
`,
|
|
26263
|
-
[`${name}.model.ts`]: `import {
|
|
26312
|
+
[`${name}.model.ts`]: `import {
|
|
26313
|
+
InjectionToken, type FeatureRequestContext,
|
|
26314
|
+
} from "@supacloud/app/core";
|
|
26264
26315
|
import type {
|
|
26265
26316
|
${className}ApproveBody,
|
|
26266
26317
|
${className}ListQuery,
|
|
26318
|
+
${className}Receipt,
|
|
26319
|
+
${className}ReceiptParams,
|
|
26320
|
+
${className}JobStatus,
|
|
26267
26321
|
${className}Row,
|
|
26322
|
+
${className}RefreshInput,
|
|
26268
26323
|
} from "./${name}.contracts";
|
|
26269
26324
|
import type { StaticDecode } from "typebox";
|
|
26270
26325
|
|
|
@@ -26272,14 +26327,25 @@ export type ${className}ListInput = StaticDecode<typeof ${className}ListQuery>;
|
|
|
26272
26327
|
export type ${className}ApproveInput = StaticDecode<typeof ${className}ApproveBody>;
|
|
26273
26328
|
export type ${className}ApproveCommand = ${className}ApproveInput & { id: string };
|
|
26274
26329
|
export type ${className}Result = StaticDecode<typeof ${className}Row>;
|
|
26330
|
+
export type ${className}ReceiptResult = StaticDecode<typeof ${className}Receipt>;
|
|
26331
|
+
export type ${className}RefreshJobInput = StaticDecode<typeof ${className}RefreshInput>;
|
|
26332
|
+
export type ${className}JobStatusResult = StaticDecode<typeof ${className}JobStatus>;
|
|
26275
26333
|
|
|
26276
26334
|
export interface ${className}ReadPort {
|
|
26277
|
-
list(input: ${className}ListInput): Promise<${className}Result[]>;
|
|
26278
|
-
detail(id: string): Promise<${className}Result>;
|
|
26335
|
+
list(context: FeatureRequestContext, input: ${className}ListInput): Promise<${className}Result[]>;
|
|
26336
|
+
detail(context: FeatureRequestContext, id: string): Promise<${className}Result>;
|
|
26279
26337
|
}
|
|
26280
26338
|
|
|
26281
26339
|
export interface ${className}WritePort {
|
|
26282
|
-
approve(input: ${className}ApproveCommand): Promise<${className}Result>;
|
|
26340
|
+
approve(context: FeatureRequestContext, input: ${className}ApproveCommand): Promise<${className}Result>;
|
|
26341
|
+
}
|
|
26342
|
+
|
|
26343
|
+
export interface ${className}ReceiptPort {
|
|
26344
|
+
lookup(context: FeatureRequestContext, input: { id: string; operationId: string }): Promise<${className}ReceiptResult>;
|
|
26345
|
+
}
|
|
26346
|
+
|
|
26347
|
+
export interface ${className}JobStatusPort {
|
|
26348
|
+
lookup(context: FeatureRequestContext, input: { id: string; operationId: string }): Promise<${className}JobStatusResult>;
|
|
26283
26349
|
}
|
|
26284
26350
|
|
|
26285
26351
|
export const ${className}Reader = new InjectionToken<${className}ReadPort>(
|
|
@@ -26288,63 +26354,117 @@ export const ${className}Reader = new InjectionToken<${className}ReadPort>(
|
|
|
26288
26354
|
export const ${className}Writer = new InjectionToken<${className}WritePort>(
|
|
26289
26355
|
"${name}.writer", { scope: "request" },
|
|
26290
26356
|
);
|
|
26357
|
+
export const ${className}Receipts = new InjectionToken<${className}ReceiptPort>(
|
|
26358
|
+
"${name}.receipts", { scope: "request" },
|
|
26359
|
+
);
|
|
26360
|
+
export const ${className}JobStatuses = new InjectionToken<${className}JobStatusPort>(
|
|
26361
|
+
"${name}.job-statuses", { scope: "request" },
|
|
26362
|
+
);
|
|
26291
26363
|
`,
|
|
26292
|
-
[`${name}.service.ts`]: `import {
|
|
26364
|
+
[`${name}.service.ts`]: `import {
|
|
26365
|
+
Inject, Injectable, Optional, REQUEST_CONTEXT, requireFeatureRequestContext,
|
|
26366
|
+
type FeatureRequestContext,
|
|
26367
|
+
} from "@supacloud/app/core";
|
|
26293
26368
|
import {
|
|
26294
26369
|
${className}Reader,
|
|
26370
|
+
${className}JobStatuses,
|
|
26371
|
+
${className}Receipts,
|
|
26295
26372
|
${className}Writer,
|
|
26296
26373
|
type ${className}ApproveInput,
|
|
26297
26374
|
type ${className}ReadPort,
|
|
26298
26375
|
type ${className}Result,
|
|
26376
|
+
type ${className}ReceiptPort,
|
|
26377
|
+
type ${className}JobStatusPort,
|
|
26378
|
+
type ${className}JobStatusResult,
|
|
26299
26379
|
type ${className}WritePort,
|
|
26300
26380
|
} from "./${name}.model";
|
|
26301
26381
|
|
|
26302
26382
|
@Injectable({ scope: "request" })
|
|
26303
26383
|
export class ${className}Service {
|
|
26304
26384
|
constructor(
|
|
26385
|
+
@Inject(REQUEST_CONTEXT) private readonly requestContext: unknown,
|
|
26305
26386
|
@Inject(${className}Reader) @Optional() private readonly reader?: ${className}ReadPort,
|
|
26306
26387
|
@Inject(${className}Writer) @Optional() private readonly writer?: ${className}WritePort,
|
|
26388
|
+
@Inject(${className}Receipts) @Optional() private readonly receipts?: ${className}ReceiptPort,
|
|
26389
|
+
@Inject(${className}JobStatuses) @Optional() private readonly jobStatuses?: ${className}JobStatusPort,
|
|
26307
26390
|
) {}
|
|
26308
26391
|
|
|
26309
|
-
|
|
26392
|
+
private context(): FeatureRequestContext {
|
|
26393
|
+
return requireFeatureRequestContext(this.requestContext);
|
|
26394
|
+
}
|
|
26395
|
+
|
|
26396
|
+
async list(input: Parameters<${className}ReadPort["list"]>[1]): Promise<${className}Result[]> {
|
|
26310
26397
|
if (!this.reader) throw new Error("Bind ${className}Reader with verified identity and tenant scope");
|
|
26311
|
-
return this.reader.list(input);
|
|
26398
|
+
return this.reader.list(this.context(), input);
|
|
26312
26399
|
}
|
|
26313
26400
|
|
|
26314
26401
|
async detail(id: string): Promise<${className}Result> {
|
|
26315
26402
|
if (!this.reader) throw new Error("Bind ${className}Reader with verified identity and tenant scope");
|
|
26316
|
-
return this.reader.detail(id);
|
|
26403
|
+
return this.reader.detail(this.context(), id);
|
|
26317
26404
|
}
|
|
26318
26405
|
|
|
26319
26406
|
async approve(id: string, input: ${className}ApproveInput): Promise<${className}Result> {
|
|
26320
26407
|
if (!this.writer) throw new Error("Bind ${className}Writer with durable command governance");
|
|
26321
|
-
return this.writer.approve({ ...input, id });
|
|
26408
|
+
return this.writer.approve(this.context(), { ...input, id });
|
|
26409
|
+
}
|
|
26410
|
+
|
|
26411
|
+
async receipt(id: string, operationId: string): Promise<Awaited<ReturnType<${className}ReceiptPort["lookup"]>>> {
|
|
26412
|
+
if (!this.receipts) throw new Error("Bind ${className}Receipts with durable receipt lookup");
|
|
26413
|
+
return this.receipts.lookup(this.context(), { id, operationId });
|
|
26414
|
+
}
|
|
26415
|
+
|
|
26416
|
+
async jobStatus(id: string, operationId: string): Promise<${className}JobStatusResult> {
|
|
26417
|
+
if (!this.jobStatuses) throw new Error("Bind ${className}JobStatuses with the existing Workflow/Queue status adapter");
|
|
26418
|
+
return this.jobStatuses.lookup(this.context(), { id, operationId });
|
|
26322
26419
|
}
|
|
26323
26420
|
}
|
|
26324
26421
|
`,
|
|
26325
26422
|
[`${name}.feature.ts`]: `import { Effect } from "effect";
|
|
26326
26423
|
import {
|
|
26327
|
-
Body, Command, Controller, defineFeatureSlice, defineFeatureSpec, Get, Inject,
|
|
26328
|
-
Injectable, Job, Param, Post, Query, type
|
|
26424
|
+
Body, Command, Controller, defineFeatureSlice, defineFeatureSpec, Get, Inject, JOB_CONTEXT,
|
|
26425
|
+
Injectable, Job, Param, Post, Query, requireFeatureRequestContext, type FeatureRequestContext,
|
|
26426
|
+
type FeatureState,
|
|
26329
26427
|
} from "@supacloud/app/core";
|
|
26330
26428
|
import {
|
|
26331
26429
|
${className}ApproveBody,
|
|
26332
26430
|
${className}ListQuery,
|
|
26431
|
+
${className}Receipt,
|
|
26432
|
+
${className}ReceiptParams,
|
|
26433
|
+
${className}JobStatus,
|
|
26333
26434
|
${className}ListResponse,
|
|
26334
26435
|
${className}Params,
|
|
26436
|
+
${className}RefreshInput,
|
|
26335
26437
|
${className}Row,
|
|
26336
26438
|
} from "./${name}.contracts";
|
|
26337
26439
|
import { ${className}Service } from "./${name}.service";
|
|
26338
|
-
import type {
|
|
26440
|
+
import type {
|
|
26441
|
+
${className}ApproveInput,
|
|
26442
|
+
${className}ListInput,
|
|
26443
|
+
${className}ReceiptResult,
|
|
26444
|
+
${className}JobStatusResult,
|
|
26445
|
+
${className}RefreshJobInput,
|
|
26446
|
+
${className}Result,
|
|
26447
|
+
} from "./${name}.model";
|
|
26339
26448
|
|
|
26340
26449
|
export const featureSpec = defineFeatureSpec({
|
|
26341
26450
|
name: ${JSON.stringify(name)},
|
|
26342
26451
|
states: ["draft", "approved"],
|
|
26452
|
+
tenantField: "tenant_id",
|
|
26343
26453
|
transitions: {
|
|
26344
26454
|
approve: {
|
|
26345
26455
|
from: "draft", to: "approved", command: "Approve${className}Command",
|
|
26346
26456
|
permission: ${JSON.stringify(permission)}, transaction: "required",
|
|
26347
26457
|
idempotency: "required", audit: ${JSON.stringify(`${name}.approved`)},
|
|
26458
|
+
job: ${JSON.stringify(`${name}.refresh`)},
|
|
26459
|
+
invalidateTags: [
|
|
26460
|
+
${JSON.stringify(`${name}.list`)},
|
|
26461
|
+
${JSON.stringify(`${name}.detail`)},
|
|
26462
|
+
${JSON.stringify(`${name}.receipt`)},
|
|
26463
|
+
],
|
|
26464
|
+
persistence: {
|
|
26465
|
+
boundary: "database", optimisticLock: "required", versionField: "version", tenantField: "tenant_id", stateField: "state",
|
|
26466
|
+
receipt: "required", reconciliation: "none",
|
|
26467
|
+
},
|
|
26348
26468
|
route: "POST /${name}/:id/approve",
|
|
26349
26469
|
},
|
|
26350
26470
|
},
|
|
@@ -26352,7 +26472,7 @@ export const featureSpec = defineFeatureSpec({
|
|
|
26352
26472
|
export type ${className}State = FeatureState<typeof featureSpec>;
|
|
26353
26473
|
|
|
26354
26474
|
@Injectable({ scope: "request" })
|
|
26355
|
-
@Query({ name: "${name}.list" })
|
|
26475
|
+
@Query({ name: "${name}.list", tenantScope: "required" })
|
|
26356
26476
|
export class List${className}Query {
|
|
26357
26477
|
constructor(@Inject(${className}Service) private readonly service: ${className}Service) {}
|
|
26358
26478
|
execute(input: ${className}ListInput): Promise<${className}Result[]> {
|
|
@@ -26367,6 +26487,7 @@ export class List${className}Query {
|
|
|
26367
26487
|
transaction: "required",
|
|
26368
26488
|
audit: ${JSON.stringify(`${name}.approved`)},
|
|
26369
26489
|
idempotency: "required",
|
|
26490
|
+
tenantScope: "required",
|
|
26370
26491
|
rpc: ${JSON.stringify(rpc)},
|
|
26371
26492
|
})
|
|
26372
26493
|
export class Approve${className}Command {
|
|
@@ -26378,11 +26499,17 @@ export class Approve${className}Command {
|
|
|
26378
26499
|
|
|
26379
26500
|
@Job({
|
|
26380
26501
|
name: ${JSON.stringify(`${name}.refresh`)},
|
|
26502
|
+
input: ${className}RefreshInput,
|
|
26381
26503
|
mode: "task",
|
|
26382
26504
|
idempotency: "required",
|
|
26505
|
+
tenantScope: "required",
|
|
26383
26506
|
})
|
|
26384
26507
|
export class Refresh${className}Job {
|
|
26385
|
-
|
|
26508
|
+
constructor(@Inject(JOB_CONTEXT) private readonly jobContext: unknown) {}
|
|
26509
|
+
|
|
26510
|
+
async run(_input: ${className}RefreshJobInput): Promise<void> {
|
|
26511
|
+
const context: FeatureRequestContext = requireFeatureRequestContext(this.jobContext);
|
|
26512
|
+
void context;
|
|
26386
26513
|
throw new Error("Bind the existing Workflow/Queue adapter before dispatching ${name}.refresh");
|
|
26387
26514
|
}
|
|
26388
26515
|
}
|
|
@@ -26397,15 +26524,17 @@ export class ${className}Controller {
|
|
|
26397
26524
|
|
|
26398
26525
|
@Get("/", {
|
|
26399
26526
|
query: ${className}ListQuery,
|
|
26527
|
+
tags: [${JSON.stringify(`${name}.list`)}],
|
|
26400
26528
|
responses: { 200: ${className}ListResponse },
|
|
26401
26529
|
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
26402
26530
|
})
|
|
26403
|
-
list(@Query("state") state?:
|
|
26531
|
+
list(@Query("state") state?: ${className}State): Effect.Effect<${className}Result[], never, never> {
|
|
26404
26532
|
return Effect.promise(() => this.listQuery.execute(state === undefined ? {} : { state }));
|
|
26405
26533
|
}
|
|
26406
26534
|
|
|
26407
26535
|
@Get("/:id", {
|
|
26408
26536
|
params: ${className}Params,
|
|
26537
|
+
tags: [${JSON.stringify(`${name}.detail`)}],
|
|
26409
26538
|
responses: { 200: ${className}Row },
|
|
26410
26539
|
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
26411
26540
|
})
|
|
@@ -26418,6 +26547,7 @@ export class ${className}Controller {
|
|
|
26418
26547
|
body: ${className}ApproveBody,
|
|
26419
26548
|
responses: { 200: ${className}Row },
|
|
26420
26549
|
command: Approve${className}Command,
|
|
26550
|
+
idempotency: "required",
|
|
26421
26551
|
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
26422
26552
|
})
|
|
26423
26553
|
approve(
|
|
@@ -26426,6 +26556,32 @@ export class ${className}Controller {
|
|
|
26426
26556
|
): Effect.Effect<${className}Result, never, never> {
|
|
26427
26557
|
return Effect.promise(() => this.approveCommand.execute(id, body));
|
|
26428
26558
|
}
|
|
26559
|
+
|
|
26560
|
+
@Get("/:id/operations/:operationId", {
|
|
26561
|
+
params: ${className}ReceiptParams,
|
|
26562
|
+
tags: [${JSON.stringify(`${name}.receipt`)}],
|
|
26563
|
+
responses: { 200: ${className}Receipt },
|
|
26564
|
+
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
26565
|
+
})
|
|
26566
|
+
receipt(
|
|
26567
|
+
@Param("id") id: string,
|
|
26568
|
+
@Param("operationId") operationId: string,
|
|
26569
|
+
): Effect.Effect<${className}ReceiptResult, never, never> {
|
|
26570
|
+
return Effect.promise(() => this.service.receipt(id, operationId));
|
|
26571
|
+
}
|
|
26572
|
+
|
|
26573
|
+
@Get("/:id/operations/:operationId/status", {
|
|
26574
|
+
params: ${className}ReceiptParams,
|
|
26575
|
+
tags: [${JSON.stringify(`${name}.job-status`)}],
|
|
26576
|
+
responses: { 200: ${className}JobStatus },
|
|
26577
|
+
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
26578
|
+
})
|
|
26579
|
+
jobStatus(
|
|
26580
|
+
@Param("id") id: string,
|
|
26581
|
+
@Param("operationId") operationId: string,
|
|
26582
|
+
): Effect.Effect<${className}JobStatusResult, never, never> {
|
|
26583
|
+
return Effect.promise(() => this.service.jobStatus(id, operationId));
|
|
26584
|
+
}
|
|
26429
26585
|
}
|
|
26430
26586
|
|
|
26431
26587
|
export const ${className}Feature = defineFeatureSlice({
|
|
@@ -26438,19 +26594,37 @@ export const ${className}Feature = defineFeatureSlice({
|
|
|
26438
26594
|
});
|
|
26439
26595
|
`,
|
|
26440
26596
|
[`${name}.feature.test.ts`]: `import { expect, test } from "bun:test";
|
|
26597
|
+
import { Refresh${className}Job } from "./${name}.feature";
|
|
26441
26598
|
import { ${className}Service } from "./${name}.service";
|
|
26599
|
+
import { ${className}Fixture, ${className}RequestContext, ${className}RequestEnvelope } from "./${name}.fixture";
|
|
26600
|
+
|
|
26601
|
+
test("${name} rejects missing trusted tenant context before adapter execution", async () => {
|
|
26602
|
+
let calls = 0;
|
|
26603
|
+
const service = new ${className}Service(undefined, {
|
|
26604
|
+
list: async () => { calls++; return []; },
|
|
26605
|
+
detail: async () => { calls++; return { id: "example", state: "draft", version: 1 }; },
|
|
26606
|
+
});
|
|
26607
|
+
await expect(service.list({})).rejects.toThrow("FEATURE_CONTEXT_REQUIRED");
|
|
26608
|
+
expect(calls).toBe(0);
|
|
26609
|
+
});
|
|
26610
|
+
|
|
26611
|
+
test("${name} rejects missing trusted job context before dispatch", async () => {
|
|
26612
|
+
const job = new Refresh${className}Job({});
|
|
26613
|
+
await expect(job.run({ id: "example", version: 1 })).rejects.toThrow("FEATURE_CONTEXT_REQUIRED");
|
|
26614
|
+
});
|
|
26442
26615
|
|
|
26443
26616
|
test("${name} fails closed until verified read and durable write ports are bound", async () => {
|
|
26444
|
-
const service = new ${className}Service();
|
|
26617
|
+
const service = new ${className}Service(${className}RequestEnvelope);
|
|
26445
26618
|
await expect(service.list({})).rejects.toThrow("Bind ${className}Reader");
|
|
26446
26619
|
await expect(service.detail("example")).rejects.toThrow("Bind ${className}Reader");
|
|
26447
26620
|
await expect(service.approve("example", { expectedVersion: 1 })).rejects.toThrow("Bind ${className}Writer");
|
|
26621
|
+
await expect(service.jobStatus("example", "operation-example")).rejects.toThrow("Bind ${className}JobStatuses");
|
|
26448
26622
|
});
|
|
26449
26623
|
|
|
26450
26624
|
test("${name} preserves denied reads and rejected writes without fallback or retry", async () => {
|
|
26451
26625
|
const denied = new Error("denied");
|
|
26452
26626
|
let attempts = 0;
|
|
26453
|
-
const service = new ${className}Service({
|
|
26627
|
+
const service = new ${className}Service(${className}RequestEnvelope, {
|
|
26454
26628
|
list: async () => { throw denied; },
|
|
26455
26629
|
detail: async () => { throw denied; },
|
|
26456
26630
|
}, {
|
|
@@ -26463,14 +26637,166 @@ test("${name} preserves denied reads and rejected writes without fallback or ret
|
|
|
26463
26637
|
});
|
|
26464
26638
|
|
|
26465
26639
|
test("${name} forwards the authoritative version and result without inventing persistence", async () => {
|
|
26466
|
-
const row = { id:
|
|
26467
|
-
const service = new ${className}Service(undefined, {
|
|
26468
|
-
async approve(input) {
|
|
26469
|
-
expect(
|
|
26640
|
+
const row = { id: ${className}Fixture.id, state: "approved", version: 2 };
|
|
26641
|
+
const service = new ${className}Service(${className}RequestEnvelope, undefined, {
|
|
26642
|
+
async approve(requestContext, input) {
|
|
26643
|
+
expect(requestContext).toMatchObject(${className}RequestContext);
|
|
26644
|
+
expect(input).toEqual({ id: ${className}Fixture.id, ...${className}Fixture.approve });
|
|
26470
26645
|
return row;
|
|
26471
26646
|
},
|
|
26472
26647
|
});
|
|
26473
|
-
expect(await service.approve(
|
|
26648
|
+
expect(await service.approve(${className}Fixture.id, ${className}Fixture.approve)).toBe(row);
|
|
26649
|
+
});
|
|
26650
|
+
`,
|
|
26651
|
+
[`${name}.fixture.ts`]: `import type { ${className}ApproveInput, ${className}Result } from "./${name}.model";
|
|
26652
|
+
|
|
26653
|
+
/**
|
|
26654
|
+
* Test data only. Production adapters must load authoritative rows and
|
|
26655
|
+
* identity from the database/authentication boundary.
|
|
26656
|
+
*/
|
|
26657
|
+
export const ${className}Fixture = {
|
|
26658
|
+
id: "example",
|
|
26659
|
+
approve: { expectedVersion: 1, reason: "reviewed" } satisfies ${className}ApproveInput,
|
|
26660
|
+
row: { id: "example", state: "draft", version: 1 } satisfies ${className}Result,
|
|
26661
|
+
} as const;
|
|
26662
|
+
|
|
26663
|
+
export const ${className}RequestContext = {
|
|
26664
|
+
requestId: "request-example",
|
|
26665
|
+
tenantId: "tenant-example",
|
|
26666
|
+
actorId: "reviewer-example",
|
|
26667
|
+
permissions: [${JSON.stringify(permission)}],
|
|
26668
|
+
} as const;
|
|
26669
|
+
|
|
26670
|
+
export const ${className}RequestEnvelope = {
|
|
26671
|
+
...${className}RequestContext,
|
|
26672
|
+
identity: { authenticated: true, subject: "reviewer-example" },
|
|
26673
|
+
access: { tenantId: "tenant-example", permissions: [${JSON.stringify(permission)}] },
|
|
26674
|
+
} as const satisfies Record<string, unknown>;
|
|
26675
|
+
`,
|
|
26676
|
+
[`${name}.e2e.test.ts`]: `import { expect, test } from "bun:test";
|
|
26677
|
+
import { ${className}Fixture } from "./${name}.fixture";
|
|
26678
|
+
|
|
26679
|
+
const baseUrl = process.env["SUPACLOUD_FEATURE_E2E_URL"];
|
|
26680
|
+
const authorization = process.env["SUPACLOUD_FEATURE_E2E_AUTHORIZATION"];
|
|
26681
|
+
const otherAuthorization = process.env["SUPACLOUD_FEATURE_E2E_OTHER_AUTHORIZATION"];
|
|
26682
|
+
const fixtureId = process.env["SUPACLOUD_FEATURE_E2E_ID"] ?? ${className}Fixture.id;
|
|
26683
|
+
const idempotencyKey = process.env["SUPACLOUD_FEATURE_E2E_IDEMPOTENCY_KEY"];
|
|
26684
|
+
const enabled = Boolean(baseUrl && authorization && idempotencyKey);
|
|
26685
|
+
const tenantIsolationEnabled = Boolean(enabled && otherAuthorization);
|
|
26686
|
+
|
|
26687
|
+
function requireConfig(): { baseUrl: string; authorization: string; idempotencyKey: string } {
|
|
26688
|
+
if (!baseUrl || !authorization || !idempotencyKey) {
|
|
26689
|
+
throw new Error(
|
|
26690
|
+
"Set SUPACLOUD_FEATURE_E2E_URL, SUPACLOUD_FEATURE_E2E_AUTHORIZATION and " +
|
|
26691
|
+
"SUPACLOUD_FEATURE_E2E_IDEMPOTENCY_KEY before running this generated E2E test.",
|
|
26692
|
+
);
|
|
26693
|
+
}
|
|
26694
|
+
return { baseUrl, authorization, idempotencyKey };
|
|
26695
|
+
}
|
|
26696
|
+
|
|
26697
|
+
async function json(response: Response): Promise<unknown> {
|
|
26698
|
+
const text = await response.text();
|
|
26699
|
+
if (!text) return undefined;
|
|
26700
|
+
try {
|
|
26701
|
+
return JSON.parse(text) as unknown;
|
|
26702
|
+
} catch {
|
|
26703
|
+
return text;
|
|
26704
|
+
}
|
|
26705
|
+
}
|
|
26706
|
+
|
|
26707
|
+
function containsFixture(value: unknown): boolean {
|
|
26708
|
+
return JSON.stringify(value).includes(fixtureId);
|
|
26709
|
+
}
|
|
26710
|
+
|
|
26711
|
+
test.skipIf(!enabled)("${name} authenticated list/detail/approval E2E", async () => {
|
|
26712
|
+
const config = requireConfig();
|
|
26713
|
+
const headers = { authorization: config.authorization, "content-type": "application/json" };
|
|
26714
|
+
const list = await fetch(new URL("/${name}", config.baseUrl), { headers });
|
|
26715
|
+
expect(list.ok).toBe(true);
|
|
26716
|
+
const detail = await fetch(
|
|
26717
|
+
new URL("/${name}/" + encodeURIComponent(fixtureId), config.baseUrl),
|
|
26718
|
+
{ headers },
|
|
26719
|
+
);
|
|
26720
|
+
expect(detail.ok).toBe(true);
|
|
26721
|
+
const approve = (key: string, body = ${className}Fixture.approve) => fetch(
|
|
26722
|
+
new URL("/${name}/" + encodeURIComponent(fixtureId) + "/approve", config.baseUrl),
|
|
26723
|
+
{
|
|
26724
|
+
method: "POST",
|
|
26725
|
+
headers: { ...headers, "idempotency-key": key },
|
|
26726
|
+
body: JSON.stringify(body),
|
|
26727
|
+
},
|
|
26728
|
+
);
|
|
26729
|
+
const first = await approve(config.idempotencyKey);
|
|
26730
|
+
const firstBody = await json(first);
|
|
26731
|
+
expect([200, 409, 422]).toContain(first.status);
|
|
26732
|
+
|
|
26733
|
+
// A lost response is recovered through the durable receipt, never by
|
|
26734
|
+
// submitting the approval again.
|
|
26735
|
+
const recovered = await fetch(
|
|
26736
|
+
new URL("/${name}/" + encodeURIComponent(fixtureId) + "/operations/" + encodeURIComponent(config.idempotencyKey), config.baseUrl),
|
|
26737
|
+
{ headers },
|
|
26738
|
+
);
|
|
26739
|
+
expect(recovered.ok).toBe(true);
|
|
26740
|
+
const recoveredBody = await json(recovered);
|
|
26741
|
+
expect(recoveredBody).toMatchObject({ operationId: config.idempotencyKey });
|
|
26742
|
+
const jobStatus = await fetch(
|
|
26743
|
+
new URL("/${name}/" + encodeURIComponent(fixtureId) + "/operations/" + encodeURIComponent(config.idempotencyKey) + "/status", config.baseUrl),
|
|
26744
|
+
{ headers },
|
|
26745
|
+
);
|
|
26746
|
+
expect(jobStatus.ok).toBe(true);
|
|
26747
|
+
expect(await json(jobStatus)).toMatchObject({ operationId: config.idempotencyKey });
|
|
26748
|
+
|
|
26749
|
+
// Replaying an identical command may return the original success or a
|
|
26750
|
+
// domain conflict, but it must never create a second successful transition.
|
|
26751
|
+
const replay = await approve(config.idempotencyKey);
|
|
26752
|
+
const replayBody = await json(replay);
|
|
26753
|
+
expect(replay.status).toBe(first.status);
|
|
26754
|
+
expect(replayBody).toEqual(firstBody);
|
|
26755
|
+
|
|
26756
|
+
// Reusing a key with a different payload must be rejected by the receipt
|
|
26757
|
+
// boundary instead of being treated as a new approval.
|
|
26758
|
+
const conflicting = await approve(
|
|
26759
|
+
config.idempotencyKey,
|
|
26760
|
+
{ ...${className}Fixture.approve, reason: "conflicting replay" },
|
|
26761
|
+
);
|
|
26762
|
+
expect([400, 409, 422]).toContain(conflicting.status);
|
|
26763
|
+
|
|
26764
|
+
// Two distinct keys racing on one version may commit at most one state
|
|
26765
|
+
// transition. A fixture that is already terminal may reject both.
|
|
26766
|
+
const [raceA, raceB] = await Promise.all([
|
|
26767
|
+
approve(config.idempotencyKey + "-race-a"),
|
|
26768
|
+
approve(config.idempotencyKey + "-race-b"),
|
|
26769
|
+
]);
|
|
26770
|
+
expect([raceA.status, raceB.status].filter((status) => status >= 200 && status < 300).length)
|
|
26771
|
+
.toBeLessThanOrEqual(1);
|
|
26772
|
+
});
|
|
26773
|
+
|
|
26774
|
+
test.skipIf(!tenantIsolationEnabled)("${name} rejects cross-tenant access", async () => {
|
|
26775
|
+
const config = requireConfig();
|
|
26776
|
+
if (!otherAuthorization) throw new Error("Missing SUPACLOUD_FEATURE_E2E_OTHER_AUTHORIZATION");
|
|
26777
|
+
const headers = {
|
|
26778
|
+
authorization: otherAuthorization,
|
|
26779
|
+
"content-type": "application/json",
|
|
26780
|
+
};
|
|
26781
|
+
const list = await fetch(new URL("/${name}", config.baseUrl), { headers });
|
|
26782
|
+
expect(list.ok).toBe(true);
|
|
26783
|
+
expect(containsFixture(await json(list))).toBe(false);
|
|
26784
|
+
|
|
26785
|
+
const detail = await fetch(
|
|
26786
|
+
new URL("/${name}/" + encodeURIComponent(fixtureId), config.baseUrl),
|
|
26787
|
+
{ headers },
|
|
26788
|
+
);
|
|
26789
|
+
expect([403, 404]).toContain(detail.status);
|
|
26790
|
+
|
|
26791
|
+
const approval = await fetch(
|
|
26792
|
+
new URL("/${name}/" + encodeURIComponent(fixtureId) + "/approve", config.baseUrl),
|
|
26793
|
+
{
|
|
26794
|
+
method: "POST",
|
|
26795
|
+
headers: { ...headers, "idempotency-key": config.idempotencyKey + "-other-tenant" },
|
|
26796
|
+
body: JSON.stringify(${className}Fixture.approve),
|
|
26797
|
+
},
|
|
26798
|
+
);
|
|
26799
|
+
expect([403, 404]).toContain(approval.status);
|
|
26474
26800
|
});
|
|
26475
26801
|
`,
|
|
26476
26802
|
[`${name}.deployment.md`]: `# ${className} feature delivery checklist
|
|
@@ -26479,12 +26805,19 @@ test("${name} forwards the authoritative version and result without inventing pe
|
|
|
26479
26805
|
- [ ] Bind ${className}Writer to a transactional RPC or persistent Command adapter.
|
|
26480
26806
|
- [ ] Register the \`${rpc}\` compiler and runtime RPC capability.
|
|
26481
26807
|
- [ ] Verify RLS, permission denial, stale version, identical replay and conflicting replay.
|
|
26808
|
+
- [ ] Verify receipt lookup recovers a lost response without resubmitting the Command.
|
|
26809
|
+
- [ ] Pass \`${className}ApproveInvalidateTags\` to the generated mutation's \`invalidateTags\` option.
|
|
26810
|
+
- [ ] Bind \`${className}JobStatuses\` to the existing Workflow/Queue status read path.
|
|
26811
|
+
- [ ] Verify status lookup is tenant-scoped and distinguishes queued, running, completed, failed and cancelled.
|
|
26482
26812
|
- [ ] Verify the refresh job uses the existing Workflow/Queue delivery path.
|
|
26483
26813
|
- [ ] Run the generated client contract and authenticated E2E test.
|
|
26484
26814
|
- [ ] Run \`supacloud app check --format json\`, package tests and the selected deployment receipt.
|
|
26485
26815
|
|
|
26486
26816
|
The scaffold does not create migrations, infer permissions, or provide an in-memory
|
|
26487
26817
|
production fallback. Database migrations remain the database source of truth.
|
|
26818
|
+
Every adapter receives a trusted framework context whose tenant and actor values
|
|
26819
|
+
come from verified identity/access data. Do not add tenant, actor, or permission
|
|
26820
|
+
fields to the request body as a substitute for this context.
|
|
26488
26821
|
The HTTP schemas describe the boundary, not a second database model. Bind their
|
|
26489
26822
|
fields to the existing database-source contract checker before delivery.
|
|
26490
26823
|
The Writer input is derived from the approval body contract plus the route ID.
|
|
@@ -26492,15 +26825,24 @@ Add request fields in the contract, not a second handwritten Writer interface.
|
|
|
26492
26825
|
Typecheck application adapters after changing a field type; the derived Port
|
|
26493
26826
|
must expose incompatible assumptions in business code.
|
|
26494
26827
|
|
|
26828
|
+
For authenticated tenant-isolation acceptance, also provide
|
|
26829
|
+
\`SUPACLOUD_FEATURE_E2E_OTHER_AUTHORIZATION\` for a principal from another tenant.
|
|
26830
|
+
The generated E2E then verifies that the fixture is absent from list results and that
|
|
26831
|
+
cross-tenant detail and approval are rejected.
|
|
26832
|
+
|
|
26495
26833
|
The named RPC adapter must invoke the continuation in the same database transaction
|
|
26496
26834
|
used by the request-scoped Writer, or own the complete command instead; never execute
|
|
26497
26835
|
both paths. Lock/check the version and state, reauthorize replays, and commit the
|
|
26498
26836
|
business write, receipt, audit and outbox together. Do not put a network side effect
|
|
26499
26837
|
inside this transaction. The refresh Job is an explicit unimplemented integration
|
|
26500
|
-
point, not an automatically scheduled workflow.
|
|
26838
|
+
point, not an automatically scheduled workflow. The generated status route is also
|
|
26839
|
+
an explicit read adapter boundary; it does not treat a queue message ID or current
|
|
26840
|
+
business row as proof of task completion.
|
|
26501
26841
|
|
|
26502
26842
|
Compile the routes with the existing client generator and use @supacloud/query for
|
|
26503
|
-
identity-scoped caching/invalidation.
|
|
26843
|
+
identity-scoped caching/invalidation. Use the generated \`${className}ApproveInvalidateTags\`
|
|
26844
|
+
constant for the approval mutation; tags are explicit and never inferred from route names.
|
|
26845
|
+
There is deliberately no second hand-maintained
|
|
26504
26846
|
HTTP client. A committed approval followed by a failed refresh is still committed;
|
|
26505
26847
|
recover its receipt instead of resending. Use the host's standard error translation
|
|
26506
26848
|
and requestId propagation; the scaffold does not claim to implement those adapters.
|
|
@@ -26683,7 +27025,7 @@ async function runLocalDevelopment(options) {
|
|
|
26683
27025
|
}
|
|
26684
27026
|
|
|
26685
27027
|
// src/shared/tools/app-verification-plan.ts
|
|
26686
|
-
import { readdir as readdir4, realpath as realpath3 } from "node:fs/promises";
|
|
27028
|
+
import { readdir as readdir4, realpath as realpath3, stat } from "node:fs/promises";
|
|
26687
27029
|
import { dirname as dirname7, isAbsolute as isAbsolute4, relative as relative5, resolve as resolve16 } from "node:path";
|
|
26688
27030
|
function inside2(root, file) {
|
|
26689
27031
|
const path = relative5(root, file).replaceAll("\\", "/");
|
|
@@ -26708,37 +27050,50 @@ async function createVerificationPlan(root, graph, target, sourceRoot = root) {
|
|
|
26708
27050
|
...(owner.jobs ?? []).flatMap((item) => (item.aspects ?? []).flatMap((aspect) => aspect.file ? [aspect.file] : [])),
|
|
26709
27051
|
...owner.controllers.flatMap((item) => item.routes.flatMap((route) => (route.aspects ?? []).flatMap((aspect) => aspect.file ? [aspect.file] : [])))
|
|
26710
27052
|
].filter((file) => typeof file === "string");
|
|
26711
|
-
const directories = [...new Set(sources.map((file) => dirname7(resolve16(sourceRoot, file))))].sort();
|
|
26712
|
-
const tests = new Set;
|
|
26713
27053
|
const manual = [];
|
|
26714
|
-
const
|
|
26715
|
-
for (const
|
|
26716
|
-
|
|
26717
|
-
|
|
27054
|
+
const validatedSources = [];
|
|
27055
|
+
for (const source of new Set(sources)) {
|
|
27056
|
+
const requested = resolve16(sourceRoot, source);
|
|
27057
|
+
if (!inside2(requestedRoot, requested)) {
|
|
27058
|
+
manual.push("A source file is outside the project; select its owning project explicitly");
|
|
26718
27059
|
continue;
|
|
26719
27060
|
}
|
|
26720
|
-
const actual = await realpath3(
|
|
27061
|
+
const actual = await realpath3(requested).catch((error) => {
|
|
26721
27062
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
26722
27063
|
return null;
|
|
26723
27064
|
throw error;
|
|
26724
27065
|
});
|
|
26725
27066
|
if (actual === null) {
|
|
26726
|
-
manual.push(`Source
|
|
27067
|
+
manual.push(`Source file is missing: ${relative5(requestedRoot, requested).replaceAll("\\", "/")}`);
|
|
26727
27068
|
continue;
|
|
26728
27069
|
}
|
|
26729
27070
|
if (!inside2(project, actual)) {
|
|
27071
|
+
manual.push("A source file is outside the project; select its owning project explicitly");
|
|
27072
|
+
continue;
|
|
27073
|
+
}
|
|
27074
|
+
if (!(await stat(actual)).isFile()) {
|
|
27075
|
+
manual.push(`Source is not a regular file: ${relative5(requestedRoot, requested).replaceAll("\\", "/")}`);
|
|
27076
|
+
continue;
|
|
27077
|
+
}
|
|
27078
|
+
validatedSources.push(actual);
|
|
27079
|
+
}
|
|
27080
|
+
const directories = [...new Set(validatedSources.map((file) => dirname7(file)))].sort();
|
|
27081
|
+
const tests = new Set;
|
|
27082
|
+
const coverage = [];
|
|
27083
|
+
for (const directory of directories) {
|
|
27084
|
+
if (!inside2(project, await realpath3(directory))) {
|
|
26730
27085
|
manual.push("A source directory is outside the project; select its owning project explicitly");
|
|
26731
27086
|
continue;
|
|
26732
27087
|
}
|
|
26733
27088
|
const local = [];
|
|
26734
|
-
for (const entry of await readdir4(
|
|
27089
|
+
for (const entry of await readdir4(directory, { withFileTypes: true })) {
|
|
26735
27090
|
if (entry.isFile() && /\.(test|spec)\.[cm]?[jt]sx?$/.test(entry.name)) {
|
|
26736
|
-
const file = relative5(project, resolve16(
|
|
27091
|
+
const file = relative5(project, resolve16(directory, entry.name)).replaceAll("\\", "/");
|
|
26737
27092
|
tests.add(file);
|
|
26738
27093
|
local.push(file);
|
|
26739
27094
|
}
|
|
26740
27095
|
}
|
|
26741
|
-
const path = relative5(project,
|
|
27096
|
+
const path = relative5(project, directory).replaceAll("\\", "/") || ".";
|
|
26742
27097
|
coverage.push({ directory: path, tests: local.sort(), basis: "colocated-not-proven" });
|
|
26743
27098
|
if (!local.length)
|
|
26744
27099
|
manual.push(`No colocated tests for source directory: ${path}`);
|
|
@@ -26753,7 +27108,7 @@ async function createVerificationPlan(root, graph, target, sourceRoot = root) {
|
|
|
26753
27108
|
source: "declaration",
|
|
26754
27109
|
target: owner.name,
|
|
26755
27110
|
ready: manual.length === 0,
|
|
26756
|
-
files: [...new Set(
|
|
27111
|
+
files: [...new Set(validatedSources.map((file) => relative5(project, file).replaceAll("\\", "/")))].sort(),
|
|
26757
27112
|
tests: selected.slice(0, 32),
|
|
26758
27113
|
commands: selected.length <= 32 ? [
|
|
26759
27114
|
...selected.map((file) => ["bun", "test", `./${file}`]),
|
|
@@ -27289,16 +27644,19 @@ async function projectCompileConfig(args) {
|
|
|
27289
27644
|
}
|
|
27290
27645
|
async function readAllowedDependencies(root) {
|
|
27291
27646
|
const path = join13(root, "package.json");
|
|
27292
|
-
if (!existsSync11(path))
|
|
27293
|
-
return { dependencies: [], devDependencies: [] };
|
|
27647
|
+
if (!existsSync11(path)) {
|
|
27648
|
+
return { dependencies: [], devDependencies: [], optionalDependencies: [], peerDependencies: [] };
|
|
27649
|
+
}
|
|
27294
27650
|
try {
|
|
27295
27651
|
const parsed = JSON.parse(await readFile4(path, "utf8"));
|
|
27296
27652
|
return {
|
|
27297
27653
|
dependencies: Object.keys(parsed.dependencies ?? {}).sort(),
|
|
27298
|
-
devDependencies: Object.keys(parsed.devDependencies ?? {}).sort()
|
|
27654
|
+
devDependencies: Object.keys(parsed.devDependencies ?? {}).sort(),
|
|
27655
|
+
optionalDependencies: Object.keys(parsed.optionalDependencies ?? {}).sort(),
|
|
27656
|
+
peerDependencies: Object.keys(parsed.peerDependencies ?? {}).sort()
|
|
27299
27657
|
};
|
|
27300
27658
|
} catch {
|
|
27301
|
-
return { dependencies: [], devDependencies: [] };
|
|
27659
|
+
return { dependencies: [], devDependencies: [], optionalDependencies: [], peerDependencies: [] };
|
|
27302
27660
|
}
|
|
27303
27661
|
}
|
|
27304
27662
|
function formatModuleContextPack(pack) {
|
|
@@ -27399,6 +27757,9 @@ async function runContext(args) {
|
|
|
27399
27757
|
` modules: ${project.modules.map((module) => module.name).join(", ") || "-"}`,
|
|
27400
27758
|
` external tokens: ${project.externalTokens.join(", ") || "-"}`,
|
|
27401
27759
|
` dependencies: ${project.allowedDependencies.dependencies.join(", ") || "-"}`,
|
|
27760
|
+
` dev dependencies: ${project.allowedDependencies.devDependencies.join(", ") || "-"}`,
|
|
27761
|
+
` optional dependencies: ${project.allowedDependencies.optionalDependencies.join(", ") || "-"}`,
|
|
27762
|
+
` peer dependencies: ${project.allowedDependencies.peerDependencies.join(", ") || "-"}`,
|
|
27402
27763
|
...project.diagnostics.map(formatDiagnostic)
|
|
27403
27764
|
].join(`
|
|
27404
27765
|
`));
|
|
@@ -32365,7 +32726,7 @@ async function runProjectDeployment(http, args) {
|
|
|
32365
32726
|
// package.json
|
|
32366
32727
|
var package_default = {
|
|
32367
32728
|
name: "@supacloud/cli",
|
|
32368
|
-
version: "0.
|
|
32729
|
+
version: "0.105.0",
|
|
32369
32730
|
description: "Project-scoped CLI for SupaCloud users",
|
|
32370
32731
|
type: "module",
|
|
32371
32732
|
main: "./dist/index.js",
|
|
@@ -32417,19 +32778,19 @@ var package_default = {
|
|
|
32417
32778
|
directory: "packages/cli"
|
|
32418
32779
|
},
|
|
32419
32780
|
dependencies: {
|
|
32420
|
-
"@supacloud/compiler": "^0.
|
|
32421
|
-
"@supacloud/delivery": "0.
|
|
32422
|
-
"@supacloud/db": "0.
|
|
32781
|
+
"@supacloud/compiler": "^0.49.0",
|
|
32782
|
+
"@supacloud/delivery": "0.21.0",
|
|
32783
|
+
"@supacloud/db": "0.20.0",
|
|
32423
32784
|
typebox: "1.3.37"
|
|
32424
32785
|
},
|
|
32425
32786
|
overrides: {
|
|
32426
|
-
"@supacloud/contracts": "0.
|
|
32427
|
-
"@supacloud/delivery": "0.
|
|
32787
|
+
"@supacloud/contracts": "0.13.0",
|
|
32788
|
+
"@supacloud/delivery": "0.21.0",
|
|
32428
32789
|
"@graphql-tools/utils": "^12.0.4"
|
|
32429
32790
|
},
|
|
32430
32791
|
devDependencies: {
|
|
32431
32792
|
"@supabase/supabase-js": "2.117.3",
|
|
32432
|
-
"@supacloud/elysia": "0.
|
|
32793
|
+
"@supacloud/elysia": "0.35.0",
|
|
32433
32794
|
"@types/bun": "^1.4.3",
|
|
32434
32795
|
effect: "4.0.5",
|
|
32435
32796
|
elysia: "2.0.0-beta.32",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* An editable vertical feature slice.
|
|
3
|
-
*
|
|
4
|
-
* remain application-owned and cannot be fabricated by the scaffold.
|
|
2
|
+
* An editable vertical feature slice. Persistence remains application-owned,
|
|
3
|
+
* while every generated port receives the trusted framework request context.
|
|
5
4
|
*/
|
|
6
5
|
export declare function featureScaffold(name: string, className: string, governance: {
|
|
7
6
|
permission: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.105.0",
|
|
4
4
|
"description": "Project-scoped CLI for SupaCloud users",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -52,19 +52,19 @@
|
|
|
52
52
|
"directory": "packages/cli"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@supacloud/compiler": "^0.
|
|
56
|
-
"@supacloud/delivery": "0.
|
|
57
|
-
"@supacloud/db": "0.
|
|
55
|
+
"@supacloud/compiler": "^0.49.0",
|
|
56
|
+
"@supacloud/delivery": "0.21.0",
|
|
57
|
+
"@supacloud/db": "0.20.0",
|
|
58
58
|
"typebox": "1.3.37"
|
|
59
59
|
},
|
|
60
60
|
"overrides": {
|
|
61
|
-
"@supacloud/contracts": "0.
|
|
62
|
-
"@supacloud/delivery": "0.
|
|
61
|
+
"@supacloud/contracts": "0.13.0",
|
|
62
|
+
"@supacloud/delivery": "0.21.0",
|
|
63
63
|
"@graphql-tools/utils": "^12.0.4"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@supabase/supabase-js": "2.117.3",
|
|
67
|
-
"@supacloud/elysia": "0.
|
|
67
|
+
"@supacloud/elysia": "0.35.0",
|
|
68
68
|
"@types/bun": "^1.4.3",
|
|
69
69
|
"effect": "4.0.5",
|
|
70
70
|
"elysia": "2.0.0-beta.32",
|