@supacloud/cli 0.99.0 → 0.101.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
@@ -3177,23 +3177,22 @@ function InstanceKeys(value) {
3177
3177
  function IsIEEE754(value) {
3178
3178
  return typeof value === "number";
3179
3179
  }
3180
- var ByteMarker;
3181
- (function(ByteMarker) {
3182
- ByteMarker[ByteMarker["Array"] = 0] = "Array";
3183
- ByteMarker[ByteMarker["BigInt"] = 1] = "BigInt";
3184
- ByteMarker[ByteMarker["Boolean"] = 2] = "Boolean";
3185
- ByteMarker[ByteMarker["Date"] = 3] = "Date";
3186
- ByteMarker[ByteMarker["Constructor"] = 4] = "Constructor";
3187
- ByteMarker[ByteMarker["Function"] = 5] = "Function";
3188
- ByteMarker[ByteMarker["Null"] = 6] = "Null";
3189
- ByteMarker[ByteMarker["Number"] = 7] = "Number";
3190
- ByteMarker[ByteMarker["Object"] = 8] = "Object";
3191
- ByteMarker[ByteMarker["RegExp"] = 9] = "RegExp";
3192
- ByteMarker[ByteMarker["String"] = 10] = "String";
3193
- ByteMarker[ByteMarker["Symbol"] = 11] = "Symbol";
3194
- ByteMarker[ByteMarker["TypeArray"] = 12] = "TypeArray";
3195
- ByteMarker[ByteMarker["Undefined"] = 13] = "Undefined";
3196
- })(ByteMarker || (ByteMarker = {}));
3180
+ var ByteMarker = {
3181
+ Array: 0,
3182
+ BigInt: 1,
3183
+ Boolean: 2,
3184
+ Date: 3,
3185
+ Constructor: 4,
3186
+ Function: 5,
3187
+ Null: 6,
3188
+ Number: 7,
3189
+ Object: 8,
3190
+ RegExp: 9,
3191
+ String: 10,
3192
+ Symbol: 11,
3193
+ TypeArray: 12,
3194
+ Undefined: 13
3195
+ };
3197
3196
  var Accumulator = BigInt("14695981039346656037");
3198
3197
  var [Prime, Size] = [BigInt("1099511628211"), BigInt("18446744073709551616")];
3199
3198
  var Bytes = Array.from({ length: 256 }).map((_, i) => BigInt(i));
@@ -15196,7 +15195,7 @@ function scaffoldFiles(framework, slug) {
15196
15195
  "package.json": JSON.stringify({
15197
15196
  private: true,
15198
15197
  type: "module",
15199
- dependencies: { elysia: "2.0.0-beta.21" }
15198
+ dependencies: { elysia: "2.0.0-beta.32" }
15200
15199
  }, null, 2) + `
15201
15200
  `,
15202
15201
  "index.ts": `import { Elysia } from "elysia";
@@ -21389,34 +21388,34 @@ var starter_metadata_default = {
21389
21388
  generatedBy: "supacloud/starter-metadata-v1",
21390
21389
  packages: {
21391
21390
  compiler: {
21392
- version: "0.46.1"
21391
+ version: "0.46.3"
21393
21392
  },
21394
21393
  app: {
21395
- version: "0.27.0",
21394
+ version: "0.27.1",
21396
21395
  dependencies: {
21397
21396
  rxjs: "7.8.2"
21398
21397
  }
21399
21398
  },
21400
21399
  elysia: {
21401
- version: "0.30.0"
21400
+ version: "0.31.0"
21402
21401
  },
21403
21402
  commands: {
21404
- version: "0.9.2"
21403
+ version: "0.9.3"
21405
21404
  },
21406
21405
  contracts: {
21407
21406
  version: "0.9.0"
21408
21407
  },
21409
21408
  db: {
21410
- version: "0.17.0",
21409
+ version: "0.17.1",
21411
21410
  peerDependencies: {
21412
- "drizzle-orm": "1.0.0-rc.5-169397b"
21411
+ "drizzle-orm": "1.0.0-rc.5-5935859"
21413
21412
  },
21414
21413
  devDependencies: {
21415
- "drizzle-kit": "1.0.0-rc.5-ab785fc"
21414
+ "drizzle-kit": "1.0.0-rc.5-5935859"
21416
21415
  }
21417
21416
  },
21418
21417
  sdk: {
21419
- version: "0.42.0",
21418
+ version: "0.43.0",
21420
21419
  peerDependencies: {
21421
21420
  "@supabase/supabase-js": "^2.117.3"
21422
21421
  }
@@ -21759,8 +21758,8 @@ function baseFiles(name, sdkDependencies) {
21759
21758
  dependencies: {
21760
21759
  "@supacloud/app": `^${appMetadata.version}`,
21761
21760
  "@supacloud/elysia": `^${elysiaMetadata.version}`,
21762
- effect: "4.0.2",
21763
- elysia: "2.0.0-beta.21",
21761
+ effect: "4.0.5",
21762
+ elysia: "2.0.0-beta.32",
21764
21763
  ...sdkDependencies ?? { rxjs: appMetadata.dependencies.rxjs }
21765
21764
  },
21766
21765
  devDependencies: {
@@ -22958,20 +22957,21 @@ ALTER TABLE public.starter_application ENABLE ROW LEVEL SECURITY;
22958
22957
  ALTER TABLE public.starter_reviews ENABLE ROW LEVEL SECURITY;
22959
22958
  ALTER TABLE public.starter_members ENABLE ROW LEVEL SECURITY;
22960
22959
  `;
22961
- var STARTER_REVIEW_POSTGRES = `import { AsyncLocalStorage } from "node:async_hooks";
22962
- import { createTransactionalCommand, plaintextCommandInput } from "@supacloud/commands";
22960
+ var STARTER_REVIEW_POSTGRES = `import { plaintextCommandInput } from "@supacloud/commands";
22963
22961
  import { CommandError } from "@supacloud/contracts";
22964
22962
  import { createPostgresCommandStore, type CommandDatabase, type CommandTransaction } from "@supacloud/db";
22965
22963
  import {
22966
- ApplicationError, createSupAuthRequestContext, requireTrustedIdentity, requireIdempotencyKey,
22964
+ ApplicationError, createSchemaDecoder, createSupAuthRequestContext, requireTrustedIdentity,
22965
+ createTransactionalCommandGovernance,
22967
22966
  type CommandInvocation, type SupAuthContextOptions,
22968
22967
  } from "@supacloud/elysia";
22969
22968
  import type { AppAdapters } from "../application";
22970
22969
  import type { ReviewStore } from "../review/review";
22971
22970
  import type { ReviewUploadPort } from "../review/uploads";
22971
+ import { ApprovalInput, ReviewResult } from "../review/contracts";
22972
22972
 
22973
- interface Approval { id: string; expectedVersion: number }
22974
- interface Review { state: "draft" | "approved"; version: number }
22973
+ const input = createSchemaDecoder(ApprovalInput, { normalize: false });
22974
+ const result = createSchemaDecoder(ReviewResult, { normalize: false });
22975
22975
 
22976
22976
  function isRecord(value: unknown): value is Record<string, unknown> {
22977
22977
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -22988,19 +22988,6 @@ function isPublicConflict(value: unknown): boolean {
22988
22988
  return value instanceof ApplicationError && value.status === 409
22989
22989
  || isRecord(value) && value["expose"] === true && value["status"] === 409 && typeof value["code"] === "string";
22990
22990
  }
22991
- function input(value: unknown): Approval {
22992
- const item = record(value);
22993
- if (typeof item["id"] !== "string" || !item["id"].length || typeof item["expectedVersion"] !== "number"
22994
- || !Number.isSafeInteger(item["expectedVersion"]) || item["expectedVersion"] < 1) throw new TypeError("Invalid approval input");
22995
- return { id: item["id"], expectedVersion: item["expectedVersion"] };
22996
- }
22997
- function result(value: unknown): Review {
22998
- const item = record(value);
22999
- if ((item["state"] !== "draft" && item["state"] !== "approved") || typeof item["version"] !== "number"
23000
- || !Number.isSafeInteger(item["version"]) || item["version"] < 1) throw new TypeError("Invalid review result");
23001
- return { state: item["state"], version: item["version"] };
23002
- }
23003
-
23004
22991
  export interface ReviewPostgresOptions {
23005
22992
  database: CommandDatabase;
23006
22993
  tenantId: string;
@@ -23038,12 +23025,7 @@ export async function createReviewPostgresAdapters(options: ReviewPostgresOption
23038
23025
  } : null;
23039
23026
  },
23040
23027
  });
23041
- const scope = new AsyncLocalStorage<CommandTransaction>();
23042
- const transaction = () => {
23043
- const tx = scope.getStore();
23044
- if (!tx) throw new Error("Review storage requires its durable command transaction");
23045
- return tx;
23046
- };
23028
+ const transaction = () => commandBinding.transaction();
23047
23029
  const store: ReviewStore = {
23048
23030
  ...options.uploads,
23049
23031
  async get(table, id) {
@@ -23072,64 +23054,30 @@ export async function createReviewPostgresAdapters(options: ReviewPostgresOption
23072
23054
  }
23073
23055
  return who.subject;
23074
23056
  }
23075
- const commandStore = createPostgresCommandStore(database);
23076
- return {
23077
- deps: { dbClient: store }, requestContext,
23078
- commandGovernance: {
23079
- authorize(invocation) {
23080
- const command = invocation.command;
23081
- if (command.name !== "review.approve" || command.permission !== "review.approve"
23082
- || command.transaction !== "required" || command.idempotency !== "required"
23083
- || command.audit !== "review.approved" || command.rpc !== undefined) {
23084
- throw new ApplicationError("Unsupported review command", { status: 501, code: "REVIEW_COMMAND_UNSUPPORTED" });
23085
- }
23086
- actor(invocation);
23087
- },
23088
- async idempotency(invocation, next) {
23089
- const command = createTransactionalCommand({
23090
- name: invocation.command.name, store: commandStore,
23091
- inputCodec: plaintextCommandInput, input, result,
23092
- async authorize(who, request, tx) {
23093
- const found = rows(await tx.query(
23094
- "SELECT r.id FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id " +
23095
- "CROSS JOIN public.starter_application a " +
23096
- "WHERE r.id=$1 AND r.owner_id=$2 AND m.enabled AND m.can_approve " +
23097
- "AND a.singleton AND a.project_id=$3 AND a.tenant_id=$4 FOR UPDATE OF r,m FOR SHARE OF a",
23098
- [request.id, who.actorId, projectId, who.tenantId],
23099
- ));
23100
- return found.length === 1 ? "allow" : "deny";
23101
- },
23102
- execute: tx => scope.run(tx, async () => {
23103
- try { return await next(); }
23104
- catch (error) {
23105
- if (isPublicConflict(error)) {
23106
- throw new CommandError("COMMAND_IDEMPOTENCY_CONFLICT");
23107
- }
23108
- throw error;
23109
- }
23110
- }),
23111
- audit: { event: "review.approved", details: request => ({ reviewId: request.id }) },
23112
- });
23113
- try {
23114
- const receipt = await command.execute(
23115
- { actorId: actor(invocation), tenantId }, requireIdempotencyKey(invocation),
23116
- { id: invocation.input.params["id"], expectedVersion: record(invocation.input.body)["expectedVersion"] },
23117
- invocation.request.signal,
23118
- );
23119
- if (receipt.status !== "confirmed") throw new CommandError("COMMAND_OUTCOME_UNKNOWN");
23120
- return receipt.result;
23121
- } catch (error) {
23122
- if (!(error instanceof CommandError)) throw error;
23123
- const status = error.code === "COMMAND_REJECTED" ? 403
23124
- : error.code === "COMMAND_IDEMPOTENCY_CONFLICT" ? 409 : 503;
23125
- throw new ApplicationError(error.code, { code: error.code, status });
23126
- }
23057
+ const commandBinding = createTransactionalCommandGovernance({
23058
+ permission: "review.approve",
23059
+ identity: invocation => ({ actorId: actor(invocation), tenantId }),
23060
+ input: invocation => ({
23061
+ ...record(invocation.input.body), id: invocation.input.params["id"],
23062
+ }),
23063
+ rejection: error => isPublicConflict(error) ? new CommandError("COMMAND_IDEMPOTENCY_CONFLICT") : undefined,
23064
+ definition: {
23065
+ name: "review.approve", store: createPostgresCommandStore(database),
23066
+ inputCodec: plaintextCommandInput, input, result,
23067
+ async authorize(who, request, tx) {
23068
+ const found = rows(await tx.query(
23069
+ "SELECT r.id FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id " +
23070
+ "CROSS JOIN public.starter_application a " +
23071
+ "WHERE r.id=$1 AND r.owner_id=$2 AND m.enabled AND m.can_approve " +
23072
+ "AND a.singleton AND a.project_id=$3 AND a.tenant_id=$4 FOR UPDATE OF r,m FOR SHARE OF a",
23073
+ [request.id, who.actorId, projectId, who.tenantId],
23074
+ ));
23075
+ return found.length === 1 ? "allow" : "deny";
23127
23076
  },
23128
- transaction(_invocation, next) { transaction(); return next(); },
23129
- // The durable command owns the authoritative audit and receipt in the same transaction.
23130
- audit: { succeeded() { transaction(); }, failed() {} },
23077
+ audit: { event: "review.approved", details: request => ({ reviewId: request.id }) },
23131
23078
  },
23132
- };
23079
+ });
23080
+ return { deps: { dbClient: store }, requestContext, commandGovernance: commandBinding.governance };
23133
23081
  }
23134
23082
  `;
23135
23083
  var STARTER_REVIEW_POSTGRES_TEST = `import { expect, test } from "bun:test";
@@ -23166,9 +23114,9 @@ test("PostgreSQL host never falls back to demo identity or unbound business stor
23166
23114
  await expect(adapters.requestContext(new Request("http://localhost/reviews/health")))
23167
23115
  .rejects.toMatchObject({ code: "AUTHENTICATION_REQUIRED" });
23168
23116
  const store = adapters.deps["dbClient"] as ReviewStore;
23169
- await expect(store.get("reviews", "example")).rejects.toThrow("durable command transaction");
23117
+ await expect(store.get("reviews", "example")).rejects.toThrow("not active");
23170
23118
  await expect(store.set("reviews", "example", { state: "approved", version: 2 }))
23171
- .rejects.toThrow("durable command transaction");
23119
+ .rejects.toThrow("not active");
23172
23120
  expect(db.queries).toHaveLength(1);
23173
23121
  });
23174
23122
  `;
@@ -23454,6 +23402,58 @@ export default defineConfig({
23454
23402
  };
23455
23403
  }
23456
23404
 
23405
+ // src/shared/tools/app-starter-query.ts
23406
+ var STARTER_REVIEW_QUERY = `import type { SupabaseClient } from "@supabase/supabase-js";
23407
+ import type { ReviewRow } from "../../db/schema";
23408
+
23409
+ // This read projection uses SQL column names shared with the Drizzle source.
23410
+ // Do not expose camelCase Drizzle property names as PostgREST column names.
23411
+ export type ReviewSummary = Pick<ReviewRow, "id" | "state" | "version">;
23412
+ export type ReviewReadDatabase = {
23413
+ public: {
23414
+ Tables: {
23415
+ starter_reviews: {
23416
+ Row: ReviewSummary;
23417
+ Insert: never;
23418
+ Update: never;
23419
+ Relationships: [];
23420
+ };
23421
+ };
23422
+ Views: Record<never, never>;
23423
+ Functions: Record<never, never>;
23424
+ };
23425
+ };
23426
+
23427
+ export interface ReviewPageOptions {
23428
+ page?: number;
23429
+ pageSize?: number;
23430
+ signal?: AbortSignal;
23431
+ }
23432
+
23433
+ // Reuse the application's user-scoped client. Identity verification and row
23434
+ // authorization belong to the gateway and database, never a caller-supplied owner.
23435
+ export async function listReviews(
23436
+ client: SupabaseClient<ReviewReadDatabase>,
23437
+ { page = 1, pageSize = 25, signal }: ReviewPageOptions = {},
23438
+ ) {
23439
+ if (!Number.isSafeInteger(page) || page < 1
23440
+ || !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100
23441
+ || !Number.isSafeInteger(page * pageSize)) {
23442
+ throw new RangeError("Invalid review page: page >= 1 and pageSize between 1 and 100 are required");
23443
+ }
23444
+ signal?.throwIfAborted();
23445
+ const start = (page - 1) * pageSize;
23446
+ const query = client.from("starter_reviews")
23447
+ .select("id,state,version", { count: "exact" })
23448
+ .order("id", { ascending: true })
23449
+ .range(start, start + pageSize - 1);
23450
+ const { data, error, count } = await (signal ? query.abortSignal(signal) : query);
23451
+ if (error) throw error;
23452
+ if (data === null || count === null) throw new Error("Incomplete review page response");
23453
+ return { data, total: count, page, pageSize, hasNextPage: start + data.length < count };
23454
+ }
23455
+ `;
23456
+
23457
23457
  // src/shared/tools/app-starter.ts
23458
23458
  var { compiler: compilerMetadata2, app: appMetadata2, elysia: elysiaMetadata2, commands: commandsMetadata, contracts: contractsMetadata, db: dbMetadata, sdk: sdkMetadata } = starter_metadata_default.packages;
23459
23459
  var json2 = (value) => `${JSON.stringify(value, null, 2)}
@@ -23461,6 +23461,7 @@ var json2 = (value) => `${JSON.stringify(value, null, 2)}
23461
23461
  function appStarterFiles(name) {
23462
23462
  return {
23463
23463
  ...starterDatabaseFiles(),
23464
+ "src/review/list-reviews.ts": STARTER_REVIEW_QUERY,
23464
23465
  "package.json": json2({
23465
23466
  name,
23466
23467
  version: "0.0.0",
@@ -23495,8 +23496,8 @@ function appStarterFiles(name) {
23495
23496
  "drizzle-orm": dbMetadata.peerDependencies["drizzle-orm"],
23496
23497
  "@supacloud/js": `^${sdkMetadata.version}`,
23497
23498
  "@supabase/supabase-js": sdkMetadata.peerDependencies["@supabase/supabase-js"],
23498
- effect: "4.0.2",
23499
- elysia: "2.0.0-beta.21",
23499
+ effect: "4.0.5",
23500
+ elysia: "2.0.0-beta.32",
23500
23501
  rxjs: appMetadata2.dependencies.rxjs
23501
23502
  },
23502
23503
  devDependencies: {
@@ -23750,11 +23751,10 @@ import {
23750
23751
  Body, Command, Controller, DB_CLIENT, Get, Inject, Param, Post,
23751
23752
  defineFeatureSlice, defineFeatureSpec, type Aspect,
23752
23753
  } from "@supacloud/app";
23753
- import { ApplicationError, assertFeatureTransition } from "@supacloud/elysia";
23754
- import { ApproveBody, HealthResult, Params, ReviewResult } from "./contracts";
23754
+ import { ApplicationError, assertFeatureTransition, createSchemaDecoder } from "@supacloud/elysia";
23755
+ import { ApproveBody, HealthResult, Params, ReviewResult, type ApprovalBody, type Review } from "./contracts";
23755
23756
  import { VerifyReviewAttachment } from "./attachment";
23756
23757
  import { ReviewUploads, ReviewUploadsController, type ReviewUploadPort } from "./uploads";
23757
- import type { ReviewRow } from "../../db/schema";
23758
23758
 
23759
23759
  export const reviewSpec = defineFeatureSpec({
23760
23760
  name: "review",
@@ -23769,7 +23769,6 @@ export const reviewSpec = defineFeatureSpec({
23769
23769
  },
23770
23770
  });
23771
23771
 
23772
- type Review = Pick<ReviewRow, "state" | "version">;
23773
23772
  // Production implementations must bind both operations to the current transaction.
23774
23773
  export interface ReviewStore extends Partial<ReviewUploadPort> {
23775
23774
  get(table: string, key: string): unknown;
@@ -23782,13 +23781,12 @@ export const requireRequest: Aspect = (context, next) => {
23782
23781
  return next();
23783
23782
  };
23784
23783
 
23784
+ const decodeReview = createSchemaDecoder(ReviewResult, { normalize: false });
23785
23785
  function readReview(value: unknown): Review {
23786
- if (typeof value !== "object" || value === null ||
23787
- !("state" in value) || (value.state !== "draft" && value.state !== "approved") ||
23788
- !("version" in value) || typeof value.version !== "number") {
23786
+ try { return decodeReview(value); }
23787
+ catch {
23789
23788
  throw new ApplicationError("Review not found", { status: 404, code: "REVIEW_NOT_FOUND" });
23790
23789
  }
23791
- return { state: value.state, version: value.version };
23792
23790
  }
23793
23791
 
23794
23792
  @Command({
@@ -23820,7 +23818,7 @@ type ReviewError =
23820
23818
  | { readonly _tag: "FEATURE_TRANSITION_CONFLICT" }
23821
23819
  | { readonly _tag: "COMMAND_IDEMPOTENCY_CONFLICT" }
23822
23820
  | { readonly _tag: "COMMAND_REJECTED" }
23823
- | { readonly _tag: "REVIEW_COMMAND_UNSUPPORTED" }
23821
+ | { readonly _tag: "COMMAND_BINDING_MISMATCH" }
23824
23822
  | { readonly _tag: "COMMAND_OUTCOME_UNKNOWN" };
23825
23823
 
23826
23824
  @Controller("/reviews")
@@ -23849,13 +23847,13 @@ export class ReviewController {
23849
23847
  { tag: "FEATURE_TRANSITION_CONFLICT", status: 409, code: "FEATURE_TRANSITION_CONFLICT" },
23850
23848
  { tag: "COMMAND_IDEMPOTENCY_CONFLICT", status: 409, code: "COMMAND_IDEMPOTENCY_CONFLICT" },
23851
23849
  { tag: "COMMAND_REJECTED", status: 403, code: "COMMAND_REJECTED" },
23852
- { tag: "REVIEW_COMMAND_UNSUPPORTED", status: 501, code: "REVIEW_COMMAND_UNSUPPORTED" },
23850
+ { tag: "COMMAND_BINDING_MISMATCH", status: 501, code: "COMMAND_BINDING_MISMATCH" },
23853
23851
  { tag: "COMMAND_OUTCOME_UNKNOWN", status: 503, code: "COMMAND_OUTCOME_UNKNOWN" },
23854
23852
  ],
23855
23853
  retry: "none",
23856
23854
  },
23857
23855
  })
23858
- approve(@Param("id") id: string, @Body() body: { expectedVersion: number }): Effect.Effect<Review, ReviewError, never> {
23856
+ approve(@Param("id") id: string, @Body() body: ApprovalBody): Effect.Effect<Review, ReviewError, never> {
23859
23857
  return Effect.promise(() => this.approveReview.execute(id, body.expectedVersion));
23860
23858
  }
23861
23859
  }
@@ -23867,13 +23865,16 @@ export const ReviewFeature = defineFeatureSlice({
23867
23865
  });
23868
23866
  `,
23869
23867
  "src/review/attachment.ts": STARTER_REVIEW_JOB,
23870
- "src/review/contracts.ts": `import { t } from "elysia";
23868
+ "src/review/contracts.ts": `import { t, type UnwrapSchema } from "elysia";
23871
23869
  export const Params = t.Object({ id: t.String({ minLength: 1 }) });
23872
- export const ApproveBody = t.Object({ expectedVersion: t.Integer({ minimum: 1 }) });
23870
+ export const ApproveBody = t.Object({ expectedVersion: t.Integer({ minimum: 1, maximum: 9007199254740991 }) });
23871
+ export const ApprovalInput = t.Object({ ...Params.properties, ...ApproveBody.properties }, { additionalProperties: false });
23872
+ export type ApprovalBody = UnwrapSchema<typeof ApproveBody>;
23873
23873
  export const ReviewResult = t.Object({
23874
23874
  state: t.Union([t.Literal("draft"), t.Literal("approved")]),
23875
- version: t.Integer({ minimum: 1 }),
23875
+ version: t.Integer({ minimum: 1, maximum: 9007199254740991 }),
23876
23876
  });
23877
+ export type Review = UnwrapSchema<typeof ReviewResult>;
23877
23878
  export const HealthResult = t.Object({ ok: t.Boolean() });
23878
23879
  export const UploadParams = t.Object({ id: t.String({ format: "uuid" }) });
23879
23880
  export const UploadBody = t.Object({
@@ -24015,6 +24016,18 @@ Ordinary reads use PostgREST through the existing user-scoped
24015
24016
  Use the caller's session, RLS and grants; never introduce a second session store.
24016
24017
  Business writes continue through the governed Command/RPC boundary.
24017
24018
 
24019
+ Use listReviews from src/review/list-reviews.ts for the paginated review list.
24020
+ Type the existing client with ReviewReadDatabase (a read-only projection derived
24021
+ from db/schema.ts), then call listReviews(client, { page: 1, pageSize: 25, signal }).
24022
+ The helper returns data, total, page, pageSize and hasNextPage, orders by the unique
24023
+ id and preserves PostgREST errors. Pages are one-based and limited to 100 rows.
24024
+ Exact counts obey RLS but may be expensive on large tables; offset pages are not
24025
+ a cross-request snapshot when records change. No owner filter replaces RLS.
24026
+ Do not create a service-role client or a second auth session for this helper.
24027
+ Existing starters can add this file without a new migration only if their reviewed
24028
+ review-table grants and identity-mapping RLS from migration 003 are already applied.
24029
+ The projection is not a full database export and grants no write access.
24030
+
24018
24031
  GraphQL is not generated or enabled by this starter. For a specific Query Contract,
24019
24032
  explicitly configure graphql.schema with a role-scoped database export and add
24020
24033
  checked query documents. Drizzle and append-only migrations remain the database
@@ -30513,7 +30526,7 @@ async function runProjectDeployment(http, args) {
30513
30526
  // package.json
30514
30527
  var package_default = {
30515
30528
  name: "@supacloud/cli",
30516
- version: "0.99.0",
30529
+ version: "0.101.0",
30517
30530
  description: "Project-scoped CLI for SupaCloud users",
30518
30531
  type: "module",
30519
30532
  main: "./dist/index.js",
@@ -30565,21 +30578,21 @@ var package_default = {
30565
30578
  directory: "packages/cli"
30566
30579
  },
30567
30580
  dependencies: {
30568
- "@supacloud/compiler": "^0.46.1",
30569
- "@supacloud/delivery": "0.18.0",
30570
- "@supacloud/db": "0.17.0",
30571
- typebox: "1.3.36"
30581
+ "@supacloud/compiler": "^0.46.3",
30582
+ "@supacloud/delivery": "0.18.1",
30583
+ "@supacloud/db": "0.17.1",
30584
+ typebox: "1.3.37"
30572
30585
  },
30573
30586
  overrides: {
30574
30587
  "@supacloud/contracts": "0.9.0",
30575
- "@supacloud/delivery": "0.18.0",
30576
- "@graphql-tools/utils": "^12.0.3"
30588
+ "@supacloud/delivery": "0.18.1",
30589
+ "@graphql-tools/utils": "^12.0.4"
30577
30590
  },
30578
30591
  devDependencies: {
30579
- "@supacloud/elysia": "^0.24.1",
30592
+ "@supacloud/elysia": "0.31.0",
30580
30593
  "@types/bun": "^1.4.3",
30581
- effect: "4.0.2",
30582
- elysia: "2.0.0-beta.21",
30594
+ effect: "4.0.5",
30595
+ elysia: "2.0.0-beta.32",
30583
30596
  typescript: "^7.0.2"
30584
30597
  }
30585
30598
  };
@@ -1,4 +1,4 @@
1
1
  export declare const STARTER_REVIEW_SCHEMA = "-- Apply with the platform migration owner, never from the HTTP process.\n-- One application database belongs to one explicitly provisioned project/tenant.\nCREATE TABLE public.starter_application (\n singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),\n project_id text NOT NULL CHECK (length(project_id) > 0),\n tenant_id text NOT NULL CHECK (length(tenant_id) > 0)\n);\nCREATE TABLE public.starter_reviews (\n id text PRIMARY KEY, owner_id text NOT NULL,\n state text NOT NULL CHECK (state IN ('draft','approved')),\n version integer NOT NULL CHECK (version > 0)\n);\nCREATE TABLE public.starter_members (\n subject text PRIMARY KEY, enabled boolean NOT NULL DEFAULT true,\n can_approve boolean NOT NULL DEFAULT false\n);\nALTER TABLE public.starter_application ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.starter_reviews ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.starter_members ENABLE ROW LEVEL SECURITY;\n";
2
- export declare const STARTER_REVIEW_POSTGRES = "import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { createTransactionalCommand, plaintextCommandInput } from \"@supacloud/commands\";\nimport { CommandError } from \"@supacloud/contracts\";\nimport { createPostgresCommandStore, type CommandDatabase, type CommandTransaction } from \"@supacloud/db\";\nimport {\n ApplicationError, createSupAuthRequestContext, requireTrustedIdentity, requireIdempotencyKey,\n type CommandInvocation, type SupAuthContextOptions,\n} from \"@supacloud/elysia\";\nimport type { AppAdapters } from \"../application\";\nimport type { ReviewStore } from \"../review/review\";\nimport type { ReviewUploadPort } from \"../review/uploads\";\n\ninterface Approval { id: string; expectedVersion: number }\ninterface Review { state: \"draft\" | \"approved\"; version: number }\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction record(value: unknown): Record<string, unknown> {\n if (!isRecord(value)) throw new TypeError(\"Invalid review record\");\n return value;\n}\nfunction rows(value: unknown): Record<string, unknown>[] {\n if (!Array.isArray(value)) throw new TypeError(\"Invalid database result\");\n return value.map(record);\n}\nfunction isPublicConflict(value: unknown): boolean {\n return value instanceof ApplicationError && value.status === 409\n || isRecord(value) && value[\"expose\"] === true && value[\"status\"] === 409 && typeof value[\"code\"] === \"string\";\n}\nfunction input(value: unknown): Approval {\n const item = record(value);\n if (typeof item[\"id\"] !== \"string\" || !item[\"id\"].length || typeof item[\"expectedVersion\"] !== \"number\"\n || !Number.isSafeInteger(item[\"expectedVersion\"]) || item[\"expectedVersion\"] < 1) throw new TypeError(\"Invalid approval input\");\n return { id: item[\"id\"], expectedVersion: item[\"expectedVersion\"] };\n}\nfunction result(value: unknown): Review {\n const item = record(value);\n if ((item[\"state\"] !== \"draft\" && item[\"state\"] !== \"approved\") || typeof item[\"version\"] !== \"number\"\n || !Number.isSafeInteger(item[\"version\"]) || item[\"version\"] < 1) throw new TypeError(\"Invalid review result\");\n return { state: item[\"state\"], version: item[\"version\"] };\n}\n\nexport interface ReviewPostgresOptions {\n database: CommandDatabase;\n tenantId: string;\n identity: Omit<SupAuthContextOptions, \"resolveAccess\">;\n uploads?: ReviewUploadPort;\n // Only database effects here: they commit/rollback with the approval and receipt.\n afterApproved?(tx: CommandTransaction, id: string, version: number): Promise<void>;\n}\n\nexport async function createReviewPostgresAdapters(options: ReviewPostgresOptions): Promise<\n AppAdapters & { requestContext: ReturnType<typeof createSupAuthRequestContext> }\n> {\n const { database, tenantId, identity } = options;\n const projectId = identity.projectId;\n const afterApproved = options.afterApproved;\n if (!tenantId.trim()) throw new TypeError(\"Review tenantId is required\");\n const query = (text: string, parameters: readonly string[] = []) =>\n database.transaction(tx => tx.query(text, parameters));\n const binding = rows(await query(\n \"SELECT project_id,tenant_id FROM public.starter_application WHERE singleton\",\n ));\n if (binding.length !== 1 || binding[0]?.[\"project_id\"] !== projectId || binding[0]?.[\"tenant_id\"] !== tenantId) {\n throw new Error(\"Review database project/tenant binding mismatch\");\n }\n const requestContext = createSupAuthRequestContext({\n ...identity,\n async resolveAccess(who) {\n const found = rows(await query(\n \"SELECT m.can_approve FROM public.starter_members m,public.starter_application a \" +\n \"WHERE m.subject=$1 AND m.enabled AND a.singleton AND a.project_id=$2 AND a.tenant_id=$3\",\n [who.subject, projectId, tenantId],\n ));\n return found.length === 1 ? { projectId, tenantId,\n permissions: found[0]?.[\"can_approve\"] === true ? [\"review.approve\"] : [],\n } : null;\n },\n });\n const scope = new AsyncLocalStorage<CommandTransaction>();\n const transaction = () => {\n const tx = scope.getStore();\n if (!tx) throw new Error(\"Review storage requires its durable command transaction\");\n return tx;\n };\n const store: ReviewStore = {\n ...options.uploads,\n async get(table, id) {\n if (table !== \"reviews\") throw new Error(\"Unsupported review table\");\n return rows(await transaction().query(\n \"SELECT state,version FROM public.starter_reviews WHERE id=$1 FOR UPDATE\", [id],\n ))[0];\n },\n async set(table, id, value) {\n if (table !== \"reviews\") throw new Error(\"Unsupported review table\");\n const review = result(value);\n const updated = rows(await transaction().query(\n \"UPDATE public.starter_reviews SET state=$2,version=$3 WHERE id=$1 RETURNING id\",\n [id, review.state, review.version],\n ));\n if (updated.length !== 1) throw new Error(\"Review write did not update one record\");\n await afterApproved?.(transaction(), id, review.version);\n },\n };\n function actor(invocation: CommandInvocation): string {\n const who = requireTrustedIdentity(invocation.requestContext);\n const access = record(record(invocation.requestContext)[\"access\"]);\n if (access[\"projectId\"] !== projectId || access[\"tenantId\"] !== tenantId\n || !Array.isArray(access[\"permissions\"]) || !access[\"permissions\"].includes(\"review.approve\")) {\n throw new ApplicationError(\"Command permission denied\", { status: 403, code: \"REVIEW_PERMISSION_DENIED\" });\n }\n return who.subject;\n }\n const commandStore = createPostgresCommandStore(database);\n return {\n deps: { dbClient: store }, requestContext,\n commandGovernance: {\n authorize(invocation) {\n const command = invocation.command;\n if (command.name !== \"review.approve\" || command.permission !== \"review.approve\"\n || command.transaction !== \"required\" || command.idempotency !== \"required\"\n || command.audit !== \"review.approved\" || command.rpc !== undefined) {\n throw new ApplicationError(\"Unsupported review command\", { status: 501, code: \"REVIEW_COMMAND_UNSUPPORTED\" });\n }\n actor(invocation);\n },\n async idempotency(invocation, next) {\n const command = createTransactionalCommand({\n name: invocation.command.name, store: commandStore,\n inputCodec: plaintextCommandInput, input, result,\n async authorize(who, request, tx) {\n const found = rows(await tx.query(\n \"SELECT r.id FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id \" +\n \"CROSS JOIN public.starter_application a \" +\n \"WHERE r.id=$1 AND r.owner_id=$2 AND m.enabled AND m.can_approve \" +\n \"AND a.singleton AND a.project_id=$3 AND a.tenant_id=$4 FOR UPDATE OF r,m FOR SHARE OF a\",\n [request.id, who.actorId, projectId, who.tenantId],\n ));\n return found.length === 1 ? \"allow\" : \"deny\";\n },\n execute: tx => scope.run(tx, async () => {\n try { return await next(); }\n catch (error) {\n if (isPublicConflict(error)) {\n throw new CommandError(\"COMMAND_IDEMPOTENCY_CONFLICT\");\n }\n throw error;\n }\n }),\n audit: { event: \"review.approved\", details: request => ({ reviewId: request.id }) },\n });\n try {\n const receipt = await command.execute(\n { actorId: actor(invocation), tenantId }, requireIdempotencyKey(invocation),\n { id: invocation.input.params[\"id\"], expectedVersion: record(invocation.input.body)[\"expectedVersion\"] },\n invocation.request.signal,\n );\n if (receipt.status !== \"confirmed\") throw new CommandError(\"COMMAND_OUTCOME_UNKNOWN\");\n return receipt.result;\n } catch (error) {\n if (!(error instanceof CommandError)) throw error;\n const status = error.code === \"COMMAND_REJECTED\" ? 403\n : error.code === \"COMMAND_IDEMPOTENCY_CONFLICT\" ? 409 : 503;\n throw new ApplicationError(error.code, { code: error.code, status });\n }\n },\n transaction(_invocation, next) { transaction(); return next(); },\n // The durable command owns the authoritative audit and receipt in the same transaction.\n audit: { succeeded() { transaction(); }, failed() {} },\n },\n };\n}\n";
3
- export declare const STARTER_REVIEW_POSTGRES_TEST = "import { expect, test } from \"bun:test\";\nimport { createReviewPostgresAdapters } from \"../src/host/review-postgres\";\nimport type { ReviewStore } from \"../src/review/review\";\n\nconst identity = {\n issuer: \"https://identity.example.test\", audience: \"review-test\", clientId: \"review-client\",\n projectId: \"review-test\", jwksUrl: \"https://identity.example.test/jwks\",\n};\nfunction database(binding: unknown) {\n const queries: string[] = [];\n return {\n queries,\n async transaction<T>(run: (tx: { query(text: string): Promise<unknown> }) => Promise<T>): Promise<T> {\n return run({ async query(text) { queries.push(text); return binding; } });\n },\n };\n}\n\ntest(\"PostgreSQL host rejects missing or mismatched application ownership without creating data\", async () => {\n for (const binding of [[], [{ project_id: \"other\", tenant_id: \"review-test\" }],\n [{ project_id: \"review-test\", tenant_id: \"other\" }]]) {\n const db = database(binding);\n await expect(createReviewPostgresAdapters({ database: db, identity, tenantId: \"review-test\" }))\n .rejects.toThrow(\"binding mismatch\");\n expect(db.queries).toEqual([\"SELECT project_id,tenant_id FROM public.starter_application WHERE singleton\"]);\n }\n});\n\ntest(\"PostgreSQL host never falls back to demo identity or unbound business storage\", async () => {\n const db = database([{ project_id: \"review-test\", tenant_id: \"review-test\" }]);\n const adapters = await createReviewPostgresAdapters({ database: db, identity, tenantId: \"review-test\" });\n await expect(adapters.requestContext(new Request(\"http://localhost/reviews/health\")))\n .rejects.toMatchObject({ code: \"AUTHENTICATION_REQUIRED\" });\n const store = adapters.deps[\"dbClient\"] as ReviewStore;\n await expect(store.get(\"reviews\", \"example\")).rejects.toThrow(\"durable command transaction\");\n await expect(store.set(\"reviews\", \"example\", { state: \"approved\", version: 2 }))\n .rejects.toThrow(\"durable command transaction\");\n expect(db.queries).toHaveLength(1);\n});\n";
2
+ export declare const STARTER_REVIEW_POSTGRES = "import { plaintextCommandInput } from \"@supacloud/commands\";\nimport { CommandError } from \"@supacloud/contracts\";\nimport { createPostgresCommandStore, type CommandDatabase, type CommandTransaction } from \"@supacloud/db\";\nimport {\n ApplicationError, createSchemaDecoder, createSupAuthRequestContext, requireTrustedIdentity,\n createTransactionalCommandGovernance,\n type CommandInvocation, type SupAuthContextOptions,\n} from \"@supacloud/elysia\";\nimport type { AppAdapters } from \"../application\";\nimport type { ReviewStore } from \"../review/review\";\nimport type { ReviewUploadPort } from \"../review/uploads\";\nimport { ApprovalInput, ReviewResult } from \"../review/contracts\";\n\nconst input = createSchemaDecoder(ApprovalInput, { normalize: false });\nconst result = createSchemaDecoder(ReviewResult, { normalize: false });\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction record(value: unknown): Record<string, unknown> {\n if (!isRecord(value)) throw new TypeError(\"Invalid review record\");\n return value;\n}\nfunction rows(value: unknown): Record<string, unknown>[] {\n if (!Array.isArray(value)) throw new TypeError(\"Invalid database result\");\n return value.map(record);\n}\nfunction isPublicConflict(value: unknown): boolean {\n return value instanceof ApplicationError && value.status === 409\n || isRecord(value) && value[\"expose\"] === true && value[\"status\"] === 409 && typeof value[\"code\"] === \"string\";\n}\nexport interface ReviewPostgresOptions {\n database: CommandDatabase;\n tenantId: string;\n identity: Omit<SupAuthContextOptions, \"resolveAccess\">;\n uploads?: ReviewUploadPort;\n // Only database effects here: they commit/rollback with the approval and receipt.\n afterApproved?(tx: CommandTransaction, id: string, version: number): Promise<void>;\n}\n\nexport async function createReviewPostgresAdapters(options: ReviewPostgresOptions): Promise<\n AppAdapters & { requestContext: ReturnType<typeof createSupAuthRequestContext> }\n> {\n const { database, tenantId, identity } = options;\n const projectId = identity.projectId;\n const afterApproved = options.afterApproved;\n if (!tenantId.trim()) throw new TypeError(\"Review tenantId is required\");\n const query = (text: string, parameters: readonly string[] = []) =>\n database.transaction(tx => tx.query(text, parameters));\n const binding = rows(await query(\n \"SELECT project_id,tenant_id FROM public.starter_application WHERE singleton\",\n ));\n if (binding.length !== 1 || binding[0]?.[\"project_id\"] !== projectId || binding[0]?.[\"tenant_id\"] !== tenantId) {\n throw new Error(\"Review database project/tenant binding mismatch\");\n }\n const requestContext = createSupAuthRequestContext({\n ...identity,\n async resolveAccess(who) {\n const found = rows(await query(\n \"SELECT m.can_approve FROM public.starter_members m,public.starter_application a \" +\n \"WHERE m.subject=$1 AND m.enabled AND a.singleton AND a.project_id=$2 AND a.tenant_id=$3\",\n [who.subject, projectId, tenantId],\n ));\n return found.length === 1 ? { projectId, tenantId,\n permissions: found[0]?.[\"can_approve\"] === true ? [\"review.approve\"] : [],\n } : null;\n },\n });\n const transaction = () => commandBinding.transaction();\n const store: ReviewStore = {\n ...options.uploads,\n async get(table, id) {\n if (table !== \"reviews\") throw new Error(\"Unsupported review table\");\n return rows(await transaction().query(\n \"SELECT state,version FROM public.starter_reviews WHERE id=$1 FOR UPDATE\", [id],\n ))[0];\n },\n async set(table, id, value) {\n if (table !== \"reviews\") throw new Error(\"Unsupported review table\");\n const review = result(value);\n const updated = rows(await transaction().query(\n \"UPDATE public.starter_reviews SET state=$2,version=$3 WHERE id=$1 RETURNING id\",\n [id, review.state, review.version],\n ));\n if (updated.length !== 1) throw new Error(\"Review write did not update one record\");\n await afterApproved?.(transaction(), id, review.version);\n },\n };\n function actor(invocation: CommandInvocation): string {\n const who = requireTrustedIdentity(invocation.requestContext);\n const access = record(record(invocation.requestContext)[\"access\"]);\n if (access[\"projectId\"] !== projectId || access[\"tenantId\"] !== tenantId\n || !Array.isArray(access[\"permissions\"]) || !access[\"permissions\"].includes(\"review.approve\")) {\n throw new ApplicationError(\"Command permission denied\", { status: 403, code: \"REVIEW_PERMISSION_DENIED\" });\n }\n return who.subject;\n }\n const commandBinding = createTransactionalCommandGovernance({\n permission: \"review.approve\",\n identity: invocation => ({ actorId: actor(invocation), tenantId }),\n input: invocation => ({\n ...record(invocation.input.body), id: invocation.input.params[\"id\"],\n }),\n rejection: error => isPublicConflict(error) ? new CommandError(\"COMMAND_IDEMPOTENCY_CONFLICT\") : undefined,\n definition: {\n name: \"review.approve\", store: createPostgresCommandStore(database),\n inputCodec: plaintextCommandInput, input, result,\n async authorize(who, request, tx) {\n const found = rows(await tx.query(\n \"SELECT r.id FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id \" +\n \"CROSS JOIN public.starter_application a \" +\n \"WHERE r.id=$1 AND r.owner_id=$2 AND m.enabled AND m.can_approve \" +\n \"AND a.singleton AND a.project_id=$3 AND a.tenant_id=$4 FOR UPDATE OF r,m FOR SHARE OF a\",\n [request.id, who.actorId, projectId, who.tenantId],\n ));\n return found.length === 1 ? \"allow\" : \"deny\";\n },\n audit: { event: \"review.approved\", details: request => ({ reviewId: request.id }) },\n },\n });\n return { deps: { dbClient: store }, requestContext, commandGovernance: commandBinding.governance };\n}\n";
3
+ export declare const STARTER_REVIEW_POSTGRES_TEST = "import { expect, test } from \"bun:test\";\nimport { createReviewPostgresAdapters } from \"../src/host/review-postgres\";\nimport type { ReviewStore } from \"../src/review/review\";\n\nconst identity = {\n issuer: \"https://identity.example.test\", audience: \"review-test\", clientId: \"review-client\",\n projectId: \"review-test\", jwksUrl: \"https://identity.example.test/jwks\",\n};\nfunction database(binding: unknown) {\n const queries: string[] = [];\n return {\n queries,\n async transaction<T>(run: (tx: { query(text: string): Promise<unknown> }) => Promise<T>): Promise<T> {\n return run({ async query(text) { queries.push(text); return binding; } });\n },\n };\n}\n\ntest(\"PostgreSQL host rejects missing or mismatched application ownership without creating data\", async () => {\n for (const binding of [[], [{ project_id: \"other\", tenant_id: \"review-test\" }],\n [{ project_id: \"review-test\", tenant_id: \"other\" }]]) {\n const db = database(binding);\n await expect(createReviewPostgresAdapters({ database: db, identity, tenantId: \"review-test\" }))\n .rejects.toThrow(\"binding mismatch\");\n expect(db.queries).toEqual([\"SELECT project_id,tenant_id FROM public.starter_application WHERE singleton\"]);\n }\n});\n\ntest(\"PostgreSQL host never falls back to demo identity or unbound business storage\", async () => {\n const db = database([{ project_id: \"review-test\", tenant_id: \"review-test\" }]);\n const adapters = await createReviewPostgresAdapters({ database: db, identity, tenantId: \"review-test\" });\n await expect(adapters.requestContext(new Request(\"http://localhost/reviews/health\")))\n .rejects.toMatchObject({ code: \"AUTHENTICATION_REQUIRED\" });\n const store = adapters.deps[\"dbClient\"] as ReviewStore;\n await expect(store.get(\"reviews\", \"example\")).rejects.toThrow(\"not active\");\n await expect(store.set(\"reviews\", \"example\", { state: \"approved\", version: 2 }))\n .rejects.toThrow(\"not active\");\n expect(db.queries).toHaveLength(1);\n});\n";
4
4
  export declare const STARTER_REVIEW_DELIVERY_HOST = "import { SQL } from \"bun\";\nimport { createClient } from \"@supabase/supabase-js\";\nimport { createBunCommandDatabase } from \"@supacloud/db/bun\";\nimport { createApplication, type CompiledModule } from \"@supacloud/elysia\";\nimport { createReviewPostgresAdapters } from \"./host/review-postgres\";\nimport { createReviewAttachmentAdapters } from \"./host/review-attachments\";\nimport { createReviewUploadAdapters } from \"./host/review-uploads\";\n\nfunction required(name: string): string {\n const value = process.env[name];\n if (!value?.trim()) throw new Error(\"Missing required host setting: \" + name);\n return value;\n}\n\nfunction databaseConnection(): SQL.Options {\n const socket = process.env[\"DATABASE_SOCKET_PATH\"];\n if (socket && process.env[\"DATABASE_URL\"]) throw new Error(\"Conflicting database connection settings\");\n if (socket) {\n return { adapter: \"postgres\", path: socket, database: required(\"DATABASE_NAME\"), username: required(\"DATABASE_USER\") };\n }\n const url = new URL(required(\"DATABASE_URL\"));\n if (url.protocol !== \"postgres:\" && url.protocol !== \"postgresql:\") throw new Error(\"Invalid database URL\");\n return {\n adapter: \"postgres\", url: url.href, hostname: url.hostname.replace(/^\\[|\\]$/g, \"\"), port: Number(url.port || 5432),\n username: decodeURIComponent(url.username), password: decodeURIComponent(url.password),\n database: decodeURIComponent(url.pathname.slice(1)),\n };\n}\n\nexport async function createDeliveryApplication(modules: CompiledModule[], lifecycle: { signal: AbortSignal }) {\n lifecycle.signal.throwIfAborted();\n const connection = databaseConnection();\n const attachments = process.env[\"REVIEW_ATTACHMENTS\"];\n if (attachments !== undefined && attachments !== \"enabled\") throw new Error(\"Invalid attachment configuration\");\n const settings = {\n tenantId: required(\"APP_TENANT_ID\"),\n identity: {\n issuer: required(\"SUPAUTH_ISSUER\"), audience: required(\"SUPAUTH_AUDIENCE\"),\n clientId: required(\"SUPAUTH_CLIENT_ID\"), projectId: required(\"SUPACLOUD_PROJECT_ID\"),\n jwksUrl: required(\"SUPAUTH_JWKS_URL\"),\n },\n };\n const pool = new SQL({ ...connection, max: 12, connectionTimeout: 5 });\n let closing: Promise<void> | undefined;\n const close = () => closing ??= pool.close({ timeout: 5 });\n let stage: \"attachments\" | \"uploads\" | \"postgres\" | \"application\" = \"attachments\";\n try {\n const database = createBunCommandDatabase(pool);\n const attachmentOptions = attachments === \"enabled\" ? {\n database, tenantId: settings.tenantId, projectId: settings.identity.projectId,\n service: createClient(required(\"SUPACLOUD_URL\"), required(\"SUPACLOUD_SERVICE_ROLE_KEY\"), {\n auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },\n }),\n } : undefined;\n const durable = attachmentOptions ? await createReviewAttachmentAdapters(attachmentOptions) : undefined;\n stage = \"uploads\";\n const uploads = attachmentOptions ? await createReviewUploadAdapters(attachmentOptions) : undefined;\n stage = \"postgres\";\n const adapters = await createReviewPostgresAdapters({\n database, tenantId: settings.tenantId, identity: settings.identity,\n ...(uploads ? { uploads } : {}),\n ...(durable ? { afterApproved: durable.enqueue } : {}),\n });\n lifecycle.signal.throwIfAborted();\n stage = \"application\";\n const app = createApplication({ name: \"review\", modules, ...adapters });\n return { fetch: (request: Request) => app.handle(request), close };\n } catch (error) {\n await close();\n const code = error !== null && typeof error === \"object\" && \"code\" in error\n && typeof error.code === \"string\" && /^[A-Z0-9_]{1,64}$/.test(error.code) ? error.code : \"UNKNOWN\";\n console.error(JSON.stringify({ event: \"review-host-initialization-failed\", stage, code }));\n // Database/identity errors can contain connection details; never echo the underlying cause.\n throw new Error(\"Review host initialization failed\");\n }\n}\n";
@@ -0,0 +1 @@
1
+ export declare const STARTER_REVIEW_QUERY = "import type { SupabaseClient } from \"@supabase/supabase-js\";\nimport type { ReviewRow } from \"../../db/schema\";\n\n// This read projection uses SQL column names shared with the Drizzle source.\n// Do not expose camelCase Drizzle property names as PostgREST column names.\nexport type ReviewSummary = Pick<ReviewRow, \"id\" | \"state\" | \"version\">;\nexport type ReviewReadDatabase = {\n public: {\n Tables: {\n starter_reviews: {\n Row: ReviewSummary;\n Insert: never;\n Update: never;\n Relationships: [];\n };\n };\n Views: Record<never, never>;\n Functions: Record<never, never>;\n };\n};\n\nexport interface ReviewPageOptions {\n page?: number;\n pageSize?: number;\n signal?: AbortSignal;\n}\n\n// Reuse the application's user-scoped client. Identity verification and row\n// authorization belong to the gateway and database, never a caller-supplied owner.\nexport async function listReviews(\n client: SupabaseClient<ReviewReadDatabase>,\n { page = 1, pageSize = 25, signal }: ReviewPageOptions = {},\n) {\n if (!Number.isSafeInteger(page) || page < 1\n || !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100\n || !Number.isSafeInteger(page * pageSize)) {\n throw new RangeError(\"Invalid review page: page >= 1 and pageSize between 1 and 100 are required\");\n }\n signal?.throwIfAborted();\n const start = (page - 1) * pageSize;\n const query = client.from(\"starter_reviews\")\n .select(\"id,state,version\", { count: \"exact\" })\n .order(\"id\", { ascending: true })\n .range(start, start + pageSize - 1);\n const { data, error, count } = await (signal ? query.abortSignal(signal) : query);\n if (error) throw error;\n if (data === null || count === null) throw new Error(\"Incomplete review page response\");\n return { data, total: count, page, pageSize, hasNextPage: start + data.length < count };\n}\n";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.99.0",
3
+ "version": "0.101.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -52,21 +52,21 @@
52
52
  "directory": "packages/cli"
53
53
  },
54
54
  "dependencies": {
55
- "@supacloud/compiler": "^0.46.1",
56
- "@supacloud/delivery": "0.18.0",
57
- "@supacloud/db": "0.17.0",
58
- "typebox": "1.3.36"
55
+ "@supacloud/compiler": "^0.46.3",
56
+ "@supacloud/delivery": "0.18.1",
57
+ "@supacloud/db": "0.17.1",
58
+ "typebox": "1.3.37"
59
59
  },
60
60
  "overrides": {
61
61
  "@supacloud/contracts": "0.9.0",
62
- "@supacloud/delivery": "0.18.0",
63
- "@graphql-tools/utils": "^12.0.3"
62
+ "@supacloud/delivery": "0.18.1",
63
+ "@graphql-tools/utils": "^12.0.4"
64
64
  },
65
65
  "devDependencies": {
66
- "@supacloud/elysia": "^0.24.1",
66
+ "@supacloud/elysia": "0.31.0",
67
67
  "@types/bun": "^1.4.3",
68
- "effect": "4.0.2",
69
- "elysia": "2.0.0-beta.21",
68
+ "effect": "4.0.5",
69
+ "elysia": "2.0.0-beta.32",
70
70
  "typescript": "^7.0.2"
71
71
  }
72
72
  }