@supacloud/lite 0.28.2 → 0.29.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/CHANGELOG.md +14 -0
- package/README.md +2 -2
- package/THIRD_PARTY_NOTICES.md +4 -4
- package/dist/cli.js +266 -36
- package/dist/index.js +220 -26
- package/package.json +13 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.29.0](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.28.3...supacloud-lite-v0.29.0) (2026-10-15)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* reduce approval infrastructure wiring and reuse business contracts ([#1948](https://github.com/vibeunion/supacloud/issues/1948)) ([7c68543](https://github.com/vibeunion/supacloud/commit/7c6854338968209c2bbd658782224db238650aeb))
|
|
9
|
+
|
|
10
|
+
## [0.28.3](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.28.2...supacloud-lite-v0.28.3) (2026-10-15)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Miscellaneous Chores
|
|
14
|
+
|
|
15
|
+
* upgrade workspace dependencies ([#1940](https://github.com/vibeunion/supacloud/issues/1940)) ([77ad418](https://github.com/vibeunion/supacloud/commit/77ad418e0568ceeeea0b59a10a3bbc451a68a95f))
|
|
16
|
+
|
|
3
17
|
## [0.28.2](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.28.1...supacloud-lite-v0.28.2) (2026-10-15)
|
|
4
18
|
|
|
5
19
|
|
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.
|
|
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.
|
|
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`
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -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.
|
|
24
|
-
- `@electric-sql/pglite-postgis` 0.2.
|
|
25
|
-
- `@electric-sql/pglite-pg_textsearch` 0.0.
|
|
26
|
-
- `@electric-sql/pglite-pgmq` 0.0.
|
|
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.
|
|
11
|
+
version: "0.29.0",
|
|
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.
|
|
66
|
-
"@electric-sql/pglite-pg_textsearch": "0.0.
|
|
67
|
-
"@electric-sql/pglite-pgmq": "0.0.
|
|
68
|
-
"@electric-sql/pglite-pgvector": "0.0.
|
|
69
|
-
"@electric-sql/pglite-postgis": "0.2.
|
|
70
|
-
"@supacloud/db": "^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.
|
|
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.
|
|
82
|
-
elysia: "2.0.0-beta.
|
|
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.
|
|
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.
|
|
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
|
|
13292
|
-
checkExpr: row.check_expr
|
|
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.
|
|
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
|
|
13348
|
-
if (
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
13553
|
-
const template = record(
|
|
13589
|
+
const templates = manifest["templates"].map((value) => {
|
|
13590
|
+
const template = record(value);
|
|
13554
13591
|
keys(template, ["file", "templateSha256", "parameters"]);
|
|
13555
|
-
if (!validFile(template
|
|
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
|
|
13595
|
+
files.add(template["file"]);
|
|
13559
13596
|
const placeholders = new Set;
|
|
13560
|
-
const parameters = template
|
|
13561
|
-
const parameter = record(
|
|
13597
|
+
const parameters = template["parameters"].map((value) => {
|
|
13598
|
+
const parameter = record(value);
|
|
13562
13599
|
keys(parameter, ["placeholder", "variable", "type", "occurrences"]);
|
|
13563
|
-
if (typeof parameter
|
|
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
|
|
13567
|
-
return
|
|
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
|
|
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.
|
|
91
|
+
version: "0.29.0",
|
|
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.
|
|
146
|
-
"@electric-sql/pglite-pg_textsearch": "0.0.
|
|
147
|
-
"@electric-sql/pglite-pgmq": "0.0.
|
|
148
|
-
"@electric-sql/pglite-pgvector": "0.0.
|
|
149
|
-
"@electric-sql/pglite-postgis": "0.2.
|
|
150
|
-
"@supacloud/db": "^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.
|
|
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.
|
|
162
|
-
elysia: "2.0.0-beta.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
13994
|
-
const template = record(
|
|
13994
|
+
const templates = manifest["templates"].map((value) => {
|
|
13995
|
+
const template = record(value);
|
|
13995
13996
|
keys(template, ["file", "templateSha256", "parameters"]);
|
|
13996
|
-
if (!validFile(template
|
|
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
|
|
14000
|
+
files.add(template["file"]);
|
|
14000
14001
|
const placeholders = new Set;
|
|
14001
|
-
const parameters = template
|
|
14002
|
-
const parameter = record(
|
|
14002
|
+
const parameters = template["parameters"].map((value) => {
|
|
14003
|
+
const parameter = record(value);
|
|
14003
14004
|
keys(parameter, ["placeholder", "variable", "type", "occurrences"]);
|
|
14004
|
-
if (typeof parameter
|
|
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
|
|
14008
|
-
return
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.29.0",
|
|
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.
|
|
58
|
-
"@electric-sql/pglite-pg_textsearch": "0.0.
|
|
59
|
-
"@electric-sql/pglite-pgmq": "0.0.
|
|
60
|
-
"@electric-sql/pglite-pgvector": "0.0.
|
|
61
|
-
"@electric-sql/pglite-postgis": "0.2.
|
|
62
|
-
"@supacloud/db": "^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.
|
|
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.
|
|
74
|
-
"elysia": "2.0.0-beta.
|
|
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.
|
|
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.
|
|
87
|
+
"@graphql-tools/utils": "^12.0.4"
|
|
87
88
|
},
|
|
88
89
|
"engines": {
|
|
89
90
|
"bun": ">=1.4.3"
|