@supacloud/lite 0.28.2 → 0.28.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.28.3](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.28.2...supacloud-lite-v0.28.3) (2026-10-15)
4
+
5
+
6
+ ### Miscellaneous Chores
7
+
8
+ * upgrade workspace dependencies ([#1940](https://github.com/vibeunion/supacloud/issues/1940)) ([77ad418](https://github.com/vibeunion/supacloud/commit/77ad418e0568ceeeea0b59a10a3bbc451a68a95f))
9
+
3
10
  ## [0.28.2](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.28.1...supacloud-lite-v0.28.2) (2026-10-15)
4
11
 
5
12
 
package/README.md CHANGED
@@ -13,7 +13,7 @@ V1 的目标不是复刻完整 Supabase 平台控制面,而是让现有应用
13
13
  ### 状态
14
14
 
15
15
  - 运行时:npm 包需要 Bun 1.4.3+;单二进制发行版已内嵌 Bun 和 PGlite 资源
16
- - 数据库:PGlite 0.5.8,或 native PostgreSQL
16
+ - 数据库:PGlite 0.5.10,或 native PostgreSQL
17
17
  - 项目模型:单进程、单项目,内部 project ref 固定为 `local`
18
18
  - 客户端:直接使用官方 `@supabase/supabase-js`
19
19
  - 数据目录:`.supacloud-lite/db`
@@ -530,7 +530,7 @@ The goal of V1 is not to replicate the full Supabase platform control plane, but
530
530
  ### Status
531
531
 
532
532
  - Runtime: the npm package requires Bun 1.4.3+; the single-binary release embeds Bun and PGlite assets
533
- - Database: PGlite 0.5.8, or native PostgreSQL
533
+ - Database: PGlite 0.5.10, or native PostgreSQL
534
534
  - Project model: single process, single project, with an internal project ref fixed as `local`
535
535
  - Client: uses the official `@supabase/supabase-js` directly
536
536
  - Data directory: `.supacloud-lite/db`
@@ -20,10 +20,10 @@ SupaCloud Lite 使用 ElectricSQL PGlite 作为嵌入式 PostgreSQL 引擎。
20
20
 
21
21
  Lite also bundles the official PGlite extension packages:
22
22
 
23
- - `@electric-sql/pglite-pgvector` 0.0.9
24
- - `@electric-sql/pglite-postgis` 0.2.8
25
- - `@electric-sql/pglite-pg_textsearch` 0.0.10
26
- - `@electric-sql/pglite-pgmq` 0.0.6
23
+ - `@electric-sql/pglite-pgvector` 0.0.11
24
+ - `@electric-sql/pglite-postgis` 0.2.10
25
+ - `@electric-sql/pglite-pg_textsearch` 0.0.12
26
+ - `@electric-sql/pglite-pgmq` 0.0.8
27
27
 
28
28
  These packages are published by ElectricSQL under Apache-2.0 and their
29
29
  extension archives are embedded in the standalone binary.
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import { dirname as dirname8, join as join10, resolve as resolve6 } from "path";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "@supacloud/lite",
11
- version: "0.28.2",
11
+ version: "0.28.3",
12
12
  description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
13
13
  type: "module",
14
14
  license: "Apache-2.0",
@@ -62,36 +62,37 @@ var package_default = {
62
62
  typecheck: "bun x tsc --noEmit -p tsconfig.json"
63
63
  },
64
64
  dependencies: {
65
- "@electric-sql/pglite": "0.5.8",
66
- "@electric-sql/pglite-pg_textsearch": "0.0.10",
67
- "@electric-sql/pglite-pgmq": "0.0.6",
68
- "@electric-sql/pglite-pgvector": "0.0.9",
69
- "@electric-sql/pglite-postgis": "0.2.8",
70
- "@supacloud/db": "^0.6.0",
65
+ "@electric-sql/pglite": "0.5.10",
66
+ "@electric-sql/pglite-pg_textsearch": "0.0.12",
67
+ "@electric-sql/pglite-pgmq": "0.0.8",
68
+ "@electric-sql/pglite-pgvector": "0.0.11",
69
+ "@electric-sql/pglite-postgis": "0.2.10",
70
+ "@supacloud/db": "^0.17.0",
71
71
  "exact-mirror": "1.2.6",
72
72
  tar: "^7.5.22",
73
- typebox: "1.3.36"
73
+ typebox: "1.3.37"
74
74
  },
75
75
  devDependencies: {
76
76
  "@supacloud/app": "file:../app",
77
+ "@supacloud/contracts": "file:../contracts",
77
78
  "@supacloud/compiler": "file:../compiler",
78
79
  "@supacloud/elysia": "file:../elysia",
79
80
  "@supabase/supabase-js": "^2.117.3",
80
81
  "@types/bun": "^1.4.3",
81
- effect: "4.0.2",
82
- elysia: "2.0.0-beta.21",
82
+ effect: "4.0.5",
83
+ elysia: "2.0.0-beta.32",
83
84
  jose: "^6.2.12",
84
85
  typescript: "^7.0.2"
85
86
  },
86
87
  overrides: {
87
- "@supacloud/db": "^0.6.0",
88
+ "@supacloud/db": "^0.17.0",
88
89
  "@supacloud/app": "file:../app",
89
90
  "@supacloud/compiler": "file:../compiler",
90
91
  "@supacloud/elysia": "file:../elysia",
91
92
  "@supacloud/contracts": "file:../contracts",
92
93
  "@supacloud/commands": "file:../commands",
93
94
  "@supacloud/delivery": "file:../delivery",
94
- "@graphql-tools/utils": "^12.0.3"
95
+ "@graphql-tools/utils": "^12.0.4"
95
96
  },
96
97
  engines: {
97
98
  bun: ">=1.4.3"
@@ -13215,6 +13216,7 @@ ORDER BY n.nspname, c.relname, p.polname
13215
13216
  var FUNCTIONS_SQL = `
13216
13217
  SELECT n.nspname AS schema,
13217
13218
  p.proname AS name,
13219
+ pg_catalog.oidvectortypes(p.proargtypes) AS identity_arguments,
13218
13220
  p.prosecdef AS security_definer,
13219
13221
  p.proconfig AS config,
13220
13222
  l.lanname AS language
@@ -13288,12 +13290,13 @@ async function readCatalog(executor, schemas = ["public"]) {
13288
13290
  name: row.name,
13289
13291
  command: POLCMD_MAP[row.command] ?? "all",
13290
13292
  roles: row.roles ?? [],
13291
- usingExpr: row.using_expr ?? undefined,
13292
- checkExpr: row.check_expr ?? undefined
13293
+ ...row.using_expr == null ? {} : { usingExpr: row.using_expr },
13294
+ ...row.check_expr == null ? {} : { checkExpr: row.check_expr }
13293
13295
  })),
13294
13296
  functions: functionRows.map((row) => ({
13295
13297
  schema: row.schema,
13296
13298
  name: row.name,
13299
+ identityArguments: row.identity_arguments,
13297
13300
  security: row.security_definer ? "definer" : "invoker",
13298
13301
  searchPath: extractSearchPath(row.config),
13299
13302
  language: row.language
@@ -13321,8 +13324,10 @@ function splitQualifiedName(name) {
13321
13324
  function isFixedSearchPath(searchPath) {
13322
13325
  if (searchPath === null)
13323
13326
  return false;
13327
+ if (searchPath.trim() === "" || searchPath.trim() === '""')
13328
+ return true;
13324
13329
  const parts = searchPath.split(",").map((part) => part.trim().replace(/^"|"$/g, ""));
13325
- return parts.every((part) => part !== "" && part.toLowerCase() !== "pg_temp");
13330
+ return parts.every((part) => part !== "" && part.toLowerCase() !== "pg_temp" && part.toLowerCase() !== "$user");
13326
13331
  }
13327
13332
  function reconcileModule(module, catalog) {
13328
13333
  const issues = [];
@@ -13330,9 +13335,22 @@ function reconcileModule(module, catalog) {
13330
13335
  const push = (severity, code, object, message) => issues.push({ severity, code, object, message });
13331
13336
  for (const policy of module.policies) {
13332
13337
  const [schema, table] = splitQualifiedName(policy.table);
13333
- const found = catalog.policies.some((cp) => cp.schema === schema && cp.table === table && cp.name === policy.name);
13338
+ const found = catalog.policies.find((cp) => cp.schema === schema && cp.table === table && cp.name === policy.name);
13334
13339
  if (!found) {
13335
13340
  push("error", "missing-policy", `${policy.table}.${policy.name}`, `\u58F0\u660E\u7684\u7B56\u7565 ${policy.name} \u5728\u8868 ${policy.table} \u7684 catalog \u4E2D\u4E0D\u5B58\u5728`);
13341
+ } else {
13342
+ if (found.command !== policy.operation) {
13343
+ push("error", "policy-command-mismatch", `${policy.table}.${policy.name}`, `\u7B56\u7565 ${policy.name} \u58F0\u660E\u4E3A ${policy.operation}\uFF0Ccatalog \u5B9E\u9645\u4E3A ${found.command}`);
13344
+ }
13345
+ if (!sameRoles(policy.roles, found.roles)) {
13346
+ push("error", "policy-roles-mismatch", `${policy.table}.${policy.name}`, `\u7B56\u7565 ${policy.name} \u58F0\u660E\u89D2\u8272\u4E0E catalog \u4E0D\u4E00\u81F4`);
13347
+ }
13348
+ if (policy.usingExpr !== undefined && policy.usingExpr !== (found.usingExpr ?? null)) {
13349
+ push("error", "policy-using-mismatch", `${policy.table}.${policy.name}`, `\u7B56\u7565 ${policy.name} \u7684 USING \u8868\u8FBE\u5F0F\u4E0E catalog \u4E0D\u4E00\u81F4`);
13350
+ }
13351
+ if (policy.checkExpr !== undefined && policy.checkExpr !== (found.checkExpr ?? null)) {
13352
+ push("error", "policy-check-mismatch", `${policy.table}.${policy.name}`, `\u7B56\u7565 ${policy.name} \u7684 WITH CHECK \u8868\u8FBE\u5F0F\u4E0E catalog \u4E0D\u4E00\u81F4`);
13353
+ }
13336
13354
  }
13337
13355
  }
13338
13356
  const declaredPolicyKeys = new Set(module.policies.map((p) => `${p.table}::${p.name}`));
@@ -13344,11 +13362,16 @@ function reconcileModule(module, catalog) {
13344
13362
  }
13345
13363
  for (const fn of module.functions) {
13346
13364
  const [schema, name] = splitQualifiedName(fn.name);
13347
- const cf = catalog.functions.find((f) => f.schema === schema && f.name === name);
13348
- if (!cf) {
13365
+ const candidates = catalog.functions.filter((f) => f.schema === schema && f.name === name);
13366
+ if (candidates.length === 0) {
13349
13367
  push("error", "missing-function", fn.name, `\u58F0\u660E\u7684\u51FD\u6570 ${fn.name} \u5728 catalog \u4E2D\u4E0D\u5B58\u5728`);
13350
13368
  continue;
13351
13369
  }
13370
+ const cf = fn.signature !== undefined ? candidates.find((candidate) => functionSignature(candidate) === fn.signature) : candidates.length === 1 ? candidates[0] : undefined;
13371
+ if (!cf) {
13372
+ push("error", fn.signature !== undefined ? "missing-function" : "ambiguous-function-overload", fn.name, fn.signature !== undefined ? `\u58F0\u660E\u7684\u51FD\u6570\u7B7E\u540D ${fn.signature} \u5728 catalog \u4E2D\u4E0D\u5B58\u5728\u6216\u7F3A\u5C11\u7B7E\u540D\u5143\u6570\u636E` : `\u51FD\u6570 ${fn.name} \u5B58\u5728\u591A\u4E2A\u91CD\u8F7D\uFF0C\u58F0\u660E\u5FC5\u987B\u5305\u542B\u5B8C\u6574 signature`);
13373
+ continue;
13374
+ }
13352
13375
  if (cf.security !== fn.security) {
13353
13376
  push("warn", "security-mismatch", fn.name, `\u51FD\u6570 ${fn.name} \u58F0\u660E\u4E3A security ${fn.security}\uFF0Ccatalog \u5B9E\u9645\u4E3A ${cf.security}`);
13354
13377
  }
@@ -13359,9 +13382,11 @@ function reconcileModule(module, catalog) {
13359
13382
  }
13360
13383
  for (const trigger of module.triggers) {
13361
13384
  const [schema, table] = splitQualifiedName(trigger.table);
13362
- const found = catalog.triggers.some((ct) => ct.schema === schema && ct.table === table && ct.name === trigger.name);
13385
+ const found = catalog.triggers.find((ct) => ct.schema === schema && ct.table === table && ct.name === trigger.name);
13363
13386
  if (!found) {
13364
13387
  push("error", "missing-trigger", `${trigger.table}.${trigger.name}`, `\u58F0\u660E\u7684\u89E6\u53D1\u5668 ${trigger.name} \u5728\u8868 ${trigger.table} \u7684 catalog \u4E2D\u4E0D\u5B58\u5728`);
13388
+ } else if (found.enabled !== (trigger.enabled ?? true)) {
13389
+ push("error", found.enabled ? "trigger-enabled-mismatch" : "disabled-trigger", `${trigger.table}.${trigger.name}`, `\u58F0\u660E\u7684\u89E6\u53D1\u5668 ${trigger.name} \u7684\u542F\u7528\u72B6\u6001\u4E0E catalog \u4E0D\u4E00\u81F4`);
13365
13390
  }
13366
13391
  }
13367
13392
  const declaredTriggerKeys = new Set(module.triggers.map((t) => `${t.table}::${t.name}`));
@@ -13374,7 +13399,9 @@ function reconcileModule(module, catalog) {
13374
13399
  for (const table of module.tables) {
13375
13400
  const [schema, name] = splitQualifiedName(table);
13376
13401
  const ct = catalog.tables.find((t) => t.schema === schema && t.name === name);
13377
- if (ct && !ct.rlsEnabled) {
13402
+ if (!ct) {
13403
+ push("error", "missing-table", table, `\u58F0\u660E\u7684\u8868 ${table} \u5728 catalog \u4E2D\u4E0D\u5B58\u5728`);
13404
+ } else if (!ct.rlsEnabled) {
13378
13405
  push("error", "rls-disabled", table, `\u5F52\u5C5E\u8868 ${table} \u672A\u5F00\u542F\u884C\u7EA7\u5B89\u5168\uFF08relrowsecurity = false\uFF09`);
13379
13406
  }
13380
13407
  }
@@ -13397,6 +13424,15 @@ function reconcileModule(module, catalog) {
13397
13424
  ok: !issues.some((issue) => issue.severity === "error")
13398
13425
  };
13399
13426
  }
13427
+ function sameRoles(expected, actual) {
13428
+ const normalize = (roles) => [...new Set(roles)].sort();
13429
+ return JSON.stringify(normalize(expected)) === JSON.stringify(normalize(actual));
13430
+ }
13431
+ function functionSignature(functionEntry) {
13432
+ if (functionEntry.identityArguments === undefined)
13433
+ return;
13434
+ return `${functionEntry.schema}.${functionEntry.name}(${functionEntry.identityArguments})`;
13435
+ }
13400
13436
  var SECURITY_DEFINER_RE = /\bsecurity\s+definer\b/i;
13401
13437
  var SET_SEARCH_PATH_RE = /\bset\s+search_path\b/i;
13402
13438
  var GRANT_TO_PUBLIC_RE = /\bgrant\b[^;]*\bto\s+public\b/i;
@@ -13477,14 +13513,15 @@ async function lintModule(module, readFile) {
13477
13513
  for (const [file, sql] of contents) {
13478
13514
  issues.push(...lintSql(sql, file));
13479
13515
  }
13480
- if (module.policies.length > 0) {
13516
+ const firstPolicy = module.policies[0];
13517
+ if (firstPolicy) {
13481
13518
  const anyEnable = module.policies.some((policy) => ENABLE_RLS_RE.test(contents.get(policy.source) ?? ""));
13482
13519
  if (!anyEnable) {
13483
13520
  issues.push({
13484
13521
  severity: "warn",
13485
13522
  code: "missing-rls-enable",
13486
13523
  message: `\u6A21\u5757 ${module.name} \u58F0\u660E\u4E86 ${module.policies.length} \u6761\u7B56\u7565\uFF0C\u4F46\u6240\u6709\u7B56\u7565\u6E90\u6587\u4EF6\u90FD\u6CA1\u6709 enable row level security`,
13487
- file: module.policies[0].source
13524
+ file: firstPolicy.source
13488
13525
  });
13489
13526
  }
13490
13527
  }
@@ -13530,10 +13567,10 @@ function keys(row, expected) {
13530
13567
  function validTarget(value) {
13531
13568
  const target = record(value);
13532
13569
  keys(target, ["environment", "projectRef"]);
13533
- if (typeof target.environment !== "string" || !/^[a-z][a-z0-9-]{0,62}$/.test(target.environment) || typeof target.projectRef !== "string" || !/^[a-z0-9][a-z0-9_-]{0,62}$/.test(target.projectRef)) {
13570
+ if (typeof target["environment"] !== "string" || !/^[a-z][a-z0-9-]{0,62}$/.test(target["environment"]) || typeof target["projectRef"] !== "string" || !/^[a-z0-9][a-z0-9_-]{0,62}$/.test(target["projectRef"])) {
13534
13571
  throw new Error("Invalid migration binding target");
13535
13572
  }
13536
- return target;
13573
+ return { environment: target["environment"], projectRef: target["projectRef"] };
13537
13574
  }
13538
13575
  function validFile(file) {
13539
13576
  return typeof file === "string" && /^[a-zA-Z0-9_./-]+\.sql$/.test(file) && !file.startsWith("/") && file.split("/").every((part) => part && part !== "." && part !== "..");
@@ -13541,32 +13578,37 @@ function validFile(file) {
13541
13578
  function parseMigrationBindingManifest(value) {
13542
13579
  const manifest = record(value);
13543
13580
  keys(manifest, ["schema", "targets", "templates"]);
13544
- if (manifest.schema !== "supacloud.migration-bindings.v1" || !Array.isArray(manifest.targets) || !manifest.targets.length || !Array.isArray(manifest.templates)) {
13581
+ if (manifest["schema"] !== "supacloud.migration-bindings.v1" || !Array.isArray(manifest["targets"]) || !manifest["targets"].length || !Array.isArray(manifest["templates"])) {
13545
13582
  throw new Error("Invalid migration binding manifest");
13546
13583
  }
13547
- const targets = manifest.targets.map(validTarget);
13584
+ const targets = manifest["targets"].map(validTarget);
13548
13585
  if (new Set(targets.map((target) => target.environment)).size !== targets.length) {
13549
13586
  throw new Error("Duplicate migration binding environment");
13550
13587
  }
13551
13588
  const files = new Set;
13552
- const templates = manifest.templates.map((value2) => {
13553
- const template = record(value2);
13589
+ const templates = manifest["templates"].map((value) => {
13590
+ const template = record(value);
13554
13591
  keys(template, ["file", "templateSha256", "parameters"]);
13555
- if (!validFile(template.file) || files.has(template.file) || typeof template.templateSha256 !== "string" || !HASH.test(template.templateSha256) || !Array.isArray(template.parameters) || !template.parameters.length) {
13592
+ if (!validFile(template["file"]) || files.has(template["file"]) || typeof template["templateSha256"] !== "string" || !HASH.test(template["templateSha256"]) || !Array.isArray(template["parameters"]) || !template["parameters"].length) {
13556
13593
  throw new Error("Invalid or duplicate migration binding template");
13557
13594
  }
13558
- files.add(template.file);
13595
+ files.add(template["file"]);
13559
13596
  const placeholders = new Set;
13560
- const parameters = template.parameters.map((value3) => {
13561
- const parameter = record(value3);
13597
+ const parameters = template["parameters"].map((value) => {
13598
+ const parameter = record(value);
13562
13599
  keys(parameter, ["placeholder", "variable", "type", "occurrences"]);
13563
- if (typeof parameter.placeholder !== "string" || !/^__[A-Z][A-Z0-9_]*__$/.test(parameter.placeholder) || placeholders.has(parameter.placeholder) || typeof parameter.variable !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(parameter.variable) || !["uuid", "https-url", "resource-name"].includes(String(parameter.type)) || !Number.isSafeInteger(parameter.occurrences) || Number(parameter.occurrences) < 1) {
13600
+ if (typeof parameter["placeholder"] !== "string" || !/^__[A-Z][A-Z0-9_]*__$/.test(parameter["placeholder"]) || placeholders.has(parameter["placeholder"]) || typeof parameter["variable"] !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(parameter["variable"]) || parameter["type"] !== "uuid" && parameter["type"] !== "https-url" && parameter["type"] !== "resource-name" || typeof parameter["occurrences"] !== "number" || !Number.isSafeInteger(parameter["occurrences"]) || parameter["occurrences"] < 1) {
13564
13601
  throw new Error("Invalid or duplicate migration binding parameter");
13565
13602
  }
13566
- placeholders.add(parameter.placeholder);
13567
- return parameter;
13603
+ placeholders.add(parameter["placeholder"]);
13604
+ return {
13605
+ placeholder: parameter["placeholder"],
13606
+ variable: parameter["variable"],
13607
+ type: parameter["type"],
13608
+ occurrences: parameter["occurrences"]
13609
+ };
13568
13610
  });
13569
- return { file: template.file, templateSha256: template.templateSha256, parameters };
13611
+ return { file: template["file"], templateSha256: template["templateSha256"], parameters };
13570
13612
  });
13571
13613
  return { schema: "supacloud.migration-bindings.v1", targets, templates };
13572
13614
  }
@@ -13654,6 +13696,194 @@ function renderMigrationBindings(options) {
13654
13696
  }
13655
13697
  };
13656
13698
  }
13699
+ var COMMAND_PERSISTENCE_SQL = `
13700
+ DO $runtime$
13701
+ BEGIN
13702
+ IF to_regprocedure('supacloud_workflows.start_run(uuid,text,text,text,jsonb,integer)') IS NULL
13703
+ OR to_regprocedure('supacloud_commands.status(jsonb)') IS NULL THEN
13704
+ RAISE EXCEPTION 'Install the SupaCloud Commands/Workflow/PGMQ runtime before command persistence';
13705
+ END IF;
13706
+ END
13707
+ $runtime$;
13708
+ CREATE SCHEMA IF NOT EXISTS supacloud_commands;
13709
+ REVOKE ALL ON SCHEMA supacloud_commands FROM PUBLIC;
13710
+ CREATE TABLE IF NOT EXISTS supacloud_commands.execution_receipts (
13711
+ tenant_id text NOT NULL,
13712
+ actor_id text NOT NULL,
13713
+ command text NOT NULL,
13714
+ operation_key text NOT NULL,
13715
+ kind text NOT NULL CHECK (kind IN ('transactional', 'external')),
13716
+ input_fingerprint text NOT NULL CHECK (input_fingerprint ~ '^[a-f0-9]{64}$'),
13717
+ input_payload text,
13718
+ dispatch_key uuid NOT NULL DEFAULT gen_random_uuid() UNIQUE,
13719
+ status text NOT NULL CHECK (status IN ('pending', 'unknown', 'confirmed', 'rejected')),
13720
+ audit_state text NOT NULL CHECK (audit_state IN ('pending', 'complete')),
13721
+ result jsonb,
13722
+ created_at timestamptz NOT NULL DEFAULT now(),
13723
+ updated_at timestamptz NOT NULL DEFAULT now(),
13724
+ PRIMARY KEY (tenant_id, actor_id, command, operation_key),
13725
+ CHECK ((status = 'confirmed') = (result IS NOT NULL)),
13726
+ CONSTRAINT execution_receipts_audit_terminal_check CHECK (
13727
+ (audit_state <> 'complete' OR status IN ('confirmed', 'rejected'))
13728
+ AND (status <> 'rejected' OR audit_state = 'complete')),
13729
+ CONSTRAINT execution_receipts_transactional_check CHECK (
13730
+ kind <> 'transactional' OR (status = 'confirmed' AND audit_state = 'complete'))
13731
+ );
13732
+ CREATE TABLE IF NOT EXISTS supacloud_commands.execution_audit (
13733
+ tenant_id text NOT NULL,
13734
+ actor_id text NOT NULL,
13735
+ command text NOT NULL,
13736
+ operation_key text NOT NULL,
13737
+ event text NOT NULL,
13738
+ details jsonb NOT NULL,
13739
+ created_at timestamptz NOT NULL DEFAULT now(),
13740
+ PRIMARY KEY (tenant_id, actor_id, command, operation_key),
13741
+ FOREIGN KEY (tenant_id, actor_id, command, operation_key)
13742
+ REFERENCES supacloud_commands.execution_receipts (tenant_id, actor_id, command, operation_key)
13743
+ );
13744
+ REVOKE ALL ON supacloud_commands.execution_receipts, supacloud_commands.execution_audit FROM PUBLIC;
13745
+ CREATE OR REPLACE FUNCTION supacloud_commands.protect_terminal_execution()
13746
+ RETURNS trigger LANGUAGE plpgsql SET search_path = '' AS $terminal$
13747
+ BEGIN
13748
+ IF OLD.status IN ('confirmed','rejected') AND (
13749
+ NEW.status IS DISTINCT FROM OLD.status OR NEW.result IS DISTINCT FROM OLD.result
13750
+ OR (OLD.audit_state = 'complete' AND NEW.audit_state IS DISTINCT FROM OLD.audit_state)) THEN
13751
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_TERMINAL_RECEIPT';
13752
+ END IF;
13753
+ RETURN NEW;
13754
+ END
13755
+ $terminal$;
13756
+ CREATE OR REPLACE TRIGGER execution_terminal_guard
13757
+ BEFORE UPDATE ON supacloud_commands.execution_receipts
13758
+ FOR EACH ROW EXECUTE FUNCTION supacloud_commands.protect_terminal_execution();
13759
+
13760
+ CREATE OR REPLACE FUNCTION supacloud_commands.require_rejection_audit()
13761
+ RETURNS trigger LANGUAGE plpgsql SET search_path = '' AS $rejection$
13762
+ BEGIN
13763
+ IF NEW.status = 'rejected' AND NOT EXISTS (
13764
+ SELECT FROM supacloud_commands.execution_audit a
13765
+ WHERE a.tenant_id=NEW.tenant_id AND a.actor_id=NEW.actor_id
13766
+ AND a.command=NEW.command AND a.operation_key=NEW.operation_key
13767
+ ) THEN
13768
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_REJECTION_AUDIT_REQUIRED';
13769
+ END IF;
13770
+ RETURN NEW;
13771
+ END
13772
+ $rejection$;
13773
+ DROP TRIGGER IF EXISTS execution_rejection_audit ON supacloud_commands.execution_receipts;
13774
+ CREATE CONSTRAINT TRIGGER execution_rejection_audit
13775
+ AFTER INSERT OR UPDATE ON supacloud_commands.execution_receipts
13776
+ DEFERRABLE INITIALLY DEFERRED
13777
+ FOR EACH ROW WHEN (NEW.status = 'rejected')
13778
+ EXECUTE FUNCTION supacloud_commands.require_rejection_audit();
13779
+ CREATE OR REPLACE FUNCTION supacloud_commands.enqueue_execution_recovery()
13780
+ RETURNS trigger LANGUAGE plpgsql SET search_path = '' AS $recovery$
13781
+ DECLARE submission jsonb;
13782
+ BEGIN
13783
+ PERFORM pg_advisory_xact_lock(hashtextextended(NEW.dispatch_key::text,0));
13784
+ SELECT supacloud_commands.snapshot(NEW.dispatch_key,false) INTO submission;
13785
+ IF submission IS NOT NULL THEN
13786
+ IF submission->>'commandType' IS DISTINCT FROM NEW.command
13787
+ OR submission->>'actorId' IS DISTINCT FROM NEW.actor_id
13788
+ OR submission->>'tenantId' IS DISTINCT FROM NEW.tenant_id
13789
+ OR NEW.operation_key <> NEW.dispatch_key::text THEN
13790
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_IDENTITY_CONFLICT';
13791
+ END IF;
13792
+ RETURN NEW;
13793
+ END IF;
13794
+ IF NEW.kind = 'external' AND NEW.status <> 'rejected'
13795
+ AND (NEW.status <> 'confirmed' OR NEW.audit_state <> 'complete') THEN
13796
+ PERFORM supacloud_workflows.start_run(
13797
+ NEW.dispatch_key, 'supacloud.command.reconcile', '1', 'reconcile',
13798
+ jsonb_build_object('commandId', NEW.dispatch_key, 'tenantId', NEW.tenant_id,
13799
+ 'actorId', NEW.actor_id, 'command', NEW.command, 'operationId', NEW.operation_key),
13800
+ 20
13801
+ );
13802
+ END IF;
13803
+ RETURN NEW;
13804
+ END
13805
+ $recovery$;
13806
+ CREATE OR REPLACE TRIGGER execution_recovery_enqueue
13807
+ AFTER INSERT ON supacloud_commands.execution_receipts
13808
+ FOR EACH ROW EXECUTE FUNCTION supacloud_commands.enqueue_execution_recovery();
13809
+
13810
+ CREATE OR REPLACE FUNCTION supacloud_commands.execution_status(request jsonb)
13811
+ RETURNS jsonb LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $status$
13812
+ SELECT jsonb_build_object('kind','execution','commandId',r.dispatch_key,
13813
+ 'execution',jsonb_build_object('tenantId',r.tenant_id,'actorId',r.actor_id,
13814
+ 'command',r.command,'operationId',r.operation_key,'dispatchKey',r.dispatch_key,
13815
+ 'status',r.status,'audit',r.audit_state)
13816
+ || CASE WHEN r.status='confirmed' THEN jsonb_build_object('result',r.result) ELSE '{}'::jsonb END,
13817
+ 'workflow',CASE WHEN w.id IS NULL THEN NULL ELSE jsonb_build_object('runId',w.id,'status',w.status) END)
13818
+ FROM supacloud_commands.execution_receipts r
13819
+ LEFT JOIN supacloud_workflows.runs w ON w.id=r.dispatch_key
13820
+ WHERE CASE WHEN request ? 'commandId' THEN r.dispatch_key::text=request->>'commandId'
13821
+ ELSE r.tenant_id=request->>'tenantId' AND r.actor_id=request->>'actorId'
13822
+ AND r.command=request->>'command' AND r.operation_key=request->>'operationId' END
13823
+ $status$;
13824
+ REVOKE ALL ON FUNCTION supacloud_commands.execution_status(jsonb) FROM PUBLIC;
13825
+ `;
13826
+ var COMMAND_PERSISTENCE_UPGRADE_SQL = `
13827
+ ALTER TABLE supacloud_commands.execution_receipts
13828
+ ADD COLUMN IF NOT EXISTS input_fingerprint text,
13829
+ ADD COLUMN IF NOT EXISTS input_payload text;
13830
+ DO $migration$
13831
+ BEGIN
13832
+ IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='supacloud_commands'
13833
+ AND table_name='execution_receipts' AND column_name='input_key') THEN
13834
+ EXECUTE 'UPDATE supacloud_commands.execution_receipts
13835
+ SET input_fingerprint=encode(sha256(convert_to(input_key,''UTF8'')),''hex''),input_payload=input_key
13836
+ WHERE input_fingerprint IS NULL';
13837
+ ALTER TABLE supacloud_commands.execution_receipts DROP COLUMN input_key;
13838
+ END IF;
13839
+ END
13840
+ $migration$;
13841
+ ALTER TABLE supacloud_commands.execution_receipts ALTER COLUMN input_fingerprint SET NOT NULL;
13842
+ ALTER TABLE supacloud_commands.execution_receipts
13843
+ DROP CONSTRAINT IF EXISTS execution_receipts_status_check,
13844
+ DROP CONSTRAINT IF EXISTS execution_receipts_check1,
13845
+ DROP CONSTRAINT IF EXISTS execution_receipts_transactional_check,
13846
+ DROP CONSTRAINT IF EXISTS execution_receipts_audit_terminal_check;
13847
+ ALTER TABLE supacloud_commands.execution_receipts
13848
+ ADD CONSTRAINT execution_receipts_status_check CHECK (status IN ('pending','unknown','confirmed','rejected')),
13849
+ ADD CONSTRAINT execution_receipts_transactional_check CHECK (
13850
+ kind <> 'transactional' OR (status = 'confirmed' AND audit_state = 'complete')),
13851
+ ADD CONSTRAINT execution_receipts_audit_terminal_check CHECK (
13852
+ (audit_state <> 'complete' OR status IN ('confirmed','rejected'))
13853
+ AND (status <> 'rejected' OR audit_state = 'complete'));
13854
+ DO $constraint$
13855
+ BEGIN
13856
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint
13857
+ WHERE conrelid='supacloud_commands.execution_receipts'::regclass
13858
+ AND conname='execution_receipts_input_fingerprint_check') THEN
13859
+ ALTER TABLE supacloud_commands.execution_receipts
13860
+ ADD CONSTRAINT execution_receipts_input_fingerprint_check CHECK (input_fingerprint ~ '^[a-f0-9]{64}$');
13861
+ END IF;
13862
+ END
13863
+ $constraint$;
13864
+ ${COMMAND_PERSISTENCE_SQL}
13865
+ DO $backfill$
13866
+ DECLARE r record;
13867
+ BEGIN
13868
+ FOR r IN SELECT * FROM supacloud_commands.execution_receipts
13869
+ WHERE kind='external' AND status<>'rejected' AND (status<>'confirmed' OR audit_state<>'complete')
13870
+ AND NOT EXISTS(SELECT FROM supacloud_workflows.runs w WHERE w.id=dispatch_key)
13871
+ LOOP
13872
+ PERFORM supacloud_workflows.start_run(
13873
+ r.dispatch_key,'supacloud.command.reconcile','1','reconcile',
13874
+ jsonb_build_object('commandId',r.dispatch_key,'tenantId',r.tenant_id,
13875
+ 'actorId',r.actor_id,'command',r.command,'operationId',r.operation_key),20
13876
+ );
13877
+ END LOOP;
13878
+ END
13879
+ $backfill$;
13880
+ DROP INDEX IF EXISTS supacloud_commands.execution_pending_recovery;
13881
+ ALTER TABLE supacloud_commands.execution_receipts
13882
+ DROP COLUMN IF EXISTS lease_id,
13883
+ DROP COLUMN IF EXISTS lease_until,
13884
+ DROP COLUMN IF EXISTS next_attempt_at,
13885
+ DROP COLUMN IF EXISTS recovery_attempts;
13886
+ `;
13657
13887
 
13658
13888
  // src/runtime/node/db-check.ts
13659
13889
  async function loadDatabaseModules(moduleFile) {
package/dist/index.js CHANGED
@@ -88,7 +88,7 @@ function randomToken(bytes = 32) {
88
88
  // package.json
89
89
  var package_default = {
90
90
  name: "@supacloud/lite",
91
- version: "0.28.2",
91
+ version: "0.28.3",
92
92
  description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
93
93
  type: "module",
94
94
  license: "Apache-2.0",
@@ -142,36 +142,37 @@ var package_default = {
142
142
  typecheck: "bun x tsc --noEmit -p tsconfig.json"
143
143
  },
144
144
  dependencies: {
145
- "@electric-sql/pglite": "0.5.8",
146
- "@electric-sql/pglite-pg_textsearch": "0.0.10",
147
- "@electric-sql/pglite-pgmq": "0.0.6",
148
- "@electric-sql/pglite-pgvector": "0.0.9",
149
- "@electric-sql/pglite-postgis": "0.2.8",
150
- "@supacloud/db": "^0.6.0",
145
+ "@electric-sql/pglite": "0.5.10",
146
+ "@electric-sql/pglite-pg_textsearch": "0.0.12",
147
+ "@electric-sql/pglite-pgmq": "0.0.8",
148
+ "@electric-sql/pglite-pgvector": "0.0.11",
149
+ "@electric-sql/pglite-postgis": "0.2.10",
150
+ "@supacloud/db": "^0.17.0",
151
151
  "exact-mirror": "1.2.6",
152
152
  tar: "^7.5.22",
153
- typebox: "1.3.36"
153
+ typebox: "1.3.37"
154
154
  },
155
155
  devDependencies: {
156
156
  "@supacloud/app": "file:../app",
157
+ "@supacloud/contracts": "file:../contracts",
157
158
  "@supacloud/compiler": "file:../compiler",
158
159
  "@supacloud/elysia": "file:../elysia",
159
160
  "@supabase/supabase-js": "^2.117.3",
160
161
  "@types/bun": "^1.4.3",
161
- effect: "4.0.2",
162
- elysia: "2.0.0-beta.21",
162
+ effect: "4.0.5",
163
+ elysia: "2.0.0-beta.32",
163
164
  jose: "^6.2.12",
164
165
  typescript: "^7.0.2"
165
166
  },
166
167
  overrides: {
167
- "@supacloud/db": "^0.6.0",
168
+ "@supacloud/db": "^0.17.0",
168
169
  "@supacloud/app": "file:../app",
169
170
  "@supacloud/compiler": "file:../compiler",
170
171
  "@supacloud/elysia": "file:../elysia",
171
172
  "@supacloud/contracts": "file:../contracts",
172
173
  "@supacloud/commands": "file:../commands",
173
174
  "@supacloud/delivery": "file:../delivery",
174
- "@graphql-tools/utils": "^12.0.3"
175
+ "@graphql-tools/utils": "^12.0.4"
175
176
  },
176
177
  engines: {
177
178
  bun: ">=1.4.3"
@@ -13971,10 +13972,10 @@ function keys(row, expected) {
13971
13972
  function validTarget(value) {
13972
13973
  const target = record(value);
13973
13974
  keys(target, ["environment", "projectRef"]);
13974
- if (typeof target.environment !== "string" || !/^[a-z][a-z0-9-]{0,62}$/.test(target.environment) || typeof target.projectRef !== "string" || !/^[a-z0-9][a-z0-9_-]{0,62}$/.test(target.projectRef)) {
13975
+ if (typeof target["environment"] !== "string" || !/^[a-z][a-z0-9-]{0,62}$/.test(target["environment"]) || typeof target["projectRef"] !== "string" || !/^[a-z0-9][a-z0-9_-]{0,62}$/.test(target["projectRef"])) {
13975
13976
  throw new Error("Invalid migration binding target");
13976
13977
  }
13977
- return target;
13978
+ return { environment: target["environment"], projectRef: target["projectRef"] };
13978
13979
  }
13979
13980
  function validFile(file) {
13980
13981
  return typeof file === "string" && /^[a-zA-Z0-9_./-]+\.sql$/.test(file) && !file.startsWith("/") && file.split("/").every((part) => part && part !== "." && part !== "..");
@@ -13982,32 +13983,37 @@ function validFile(file) {
13982
13983
  function parseMigrationBindingManifest(value) {
13983
13984
  const manifest = record(value);
13984
13985
  keys(manifest, ["schema", "targets", "templates"]);
13985
- if (manifest.schema !== "supacloud.migration-bindings.v1" || !Array.isArray(manifest.targets) || !manifest.targets.length || !Array.isArray(manifest.templates)) {
13986
+ if (manifest["schema"] !== "supacloud.migration-bindings.v1" || !Array.isArray(manifest["targets"]) || !manifest["targets"].length || !Array.isArray(manifest["templates"])) {
13986
13987
  throw new Error("Invalid migration binding manifest");
13987
13988
  }
13988
- const targets = manifest.targets.map(validTarget);
13989
+ const targets = manifest["targets"].map(validTarget);
13989
13990
  if (new Set(targets.map((target) => target.environment)).size !== targets.length) {
13990
13991
  throw new Error("Duplicate migration binding environment");
13991
13992
  }
13992
13993
  const files = new Set;
13993
- const templates = manifest.templates.map((value2) => {
13994
- const template = record(value2);
13994
+ const templates = manifest["templates"].map((value) => {
13995
+ const template = record(value);
13995
13996
  keys(template, ["file", "templateSha256", "parameters"]);
13996
- if (!validFile(template.file) || files.has(template.file) || typeof template.templateSha256 !== "string" || !HASH.test(template.templateSha256) || !Array.isArray(template.parameters) || !template.parameters.length) {
13997
+ if (!validFile(template["file"]) || files.has(template["file"]) || typeof template["templateSha256"] !== "string" || !HASH.test(template["templateSha256"]) || !Array.isArray(template["parameters"]) || !template["parameters"].length) {
13997
13998
  throw new Error("Invalid or duplicate migration binding template");
13998
13999
  }
13999
- files.add(template.file);
14000
+ files.add(template["file"]);
14000
14001
  const placeholders = new Set;
14001
- const parameters = template.parameters.map((value3) => {
14002
- const parameter = record(value3);
14002
+ const parameters = template["parameters"].map((value) => {
14003
+ const parameter = record(value);
14003
14004
  keys(parameter, ["placeholder", "variable", "type", "occurrences"]);
14004
- if (typeof parameter.placeholder !== "string" || !/^__[A-Z][A-Z0-9_]*__$/.test(parameter.placeholder) || placeholders.has(parameter.placeholder) || typeof parameter.variable !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(parameter.variable) || !["uuid", "https-url", "resource-name"].includes(String(parameter.type)) || !Number.isSafeInteger(parameter.occurrences) || Number(parameter.occurrences) < 1) {
14005
+ if (typeof parameter["placeholder"] !== "string" || !/^__[A-Z][A-Z0-9_]*__$/.test(parameter["placeholder"]) || placeholders.has(parameter["placeholder"]) || typeof parameter["variable"] !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(parameter["variable"]) || parameter["type"] !== "uuid" && parameter["type"] !== "https-url" && parameter["type"] !== "resource-name" || typeof parameter["occurrences"] !== "number" || !Number.isSafeInteger(parameter["occurrences"]) || parameter["occurrences"] < 1) {
14005
14006
  throw new Error("Invalid or duplicate migration binding parameter");
14006
14007
  }
14007
- placeholders.add(parameter.placeholder);
14008
- return parameter;
14008
+ placeholders.add(parameter["placeholder"]);
14009
+ return {
14010
+ placeholder: parameter["placeholder"],
14011
+ variable: parameter["variable"],
14012
+ type: parameter["type"],
14013
+ occurrences: parameter["occurrences"]
14014
+ };
14009
14015
  });
14010
- return { file: template.file, templateSha256: template.templateSha256, parameters };
14016
+ return { file: template["file"], templateSha256: template["templateSha256"], parameters };
14011
14017
  });
14012
14018
  return { schema: "supacloud.migration-bindings.v1", targets, templates };
14013
14019
  }
@@ -14095,6 +14101,194 @@ function renderMigrationBindings(options) {
14095
14101
  }
14096
14102
  };
14097
14103
  }
14104
+ var COMMAND_PERSISTENCE_SQL = `
14105
+ DO $runtime$
14106
+ BEGIN
14107
+ IF to_regprocedure('supacloud_workflows.start_run(uuid,text,text,text,jsonb,integer)') IS NULL
14108
+ OR to_regprocedure('supacloud_commands.status(jsonb)') IS NULL THEN
14109
+ RAISE EXCEPTION 'Install the SupaCloud Commands/Workflow/PGMQ runtime before command persistence';
14110
+ END IF;
14111
+ END
14112
+ $runtime$;
14113
+ CREATE SCHEMA IF NOT EXISTS supacloud_commands;
14114
+ REVOKE ALL ON SCHEMA supacloud_commands FROM PUBLIC;
14115
+ CREATE TABLE IF NOT EXISTS supacloud_commands.execution_receipts (
14116
+ tenant_id text NOT NULL,
14117
+ actor_id text NOT NULL,
14118
+ command text NOT NULL,
14119
+ operation_key text NOT NULL,
14120
+ kind text NOT NULL CHECK (kind IN ('transactional', 'external')),
14121
+ input_fingerprint text NOT NULL CHECK (input_fingerprint ~ '^[a-f0-9]{64}$'),
14122
+ input_payload text,
14123
+ dispatch_key uuid NOT NULL DEFAULT gen_random_uuid() UNIQUE,
14124
+ status text NOT NULL CHECK (status IN ('pending', 'unknown', 'confirmed', 'rejected')),
14125
+ audit_state text NOT NULL CHECK (audit_state IN ('pending', 'complete')),
14126
+ result jsonb,
14127
+ created_at timestamptz NOT NULL DEFAULT now(),
14128
+ updated_at timestamptz NOT NULL DEFAULT now(),
14129
+ PRIMARY KEY (tenant_id, actor_id, command, operation_key),
14130
+ CHECK ((status = 'confirmed') = (result IS NOT NULL)),
14131
+ CONSTRAINT execution_receipts_audit_terminal_check CHECK (
14132
+ (audit_state <> 'complete' OR status IN ('confirmed', 'rejected'))
14133
+ AND (status <> 'rejected' OR audit_state = 'complete')),
14134
+ CONSTRAINT execution_receipts_transactional_check CHECK (
14135
+ kind <> 'transactional' OR (status = 'confirmed' AND audit_state = 'complete'))
14136
+ );
14137
+ CREATE TABLE IF NOT EXISTS supacloud_commands.execution_audit (
14138
+ tenant_id text NOT NULL,
14139
+ actor_id text NOT NULL,
14140
+ command text NOT NULL,
14141
+ operation_key text NOT NULL,
14142
+ event text NOT NULL,
14143
+ details jsonb NOT NULL,
14144
+ created_at timestamptz NOT NULL DEFAULT now(),
14145
+ PRIMARY KEY (tenant_id, actor_id, command, operation_key),
14146
+ FOREIGN KEY (tenant_id, actor_id, command, operation_key)
14147
+ REFERENCES supacloud_commands.execution_receipts (tenant_id, actor_id, command, operation_key)
14148
+ );
14149
+ REVOKE ALL ON supacloud_commands.execution_receipts, supacloud_commands.execution_audit FROM PUBLIC;
14150
+ CREATE OR REPLACE FUNCTION supacloud_commands.protect_terminal_execution()
14151
+ RETURNS trigger LANGUAGE plpgsql SET search_path = '' AS $terminal$
14152
+ BEGIN
14153
+ IF OLD.status IN ('confirmed','rejected') AND (
14154
+ NEW.status IS DISTINCT FROM OLD.status OR NEW.result IS DISTINCT FROM OLD.result
14155
+ OR (OLD.audit_state = 'complete' AND NEW.audit_state IS DISTINCT FROM OLD.audit_state)) THEN
14156
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_TERMINAL_RECEIPT';
14157
+ END IF;
14158
+ RETURN NEW;
14159
+ END
14160
+ $terminal$;
14161
+ CREATE OR REPLACE TRIGGER execution_terminal_guard
14162
+ BEFORE UPDATE ON supacloud_commands.execution_receipts
14163
+ FOR EACH ROW EXECUTE FUNCTION supacloud_commands.protect_terminal_execution();
14164
+
14165
+ CREATE OR REPLACE FUNCTION supacloud_commands.require_rejection_audit()
14166
+ RETURNS trigger LANGUAGE plpgsql SET search_path = '' AS $rejection$
14167
+ BEGIN
14168
+ IF NEW.status = 'rejected' AND NOT EXISTS (
14169
+ SELECT FROM supacloud_commands.execution_audit a
14170
+ WHERE a.tenant_id=NEW.tenant_id AND a.actor_id=NEW.actor_id
14171
+ AND a.command=NEW.command AND a.operation_key=NEW.operation_key
14172
+ ) THEN
14173
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_REJECTION_AUDIT_REQUIRED';
14174
+ END IF;
14175
+ RETURN NEW;
14176
+ END
14177
+ $rejection$;
14178
+ DROP TRIGGER IF EXISTS execution_rejection_audit ON supacloud_commands.execution_receipts;
14179
+ CREATE CONSTRAINT TRIGGER execution_rejection_audit
14180
+ AFTER INSERT OR UPDATE ON supacloud_commands.execution_receipts
14181
+ DEFERRABLE INITIALLY DEFERRED
14182
+ FOR EACH ROW WHEN (NEW.status = 'rejected')
14183
+ EXECUTE FUNCTION supacloud_commands.require_rejection_audit();
14184
+ CREATE OR REPLACE FUNCTION supacloud_commands.enqueue_execution_recovery()
14185
+ RETURNS trigger LANGUAGE plpgsql SET search_path = '' AS $recovery$
14186
+ DECLARE submission jsonb;
14187
+ BEGIN
14188
+ PERFORM pg_advisory_xact_lock(hashtextextended(NEW.dispatch_key::text,0));
14189
+ SELECT supacloud_commands.snapshot(NEW.dispatch_key,false) INTO submission;
14190
+ IF submission IS NOT NULL THEN
14191
+ IF submission->>'commandType' IS DISTINCT FROM NEW.command
14192
+ OR submission->>'actorId' IS DISTINCT FROM NEW.actor_id
14193
+ OR submission->>'tenantId' IS DISTINCT FROM NEW.tenant_id
14194
+ OR NEW.operation_key <> NEW.dispatch_key::text THEN
14195
+ RAISE EXCEPTION 'SUPACLOUD_COMMAND_IDENTITY_CONFLICT';
14196
+ END IF;
14197
+ RETURN NEW;
14198
+ END IF;
14199
+ IF NEW.kind = 'external' AND NEW.status <> 'rejected'
14200
+ AND (NEW.status <> 'confirmed' OR NEW.audit_state <> 'complete') THEN
14201
+ PERFORM supacloud_workflows.start_run(
14202
+ NEW.dispatch_key, 'supacloud.command.reconcile', '1', 'reconcile',
14203
+ jsonb_build_object('commandId', NEW.dispatch_key, 'tenantId', NEW.tenant_id,
14204
+ 'actorId', NEW.actor_id, 'command', NEW.command, 'operationId', NEW.operation_key),
14205
+ 20
14206
+ );
14207
+ END IF;
14208
+ RETURN NEW;
14209
+ END
14210
+ $recovery$;
14211
+ CREATE OR REPLACE TRIGGER execution_recovery_enqueue
14212
+ AFTER INSERT ON supacloud_commands.execution_receipts
14213
+ FOR EACH ROW EXECUTE FUNCTION supacloud_commands.enqueue_execution_recovery();
14214
+
14215
+ CREATE OR REPLACE FUNCTION supacloud_commands.execution_status(request jsonb)
14216
+ RETURNS jsonb LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $status$
14217
+ SELECT jsonb_build_object('kind','execution','commandId',r.dispatch_key,
14218
+ 'execution',jsonb_build_object('tenantId',r.tenant_id,'actorId',r.actor_id,
14219
+ 'command',r.command,'operationId',r.operation_key,'dispatchKey',r.dispatch_key,
14220
+ 'status',r.status,'audit',r.audit_state)
14221
+ || CASE WHEN r.status='confirmed' THEN jsonb_build_object('result',r.result) ELSE '{}'::jsonb END,
14222
+ 'workflow',CASE WHEN w.id IS NULL THEN NULL ELSE jsonb_build_object('runId',w.id,'status',w.status) END)
14223
+ FROM supacloud_commands.execution_receipts r
14224
+ LEFT JOIN supacloud_workflows.runs w ON w.id=r.dispatch_key
14225
+ WHERE CASE WHEN request ? 'commandId' THEN r.dispatch_key::text=request->>'commandId'
14226
+ ELSE r.tenant_id=request->>'tenantId' AND r.actor_id=request->>'actorId'
14227
+ AND r.command=request->>'command' AND r.operation_key=request->>'operationId' END
14228
+ $status$;
14229
+ REVOKE ALL ON FUNCTION supacloud_commands.execution_status(jsonb) FROM PUBLIC;
14230
+ `;
14231
+ var COMMAND_PERSISTENCE_UPGRADE_SQL = `
14232
+ ALTER TABLE supacloud_commands.execution_receipts
14233
+ ADD COLUMN IF NOT EXISTS input_fingerprint text,
14234
+ ADD COLUMN IF NOT EXISTS input_payload text;
14235
+ DO $migration$
14236
+ BEGIN
14237
+ IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema='supacloud_commands'
14238
+ AND table_name='execution_receipts' AND column_name='input_key') THEN
14239
+ EXECUTE 'UPDATE supacloud_commands.execution_receipts
14240
+ SET input_fingerprint=encode(sha256(convert_to(input_key,''UTF8'')),''hex''),input_payload=input_key
14241
+ WHERE input_fingerprint IS NULL';
14242
+ ALTER TABLE supacloud_commands.execution_receipts DROP COLUMN input_key;
14243
+ END IF;
14244
+ END
14245
+ $migration$;
14246
+ ALTER TABLE supacloud_commands.execution_receipts ALTER COLUMN input_fingerprint SET NOT NULL;
14247
+ ALTER TABLE supacloud_commands.execution_receipts
14248
+ DROP CONSTRAINT IF EXISTS execution_receipts_status_check,
14249
+ DROP CONSTRAINT IF EXISTS execution_receipts_check1,
14250
+ DROP CONSTRAINT IF EXISTS execution_receipts_transactional_check,
14251
+ DROP CONSTRAINT IF EXISTS execution_receipts_audit_terminal_check;
14252
+ ALTER TABLE supacloud_commands.execution_receipts
14253
+ ADD CONSTRAINT execution_receipts_status_check CHECK (status IN ('pending','unknown','confirmed','rejected')),
14254
+ ADD CONSTRAINT execution_receipts_transactional_check CHECK (
14255
+ kind <> 'transactional' OR (status = 'confirmed' AND audit_state = 'complete')),
14256
+ ADD CONSTRAINT execution_receipts_audit_terminal_check CHECK (
14257
+ (audit_state <> 'complete' OR status IN ('confirmed','rejected'))
14258
+ AND (status <> 'rejected' OR audit_state = 'complete'));
14259
+ DO $constraint$
14260
+ BEGIN
14261
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint
14262
+ WHERE conrelid='supacloud_commands.execution_receipts'::regclass
14263
+ AND conname='execution_receipts_input_fingerprint_check') THEN
14264
+ ALTER TABLE supacloud_commands.execution_receipts
14265
+ ADD CONSTRAINT execution_receipts_input_fingerprint_check CHECK (input_fingerprint ~ '^[a-f0-9]{64}$');
14266
+ END IF;
14267
+ END
14268
+ $constraint$;
14269
+ ${COMMAND_PERSISTENCE_SQL}
14270
+ DO $backfill$
14271
+ DECLARE r record;
14272
+ BEGIN
14273
+ FOR r IN SELECT * FROM supacloud_commands.execution_receipts
14274
+ WHERE kind='external' AND status<>'rejected' AND (status<>'confirmed' OR audit_state<>'complete')
14275
+ AND NOT EXISTS(SELECT FROM supacloud_workflows.runs w WHERE w.id=dispatch_key)
14276
+ LOOP
14277
+ PERFORM supacloud_workflows.start_run(
14278
+ r.dispatch_key,'supacloud.command.reconcile','1','reconcile',
14279
+ jsonb_build_object('commandId',r.dispatch_key,'tenantId',r.tenant_id,
14280
+ 'actorId',r.actor_id,'command',r.command,'operationId',r.operation_key),20
14281
+ );
14282
+ END LOOP;
14283
+ END
14284
+ $backfill$;
14285
+ DROP INDEX IF EXISTS supacloud_commands.execution_pending_recovery;
14286
+ ALTER TABLE supacloud_commands.execution_receipts
14287
+ DROP COLUMN IF EXISTS lease_id,
14288
+ DROP COLUMN IF EXISTS lease_until,
14289
+ DROP COLUMN IF EXISTS next_attempt_at,
14290
+ DROP COLUMN IF EXISTS recovery_attempts;
14291
+ `;
14098
14292
 
14099
14293
  // src/runtime/node/project.ts
14100
14294
  async function loadSupabaseProject(projectDir, seed = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/lite",
3
- "version": "0.28.2",
3
+ "version": "0.28.3",
4
4
  "description": "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -54,36 +54,37 @@
54
54
  "typecheck": "bun x tsc --noEmit -p tsconfig.json"
55
55
  },
56
56
  "dependencies": {
57
- "@electric-sql/pglite": "0.5.8",
58
- "@electric-sql/pglite-pg_textsearch": "0.0.10",
59
- "@electric-sql/pglite-pgmq": "0.0.6",
60
- "@electric-sql/pglite-pgvector": "0.0.9",
61
- "@electric-sql/pglite-postgis": "0.2.8",
62
- "@supacloud/db": "^0.6.0",
57
+ "@electric-sql/pglite": "0.5.10",
58
+ "@electric-sql/pglite-pg_textsearch": "0.0.12",
59
+ "@electric-sql/pglite-pgmq": "0.0.8",
60
+ "@electric-sql/pglite-pgvector": "0.0.11",
61
+ "@electric-sql/pglite-postgis": "0.2.10",
62
+ "@supacloud/db": "^0.17.0",
63
63
  "exact-mirror": "1.2.6",
64
64
  "tar": "^7.5.22",
65
- "typebox": "1.3.36"
65
+ "typebox": "1.3.37"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@supacloud/app": "file:../app",
69
+ "@supacloud/contracts": "file:../contracts",
69
70
  "@supacloud/compiler": "file:../compiler",
70
71
  "@supacloud/elysia": "file:../elysia",
71
72
  "@supabase/supabase-js": "^2.117.3",
72
73
  "@types/bun": "^1.4.3",
73
- "effect": "4.0.2",
74
- "elysia": "2.0.0-beta.21",
74
+ "effect": "4.0.5",
75
+ "elysia": "2.0.0-beta.32",
75
76
  "jose": "^6.2.12",
76
77
  "typescript": "^7.0.2"
77
78
  },
78
79
  "overrides": {
79
- "@supacloud/db": "^0.6.0",
80
+ "@supacloud/db": "^0.17.0",
80
81
  "@supacloud/app": "file:../app",
81
82
  "@supacloud/compiler": "file:../compiler",
82
83
  "@supacloud/elysia": "file:../elysia",
83
84
  "@supacloud/contracts": "file:../contracts",
84
85
  "@supacloud/commands": "file:../commands",
85
86
  "@supacloud/delivery": "file:../delivery",
86
- "@graphql-tools/utils": "^12.0.3"
87
+ "@graphql-tools/utils": "^12.0.4"
87
88
  },
88
89
  "engines": {
89
90
  "bun": ">=1.4.3"