@supacloud/cli 0.106.1 → 0.106.2

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
@@ -21588,16 +21588,16 @@ var starter_metadata_default = {
21588
21588
  generatedBy: "supacloud/starter-metadata-v1",
21589
21589
  packages: {
21590
21590
  compiler: {
21591
- version: "0.50.0"
21591
+ version: "0.50.1"
21592
21592
  },
21593
21593
  app: {
21594
- version: "0.31.0",
21594
+ version: "0.31.1",
21595
21595
  dependencies: {
21596
21596
  rxjs: "7.8.2"
21597
21597
  }
21598
21598
  },
21599
21599
  elysia: {
21600
- version: "0.35.1"
21600
+ version: "0.35.2"
21601
21601
  },
21602
21602
  commands: {
21603
21603
  version: "0.11.0"
@@ -26254,8 +26254,11 @@ test("${name} controller preserves an asynchronous read failure", async () => {
26254
26254
  function featureScaffold(name, className, governance) {
26255
26255
  const commandName = `${name}.approve`;
26256
26256
  const { permission, rpc, database } = governance;
26257
+ const databaseImportPath = governance.databaseImportPath ?? `../../../${database.source.replace(/\.(?:[cm]?tsx?|mts|cts)$/, "")}`;
26258
+ const rowTypeRoot = database.rowType.split(".")[0];
26257
26259
  return {
26258
26260
  [`${name}.contracts.ts`]: `import { t } from "elysia";
26261
+ import type { ${rowTypeRoot} } from "${databaseImportPath}";
26259
26262
 
26260
26263
  export const ${className}Params = t.Object({
26261
26264
  id: t.String({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9_-]+$" }),
@@ -26271,12 +26274,15 @@ export const ${className}ApproveBody = t.Object({
26271
26274
  expectedVersion: t.Integer({ minimum: 1 }),
26272
26275
  reason: t.Optional(t.String({ maxLength: 2000 })),
26273
26276
  });
26277
+ /** Transport projection; the business result type is derived from the database source below. */
26274
26278
  export const ${className}Row = t.Object({
26275
26279
  id: t.String(),
26276
26280
  state: t.String(),
26277
26281
  version: t.Integer({ minimum: 1 }),
26278
26282
  });
26283
+ export type ${className}DatabaseRow = ${database.rowType};
26279
26284
  export const ${className}Receipt = t.Object({
26285
+ id: t.String({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9_-]+$" }),
26280
26286
  operationId: t.String({ minLength: 1, maxLength: 512 }),
26281
26287
  status: t.Union([
26282
26288
  t.Literal("pending"), t.Literal("confirmed"), t.Literal("failed"), t.Literal("unknown"),
@@ -26285,6 +26291,7 @@ export const ${className}Receipt = t.Object({
26285
26291
  version: t.Integer({ minimum: 1 }),
26286
26292
  });
26287
26293
  export const ${className}JobStatus = t.Object({
26294
+ id: t.String({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9_-]+$" }),
26288
26295
  operationId: t.String({ minLength: 1, maxLength: 512 }),
26289
26296
  status: t.Union([
26290
26297
  t.Literal("queued"), t.Literal("running"), t.Literal("completed"),
@@ -26297,20 +26304,23 @@ export const ${className}QueryTags = {
26297
26304
  list: ${JSON.stringify(`${name}.list`)},
26298
26305
  detail: ${JSON.stringify(`${name}.detail`)},
26299
26306
  receipt: ${JSON.stringify(`${name}.receipt`)},
26307
+ jobStatus: ${JSON.stringify(`${name}.job-status`)},
26300
26308
  } as const;
26301
26309
  export const ${className}ApproveInvalidateTags = [
26302
26310
  ${className}QueryTags.list,
26303
26311
  ${className}QueryTags.detail,
26304
26312
  ${className}QueryTags.receipt,
26313
+ ${className}QueryTags.jobStatus,
26305
26314
  ] as const;
26306
26315
  export const ${className}ListResponse = t.Array(${className}Row);
26307
26316
  export const ${className}RefreshInput = t.Object({
26308
26317
  id: t.String({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9_-]+$" }),
26309
26318
  version: t.Integer({ minimum: 1 }),
26319
+ operationId: t.String({ minLength: 1, maxLength: 512, pattern: "^[A-Za-z0-9._:-]+$" }),
26310
26320
  });
26311
26321
  `,
26312
26322
  [`${name}.model.ts`]: `import {
26313
- InjectionToken, type FeatureRequestContext,
26323
+ InjectionToken, type FeatureJobPort, type FeatureOperationPort, type FeatureRequestContext,
26314
26324
  } from "@supacloud/app/core";
26315
26325
  import type {
26316
26326
  ${className}ApproveBody,
@@ -26318,6 +26328,7 @@ import type {
26318
26328
  ${className}Receipt,
26319
26329
  ${className}ReceiptParams,
26320
26330
  ${className}JobStatus,
26331
+ ${className}DatabaseRow,
26321
26332
  ${className}Row,
26322
26333
  ${className}RefreshInput,
26323
26334
  } from "./${name}.contracts";
@@ -26326,7 +26337,7 @@ import type { StaticDecode } from "typebox";
26326
26337
  export type ${className}ListInput = StaticDecode<typeof ${className}ListQuery>;
26327
26338
  export type ${className}ApproveInput = StaticDecode<typeof ${className}ApproveBody>;
26328
26339
  export type ${className}ApproveCommand = ${className}ApproveInput & { id: string };
26329
- export type ${className}Result = StaticDecode<typeof ${className}Row>;
26340
+ export type ${className}Result = ${className}DatabaseRow;
26330
26341
  export type ${className}ReceiptResult = StaticDecode<typeof ${className}Receipt>;
26331
26342
  export type ${className}RefreshJobInput = StaticDecode<typeof ${className}RefreshInput>;
26332
26343
  export type ${className}JobStatusResult = StaticDecode<typeof ${className}JobStatus>;
@@ -26340,13 +26351,11 @@ export interface ${className}WritePort {
26340
26351
  approve(context: FeatureRequestContext, input: ${className}ApproveCommand): Promise<${className}Result>;
26341
26352
  }
26342
26353
 
26343
- export interface ${className}ReceiptPort {
26344
- lookup(context: FeatureRequestContext, input: { id: string; operationId: string }): Promise<${className}ReceiptResult>;
26345
- }
26354
+ export interface ${className}ReceiptPort extends FeatureOperationPort<${className}ReceiptResult> {}
26346
26355
 
26347
- export interface ${className}JobStatusPort {
26348
- lookup(context: FeatureRequestContext, input: { id: string; operationId: string }): Promise<${className}JobStatusResult>;
26349
- }
26356
+ export interface ${className}JobStatusPort extends FeatureOperationPort<${className}JobStatusResult> {}
26357
+
26358
+ export interface ${className}RefreshPort extends FeatureJobPort<${className}RefreshJobInput> {}
26350
26359
 
26351
26360
  export const ${className}Reader = new InjectionToken<${className}ReadPort>(
26352
26361
  "${name}.reader", { scope: "request" },
@@ -26360,9 +26369,13 @@ export const ${className}Receipts = new InjectionToken<${className}ReceiptPort>(
26360
26369
  export const ${className}JobStatuses = new InjectionToken<${className}JobStatusPort>(
26361
26370
  "${name}.job-statuses", { scope: "request" },
26362
26371
  );
26372
+ export const ${className}Refresher = new InjectionToken<${className}RefreshPort>(
26373
+ "${name}.refresher", { scope: "job" },
26374
+ );
26363
26375
  `,
26364
26376
  [`${name}.service.ts`]: `import {
26365
- Inject, Injectable, Optional, REQUEST_CONTEXT, requireFeatureRequestContext,
26377
+ FeatureAdapterError, Inject, Injectable, Optional, REQUEST_CONTEXT, requireFeatureOperationResult,
26378
+ requireFeatureRequestContext, requireFeatureTransitionResult,
26366
26379
  type FeatureRequestContext,
26367
26380
  } from "@supacloud/app/core";
26368
26381
  import {
@@ -26394,35 +26407,44 @@ export class ${className}Service {
26394
26407
  }
26395
26408
 
26396
26409
  async list(input: Parameters<${className}ReadPort["list"]>[1]): Promise<${className}Result[]> {
26397
- if (!this.reader) throw new Error("Bind ${className}Reader with verified identity and tenant scope");
26410
+ if (!this.reader) throw new FeatureAdapterError("${name}.reader");
26398
26411
  return this.reader.list(this.context(), input);
26399
26412
  }
26400
26413
 
26401
26414
  async detail(id: string): Promise<${className}Result> {
26402
- if (!this.reader) throw new Error("Bind ${className}Reader with verified identity and tenant scope");
26415
+ if (!this.reader) throw new FeatureAdapterError("${name}.reader");
26403
26416
  return this.reader.detail(this.context(), id);
26404
26417
  }
26405
26418
 
26406
26419
  async approve(id: string, input: ${className}ApproveInput): Promise<${className}Result> {
26407
- if (!this.writer) throw new Error("Bind ${className}Writer with durable command governance");
26408
- return this.writer.approve(this.context(), { ...input, id });
26420
+ if (!this.writer) throw new FeatureAdapterError("${name}.writer");
26421
+ return requireFeatureTransitionResult(
26422
+ id,
26423
+ "approved",
26424
+ input.expectedVersion,
26425
+ await this.writer.approve(this.context(), { ...input, id }),
26426
+ );
26409
26427
  }
26410
26428
 
26411
26429
  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 });
26430
+ if (!this.receipts) throw new FeatureAdapterError("${name}.receipts");
26431
+ const input = { id, operationId };
26432
+ // Keep the comparison identity separate from mutable adapter arguments.
26433
+ return requireFeatureOperationResult(input, await this.receipts.lookup(this.context(), { ...input }));
26414
26434
  }
26415
26435
 
26416
26436
  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 });
26437
+ if (!this.jobStatuses) throw new FeatureAdapterError("${name}.job-statuses");
26438
+ const input = { id, operationId };
26439
+ // Keep the comparison identity separate from mutable adapter arguments.
26440
+ return requireFeatureOperationResult(input, await this.jobStatuses.lookup(this.context(), { ...input }));
26419
26441
  }
26420
26442
  }
26421
26443
  `,
26422
26444
  [`${name}.feature.ts`]: `import { Effect } from "effect";
26423
26445
  import {
26424
- Body, Command, Controller, defineFeatureSlice, defineFeatureSpec, Get, Inject, JOB_CONTEXT,
26425
- Injectable, Job, Param, Post, Query, requireFeatureRequestContext, type FeatureRequestContext,
26446
+ Body, Command, Controller, defineFeatureSlice, defineFeatureSpec, FeatureAdapterError, Get, Inject, JOB_CONTEXT,
26447
+ Injectable, Job, Optional, Param, Post, Query, requireFeatureJobContext, type FeatureRequestContext,
26426
26448
  type FeatureState,
26427
26449
  } from "@supacloud/app/core";
26428
26450
  import {
@@ -26436,12 +26458,14 @@ import {
26436
26458
  ${className}RefreshInput,
26437
26459
  ${className}Row,
26438
26460
  } from "./${name}.contracts";
26461
+ import { ${className}Refresher } from "./${name}.model";
26439
26462
  import { ${className}Service } from "./${name}.service";
26440
26463
  import type {
26441
26464
  ${className}ApproveInput,
26442
26465
  ${className}ListInput,
26443
26466
  ${className}ReceiptResult,
26444
26467
  ${className}JobStatusResult,
26468
+ ${className}RefreshPort,
26445
26469
  ${className}RefreshJobInput,
26446
26470
  ${className}Result,
26447
26471
  } from "./${name}.model";
@@ -26461,6 +26485,7 @@ export const featureSpec = defineFeatureSpec({
26461
26485
  ${JSON.stringify(`${name}.list`)},
26462
26486
  ${JSON.stringify(`${name}.detail`)},
26463
26487
  ${JSON.stringify(`${name}.receipt`)},
26488
+ ${JSON.stringify(`${name}.job-status`)},
26464
26489
  ],
26465
26490
  persistence: {
26466
26491
  boundary: "database", optimisticLock: "required", versionField: "version", tenantField: "tenant_id", stateField: "state",
@@ -26471,6 +26496,39 @@ export const featureSpec = defineFeatureSpec({
26471
26496
  },
26472
26497
  });
26473
26498
  export type ${className}State = FeatureState<typeof featureSpec>;
26499
+ type ${className}ListError =
26500
+ | { readonly _tag: "AUTHENTICATION_REQUIRED" }
26501
+ | { readonly _tag: "FEATURE_CONTEXT_REQUIRED" }
26502
+ | { readonly _tag: "PERMISSION_DENIED" }
26503
+ | { readonly _tag: "FEATURE_NOT_FOUND" }
26504
+ | { readonly _tag: "FEATURE_ADAPTER_UNAVAILABLE" };
26505
+ type ${className}ApproveError =
26506
+ | { readonly _tag: "AUTHENTICATION_REQUIRED" }
26507
+ | { readonly _tag: "FEATURE_CONTEXT_REQUIRED" }
26508
+ | { readonly _tag: "PERMISSION_DENIED" }
26509
+ | { readonly _tag: "FEATURE_TRANSITION_CONFLICT" }
26510
+ | { readonly _tag: "FEATURE_ENTITY_RESULT_MISMATCH" }
26511
+ | { readonly _tag: "VERSION_CONFLICT" }
26512
+ | { readonly _tag: "COMMAND_IDEMPOTENCY_CONFLICT" }
26513
+ | { readonly _tag: "COMMAND_OUTCOME_UNKNOWN" }
26514
+ | { readonly _tag: "FEATURE_ADAPTER_UNAVAILABLE" };
26515
+ type ${className}ReceiptError =
26516
+ | { readonly _tag: "AUTHENTICATION_REQUIRED" }
26517
+ | { readonly _tag: "FEATURE_CONTEXT_REQUIRED" }
26518
+ | { readonly _tag: "PERMISSION_DENIED" }
26519
+ | { readonly _tag: "FEATURE_NOT_FOUND" }
26520
+ | { readonly _tag: "COMMAND_RECEIPT_INVALID" }
26521
+ | { readonly _tag: "FEATURE_OPERATION_RESULT_MISMATCH" }
26522
+ | { readonly _tag: "COMMAND_OUTCOME_UNKNOWN" }
26523
+ | { readonly _tag: "FEATURE_ADAPTER_UNAVAILABLE" };
26524
+ type ${className}JobStatusError =
26525
+ | { readonly _tag: "AUTHENTICATION_REQUIRED" }
26526
+ | { readonly _tag: "FEATURE_CONTEXT_REQUIRED" }
26527
+ | { readonly _tag: "PERMISSION_DENIED" }
26528
+ | { readonly _tag: "FEATURE_NOT_FOUND" }
26529
+ | { readonly _tag: "FEATURE_OPERATION_RESULT_MISMATCH" }
26530
+ | { readonly _tag: "COMMAND_OUTCOME_UNKNOWN" }
26531
+ | { readonly _tag: "FEATURE_ADAPTER_UNAVAILABLE" };
26474
26532
 
26475
26533
  @Injectable({ scope: "request" })
26476
26534
  @Query({ name: "${name}.list", tenantScope: "required" })
@@ -26506,12 +26564,15 @@ export class Approve${className}Command {
26506
26564
  tenantScope: "required",
26507
26565
  })
26508
26566
  export class Refresh${className}Job {
26509
- constructor(@Inject(JOB_CONTEXT) private readonly jobContext: unknown) {}
26567
+ constructor(
26568
+ @Inject(JOB_CONTEXT) private readonly jobContext: unknown,
26569
+ @Inject(${className}Refresher) @Optional() private readonly refresher?: ${className}RefreshPort,
26570
+ ) {}
26510
26571
 
26511
- async run(_input: ${className}RefreshJobInput): Promise<void> {
26512
- const context: FeatureRequestContext = requireFeatureRequestContext(this.jobContext);
26513
- void context;
26514
- throw new Error("Bind the existing Workflow/Queue adapter before dispatching ${name}.refresh");
26572
+ async run(input: ${className}RefreshJobInput): Promise<void> {
26573
+ const context: FeatureRequestContext = requireFeatureJobContext(this.jobContext, input.operationId);
26574
+ if (!this.refresher) throw new FeatureAdapterError("${name}.refresher");
26575
+ await this.refresher.run(context, input);
26515
26576
  }
26516
26577
  }
26517
26578
 
@@ -26527,9 +26588,18 @@ export class ${className}Controller {
26527
26588
  query: ${className}ListQuery,
26528
26589
  tags: [${JSON.stringify(`${name}.list`)}],
26529
26590
  responses: { 200: ${className}ListResponse },
26530
- effect: { required: true, dependencies: [], errors: [], retry: "none" },
26591
+ effect: {
26592
+ required: true, dependencies: [], retry: "none",
26593
+ errors: [
26594
+ { tag: "AUTHENTICATION_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
26595
+ { tag: "FEATURE_CONTEXT_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
26596
+ { tag: "PERMISSION_DENIED", status: 403, code: "PERMISSION_DENIED" },
26597
+ { tag: "FEATURE_NOT_FOUND", status: 404, code: "FEATURE_NOT_FOUND" },
26598
+ { tag: "FEATURE_ADAPTER_UNAVAILABLE", status: 503, code: "FEATURE_ADAPTER_UNAVAILABLE" },
26599
+ ],
26600
+ },
26531
26601
  })
26532
- list(@Query("state") state?: ${className}State): Effect.Effect<${className}Result[], never, never> {
26602
+ list(@Query("state") state?: ${className}State): Effect.Effect<${className}Result[], ${className}ListError, never> {
26533
26603
  return Effect.promise(() => this.listQuery.execute(state === undefined ? {} : { state }));
26534
26604
  }
26535
26605
 
@@ -26537,9 +26607,18 @@ export class ${className}Controller {
26537
26607
  params: ${className}Params,
26538
26608
  tags: [${JSON.stringify(`${name}.detail`)}],
26539
26609
  responses: { 200: ${className}Row },
26540
- effect: { required: true, dependencies: [], errors: [], retry: "none" },
26610
+ effect: {
26611
+ required: true, dependencies: [], retry: "none",
26612
+ errors: [
26613
+ { tag: "AUTHENTICATION_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
26614
+ { tag: "FEATURE_CONTEXT_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
26615
+ { tag: "PERMISSION_DENIED", status: 403, code: "PERMISSION_DENIED" },
26616
+ { tag: "FEATURE_NOT_FOUND", status: 404, code: "FEATURE_NOT_FOUND" },
26617
+ { tag: "FEATURE_ADAPTER_UNAVAILABLE", status: 503, code: "FEATURE_ADAPTER_UNAVAILABLE" },
26618
+ ],
26619
+ },
26541
26620
  })
26542
- detail(@Param("id") id: string): Effect.Effect<${className}Result, never, never> {
26621
+ detail(@Param("id") id: string): Effect.Effect<${className}Result, ${className}ListError, never> {
26543
26622
  return Effect.promise(() => this.service.detail(id));
26544
26623
  }
26545
26624
 
@@ -26549,12 +26628,25 @@ export class ${className}Controller {
26549
26628
  responses: { 200: ${className}Row },
26550
26629
  command: Approve${className}Command,
26551
26630
  idempotency: "required",
26552
- effect: { required: true, dependencies: [], errors: [], retry: "none" },
26631
+ effect: {
26632
+ required: true, dependencies: [], retry: "none",
26633
+ errors: [
26634
+ { tag: "AUTHENTICATION_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
26635
+ { tag: "FEATURE_CONTEXT_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
26636
+ { tag: "PERMISSION_DENIED", status: 403, code: "PERMISSION_DENIED" },
26637
+ { tag: "FEATURE_TRANSITION_CONFLICT", status: 409, code: "FEATURE_TRANSITION_CONFLICT" },
26638
+ { tag: "FEATURE_ENTITY_RESULT_MISMATCH", status: 503, code: "FEATURE_ENTITY_RESULT_MISMATCH" },
26639
+ { tag: "VERSION_CONFLICT", status: 409, code: "VERSION_CONFLICT" },
26640
+ { tag: "COMMAND_IDEMPOTENCY_CONFLICT", status: 409, code: "COMMAND_IDEMPOTENCY_CONFLICT" },
26641
+ { tag: "COMMAND_OUTCOME_UNKNOWN", status: 503, code: "COMMAND_OUTCOME_UNKNOWN" },
26642
+ { tag: "FEATURE_ADAPTER_UNAVAILABLE", status: 503, code: "FEATURE_ADAPTER_UNAVAILABLE" },
26643
+ ],
26644
+ },
26553
26645
  })
26554
26646
  approve(
26555
26647
  @Param("id") id: string,
26556
26648
  @Body() body: ${className}ApproveInput,
26557
- ): Effect.Effect<${className}Result, never, never> {
26649
+ ): Effect.Effect<${className}Result, ${className}ApproveError, never> {
26558
26650
  return Effect.promise(() => this.approveCommand.execute(id, body));
26559
26651
  }
26560
26652
 
@@ -26562,12 +26654,24 @@ export class ${className}Controller {
26562
26654
  params: ${className}ReceiptParams,
26563
26655
  tags: [${JSON.stringify(`${name}.receipt`)}],
26564
26656
  responses: { 200: ${className}Receipt },
26565
- effect: { required: true, dependencies: [], errors: [], retry: "none" },
26657
+ effect: {
26658
+ required: true, dependencies: [], retry: "none",
26659
+ errors: [
26660
+ { tag: "AUTHENTICATION_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
26661
+ { tag: "FEATURE_CONTEXT_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
26662
+ { tag: "PERMISSION_DENIED", status: 403, code: "PERMISSION_DENIED" },
26663
+ { tag: "FEATURE_NOT_FOUND", status: 404, code: "FEATURE_NOT_FOUND" },
26664
+ { tag: "COMMAND_RECEIPT_INVALID", status: 503, code: "COMMAND_RECEIPT_INVALID" },
26665
+ { tag: "FEATURE_OPERATION_RESULT_MISMATCH", status: 503, code: "FEATURE_OPERATION_RESULT_MISMATCH" },
26666
+ { tag: "COMMAND_OUTCOME_UNKNOWN", status: 503, code: "COMMAND_OUTCOME_UNKNOWN" },
26667
+ { tag: "FEATURE_ADAPTER_UNAVAILABLE", status: 503, code: "FEATURE_ADAPTER_UNAVAILABLE" },
26668
+ ],
26669
+ },
26566
26670
  })
26567
26671
  receipt(
26568
26672
  @Param("id") id: string,
26569
26673
  @Param("operationId") operationId: string,
26570
- ): Effect.Effect<${className}ReceiptResult, never, never> {
26674
+ ): Effect.Effect<${className}ReceiptResult, ${className}ReceiptError, never> {
26571
26675
  return Effect.promise(() => this.service.receipt(id, operationId));
26572
26676
  }
26573
26677
 
@@ -26575,12 +26679,23 @@ export class ${className}Controller {
26575
26679
  params: ${className}ReceiptParams,
26576
26680
  tags: [${JSON.stringify(`${name}.job-status`)}],
26577
26681
  responses: { 200: ${className}JobStatus },
26578
- effect: { required: true, dependencies: [], errors: [], retry: "none" },
26682
+ effect: {
26683
+ required: true, dependencies: [], retry: "none",
26684
+ errors: [
26685
+ { tag: "AUTHENTICATION_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
26686
+ { tag: "FEATURE_CONTEXT_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
26687
+ { tag: "PERMISSION_DENIED", status: 403, code: "PERMISSION_DENIED" },
26688
+ { tag: "FEATURE_NOT_FOUND", status: 404, code: "FEATURE_NOT_FOUND" },
26689
+ { tag: "FEATURE_OPERATION_RESULT_MISMATCH", status: 503, code: "FEATURE_OPERATION_RESULT_MISMATCH" },
26690
+ { tag: "COMMAND_OUTCOME_UNKNOWN", status: 503, code: "COMMAND_OUTCOME_UNKNOWN" },
26691
+ { tag: "FEATURE_ADAPTER_UNAVAILABLE", status: 503, code: "FEATURE_ADAPTER_UNAVAILABLE" },
26692
+ ],
26693
+ },
26579
26694
  })
26580
26695
  jobStatus(
26581
26696
  @Param("id") id: string,
26582
26697
  @Param("operationId") operationId: string,
26583
- ): Effect.Effect<${className}JobStatusResult, never, never> {
26698
+ ): Effect.Effect<${className}JobStatusResult, ${className}JobStatusError, never> {
26584
26699
  return Effect.promise(() => this.service.jobStatus(id, operationId));
26585
26700
  }
26586
26701
  }
@@ -26597,7 +26712,9 @@ export const ${className}Feature = defineFeatureSlice({
26597
26712
  [`${name}.feature.test.ts`]: `import { expect, test } from "bun:test";
26598
26713
  import { Refresh${className}Job } from "./${name}.feature";
26599
26714
  import { ${className}Service } from "./${name}.service";
26600
- import { ${className}Fixture, ${className}RequestContext, ${className}RequestEnvelope } from "./${name}.fixture";
26715
+ import {
26716
+ ${className}Fixture, ${className}JobRequestEnvelope, ${className}RequestContext, ${className}RequestEnvelope,
26717
+ } from "./${name}.fixture";
26601
26718
 
26602
26719
  test("${name} rejects missing trusted tenant context before adapter execution", async () => {
26603
26720
  let calls = 0;
@@ -26611,28 +26728,54 @@ test("${name} rejects missing trusted tenant context before adapter execution",
26611
26728
 
26612
26729
  test("${name} rejects missing trusted job context before dispatch", async () => {
26613
26730
  const job = new Refresh${className}Job({});
26614
- await expect(job.run({ id: "example", version: 1 })).rejects.toThrow("FEATURE_CONTEXT_REQUIRED");
26731
+ await expect(job.run({ id: "example", version: 1, operationId: "operation-example" }))
26732
+ .rejects.toThrow("FEATURE_CONTEXT_REQUIRED");
26733
+ });
26734
+
26735
+ test("${name} rejects a job context correlated to a different operation", async () => {
26736
+ const job = new Refresh${className}Job({
26737
+ ...${className}RequestEnvelope,
26738
+ idempotencyKey: "foreign-operation",
26739
+ });
26740
+ await expect(job.run({ id: "example", version: 1, operationId: "operation-example" }))
26741
+ .rejects.toMatchObject({ _tag: "FEATURE_JOB_OPERATION_MISMATCH" });
26742
+ });
26743
+
26744
+ test("${name} runs the refresh adapter with trusted tenant context", async () => {
26745
+ let received: unknown;
26746
+ const job = new Refresh${className}Job(${className}JobRequestEnvelope, {
26747
+ async run(context, input) {
26748
+ received = { context, input };
26749
+ },
26750
+ });
26751
+ await expect(job.run({ id: "example", version: 1, operationId: "operation-example" }))
26752
+ .resolves.toBeUndefined();
26753
+ expect(received).toEqual({
26754
+ context: { ...${className}RequestContext, idempotencyKey: "operation-example" },
26755
+ input: { id: "example", version: 1, operationId: "operation-example" },
26756
+ });
26615
26757
  });
26616
26758
 
26617
26759
  test("${name} fails closed until verified read and durable write ports are bound", async () => {
26618
26760
  const service = new ${className}Service(${className}RequestEnvelope);
26619
- await expect(service.list({})).rejects.toThrow("Bind ${className}Reader");
26620
- await expect(service.detail("example")).rejects.toThrow("Bind ${className}Reader");
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");
26761
+ await expect(service.list({})).rejects.toThrow("FEATURE_ADAPTER_UNAVAILABLE");
26762
+ await expect(service.detail("example")).rejects.toThrow("FEATURE_ADAPTER_UNAVAILABLE");
26763
+ await expect(service.approve("example", { expectedVersion: 1 })).rejects.toThrow("FEATURE_ADAPTER_UNAVAILABLE");
26764
+ await expect(service.jobStatus("example", "operation-example")).rejects.toThrow("FEATURE_ADAPTER_UNAVAILABLE");
26623
26765
  });
26624
26766
 
26625
26767
  test("${name} preserves denied reads and rejected writes without fallback or retry", async () => {
26626
26768
  const denied = new Error("denied");
26627
26769
  let attempts = 0;
26628
- const service = new ${className}Service(${className}RequestEnvelope, {
26770
+ const service = new ${className}Service(${className}RequestEnvelope, undefined, {
26771
+ approve: async () => { attempts++; throw denied; },
26772
+ });
26773
+ const readService = new ${className}Service(${className}RequestEnvelope, {
26629
26774
  list: async () => { throw denied; },
26630
26775
  detail: async () => { throw denied; },
26631
- }, {
26632
- approve: async () => { attempts++; throw denied; },
26633
26776
  });
26634
- await expect(service.list({})).rejects.toBe(denied);
26635
- await expect(service.detail("example")).rejects.toBe(denied);
26777
+ await expect(readService.list({})).rejects.toBe(denied);
26778
+ await expect(readService.detail("example")).rejects.toBe(denied);
26636
26779
  await expect(service.approve("example", { expectedVersion: 1 })).rejects.toBe(denied);
26637
26780
  expect(attempts).toBe(1);
26638
26781
  });
@@ -26648,6 +26791,78 @@ test("${name} forwards the authoritative version and result without inventing pe
26648
26791
  });
26649
26792
  expect(await service.approve(${className}Fixture.id, ${className}Fixture.approve)).toBe(row);
26650
26793
  });
26794
+
26795
+ test("${name} rejects a writer result that does not prove the declared transition", async () => {
26796
+ const service = new ${className}Service(${className}RequestEnvelope, undefined, {
26797
+ approve: async () => ({ id: "example", state: "draft", version: 2 }),
26798
+ });
26799
+ await expect(service.approve("example", { expectedVersion: 1 }))
26800
+ .rejects.toMatchObject({ _tag: "FEATURE_TRANSITION_CONFLICT" });
26801
+ const stale = new ${className}Service(${className}RequestEnvelope, undefined, {
26802
+ approve: async () => ({ id: "example", state: "approved", version: 1 }),
26803
+ });
26804
+ await expect(stale.approve("example", { expectedVersion: 1 }))
26805
+ .rejects.toMatchObject({ _tag: "VERSION_CONFLICT" });
26806
+ const foreign = new ${className}Service(${className}RequestEnvelope, undefined, {
26807
+ approve: async () => ({ id: "foreign", state: "approved", version: 2 }),
26808
+ });
26809
+ await expect(foreign.approve("example", { expectedVersion: 1 }))
26810
+ .rejects.toMatchObject({ _tag: "FEATURE_ENTITY_RESULT_MISMATCH" });
26811
+ });
26812
+
26813
+ test("${name} rejects operation lookup results for a different operation", async () => {
26814
+ const service = new ${className}Service(${className}RequestEnvelope, undefined, undefined, {
26815
+ lookup: async () => ({
26816
+ id: "example", operationId: "foreign-operation", status: "confirmed", state: "approved", version: 2,
26817
+ }),
26818
+ }, {
26819
+ lookup: async () => ({ id: "example", operationId: "foreign-operation", status: "completed" }),
26820
+ });
26821
+ await expect(service.receipt("example", "operation-example"))
26822
+ .rejects.toThrow("FEATURE_OPERATION_RESULT_MISMATCH");
26823
+ await expect(service.jobStatus("example", "operation-example"))
26824
+ .rejects.toThrow("FEATURE_OPERATION_RESULT_MISMATCH");
26825
+ });
26826
+
26827
+
26828
+ for (const field of ["id", "operationId"] as const) {
26829
+ test("${name} isolates lookup identity when an adapter rewrites " + field, async () => {
26830
+ let calls = 0;
26831
+ const service = new ${className}Service(${className}RequestEnvelope, undefined, undefined, {
26832
+ async lookup(_context, input) {
26833
+ calls++;
26834
+ Object.assign(input, { [field]: "foreign" });
26835
+ return { ...input, status: "confirmed", state: "approved", version: 2 };
26836
+ },
26837
+ }, {
26838
+ async lookup(_context, input) {
26839
+ calls++;
26840
+ Object.assign(input, { [field]: "foreign" });
26841
+ return { ...input, status: "completed" };
26842
+ },
26843
+ });
26844
+ await expect(service.receipt("example", "operation-example"))
26845
+ .rejects.toMatchObject({ _tag: "FEATURE_OPERATION_RESULT_MISMATCH" });
26846
+ await expect(service.jobStatus("example", "operation-example"))
26847
+ .rejects.toMatchObject({ _tag: "FEATURE_OPERATION_RESULT_MISMATCH" });
26848
+ expect(calls).toBe(2);
26849
+ });
26850
+ }
26851
+
26852
+ test("${name} preserves matching lookup results and does not replay failures", async () => {
26853
+ const receipt = { id: "example", operationId: "operation-example", status: "unknown" as const, state: "draft", version: 1 };
26854
+ const status = { id: "example", operationId: "operation-example", status: "queued" as const };
26855
+ let calls = 0;
26856
+ const service = new ${className}Service(${className}RequestEnvelope, undefined, undefined, {
26857
+ async lookup() { calls++; return receipt; },
26858
+ }, {
26859
+ async lookup() { calls++; return status; },
26860
+ });
26861
+ expect(await service.receipt("example", "operation-example")).toBe(receipt);
26862
+ expect(await service.jobStatus("example", "operation-example")).toBe(status);
26863
+ expect(calls).toBe(2);
26864
+ });
26865
+
26651
26866
  `,
26652
26867
  [`${name}.fixture.ts`]: `import type { ${className}ApproveInput, ${className}Result } from "./${name}.model";
26653
26868
 
@@ -26673,6 +26888,11 @@ export const ${className}RequestEnvelope = {
26673
26888
  identity: { authenticated: true, subject: "reviewer-example" },
26674
26889
  access: { tenantId: "tenant-example", permissions: [${JSON.stringify(permission)}] },
26675
26890
  } as const satisfies Record<string, unknown>;
26891
+
26892
+ export const ${className}JobRequestEnvelope = {
26893
+ ...${className}RequestEnvelope,
26894
+ idempotencyKey: "operation-example",
26895
+ } as const satisfies Record<string, unknown>;
26676
26896
  `,
26677
26897
  [`${name}.e2e.test.ts`]: `import { expect, test } from "bun:test";
26678
26898
  import { ${className}Fixture } from "./${name}.fixture";
@@ -26845,8 +27065,12 @@ identity-scoped caching/invalidation. Use the generated \`${className}ApproveInv
26845
27065
  constant for the approval mutation; tags are explicit and never inferred from route names.
26846
27066
  There is deliberately no second hand-maintained
26847
27067
  HTTP client. A committed approval followed by a failed refresh is still committed;
26848
- recover its receipt instead of resending. Use the host's standard error translation
26849
- and requestId propagation; the scaffold does not claim to implement those adapters.
27068
+ recover its receipt instead of resending. Host domain errors must carry a stable
27069
+ _tag, tag, code or error name matching the declared route mapping; an Error message
27070
+ alone is not a machine-readable code. Context failures map to authentication-required,
27071
+ and foreign operation results fail with FEATURE_OPERATION_RESULT_MISMATCH on both
27072
+ lookup routes. Unknown failures remain generic server errors without exposing messages.
27073
+ RequestId propagation and durable adapter implementation remain host responsibilities.
26850
27074
 
26851
27075
  ## Required acceptance (not executed by generation)
26852
27076
 
@@ -27538,7 +27762,13 @@ async function generateScaffold(args) {
27538
27762
  if (kind === "module")
27539
27763
  writes.push({ path: modulePath, content: moduleScaffold(name), overwrite: args.force === true });
27540
27764
  else
27541
- for (const [file, content] of Object.entries(kind === "feature" ? featureScaffold(name, pascalName(name), featureGovernance) : resourceScaffold(name, pascalName(name)))) {
27765
+ for (const [file, content] of Object.entries(kind === "feature" ? featureScaffold(name, pascalName(name), {
27766
+ ...featureGovernance,
27767
+ databaseImportPath: (() => {
27768
+ const source = relative7(join13(root, dir, name), join13(root, featureGovernance.database.source)).replaceAll("\\", "/").replace(/\.(?:[cm]?tsx?|mts|cts)$/, "");
27769
+ return source.startsWith(".") ? source : `./${source}`;
27770
+ })()
27771
+ }) : resourceScaffold(name, pascalName(name)))) {
27542
27772
  writes.push({ path: join13(dir, name, file).replaceAll("\\", "/"), content });
27543
27773
  }
27544
27774
  } else {
@@ -32753,7 +32983,7 @@ async function runProjectDeployment(http, args) {
32753
32983
  // package.json
32754
32984
  var package_default = {
32755
32985
  name: "@supacloud/cli",
32756
- version: "0.106.1",
32986
+ version: "0.106.2",
32757
32987
  description: "Project-scoped CLI for SupaCloud users",
32758
32988
  type: "module",
32759
32989
  main: "./dist/index.js",
@@ -32805,7 +33035,7 @@ var package_default = {
32805
33035
  directory: "packages/cli"
32806
33036
  },
32807
33037
  dependencies: {
32808
- "@supacloud/compiler": "^0.50.0",
33038
+ "@supacloud/compiler": "^0.50.1",
32809
33039
  "@supacloud/delivery": "0.21.0",
32810
33040
  "@supacloud/db": "0.20.0",
32811
33041
  typebox: "1.3.37"
@@ -32817,7 +33047,7 @@ var package_default = {
32817
33047
  },
32818
33048
  devDependencies: {
32819
33049
  "@supabase/supabase-js": "2.118.0",
32820
- "@supacloud/elysia": "0.35.1",
33050
+ "@supacloud/elysia": "0.35.2",
32821
33051
  "@types/bun": "^1.4.3",
32822
33052
  effect: "4.0.5",
32823
33053
  elysia: "2.0.0-beta.32",
@@ -10,4 +10,5 @@ export declare function featureScaffold(name: string, className: string, governa
10
10
  table: string;
11
11
  rowType: string;
12
12
  };
13
+ databaseImportPath?: string;
13
14
  }): Record<string, string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.106.1",
3
+ "version": "0.106.2",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -52,7 +52,7 @@
52
52
  "directory": "packages/cli"
53
53
  },
54
54
  "dependencies": {
55
- "@supacloud/compiler": "^0.50.0",
55
+ "@supacloud/compiler": "^0.50.1",
56
56
  "@supacloud/delivery": "0.21.0",
57
57
  "@supacloud/db": "0.20.0",
58
58
  "typebox": "1.3.37"
@@ -64,7 +64,7 @@
64
64
  },
65
65
  "devDependencies": {
66
66
  "@supabase/supabase-js": "2.118.0",
67
- "@supacloud/elysia": "0.35.1",
67
+ "@supacloud/elysia": "0.35.2",
68
68
  "@types/bun": "^1.4.3",
69
69
  "effect": "4.0.5",
70
70
  "elysia": "2.0.0-beta.32",