@supacloud/cli 0.104.0 → 0.106.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
|
@@ -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.");
|
|
@@ -21481,9 +21489,13 @@ function uniqueToolName(name, moduleName, used) {
|
|
|
21481
21489
|
}
|
|
21482
21490
|
function findRouteForCommand(manifest, commandClassName) {
|
|
21483
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;
|
|
21484
21495
|
for (const controller of module.controllers) {
|
|
21485
21496
|
for (const route of controller.routes) {
|
|
21486
|
-
|
|
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)
|
|
21487
21499
|
continue;
|
|
21488
21500
|
return {
|
|
21489
21501
|
httpMethod: route.method,
|
|
@@ -21576,25 +21588,25 @@ var starter_metadata_default = {
|
|
|
21576
21588
|
generatedBy: "supacloud/starter-metadata-v1",
|
|
21577
21589
|
packages: {
|
|
21578
21590
|
compiler: {
|
|
21579
|
-
version: "0.
|
|
21591
|
+
version: "0.50.0"
|
|
21580
21592
|
},
|
|
21581
21593
|
app: {
|
|
21582
|
-
version: "0.
|
|
21594
|
+
version: "0.31.0",
|
|
21583
21595
|
dependencies: {
|
|
21584
21596
|
rxjs: "7.8.2"
|
|
21585
21597
|
}
|
|
21586
21598
|
},
|
|
21587
21599
|
elysia: {
|
|
21588
|
-
version: "0.
|
|
21600
|
+
version: "0.35.1"
|
|
21589
21601
|
},
|
|
21590
21602
|
commands: {
|
|
21591
|
-
version: "0.
|
|
21603
|
+
version: "0.11.0"
|
|
21592
21604
|
},
|
|
21593
21605
|
contracts: {
|
|
21594
|
-
version: "0.
|
|
21606
|
+
version: "0.13.0"
|
|
21595
21607
|
},
|
|
21596
21608
|
db: {
|
|
21597
|
-
version: "0.
|
|
21609
|
+
version: "0.20.0",
|
|
21598
21610
|
peerDependencies: {
|
|
21599
21611
|
"drizzle-orm": "1.0.0-rc.5-5935859"
|
|
21600
21612
|
},
|
|
@@ -21603,7 +21615,7 @@ var starter_metadata_default = {
|
|
|
21603
21615
|
}
|
|
21604
21616
|
},
|
|
21605
21617
|
sdk: {
|
|
21606
|
-
version: "0.
|
|
21618
|
+
version: "0.45.1",
|
|
21607
21619
|
peerDependencies: {
|
|
21608
21620
|
"@supabase/supabase-js": "^2.117.3"
|
|
21609
21621
|
}
|
|
@@ -26241,13 +26253,17 @@ test("${name} controller preserves an asynchronous read failure", async () => {
|
|
|
26241
26253
|
// src/shared/tools/app-feature.ts
|
|
26242
26254
|
function featureScaffold(name, className, governance) {
|
|
26243
26255
|
const commandName = `${name}.approve`;
|
|
26244
|
-
const { permission, rpc } = governance;
|
|
26256
|
+
const { permission, rpc, database } = governance;
|
|
26245
26257
|
return {
|
|
26246
26258
|
[`${name}.contracts.ts`]: `import { t } from "elysia";
|
|
26247
26259
|
|
|
26248
26260
|
export const ${className}Params = t.Object({
|
|
26249
26261
|
id: t.String({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9_-]+$" }),
|
|
26250
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
|
+
});
|
|
26251
26267
|
export const ${className}ListQuery = t.Object({
|
|
26252
26268
|
state: t.Optional(t.Union([t.Literal("draft"), t.Literal("approved")])),
|
|
26253
26269
|
});
|
|
@@ -26260,6 +26276,33 @@ export const ${className}Row = t.Object({
|
|
|
26260
26276
|
state: t.String(),
|
|
26261
26277
|
version: t.Integer({ minimum: 1 }),
|
|
26262
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;
|
|
26263
26306
|
export const ${className}ListResponse = t.Array(${className}Row);
|
|
26264
26307
|
export const ${className}RefreshInput = t.Object({
|
|
26265
26308
|
id: t.String({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9_-]+$" }),
|
|
@@ -26272,6 +26315,9 @@ export const ${className}RefreshInput = t.Object({
|
|
|
26272
26315
|
import type {
|
|
26273
26316
|
${className}ApproveBody,
|
|
26274
26317
|
${className}ListQuery,
|
|
26318
|
+
${className}Receipt,
|
|
26319
|
+
${className}ReceiptParams,
|
|
26320
|
+
${className}JobStatus,
|
|
26275
26321
|
${className}Row,
|
|
26276
26322
|
${className}RefreshInput,
|
|
26277
26323
|
} from "./${name}.contracts";
|
|
@@ -26281,7 +26327,9 @@ export type ${className}ListInput = StaticDecode<typeof ${className}ListQuery>;
|
|
|
26281
26327
|
export type ${className}ApproveInput = StaticDecode<typeof ${className}ApproveBody>;
|
|
26282
26328
|
export type ${className}ApproveCommand = ${className}ApproveInput & { id: string };
|
|
26283
26329
|
export type ${className}Result = StaticDecode<typeof ${className}Row>;
|
|
26330
|
+
export type ${className}ReceiptResult = StaticDecode<typeof ${className}Receipt>;
|
|
26284
26331
|
export type ${className}RefreshJobInput = StaticDecode<typeof ${className}RefreshInput>;
|
|
26332
|
+
export type ${className}JobStatusResult = StaticDecode<typeof ${className}JobStatus>;
|
|
26285
26333
|
|
|
26286
26334
|
export interface ${className}ReadPort {
|
|
26287
26335
|
list(context: FeatureRequestContext, input: ${className}ListInput): Promise<${className}Result[]>;
|
|
@@ -26292,12 +26340,26 @@ export interface ${className}WritePort {
|
|
|
26292
26340
|
approve(context: FeatureRequestContext, input: ${className}ApproveCommand): Promise<${className}Result>;
|
|
26293
26341
|
}
|
|
26294
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>;
|
|
26349
|
+
}
|
|
26350
|
+
|
|
26295
26351
|
export const ${className}Reader = new InjectionToken<${className}ReadPort>(
|
|
26296
26352
|
"${name}.reader", { scope: "request" },
|
|
26297
26353
|
);
|
|
26298
26354
|
export const ${className}Writer = new InjectionToken<${className}WritePort>(
|
|
26299
26355
|
"${name}.writer", { scope: "request" },
|
|
26300
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
|
+
);
|
|
26301
26363
|
`,
|
|
26302
26364
|
[`${name}.service.ts`]: `import {
|
|
26303
26365
|
Inject, Injectable, Optional, REQUEST_CONTEXT, requireFeatureRequestContext,
|
|
@@ -26305,10 +26367,15 @@ export const ${className}Writer = new InjectionToken<${className}WritePort>(
|
|
|
26305
26367
|
} from "@supacloud/app/core";
|
|
26306
26368
|
import {
|
|
26307
26369
|
${className}Reader,
|
|
26370
|
+
${className}JobStatuses,
|
|
26371
|
+
${className}Receipts,
|
|
26308
26372
|
${className}Writer,
|
|
26309
26373
|
type ${className}ApproveInput,
|
|
26310
26374
|
type ${className}ReadPort,
|
|
26311
26375
|
type ${className}Result,
|
|
26376
|
+
type ${className}ReceiptPort,
|
|
26377
|
+
type ${className}JobStatusPort,
|
|
26378
|
+
type ${className}JobStatusResult,
|
|
26312
26379
|
type ${className}WritePort,
|
|
26313
26380
|
} from "./${name}.model";
|
|
26314
26381
|
|
|
@@ -26318,6 +26385,8 @@ export class ${className}Service {
|
|
|
26318
26385
|
@Inject(REQUEST_CONTEXT) private readonly requestContext: unknown,
|
|
26319
26386
|
@Inject(${className}Reader) @Optional() private readonly reader?: ${className}ReadPort,
|
|
26320
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,
|
|
26321
26390
|
) {}
|
|
26322
26391
|
|
|
26323
26392
|
private context(): FeatureRequestContext {
|
|
@@ -26338,6 +26407,16 @@ export class ${className}Service {
|
|
|
26338
26407
|
if (!this.writer) throw new Error("Bind ${className}Writer with durable command governance");
|
|
26339
26408
|
return this.writer.approve(this.context(), { ...input, id });
|
|
26340
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 });
|
|
26419
|
+
}
|
|
26341
26420
|
}
|
|
26342
26421
|
`,
|
|
26343
26422
|
[`${name}.feature.ts`]: `import { Effect } from "effect";
|
|
@@ -26349,6 +26428,9 @@ import {
|
|
|
26349
26428
|
import {
|
|
26350
26429
|
${className}ApproveBody,
|
|
26351
26430
|
${className}ListQuery,
|
|
26431
|
+
${className}Receipt,
|
|
26432
|
+
${className}ReceiptParams,
|
|
26433
|
+
${className}JobStatus,
|
|
26352
26434
|
${className}ListResponse,
|
|
26353
26435
|
${className}Params,
|
|
26354
26436
|
${className}RefreshInput,
|
|
@@ -26358,6 +26440,8 @@ import { ${className}Service } from "./${name}.service";
|
|
|
26358
26440
|
import type {
|
|
26359
26441
|
${className}ApproveInput,
|
|
26360
26442
|
${className}ListInput,
|
|
26443
|
+
${className}ReceiptResult,
|
|
26444
|
+
${className}JobStatusResult,
|
|
26361
26445
|
${className}RefreshJobInput,
|
|
26362
26446
|
${className}Result,
|
|
26363
26447
|
} from "./${name}.model";
|
|
@@ -26365,6 +26449,7 @@ import type {
|
|
|
26365
26449
|
export const featureSpec = defineFeatureSpec({
|
|
26366
26450
|
name: ${JSON.stringify(name)},
|
|
26367
26451
|
states: ["draft", "approved"],
|
|
26452
|
+
database: ${JSON.stringify(database)},
|
|
26368
26453
|
tenantField: "tenant_id",
|
|
26369
26454
|
transitions: {
|
|
26370
26455
|
approve: {
|
|
@@ -26372,6 +26457,11 @@ export const featureSpec = defineFeatureSpec({
|
|
|
26372
26457
|
permission: ${JSON.stringify(permission)}, transaction: "required",
|
|
26373
26458
|
idempotency: "required", audit: ${JSON.stringify(`${name}.approved`)},
|
|
26374
26459
|
job: ${JSON.stringify(`${name}.refresh`)},
|
|
26460
|
+
invalidateTags: [
|
|
26461
|
+
${JSON.stringify(`${name}.list`)},
|
|
26462
|
+
${JSON.stringify(`${name}.detail`)},
|
|
26463
|
+
${JSON.stringify(`${name}.receipt`)},
|
|
26464
|
+
],
|
|
26375
26465
|
persistence: {
|
|
26376
26466
|
boundary: "database", optimisticLock: "required", versionField: "version", tenantField: "tenant_id", stateField: "state",
|
|
26377
26467
|
receipt: "required", reconciliation: "none",
|
|
@@ -26435,6 +26525,7 @@ export class ${className}Controller {
|
|
|
26435
26525
|
|
|
26436
26526
|
@Get("/", {
|
|
26437
26527
|
query: ${className}ListQuery,
|
|
26528
|
+
tags: [${JSON.stringify(`${name}.list`)}],
|
|
26438
26529
|
responses: { 200: ${className}ListResponse },
|
|
26439
26530
|
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
26440
26531
|
})
|
|
@@ -26444,6 +26535,7 @@ export class ${className}Controller {
|
|
|
26444
26535
|
|
|
26445
26536
|
@Get("/:id", {
|
|
26446
26537
|
params: ${className}Params,
|
|
26538
|
+
tags: [${JSON.stringify(`${name}.detail`)}],
|
|
26447
26539
|
responses: { 200: ${className}Row },
|
|
26448
26540
|
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
26449
26541
|
})
|
|
@@ -26456,6 +26548,7 @@ export class ${className}Controller {
|
|
|
26456
26548
|
body: ${className}ApproveBody,
|
|
26457
26549
|
responses: { 200: ${className}Row },
|
|
26458
26550
|
command: Approve${className}Command,
|
|
26551
|
+
idempotency: "required",
|
|
26459
26552
|
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
26460
26553
|
})
|
|
26461
26554
|
approve(
|
|
@@ -26464,6 +26557,32 @@ export class ${className}Controller {
|
|
|
26464
26557
|
): Effect.Effect<${className}Result, never, never> {
|
|
26465
26558
|
return Effect.promise(() => this.approveCommand.execute(id, body));
|
|
26466
26559
|
}
|
|
26560
|
+
|
|
26561
|
+
@Get("/:id/operations/:operationId", {
|
|
26562
|
+
params: ${className}ReceiptParams,
|
|
26563
|
+
tags: [${JSON.stringify(`${name}.receipt`)}],
|
|
26564
|
+
responses: { 200: ${className}Receipt },
|
|
26565
|
+
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
26566
|
+
})
|
|
26567
|
+
receipt(
|
|
26568
|
+
@Param("id") id: string,
|
|
26569
|
+
@Param("operationId") operationId: string,
|
|
26570
|
+
): Effect.Effect<${className}ReceiptResult, never, never> {
|
|
26571
|
+
return Effect.promise(() => this.service.receipt(id, operationId));
|
|
26572
|
+
}
|
|
26573
|
+
|
|
26574
|
+
@Get("/:id/operations/:operationId/status", {
|
|
26575
|
+
params: ${className}ReceiptParams,
|
|
26576
|
+
tags: [${JSON.stringify(`${name}.job-status`)}],
|
|
26577
|
+
responses: { 200: ${className}JobStatus },
|
|
26578
|
+
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
26579
|
+
})
|
|
26580
|
+
jobStatus(
|
|
26581
|
+
@Param("id") id: string,
|
|
26582
|
+
@Param("operationId") operationId: string,
|
|
26583
|
+
): Effect.Effect<${className}JobStatusResult, never, never> {
|
|
26584
|
+
return Effect.promise(() => this.service.jobStatus(id, operationId));
|
|
26585
|
+
}
|
|
26467
26586
|
}
|
|
26468
26587
|
|
|
26469
26588
|
export const ${className}Feature = defineFeatureSlice({
|
|
@@ -26500,6 +26619,7 @@ test("${name} fails closed until verified read and durable write ports are bound
|
|
|
26500
26619
|
await expect(service.list({})).rejects.toThrow("Bind ${className}Reader");
|
|
26501
26620
|
await expect(service.detail("example")).rejects.toThrow("Bind ${className}Reader");
|
|
26502
26621
|
await expect(service.approve("example", { expectedVersion: 1 })).rejects.toThrow("Bind ${className}Writer");
|
|
26622
|
+
await expect(service.jobStatus("example", "operation-example")).rejects.toThrow("Bind ${className}JobStatuses");
|
|
26503
26623
|
});
|
|
26504
26624
|
|
|
26505
26625
|
test("${name} preserves denied reads and rejected writes without fallback or retry", async () => {
|
|
@@ -26611,6 +26731,22 @@ test.skipIf(!enabled)("${name} authenticated list/detail/approval E2E", async ()
|
|
|
26611
26731
|
const firstBody = await json(first);
|
|
26612
26732
|
expect([200, 409, 422]).toContain(first.status);
|
|
26613
26733
|
|
|
26734
|
+
// A lost response is recovered through the durable receipt, never by
|
|
26735
|
+
// submitting the approval again.
|
|
26736
|
+
const recovered = await fetch(
|
|
26737
|
+
new URL("/${name}/" + encodeURIComponent(fixtureId) + "/operations/" + encodeURIComponent(config.idempotencyKey), config.baseUrl),
|
|
26738
|
+
{ headers },
|
|
26739
|
+
);
|
|
26740
|
+
expect(recovered.ok).toBe(true);
|
|
26741
|
+
const recoveredBody = await json(recovered);
|
|
26742
|
+
expect(recoveredBody).toMatchObject({ operationId: config.idempotencyKey });
|
|
26743
|
+
const jobStatus = await fetch(
|
|
26744
|
+
new URL("/${name}/" + encodeURIComponent(fixtureId) + "/operations/" + encodeURIComponent(config.idempotencyKey) + "/status", config.baseUrl),
|
|
26745
|
+
{ headers },
|
|
26746
|
+
);
|
|
26747
|
+
expect(jobStatus.ok).toBe(true);
|
|
26748
|
+
expect(await json(jobStatus)).toMatchObject({ operationId: config.idempotencyKey });
|
|
26749
|
+
|
|
26614
26750
|
// Replaying an identical command may return the original success or a
|
|
26615
26751
|
// domain conflict, but it must never create a second successful transition.
|
|
26616
26752
|
const replay = await approve(config.idempotencyKey);
|
|
@@ -26670,6 +26806,10 @@ test.skipIf(!tenantIsolationEnabled)("${name} rejects cross-tenant access", asyn
|
|
|
26670
26806
|
- [ ] Bind ${className}Writer to a transactional RPC or persistent Command adapter.
|
|
26671
26807
|
- [ ] Register the \`${rpc}\` compiler and runtime RPC capability.
|
|
26672
26808
|
- [ ] Verify RLS, permission denial, stale version, identical replay and conflicting replay.
|
|
26809
|
+
- [ ] Verify receipt lookup recovers a lost response without resubmitting the Command.
|
|
26810
|
+
- [ ] Pass \`${className}ApproveInvalidateTags\` to the generated mutation's \`invalidateTags\` option.
|
|
26811
|
+
- [ ] Bind \`${className}JobStatuses\` to the existing Workflow/Queue status read path.
|
|
26812
|
+
- [ ] Verify status lookup is tenant-scoped and distinguishes queued, running, completed, failed and cancelled.
|
|
26673
26813
|
- [ ] Verify the refresh job uses the existing Workflow/Queue delivery path.
|
|
26674
26814
|
- [ ] Run the generated client contract and authenticated E2E test.
|
|
26675
26815
|
- [ ] Run \`supacloud app check --format json\`, package tests and the selected deployment receipt.
|
|
@@ -26696,10 +26836,14 @@ used by the request-scoped Writer, or own the complete command instead; never ex
|
|
|
26696
26836
|
both paths. Lock/check the version and state, reauthorize replays, and commit the
|
|
26697
26837
|
business write, receipt, audit and outbox together. Do not put a network side effect
|
|
26698
26838
|
inside this transaction. The refresh Job is an explicit unimplemented integration
|
|
26699
|
-
point, not an automatically scheduled workflow.
|
|
26839
|
+
point, not an automatically scheduled workflow. The generated status route is also
|
|
26840
|
+
an explicit read adapter boundary; it does not treat a queue message ID or current
|
|
26841
|
+
business row as proof of task completion.
|
|
26700
26842
|
|
|
26701
26843
|
Compile the routes with the existing client generator and use @supacloud/query for
|
|
26702
|
-
identity-scoped caching/invalidation.
|
|
26844
|
+
identity-scoped caching/invalidation. Use the generated \`${className}ApproveInvalidateTags\`
|
|
26845
|
+
constant for the approval mutation; tags are explicit and never inferred from route names.
|
|
26846
|
+
There is deliberately no second hand-maintained
|
|
26703
26847
|
HTTP client. A committed approval followed by a failed refresh is still committed;
|
|
26704
26848
|
recover its receipt instead of resending. Use the host's standard error translation
|
|
26705
26849
|
and requestId propagation; the scaffold does not claim to implement those adapters.
|
|
@@ -27272,6 +27416,12 @@ function requireFeatureCapability(value, flag) {
|
|
|
27272
27416
|
}
|
|
27273
27417
|
return value;
|
|
27274
27418
|
}
|
|
27419
|
+
function requireFeatureDatabase(value, flag, pattern) {
|
|
27420
|
+
if (typeof value !== "string" || !pattern.test(value)) {
|
|
27421
|
+
throw new ScaffoldError("SCAFFOLD_OPTION_INVALID", `feature requires an explicit valid --${flag}`);
|
|
27422
|
+
}
|
|
27423
|
+
return value;
|
|
27424
|
+
}
|
|
27275
27425
|
function moduleScaffold(name) {
|
|
27276
27426
|
return `import { Module } from "@supacloud/app";
|
|
27277
27427
|
|
|
@@ -27370,7 +27520,12 @@ async function generateScaffold(args) {
|
|
|
27370
27520
|
}
|
|
27371
27521
|
const featureGovernance = kind === "feature" ? {
|
|
27372
27522
|
permission: requireFeatureCapability(args.permission, "permission"),
|
|
27373
|
-
rpc: requireFeatureCapability(args.rpc, "rpc")
|
|
27523
|
+
rpc: requireFeatureCapability(args.rpc, "rpc"),
|
|
27524
|
+
database: {
|
|
27525
|
+
source: requireFeatureDatabase(args.database_source, "database-source", /^(?!\/)(?!.*\\)(?!.*(?:^|\/)\.\.(?:\/|$))[A-Za-z0-9._/-]+$/),
|
|
27526
|
+
table: requireFeatureDatabase(args.database_table, "database-table", /^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/),
|
|
27527
|
+
rowType: requireFeatureDatabase(args.database_type, "database-type", /^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/)
|
|
27528
|
+
}
|
|
27374
27529
|
} : undefined;
|
|
27375
27530
|
const writes = [];
|
|
27376
27531
|
let modulePath;
|
|
@@ -28648,7 +28803,7 @@ Execution: skipped`);
|
|
|
28648
28803
|
return format === "json" || json ? result : formatPromotionExecution(result);
|
|
28649
28804
|
}
|
|
28650
28805
|
async function runAppTool(request, options = {}) {
|
|
28651
|
-
if (request.dry_run !== undefined && request["dry-run"] !== undefined || request.register_in !== undefined && request["register-in"] !== undefined || request.database_url !== undefined && request["database-url"] !== undefined || request.request_id !== undefined && request["request-id"] !== undefined) {
|
|
28806
|
+
if (request.dry_run !== undefined && request["dry-run"] !== undefined || request.register_in !== undefined && request["register-in"] !== undefined || request.database_url !== undefined && request["database-url"] !== undefined || request.database_source !== undefined && request["database-source"] !== undefined || request.database_table !== undefined && request["database-table"] !== undefined || request.database_type !== undefined && request["database-type"] !== undefined || request.request_id !== undefined && request["request-id"] !== undefined) {
|
|
28652
28807
|
throw new ScaffoldError("SCAFFOLD_OPTION_INVALID", "Do not combine hyphenated and underscored aliases of the same flag");
|
|
28653
28808
|
}
|
|
28654
28809
|
request = {
|
|
@@ -28656,6 +28811,9 @@ async function runAppTool(request, options = {}) {
|
|
|
28656
28811
|
...request["dry-run"] === undefined ? {} : { dry_run: request["dry-run"] },
|
|
28657
28812
|
...request["register-in"] === undefined ? {} : { register_in: request["register-in"] },
|
|
28658
28813
|
...request["database-url"] === undefined ? {} : { database_url: request["database-url"] },
|
|
28814
|
+
...request["database-source"] === undefined ? {} : { database_source: request["database-source"] },
|
|
28815
|
+
...request["database-table"] === undefined ? {} : { database_table: request["database-table"] },
|
|
28816
|
+
...request["database-type"] === undefined ? {} : { database_type: request["database-type"] },
|
|
28659
28817
|
...request["request-id"] === undefined ? {} : { request_id: request["request-id"] }
|
|
28660
28818
|
};
|
|
28661
28819
|
if (request.events !== undefined || request.request_id !== undefined) {
|
|
@@ -28831,6 +28989,12 @@ function registerAppTools(server, options = {}) {
|
|
|
28831
28989
|
module: optional(String2(), "[generate] Target feature module (required for command/query/controller)"),
|
|
28832
28990
|
permission: optional(String2(), "[generate feature] Explicit approval permission; not inferred from name"),
|
|
28833
28991
|
rpc: optional(String2(), "[generate feature] Explicit durable RPC capability; must be registered by the host"),
|
|
28992
|
+
database_source: optional(String2(), "[generate feature] Project-relative database source path"),
|
|
28993
|
+
database_table: optional(String2(), "[generate feature] Schema-qualified table owned by the feature"),
|
|
28994
|
+
database_type: optional(String2(), "[generate feature] Exported row type derived from the database source"),
|
|
28995
|
+
"database-source": optional(String2(), "[generate feature] CLI alias of database_source"),
|
|
28996
|
+
"database-table": optional(String2(), "[generate feature] CLI alias of database_table"),
|
|
28997
|
+
"database-type": optional(String2(), "[generate feature] CLI alias of database_type"),
|
|
28834
28998
|
dir: optional(String2(), "[generate] Feature root directory (default: src/features)"),
|
|
28835
28999
|
force: optional(Boolean2(), "[generate] Overwrite a single regular file; resource/feature/controller never overwrite"),
|
|
28836
29000
|
dry_run: optional(Boolean2(), "[generate] Validate and preview the complete change set without writing"),
|
|
@@ -28865,6 +29029,12 @@ function registerAppAliases(server) {
|
|
|
28865
29029
|
module: optional(String2(), "[generate] Target feature module"),
|
|
28866
29030
|
permission: optional(String2(), "[generate feature] Explicit approval permission; not inferred from name"),
|
|
28867
29031
|
rpc: optional(String2(), "[generate feature] Explicit durable RPC capability; must be registered by the host"),
|
|
29032
|
+
database_source: optional(String2(), "[generate feature] Project-relative database source path"),
|
|
29033
|
+
database_table: optional(String2(), "[generate feature] Schema-qualified table owned by the feature"),
|
|
29034
|
+
database_type: optional(String2(), "[generate feature] Exported row type derived from the database source"),
|
|
29035
|
+
"database-source": optional(String2(), "[generate feature] CLI alias of database_source"),
|
|
29036
|
+
"database-table": optional(String2(), "[generate feature] CLI alias of database_table"),
|
|
29037
|
+
"database-type": optional(String2(), "[generate feature] CLI alias of database_type"),
|
|
28868
29038
|
dir: optional(String2(), "[generate] Feature root directory (default: src/features)"),
|
|
28869
29039
|
force: optional(Boolean2(), "[generate] Overwrite a single regular file; resource/feature/controller never overwrite"),
|
|
28870
29040
|
dry_run: optional(Boolean2(), "[generate] Validate and preview the complete change set without writing"),
|
|
@@ -32583,7 +32753,7 @@ async function runProjectDeployment(http, args) {
|
|
|
32583
32753
|
// package.json
|
|
32584
32754
|
var package_default = {
|
|
32585
32755
|
name: "@supacloud/cli",
|
|
32586
|
-
version: "0.
|
|
32756
|
+
version: "0.106.0",
|
|
32587
32757
|
description: "Project-scoped CLI for SupaCloud users",
|
|
32588
32758
|
type: "module",
|
|
32589
32759
|
main: "./dist/index.js",
|
|
@@ -32635,19 +32805,19 @@ var package_default = {
|
|
|
32635
32805
|
directory: "packages/cli"
|
|
32636
32806
|
},
|
|
32637
32807
|
dependencies: {
|
|
32638
|
-
"@supacloud/compiler": "^0.
|
|
32639
|
-
"@supacloud/delivery": "0.
|
|
32640
|
-
"@supacloud/db": "0.
|
|
32808
|
+
"@supacloud/compiler": "^0.50.0",
|
|
32809
|
+
"@supacloud/delivery": "0.21.0",
|
|
32810
|
+
"@supacloud/db": "0.20.0",
|
|
32641
32811
|
typebox: "1.3.37"
|
|
32642
32812
|
},
|
|
32643
32813
|
overrides: {
|
|
32644
|
-
"@supacloud/contracts": "0.
|
|
32645
|
-
"@supacloud/delivery": "0.
|
|
32814
|
+
"@supacloud/contracts": "0.13.0",
|
|
32815
|
+
"@supacloud/delivery": "0.21.0",
|
|
32646
32816
|
"@graphql-tools/utils": "^12.0.4"
|
|
32647
32817
|
},
|
|
32648
32818
|
devDependencies: {
|
|
32649
|
-
"@supabase/supabase-js": "2.
|
|
32650
|
-
"@supacloud/elysia": "0.
|
|
32819
|
+
"@supabase/supabase-js": "2.118.0",
|
|
32820
|
+
"@supacloud/elysia": "0.35.1",
|
|
32651
32821
|
"@types/bun": "^1.4.3",
|
|
32652
32822
|
effect: "4.0.5",
|
|
32653
32823
|
elysia: "2.0.0-beta.32",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -72,6 +72,12 @@ export interface AppToolArguments {
|
|
|
72
72
|
module?: string;
|
|
73
73
|
permission?: string;
|
|
74
74
|
rpc?: string;
|
|
75
|
+
database_source?: string;
|
|
76
|
+
database_table?: string;
|
|
77
|
+
database_type?: string;
|
|
78
|
+
"database-source"?: string;
|
|
79
|
+
"database-table"?: string;
|
|
80
|
+
"database-type"?: string;
|
|
75
81
|
dir?: string;
|
|
76
82
|
force?: boolean;
|
|
77
83
|
dry_run?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.106.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.50.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
|
-
"@supabase/supabase-js": "2.
|
|
67
|
-
"@supacloud/elysia": "0.
|
|
66
|
+
"@supabase/supabase-js": "2.118.0",
|
|
67
|
+
"@supacloud/elysia": "0.35.1",
|
|
68
68
|
"@types/bun": "^1.4.3",
|
|
69
69
|
"effect": "4.0.5",
|
|
70
70
|
"elysia": "2.0.0-beta.32",
|