@supacloud/lite 0.19.0 → 0.20.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 +7 -0
- package/README.md +24 -2
- package/dist/cli.js +225 -69
- package/dist/index.js +4 -3
- package/dist/runtime/db/pglite-engine.d.ts +3 -1
- package/dist/runtime/db/pglite-engine.d.ts.map +1 -1
- package/dist/runtime/db/schema-diff.d.ts +14 -2
- package/dist/runtime/db/schema-diff.d.ts.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.20.0](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.19.0...supacloud-lite-v0.20.0) (2026-10-05)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add agent backend authoring, runtime and observability workflows ([#1648](https://github.com/vibeunion/supacloud/issues/1648)) ([59a66d1](https://github.com/vibeunion/supacloud/commit/59a66d184a47d19c7f8895f24a6ba2c7e74e93d0))
|
|
9
|
+
|
|
3
10
|
## [0.19.0](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.18.0...supacloud-lite-v0.19.0) (2026-10-03)
|
|
4
11
|
|
|
5
12
|
|
package/README.md
CHANGED
|
@@ -162,7 +162,7 @@ supacloud-lite keys [--service-role]
|
|
|
162
162
|
supacloud-lite gen types [-o database.types.ts]
|
|
163
163
|
supacloud-lite db reset
|
|
164
164
|
supacloud-lite db diff [-f migration_name]
|
|
165
|
-
supacloud-lite db pull [migration_name]
|
|
165
|
+
supacloud-lite db pull [migration_name] [--baseline]
|
|
166
166
|
supacloud-lite db check [--module-file supabase/db/modules.ts]
|
|
167
167
|
supacloud-lite snapshot create [-o backup.tar.gz]
|
|
168
168
|
supacloud-lite snapshot restore <backup.tar.gz> [--force]
|
|
@@ -174,6 +174,10 @@ supacloud-lite version
|
|
|
174
174
|
|
|
175
175
|
首次初始化或 state 目录不存在/为空时,先运行 `supacloud-lite migrate`。`db reset` 只接受已经初始化且保留有效 `secrets.json` 标记的 state;它不会为 reset 创建或覆盖项目 secrets。
|
|
176
176
|
|
|
177
|
+
`db diff` / `db pull` 只读检查现有数据库,不会初始化 live backend 或自动应用待执行迁移;已应用历史必须与本地迁移精确一致。生成的 DDL 先在不含 seed 的临时影子数据库中重放并核对受支持的 Catalog。检测到不能完整重建的 view/function/trigger/policy、权限、RLS 或非追加 enum 变更时拒绝生成,改用官方 Supabase pg-delta;这不是完整 PostgreSQL Catalog 覆盖。
|
|
178
|
+
|
|
179
|
+
`db pull` 默认写入 `.supacloud-lite/schema-drafts`,不修改迁移账本。仅对已审核且已存在于数据库的变更使用 `--baseline`:它记录账本而不执行 DDL,提交确认后才发布到迁移目录,禁止覆盖已有文件。若失败后留下 `.sql.pending`,停止启动/迁移,保留文件并检查对应账本版本与 SQL;只有确认匹配提交后才能发布该文件。不要猜测提交结果或直接重放捕获的 DDL。库调用方在 `baseline: false` 时也应把 `migrationsDir` 指向草稿目录。恢复步骤及文件/数据库非原子边界见仓库 `docs/select-agent-backend.md`。
|
|
180
|
+
|
|
177
181
|
`db check` 是只读治理命令:加载 `@supacloud/db` 的 `defineDatabaseModule` 清单(默认 `supabase/db/modules.ts`),对声明的 SQL 源做静态 lint,并把声明的表/RLS 策略/RPC/授权与 Lite 数据库的真实 Catalog 对账(pg_policy、pg_proc、grants)。存在 error 级问题(声明缺失、RLS 未启用、security definer 未固定 search_path、PUBLIC 授权)时以非零退出码结束,可接入 CI;不会修改数据库。
|
|
178
182
|
|
|
179
183
|
通用参数:
|
|
@@ -562,7 +566,7 @@ supacloud-lite keys [--service-role]
|
|
|
562
566
|
supacloud-lite gen types [-o database.types.ts]
|
|
563
567
|
supacloud-lite db reset
|
|
564
568
|
supacloud-lite db diff [-f migration_name]
|
|
565
|
-
supacloud-lite db pull [migration_name]
|
|
569
|
+
supacloud-lite db pull [migration_name] [--baseline]
|
|
566
570
|
supacloud-lite db check [--module-file supabase/db/modules.ts]
|
|
567
571
|
supacloud-lite snapshot create [-o backup.tar.gz]
|
|
568
572
|
supacloud-lite snapshot restore <backup.tar.gz> [--force]
|
|
@@ -713,6 +717,24 @@ In-memory databases have no persistable data, so `snapshot` and `upgrade` reject
|
|
|
713
717
|
|
|
714
718
|
### Application Compatibility
|
|
715
719
|
|
|
720
|
+
`db diff` / `db pull` inspect existing live state without backend bootstrap or
|
|
721
|
+
pending migration application. Applied history must match local migrations
|
|
722
|
+
exactly. Generated DDL is replayed in a disposable, seed-free shadow and checked
|
|
723
|
+
against the supported catalog. Detected unsupported view/function/trigger/policy,
|
|
724
|
+
ACL, RLS or non-append enum changes fail closed with pg-delta guidance; this is
|
|
725
|
+
not complete PostgreSQL catalog coverage.
|
|
726
|
+
|
|
727
|
+
CLI `db pull` writes to `.supacloud-lite/schema-drafts` by default and never
|
|
728
|
+
implicitly updates the ledger. Use `--baseline` only for reviewed DDL already
|
|
729
|
+
present in live state: it records history without executing DDL and publishes
|
|
730
|
+
the migration after confirmed commit, without overwriting existing files. On an
|
|
731
|
+
uncertain outcome, stop startup/migration, retain `.sql.pending`, and compare the
|
|
732
|
+
ledger version and exact SQL before publishing or retrying. Never guess commit
|
|
733
|
+
state or replay captured DDL against the already-changed database. Library
|
|
734
|
+
callers should also use a draft `migrationsDir` when `baseline` is false. See
|
|
735
|
+
repository document `docs/select-agent-backend.md` for recovery and the
|
|
736
|
+
non-atomic filesystem/database boundary.
|
|
737
|
+
|
|
716
738
|
Lite accepts flat SQL and timestamp folders containing `migration.sql`, validates
|
|
717
739
|
duplicate versions and applied SQL drift before pending migrations, and reuses
|
|
718
740
|
`@supacloud/db` for reviewed target bindings. `migrate --json` emits a hash-only
|
package/dist/cli.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
|
-
import { existsSync as
|
|
5
|
+
import { existsSync as existsSync5 } from "fs";
|
|
6
6
|
import { mkdir as mkdir7, rm as rm6, writeFile as writeFile7 } from "fs/promises";
|
|
7
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.20.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",
|
|
@@ -6249,7 +6249,7 @@ begin
|
|
|
6249
6249
|
perform set_config('TimeZone', configured_timezone, false);
|
|
6250
6250
|
end $$;
|
|
6251
6251
|
`;
|
|
6252
|
-
async function createPgliteEngine(dataDir) {
|
|
6252
|
+
async function createPgliteEngine(dataDir, options = {}) {
|
|
6253
6253
|
const releaseLock = await acquireDataDirLock(dataDir, "PGlite");
|
|
6254
6254
|
let PGlite, extensions;
|
|
6255
6255
|
const standaloneAssets = getStandaloneAssets();
|
|
@@ -6304,7 +6304,8 @@ async function createPgliteEngine(dataDir) {
|
|
|
6304
6304
|
});
|
|
6305
6305
|
await pg.waitReady;
|
|
6306
6306
|
try {
|
|
6307
|
-
|
|
6307
|
+
if (!options.inspectOnly)
|
|
6308
|
+
await pg.exec(INITIALIZE_TIMEZONE_SQL);
|
|
6308
6309
|
} catch (error) {
|
|
6309
6310
|
const [cleanup] = await Promise.allSettled([pg.close()]);
|
|
6310
6311
|
if (cleanup.status === "rejected") {
|
|
@@ -11123,9 +11124,120 @@ async function snapshotSchema(db, schema = "public") {
|
|
|
11123
11124
|
const enums = new Map;
|
|
11124
11125
|
for (const r of en.rows)
|
|
11125
11126
|
enums.set(r.name, r.labels);
|
|
11126
|
-
|
|
11127
|
+
const viewRows = await db.query(`select c.relname as name, c.relkind::text || ':' || pg_get_viewdef(c.oid, true) as definition
|
|
11128
|
+
from pg_class c
|
|
11129
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
11130
|
+
where n.nspname = $1 and c.relkind in ('v','m')
|
|
11131
|
+
order by c.relname`, [schema]);
|
|
11132
|
+
const views = new Map;
|
|
11133
|
+
for (const row of viewRows.rows)
|
|
11134
|
+
views.set(row.name, row.definition);
|
|
11135
|
+
const functionRows = await db.query(`select n.nspname || '.' || p.proname || '(' ||
|
|
11136
|
+
pg_get_function_identity_arguments(p.oid) || ')' as identity,
|
|
11137
|
+
pg_get_functiondef(p.oid) as definition
|
|
11138
|
+
from pg_proc p
|
|
11139
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
11140
|
+
where n.nspname = $1 and p.prokind in ('f', 'p')
|
|
11141
|
+
order by p.proname, pg_get_function_identity_arguments(p.oid)`, [schema]);
|
|
11142
|
+
const functions = new Map;
|
|
11143
|
+
for (const row of functionRows.rows)
|
|
11144
|
+
functions.set(row.identity, row.definition);
|
|
11145
|
+
const triggerRows = await db.query(`select c.relname as table, t.tgname as name, pg_get_triggerdef(t.oid, true) as definition
|
|
11146
|
+
from pg_trigger t
|
|
11147
|
+
join pg_class c on c.oid = t.tgrelid
|
|
11148
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
11149
|
+
where n.nspname = $1 and not t.tgisinternal
|
|
11150
|
+
order by c.relname, t.tgname`, [schema]);
|
|
11151
|
+
const triggers = new Map;
|
|
11152
|
+
for (const row of triggerRows.rows)
|
|
11153
|
+
triggers.set(`${row.table}.${row.name}`, row.definition);
|
|
11154
|
+
const policyRows = await db.query(`select tablename as table, policyname as name, permissive,
|
|
11155
|
+
roles, cmd as command, qual as using_expression,
|
|
11156
|
+
with_check as check_expression
|
|
11157
|
+
from pg_policies
|
|
11158
|
+
where schemaname = $1
|
|
11159
|
+
order by tablename, policyname`, [schema]);
|
|
11160
|
+
const policies = new Map;
|
|
11161
|
+
for (const row of policyRows.rows)
|
|
11162
|
+
policies.set(`${row.table}.${row.name}`, renderPolicy(schema, row));
|
|
11163
|
+
const attributesRows = await db.query(`select 'relation/' || c.relname as name,
|
|
11164
|
+
jsonb_build_array(c.relkind, c.relpersistence, c.relrowsecurity, c.relforcerowsecurity,
|
|
11165
|
+
c.relispartition, pg_get_expr(c.relpartbound, c.oid),
|
|
11166
|
+
pg_get_partkeydef(c.oid), pg_get_userbyid(c.relowner),
|
|
11167
|
+
c.relacl::text, c.reloptions)::text as definition
|
|
11168
|
+
from pg_class c join pg_namespace n on n.oid = c.relnamespace
|
|
11169
|
+
where n.nspname = $1 and c.relkind in ('r','p','v','m','S','f')
|
|
11170
|
+
union all
|
|
11171
|
+
select 'column/' || c.relname || '/' || a.attname,
|
|
11172
|
+
jsonb_build_array(a.attidentity, a.attgenerated, a.attacl::text,
|
|
11173
|
+
a.attcollation::regcollation::text)::text
|
|
11174
|
+
from pg_attribute a join pg_class c on c.oid = a.attrelid
|
|
11175
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
11176
|
+
where n.nspname = $1 and c.relkind in ('r','p') and a.attnum > 0 and not a.attisdropped
|
|
11177
|
+
union all
|
|
11178
|
+
select 'function/' || p.proname || '(' || pg_get_function_identity_arguments(p.oid) || ')',
|
|
11179
|
+
jsonb_build_array(pg_get_userbyid(p.proowner), p.proacl::text)::text
|
|
11180
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
11181
|
+
where n.nspname = $1 and p.prokind in ('f','p')
|
|
11182
|
+
union all
|
|
11183
|
+
select 'schema/' || n.nspname,
|
|
11184
|
+
jsonb_build_array(pg_get_userbyid(n.nspowner), n.nspacl::text)::text
|
|
11185
|
+
from pg_namespace n where n.nspname = $1
|
|
11186
|
+
union all
|
|
11187
|
+
select 'sequence/' || c.relname,
|
|
11188
|
+
jsonb_build_array(s.seqtypid::regtype::text, s.seqstart, s.seqincrement,
|
|
11189
|
+
s.seqmax, s.seqmin, s.seqcache, s.seqcycle)::text
|
|
11190
|
+
from pg_sequence s join pg_class c on c.oid = s.seqrelid
|
|
11191
|
+
join pg_namespace n on n.oid = c.relnamespace where n.nspname = $1
|
|
11192
|
+
union all
|
|
11193
|
+
select 'trigger/' || c.relname || '/' || t.tgname, t.tgenabled::text
|
|
11194
|
+
from pg_trigger t join pg_class c on c.oid = t.tgrelid
|
|
11195
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
11196
|
+
where n.nspname = $1 and not t.tgisinternal
|
|
11197
|
+
union all
|
|
11198
|
+
select 'type/' || t.typname,
|
|
11199
|
+
jsonb_build_array(t.typtype, t.typbasetype::regtype::text, t.typtypmod,
|
|
11200
|
+
t.typnotnull, t.typdefault, t.typacl::text,
|
|
11201
|
+
(select array_agg(pg_get_constraintdef(con.oid) order by con.conname)
|
|
11202
|
+
from pg_constraint con where con.contypid = t.oid))::text
|
|
11203
|
+
from pg_type t join pg_namespace n on n.oid = t.typnamespace
|
|
11204
|
+
where n.nspname = $1 and t.typtype in ('d','r','m')
|
|
11205
|
+
union all
|
|
11206
|
+
select 'defaults/' || pg_get_userbyid(d.defaclrole) || '/' || d.defaclobjtype::text,
|
|
11207
|
+
d.defaclacl::text
|
|
11208
|
+
from pg_default_acl d join pg_namespace n on n.oid = d.defaclnamespace where n.nspname = $1`, [schema]);
|
|
11209
|
+
const attributes = new Map(attributesRows.rows.map((row) => [row.name, row.definition]));
|
|
11210
|
+
return { tables, constraints, indexes, enums, views, functions, triggers, policies, attributes };
|
|
11211
|
+
}
|
|
11212
|
+
function schemasEqual(left, right) {
|
|
11213
|
+
const canonical = (value) => {
|
|
11214
|
+
if (value instanceof Map)
|
|
11215
|
+
return [...value].sort(([a], [b]) => String(a).localeCompare(String(b))).map(([key, entry]) => [key, canonical(entry)]);
|
|
11216
|
+
if (Array.isArray(value))
|
|
11217
|
+
return value.map(canonical);
|
|
11218
|
+
if (value !== null && typeof value === "object")
|
|
11219
|
+
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, canonical(entry)]));
|
|
11220
|
+
return value;
|
|
11221
|
+
};
|
|
11222
|
+
return JSON.stringify(canonical(left)) === JSON.stringify(canonical(right));
|
|
11127
11223
|
}
|
|
11128
11224
|
function diffSchemas(from, to, schema = "public") {
|
|
11225
|
+
for (const [key, value] of from.attributes) {
|
|
11226
|
+
if (to.attributes.has(key) && to.attributes.get(key) !== value) {
|
|
11227
|
+
throw new Error("Lite table diff cannot safely migrate catalog attributes; use the official Supabase pg-delta adapter");
|
|
11228
|
+
}
|
|
11229
|
+
}
|
|
11230
|
+
for (const [name, labels] of from.enums) {
|
|
11231
|
+
const next = to.enums.get(name);
|
|
11232
|
+
if (!next || labels.some((label, index) => next[index] !== label)) {
|
|
11233
|
+
throw new Error("Lite table diff only supports appending enum values; use the official Supabase pg-delta adapter");
|
|
11234
|
+
}
|
|
11235
|
+
}
|
|
11236
|
+
for (const kind of ["views", "functions", "triggers", "policies"]) {
|
|
11237
|
+
if (from[kind].size !== to[kind].size || [...from[kind]].some(([name, definition]) => to[kind].get(name) !== definition)) {
|
|
11238
|
+
throw new Error(`Lite table diff cannot safely migrate ${kind}; use the official Supabase pg-delta adapter`);
|
|
11239
|
+
}
|
|
11240
|
+
}
|
|
11129
11241
|
const out = [];
|
|
11130
11242
|
const q = (n) => quoteIdent(n);
|
|
11131
11243
|
const tbl = (t) => `${q(schema)}.${q(t)}`;
|
|
@@ -11196,6 +11308,17 @@ ${colDefs.map((c) => ` ${c}`).join(`,
|
|
|
11196
11308
|
});
|
|
11197
11309
|
return out;
|
|
11198
11310
|
}
|
|
11311
|
+
function renderPolicy(schema, row) {
|
|
11312
|
+
const roles = row.roles.length ? row.roles.map((role) => quoteIdent(role)).join(", ") : "public";
|
|
11313
|
+
const command = row.command.toLowerCase() === "*" ? "all" : row.command.toLowerCase();
|
|
11314
|
+
const mode = row.permissive.toLowerCase() === "permissive" ? "permissive" : "restrictive";
|
|
11315
|
+
let sql = `create policy ${quoteIdent(row.name)} on ${quoteIdent(schema)}.${quoteIdent(row.table)} as ${mode} for ${command} to ${roles}`;
|
|
11316
|
+
if (row.using_expression)
|
|
11317
|
+
sql += ` using (${row.using_expression})`;
|
|
11318
|
+
if (row.check_expression)
|
|
11319
|
+
sql += ` with check (${row.check_expression})`;
|
|
11320
|
+
return sql;
|
|
11321
|
+
}
|
|
11199
11322
|
function columnClause(c, q) {
|
|
11200
11323
|
const isSerialDefault = c.default !== null && /^nextval\(/i.test(c.default);
|
|
11201
11324
|
if (isSerialDefault) {
|
|
@@ -11571,8 +11694,8 @@ function withCors(res) {
|
|
|
11571
11694
|
}
|
|
11572
11695
|
|
|
11573
11696
|
// src/runtime/node/db-diff.ts
|
|
11574
|
-
import { mkdtempSync as mkdtempSync2 } from "fs";
|
|
11575
|
-
import { mkdir as mkdir2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
11697
|
+
import { existsSync as existsSync3, mkdtempSync as mkdtempSync2 } from "fs";
|
|
11698
|
+
import { link, mkdir as mkdir2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
11576
11699
|
import { tmpdir as tmpdir2 } from "os";
|
|
11577
11700
|
import { dirname as dirname2, join as join2 } from "path";
|
|
11578
11701
|
|
|
@@ -12673,6 +12796,24 @@ function removeStalePidFile(pidPath) {
|
|
|
12673
12796
|
|
|
12674
12797
|
// src/runtime/node/db-diff.ts
|
|
12675
12798
|
async function computeDbDiff(opts) {
|
|
12799
|
+
return withDelta(opts, false, async (ddl) => ddl);
|
|
12800
|
+
}
|
|
12801
|
+
async function assertHistory(tx, migrations) {
|
|
12802
|
+
const exists = await tx.query("select to_regclass('supabase_migrations.schema_migrations')::text as ledger");
|
|
12803
|
+
const rows = exists.rows[0]?.ledger ? (await tx.query("select version, statements from supabase_migrations.schema_migrations")).rows : [];
|
|
12804
|
+
const expected = new Map;
|
|
12805
|
+
for (const migration of migrations) {
|
|
12806
|
+
const version = migration.name.match(/^(\d+)/)?.[1] ?? migration.name;
|
|
12807
|
+
if (expected.has(version))
|
|
12808
|
+
throw new Error(`duplicate migration version: ${version}`);
|
|
12809
|
+
expected.set(version, migration.sql);
|
|
12810
|
+
}
|
|
12811
|
+
if (rows.length !== expected.size || rows.some((row) => expected.get(row.version) !== row.statements?.join(`
|
|
12812
|
+
`))) {
|
|
12813
|
+
throw new Error("Schema authoring requires matching applied migration history; resolve pending, missing or changed migrations first");
|
|
12814
|
+
}
|
|
12815
|
+
}
|
|
12816
|
+
async function withDelta(opts, baseline, action) {
|
|
12676
12817
|
const schema = opts.schema ?? "public";
|
|
12677
12818
|
let shadow;
|
|
12678
12819
|
let live;
|
|
@@ -12685,18 +12826,29 @@ async function computeDbDiff(opts) {
|
|
|
12685
12826
|
migrations: opts.migrations,
|
|
12686
12827
|
startRuntimeServices: false
|
|
12687
12828
|
});
|
|
12688
|
-
|
|
12829
|
+
if (!unclaimedLiveEngine && (!opts.liveDataDir || !existsSync3(opts.liveDataDir))) {
|
|
12830
|
+
throw new Error("An existing live database is required for schema authoring");
|
|
12831
|
+
}
|
|
12832
|
+
live = unclaimedLiveEngine ?? await createPgliteEngine(opts.liveDataDir, { inspectOnly: true });
|
|
12689
12833
|
unclaimedLiveEngine = undefined;
|
|
12690
|
-
live = await createBackend({
|
|
12691
|
-
runtimeMode: opts.runtimeMode,
|
|
12692
|
-
engine: liveEngine,
|
|
12693
|
-
dataDir: liveEngine ? undefined : opts.liveDataDir,
|
|
12694
|
-
migrations: opts.migrations,
|
|
12695
|
-
startRuntimeServices: false
|
|
12696
|
-
});
|
|
12697
12834
|
const shadowSnap = await snapshotSchema(shadow.db, schema);
|
|
12698
|
-
|
|
12699
|
-
|
|
12835
|
+
return await live.transaction(async (tx) => {
|
|
12836
|
+
await tx.exec(`set transaction isolation level repeatable read${baseline ? "" : " read only"}`);
|
|
12837
|
+
await assertHistory(tx, opts.migrations);
|
|
12838
|
+
const liveSnap = await snapshotSchema(tx, schema);
|
|
12839
|
+
const ddl = diffSchemas(shadowSnap, liveSnap, schema);
|
|
12840
|
+
try {
|
|
12841
|
+
if (ddl.length)
|
|
12842
|
+
await shadow.db.exec(ddl.join(`
|
|
12843
|
+
`));
|
|
12844
|
+
if (!schemasEqual(await snapshotSchema(shadow.db, schema), liveSnap)) {
|
|
12845
|
+
throw new Error("Generated DDL does not reproduce the live schema");
|
|
12846
|
+
}
|
|
12847
|
+
} catch (cause) {
|
|
12848
|
+
throw new Error("Lite schema replay validation failed; use the official Supabase pg-delta adapter", { cause });
|
|
12849
|
+
}
|
|
12850
|
+
return action(ddl, tx);
|
|
12851
|
+
});
|
|
12700
12852
|
} catch (error) {
|
|
12701
12853
|
operationFailed = true;
|
|
12702
12854
|
throw error;
|
|
@@ -12737,55 +12889,52 @@ async function createTemporaryNativeEngine(installDir) {
|
|
|
12737
12889
|
};
|
|
12738
12890
|
}
|
|
12739
12891
|
async function pullSchema(opts) {
|
|
12740
|
-
const
|
|
12741
|
-
|
|
12742
|
-
|
|
12743
|
-
|
|
12744
|
-
|
|
12892
|
+
const stamp = opts.stamp ?? new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
|
12893
|
+
const name = opts.name ?? "remote_schema";
|
|
12894
|
+
if (!/^\d{14}$/.test(stamp) || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,100}$/.test(name)) {
|
|
12895
|
+
await opts.liveEngine?.close();
|
|
12896
|
+
throw new Error("Invalid migration timestamp or name");
|
|
12897
|
+
}
|
|
12898
|
+
if (opts.baseline && !opts.migrationsDir) {
|
|
12899
|
+
await opts.liveEngine?.close();
|
|
12900
|
+
throw new Error("Baseline requires an output directory");
|
|
12901
|
+
}
|
|
12902
|
+
let stagedPath = null;
|
|
12745
12903
|
try {
|
|
12746
|
-
|
|
12747
|
-
|
|
12748
|
-
|
|
12749
|
-
|
|
12750
|
-
startRuntimeServices: false
|
|
12751
|
-
});
|
|
12752
|
-
const liveEngine = unclaimedLiveEngine;
|
|
12753
|
-
unclaimedLiveEngine = undefined;
|
|
12754
|
-
live = await createBackend({
|
|
12755
|
-
runtimeMode: opts.runtimeMode,
|
|
12756
|
-
engine: liveEngine,
|
|
12757
|
-
dataDir: liveEngine ? undefined : opts.liveDataDir,
|
|
12758
|
-
migrations: opts.migrations,
|
|
12759
|
-
startRuntimeServices: false
|
|
12760
|
-
});
|
|
12761
|
-
const ddl = diffSchemas(await snapshotSchema(shadow.db, schema), await snapshotSchema(live.db, schema), schema);
|
|
12762
|
-
if (ddl.length === 0)
|
|
12763
|
-
return { ddl, version: null, path: null };
|
|
12764
|
-
const stamp = opts.stamp ?? new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
|
12765
|
-
const name = opts.name ?? "remote_schema";
|
|
12766
|
-
const body = ddl.join(`
|
|
12904
|
+
const result = await withDelta(opts, opts.baseline === true, async (ddl, tx) => {
|
|
12905
|
+
if (ddl.length === 0)
|
|
12906
|
+
return { ddl, version: null, path: null };
|
|
12907
|
+
const body = ddl.join(`
|
|
12767
12908
|
|
|
12768
12909
|
`) + `
|
|
12769
12910
|
`;
|
|
12770
|
-
|
|
12771
|
-
|
|
12772
|
-
|
|
12773
|
-
|
|
12774
|
-
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
12911
|
+
let path = null;
|
|
12912
|
+
if (opts.migrationsDir) {
|
|
12913
|
+
await mkdir2(opts.migrationsDir, { recursive: true });
|
|
12914
|
+
path = join2(opts.migrationsDir, `${stamp}_${name}.sql`);
|
|
12915
|
+
if (existsSync3(path))
|
|
12916
|
+
throw new Error("Migration file already exists");
|
|
12917
|
+
if (opts.baseline) {
|
|
12918
|
+
const pending = `${path}.pending`;
|
|
12919
|
+
await writeFile2(pending, body, { flag: "wx" });
|
|
12920
|
+
stagedPath = pending;
|
|
12921
|
+
await tx.query("insert into supabase_migrations.schema_migrations(version, name, statements) values ($1, $2, $3)", [stamp, `${stamp}_${name}`, [body]]);
|
|
12922
|
+
} else {
|
|
12923
|
+
await writeFile2(path, body, { flag: "wx" });
|
|
12924
|
+
}
|
|
12925
|
+
}
|
|
12926
|
+
return { ddl, version: stamp, path };
|
|
12927
|
+
});
|
|
12928
|
+
if (stagedPath && result.path) {
|
|
12929
|
+
await link(stagedPath, result.path);
|
|
12930
|
+
await rm(stagedPath);
|
|
12931
|
+
}
|
|
12932
|
+
return result;
|
|
12779
12933
|
} catch (error) {
|
|
12780
|
-
|
|
12781
|
-
|
|
12782
|
-
} finally {
|
|
12783
|
-
try {
|
|
12784
|
-
await closeResources(unclaimedLiveEngine, live, shadow);
|
|
12785
|
-
} catch (error) {
|
|
12786
|
-
if (!operationFailed)
|
|
12787
|
-
throw error;
|
|
12934
|
+
if (stagedPath) {
|
|
12935
|
+
throw new Error(`Baseline outcome requires inspection; staged SQL retained at ${stagedPath}. Verify the ledger before publishing it.`, { cause: error });
|
|
12788
12936
|
}
|
|
12937
|
+
throw error;
|
|
12789
12938
|
}
|
|
12790
12939
|
}
|
|
12791
12940
|
async function closeResources(...resources) {
|
|
@@ -14110,7 +14259,7 @@ function isNotFound(error) {
|
|
|
14110
14259
|
}
|
|
14111
14260
|
|
|
14112
14261
|
// src/project-runtime.ts
|
|
14113
|
-
import { chmod, link, lstat as lstat2, mkdir as mkdir5, readFile as readFile7, realpath as realpath2, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
|
|
14262
|
+
import { chmod, link as link2, lstat as lstat2, mkdir as mkdir5, readFile as readFile7, realpath as realpath2, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
|
|
14114
14263
|
import { dirname as dirname6, isAbsolute, join as join8, parse, relative, resolve as resolve4 } from "path";
|
|
14115
14264
|
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
14116
14265
|
|
|
@@ -14219,7 +14368,7 @@ import { pathToFileURL as pathToFileURL2 } from "url";
|
|
|
14219
14368
|
// src/runtime/node/bundle-function.ts
|
|
14220
14369
|
import { createHash as createHash4 } from "crypto";
|
|
14221
14370
|
import { mkdir as mkdir4, readFile as readFile5, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
14222
|
-
import { existsSync as
|
|
14371
|
+
import { existsSync as existsSync4 } from "fs";
|
|
14223
14372
|
import { tmpdir as tmpdir3 } from "os";
|
|
14224
14373
|
import { join as join6 } from "path";
|
|
14225
14374
|
function rewriteRemoteSpecifier(spec) {
|
|
@@ -14233,7 +14382,7 @@ var HTTP_CACHE = join6(tmpdir3(), "supacloud-lite-fn-http");
|
|
|
14233
14382
|
async function fetchModule(url) {
|
|
14234
14383
|
const key = createHash4("sha256").update(url).digest("hex");
|
|
14235
14384
|
const cached = join6(HTTP_CACHE, key);
|
|
14236
|
-
if (
|
|
14385
|
+
if (existsSync4(cached))
|
|
14237
14386
|
return readFile5(cached, "utf8");
|
|
14238
14387
|
const res = await fetch(url, { redirect: "follow" });
|
|
14239
14388
|
if (!res.ok)
|
|
@@ -14679,7 +14828,7 @@ async function ensureProjectSecrets(paths) {
|
|
|
14679
14828
|
try {
|
|
14680
14829
|
await writeFile5(temporaryFile, `${JSON.stringify(candidate, null, 2)}
|
|
14681
14830
|
`, { mode: 384, flag: "wx" });
|
|
14682
|
-
await
|
|
14831
|
+
await link2(temporaryFile, paths.secretsFile);
|
|
14683
14832
|
stored = candidate;
|
|
14684
14833
|
} catch (error) {
|
|
14685
14834
|
if (error.code !== "EEXIST")
|
|
@@ -15472,6 +15621,8 @@ function parseArgs(argv) {
|
|
|
15472
15621
|
options.output = resolve6(next());
|
|
15473
15622
|
else if (argument === "--file" || argument === "-f")
|
|
15474
15623
|
options.diffFile = next();
|
|
15624
|
+
else if (argument === "--baseline")
|
|
15625
|
+
options.baseline = true;
|
|
15475
15626
|
else if (argument === "--module-file")
|
|
15476
15627
|
options.moduleFile = resolve6(next());
|
|
15477
15628
|
else if (argument === "--service-role")
|
|
@@ -15580,7 +15731,7 @@ ${privilegedKey}
|
|
|
15580
15731
|
if (options.identityModule ?? config.lite.identityModule) {
|
|
15581
15732
|
report.identity = { mode: "external", verification: "unverified" };
|
|
15582
15733
|
}
|
|
15583
|
-
if (paths.dataDir &&
|
|
15734
|
+
if (paths.dataDir && existsSync5(join10(paths.dataDir, "PG_VERSION"))) {
|
|
15584
15735
|
const engine = paths.databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, installDir: options.postgresDir, log: quietLog, replication }) : await createPgliteEngine(paths.dataDir);
|
|
15585
15736
|
try {
|
|
15586
15737
|
report.graphql = await inspectGraphql(engine);
|
|
@@ -15720,10 +15871,12 @@ async function runDbCommand(options) {
|
|
|
15720
15871
|
`)}
|
|
15721
15872
|
`;
|
|
15722
15873
|
if (options.diffFile) {
|
|
15874
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,100}$/.test(options.diffFile))
|
|
15875
|
+
throw new Error("Invalid migration name");
|
|
15723
15876
|
const stamp = timestamp();
|
|
15724
15877
|
const output = join10(paths.projectDir, "supabase", "migrations", `${stamp}_${options.diffFile}.sql`);
|
|
15725
15878
|
await mkdir7(join10(paths.projectDir, "supabase", "migrations"), { recursive: true });
|
|
15726
|
-
await writeFile7(output, source);
|
|
15879
|
+
await writeFile7(output, source, { flag: "wx" });
|
|
15727
15880
|
await writeStandardOutput(`Wrote ${output}
|
|
15728
15881
|
`);
|
|
15729
15882
|
} else
|
|
@@ -15743,14 +15896,16 @@ async function runDbCommand(options) {
|
|
|
15743
15896
|
migrations: project.migrations,
|
|
15744
15897
|
makeShadowEngine: paths.databaseEngine === "native" ? () => createTemporaryNativeEngine(options.postgresDir) : undefined,
|
|
15745
15898
|
runtimeMode: config.lite.runtimeMode,
|
|
15746
|
-
migrationsDir: join10(paths.projectDir, "supabase", "migrations"),
|
|
15899
|
+
migrationsDir: options.baseline ? join10(paths.projectDir, "supabase", "migrations") : join10(paths.projectDir, ".supacloud-lite", "schema-drafts"),
|
|
15900
|
+
baseline: options.baseline,
|
|
15747
15901
|
name: options.positionals[1] ?? "remote_schema"
|
|
15748
15902
|
});
|
|
15749
15903
|
if (!result.path)
|
|
15750
15904
|
await writeStandardError(`No schema changes to pull.
|
|
15751
15905
|
`);
|
|
15752
15906
|
else
|
|
15753
|
-
await writeStandardOutput(`Wrote ${result.path} and recorded
|
|
15907
|
+
await writeStandardOutput(options.baseline ? `Wrote ${result.path} and recorded the existing schema as applied; no DDL was executed.
|
|
15908
|
+
` : `Wrote review draft ${result.path}. Live database and migration history were not changed. Use db pull --baseline after review.
|
|
15754
15909
|
`);
|
|
15755
15910
|
return;
|
|
15756
15911
|
}
|
|
@@ -15908,7 +16063,7 @@ Commands:
|
|
|
15908
16063
|
gen types emit Supabase-shaped TypeScript database types
|
|
15909
16064
|
db reset reset initialized database/storage and re-run migrations
|
|
15910
16065
|
db diff print schema changes outside migrations
|
|
15911
|
-
db pull [name] write live schema changes
|
|
16066
|
+
db pull [name] write live schema changes to schema-drafts for review
|
|
15912
16067
|
db check reconcile database module manifests (@supacloud/db) against the live catalog
|
|
15913
16068
|
snapshot create create a compressed database/storage/secrets snapshot
|
|
15914
16069
|
snapshot restore <f> restore a snapshot into an empty target
|
|
@@ -15945,6 +16100,7 @@ Options:
|
|
|
15945
16100
|
--identity-module trusted external identity bridge module
|
|
15946
16101
|
-o, --output <p> output file for gen types
|
|
15947
16102
|
-f, --file <name> migration suffix for db diff
|
|
16103
|
+
--baseline db pull: write and record already-present schema as a migration
|
|
15948
16104
|
--module-file <p> database module manifest for db check (default supabase/db/modules.ts)
|
|
15949
16105
|
--force replace non-empty restore targets and retain rollback copies
|
|
15950
16106
|
`);
|
package/dist/index.js
CHANGED
|
@@ -83,7 +83,7 @@ function randomToken(bytes = 32) {
|
|
|
83
83
|
// package.json
|
|
84
84
|
var package_default = {
|
|
85
85
|
name: "@supacloud/lite",
|
|
86
|
-
version: "0.
|
|
86
|
+
version: "0.20.0",
|
|
87
87
|
description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
|
|
88
88
|
type: "module",
|
|
89
89
|
license: "Apache-2.0",
|
|
@@ -6240,7 +6240,7 @@ begin
|
|
|
6240
6240
|
perform set_config('TimeZone', configured_timezone, false);
|
|
6241
6241
|
end $$;
|
|
6242
6242
|
`;
|
|
6243
|
-
async function createPgliteEngine(dataDir) {
|
|
6243
|
+
async function createPgliteEngine(dataDir, options = {}) {
|
|
6244
6244
|
const releaseLock = await acquireDataDirLock(dataDir, "PGlite");
|
|
6245
6245
|
let PGlite, extensions;
|
|
6246
6246
|
const standaloneAssets = getStandaloneAssets();
|
|
@@ -6295,7 +6295,8 @@ async function createPgliteEngine(dataDir) {
|
|
|
6295
6295
|
});
|
|
6296
6296
|
await pg.waitReady;
|
|
6297
6297
|
try {
|
|
6298
|
-
|
|
6298
|
+
if (!options.inspectOnly)
|
|
6299
|
+
await pg.exec(INITIALIZE_TIMEZONE_SQL);
|
|
6299
6300
|
} catch (error) {
|
|
6300
6301
|
const [cleanup] = await Promise.allSettled([pg.close()]);
|
|
6301
6302
|
if (cleanup.status === "rejected") {
|
|
@@ -5,5 +5,7 @@ import type { DbEngine } from './engine.js';
|
|
|
5
5
|
* extensions Supabase enables by default.
|
|
6
6
|
* @throws if the PGlite WASM bundle isn't available in this build (use the native engine instead).
|
|
7
7
|
*/
|
|
8
|
-
export declare function createPgliteEngine(dataDir?: string
|
|
8
|
+
export declare function createPgliteEngine(dataDir?: string, options?: {
|
|
9
|
+
inspectOnly?: boolean;
|
|
10
|
+
}): Promise<DbEngine>;
|
|
9
11
|
//# sourceMappingURL=pglite-engine.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pglite-engine.d.ts","sourceRoot":"","sources":["../../../src/runtime/db/pglite-engine.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAA8C,MAAM,aAAa,CAAA;AA2BvF;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"pglite-engine.d.ts","sourceRoot":"","sources":["../../../src/runtime/db/pglite-engine.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAA8C,MAAM,aAAa,CAAA;AA2BvF;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CA8GrH"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { DbEngine } from './engine.js';
|
|
2
2
|
interface ColumnSnap {
|
|
3
3
|
name: string;
|
|
4
4
|
/** fully-specified type from format_type (e.g. `character varying(255)`) */
|
|
@@ -13,6 +13,7 @@ interface TableSnap {
|
|
|
13
13
|
/** column names in declaration order, so DDL is emitted deterministically */
|
|
14
14
|
order: string[];
|
|
15
15
|
}
|
|
16
|
+
type DefinitionMap = Map<string, string>;
|
|
16
17
|
/** A structured snapshot of one schema, diffable into DDL by {@link diffSchemas}. */
|
|
17
18
|
export interface SchemaSnapshot {
|
|
18
19
|
/** table name → its columns */
|
|
@@ -23,9 +24,20 @@ export interface SchemaSnapshot {
|
|
|
23
24
|
indexes: Map<string, Map<string, string>>;
|
|
24
25
|
/** enum type name → its labels in sort order */
|
|
25
26
|
enums: Map<string, string[]>;
|
|
27
|
+
/** view name → view definition */
|
|
28
|
+
views: DefinitionMap;
|
|
29
|
+
/** function identity → function definition */
|
|
30
|
+
functions: DefinitionMap;
|
|
31
|
+
/** table/name → trigger definition */
|
|
32
|
+
triggers: DefinitionMap;
|
|
33
|
+
/** table/name → policy definition */
|
|
34
|
+
policies: DefinitionMap;
|
|
35
|
+
/** Catalog attributes which table DDL must preserve, including grants and identity. */
|
|
36
|
+
attributes: DefinitionMap;
|
|
26
37
|
}
|
|
27
38
|
/** Snapshot a schema's tables, columns, constraints, indexes, and enums. */
|
|
28
|
-
export declare function snapshotSchema(db:
|
|
39
|
+
export declare function snapshotSchema(db: Pick<DbEngine, 'query'>, schema?: string): Promise<SchemaSnapshot>;
|
|
40
|
+
export declare function schemasEqual(left: SchemaSnapshot, right: SchemaSnapshot): boolean;
|
|
29
41
|
/** DDL to turn `from` into `to`, scoped to one schema. Empty array = no changes. */
|
|
30
42
|
export declare function diffSchemas(from: SchemaSnapshot, to: SchemaSnapshot, schema?: string): string[];
|
|
31
43
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-diff.d.ts","sourceRoot":"","sources":["../../../src/runtime/db/schema-diff.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"schema-diff.d.ts","sourceRoot":"","sources":["../../../src/runtime/db/schema-diff.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE3C,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;IACjB,mDAAmD;IACnD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CACvB;AACD,UAAU,SAAS;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IAChC,6EAA6E;IAC7E,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB;AAED,KAAK,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAExC,qFAAqF;AACrF,MAAM,WAAW,cAAc;IAC7B,+BAA+B;IAC/B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IAC9B,uEAAuE;IACvE,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC7C,mGAAmG;IACnG,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACzC,gDAAgD;IAChD,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IAC5B,kCAAkC;IAClC,KAAK,EAAE,aAAa,CAAA;IACpB,8CAA8C;IAC9C,SAAS,EAAE,aAAa,CAAA;IACxB,sCAAsC;IACtC,QAAQ,EAAE,aAAa,CAAA;IACvB,qCAAqC;IACrC,QAAQ,EAAE,aAAa,CAAA;IACvB,uFAAuF;IACvF,UAAU,EAAE,aAAa,CAAA;CAC1B;AAED,4EAA4E;AAC5E,wBAAsB,cAAc,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,MAAM,SAAW,GAAG,OAAO,CAAC,cAAc,CAAC,CA+K5G;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAWjF;AAED,oFAAoF;AACpF,wBAAgB,WAAW,CAAC,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,cAAc,EAAE,MAAM,SAAW,GAAG,MAAM,EAAE,CAkGjG"}
|