@supacloud/lite 0.19.0 → 0.21.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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.21.0](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.20.0...supacloud-lite-v0.21.0) (2026-10-05)
4
+
5
+
6
+ ### Features
7
+
8
+ * **elysia:** Elysia 2 AOT evaluation and end-to-end typing architecture ([#1654](https://github.com/vibeunion/supacloud/issues/1654)) ([39ee49f](https://github.com/vibeunion/supacloud/commit/39ee49f760e85e789c43c1d18cf5729db592f6bc))
9
+
10
+ ## [0.20.0](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.19.0...supacloud-lite-v0.20.0) (2026-10-05)
11
+
12
+
13
+ ### Features
14
+
15
+ * add agent backend authoring, runtime and observability workflows ([#1648](https://github.com/vibeunion/supacloud/issues/1648)) ([59a66d1](https://github.com/vibeunion/supacloud/commit/59a66d184a47d19c7f8895f24a6ba2c7e74e93d0))
16
+
3
17
  ## [0.19.0](https://github.com/vibeunion/supacloud/compare/supacloud-lite-v0.18.0...supacloud-lite-v0.19.0) (2026-10-03)
4
18
 
5
19
 
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 existsSync4 } from "fs";
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.19.0",
11
+ version: "0.21.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",
@@ -74,7 +74,7 @@ var package_default = {
74
74
  "@supacloud/elysia": "file:../elysia",
75
75
  "@supabase/supabase-js": "^2.115.0",
76
76
  "@types/bun": "^1.4.2",
77
- elysia: "2.0.0-beta.19",
77
+ elysia: "2.0.0-beta.21",
78
78
  jose: "^6.2.11",
79
79
  typescript: "^7.0.2"
80
80
  },
@@ -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
- await pg.exec(INITIALIZE_TIMEZONE_SQL);
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
- return { tables, constraints, indexes, enums };
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
- const liveEngine = unclaimedLiveEngine;
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
- const liveSnap = await snapshotSchema(live.db, schema);
12699
- return diffSchemas(shadowSnap, liveSnap, schema);
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 schema = opts.schema ?? "public";
12741
- let shadow;
12742
- let live;
12743
- let unclaimedLiveEngine = opts.liveEngine;
12744
- let operationFailed = false;
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
- shadow = await createBackend({
12747
- runtimeMode: opts.runtimeMode,
12748
- engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
12749
- migrations: opts.migrations,
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
- let path = null;
12771
- if (opts.migrationsDir) {
12772
- await mkdir2(opts.migrationsDir, { recursive: true });
12773
- path = join2(opts.migrationsDir, `${stamp}_${name}.sql`);
12774
- await writeFile2(path, body);
12775
- }
12776
- await live.db.query(`insert into supabase_migrations.schema_migrations (version, name, statements)
12777
- values ($1, $2, $3) on conflict (version) do nothing`, [stamp, `${stamp}_${name}`, [body]]);
12778
- return { ddl, version: stamp, path };
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
- operationFailed = true;
12781
- throw error;
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 existsSync3 } from "fs";
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 (existsSync3(cached))
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 link(temporaryFile, paths.secretsFile);
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 && existsSync4(join10(paths.dataDir, "PG_VERSION"))) {
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 version ${result.version} as applied.
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 as an applied migration
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.19.0",
86
+ version: "0.21.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",
@@ -149,7 +149,7 @@ var package_default = {
149
149
  "@supacloud/elysia": "file:../elysia",
150
150
  "@supabase/supabase-js": "^2.115.0",
151
151
  "@types/bun": "^1.4.2",
152
- elysia: "2.0.0-beta.19",
152
+ elysia: "2.0.0-beta.21",
153
153
  jose: "^6.2.11",
154
154
  typescript: "^7.0.2"
155
155
  },
@@ -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
- await pg.exec(INITIALIZE_TIMEZONE_SQL);
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): Promise<DbEngine>;
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,CA8G5E"}
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 { Database } from './database.js';
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: Database, schema?: string): Promise<SchemaSnapshot>;
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":"AAcA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAE7C,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,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;CAC7B;AAED,4EAA4E;AAC5E,wBAAsB,cAAc,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,SAAW,GAAG,OAAO,CAAC,cAAc,CAAC,CAqE7F;AAED,oFAAoF;AACpF,wBAAgB,WAAW,CAAC,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,cAAc,EAAE,MAAM,SAAW,GAAG,MAAM,EAAE,CAiFjG"}
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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/lite",
3
- "version": "0.19.0",
3
+ "version": "0.21.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",
@@ -66,7 +66,7 @@
66
66
  "@supacloud/elysia": "file:../elysia",
67
67
  "@supabase/supabase-js": "^2.115.0",
68
68
  "@types/bun": "^1.4.2",
69
- "elysia": "2.0.0-beta.19",
69
+ "elysia": "2.0.0-beta.21",
70
70
  "jose": "^6.2.11",
71
71
  "typescript": "^7.0.2"
72
72
  },