@devdogsuga/devtools 0.1.51-next.549 → 0.1.51-next.552
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/README.md
CHANGED
|
@@ -106,17 +106,17 @@ pnpm devtools setup
|
|
|
106
106
|
|
|
107
107
|
Anywhere else, the one-off form is `pnpm dlx @devdogsuga/devtools@next setup`.
|
|
108
108
|
|
|
109
|
-
| Command
|
|
110
|
-
|
|
|
111
|
-
| `setup [--local \| --hosted]`
|
|
112
|
-
| `db sync [--rebuild] [--yes]`
|
|
113
|
-
| `db types [--download] [--pack <file>]`
|
|
114
|
-
| `db hash`
|
|
115
|
-
| `dev-data [--member <email\|id>]`
|
|
116
|
-
| `oauth`
|
|
117
|
-
| `doctor`
|
|
118
|
-
| `supabase <args>`
|
|
119
|
-
| `check env-example`
|
|
109
|
+
| Command | What it does |
|
|
110
|
+
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
111
|
+
| `setup [--local \| --hosted]` | Writes `.env` from `.env.example`; starts the Docker stack (or takes your own hosted project's URL, keys and session-pooler `DB_URL`); `db sync`; the types; then `oauth`. |
|
|
112
|
+
| `db sync [--rebuild] [--strict] [--yes]` | Makes your development database match `supabase/schemas/`: recreates what it lost, applies schema changes you pulled, warns about extras (`--strict` fails on them), refuses to alter what is defined differently. Never resets it; `--rebuild` is the explicit reset (local only) and registers sign in again afterwards. |
|
|
113
|
+
| `db types [--download] [--pack <file>]` | Generates the TypeScript types or Dart models from your database, or downloads the ones CI built for your exact schema hash. `--pack` writes the release asset (CI). |
|
|
114
|
+
| `db hash` | Prints the schema hash that names the generated types. |
|
|
115
|
+
| `dev-data [--member <email\|id>]` | Runs the product's `devData.file` SQL, in one transaction, as the member who last signed in with DevDogs. Refuses the product's production project. |
|
|
116
|
+
| `oauth` | "Sign in with DevDogs" on your database (the same wizard as the platform's). |
|
|
117
|
+
| `doctor` | Docker, the pinned Supabase CLI, `.env`, the database, whether the types match the schema. |
|
|
118
|
+
| `supabase <args>` | The Supabase CLI at the version the product pins. |
|
|
119
|
+
| `check env-example` | CI: `.env.example` holds public values only (it ships in the app). |
|
|
120
120
|
|
|
121
121
|
How it works, in short:
|
|
122
122
|
|
|
@@ -127,7 +127,14 @@ How it works, in short:
|
|
|
127
127
|
needs `--yes`. It then regenerates the types (and leaves them alone when nothing changed and they are current).
|
|
128
128
|
On a hosted project, a machine with no record (a second laptop, a deleted `supabase/migrations/`) first
|
|
129
129
|
restores it from the project's own history table (`migration fetch --db-url`), so the diff is not taken
|
|
130
|
-
against an empty database.
|
|
130
|
+
against an empty database. The record belongs to one database (`.devtools/record.json` notes which, and which
|
|
131
|
+
files it has applied): another target's record is set aside and that database's own history is fetched, and a
|
|
132
|
+
migration written but never applied is pending, not applied; it is applied on the next sync, with the `--yes`
|
|
133
|
+
question for drops.
|
|
134
|
+
- **What a member changed.** A column that looks renamed (missing here, an extra of the same type beside it) is
|
|
135
|
+
refused, not recreated empty. A difference in privileges on something that exists is applied as a GRANT of what
|
|
136
|
+
`supabase/schemas` grants and reported; privileges beyond that are not taken away. Functions are named with
|
|
137
|
+
their argument types, and a missing overload is recreated beside the member's own.
|
|
131
138
|
- **Types are never committed.** `db types` writes them to the path in the config, which is git-ignored.
|
|
132
139
|
The schema hash covers the sorted schema paths, their LF-normalised contents and the generator version (the
|
|
133
140
|
pinned CLI, or supadart); CI uploads a `<product>-types-<hash>.tgz` asset to a rolling `generated-types`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as execSync, r as spawnProcess } from "./spawn-BvLQzc_K.js";
|
|
2
2
|
import { n as discoverRepoRoot, r as findRepoRoot } from "./root-RaJ8ECCO.js";
|
|
3
3
|
import { A as findApp, M as listApps, N as resolveAppPath, O as appWorkspaceRoot, P as resolveLayout, _ as dryRunKind, c as supabaseRoot, d as loadEnvLoad, f as loadEnvSession, g as runInGroup, h as reportRan, i as runWithStderr, j as layoutAt, k as devdogsugaRootOrNull, l as getEnvSync, n as reportRuns, o as supabase, p as repoPeerUrl, r as run$1, t as dbPush, u as loadEnv, v as isDryRun, x as wouldRunLine } from "./run-D5iI9zve.js";
|
|
4
|
-
import { A as renderHelp, C as loadProductConfig, D as catalog, M as probeEnvironment, O as helpPath, T as isProductMode, _ as explain, a as recordEnteredTier, b as ProductConfigError, c as nonEmpty, d as sessionLabel, g as errorMessage, h as bail, i as beginInvocation, j as describeEnvironment, k as renderCommandList, l as resolveDbConnection, n as bareGroupStartPath, o as recordResolved, r as runMenu, s as reproducibleCommand, t as bareGroupStartFlags, u as sessionIsLocal, v as explainError, w as activeCatalog, x as currentProduct, y as unwrap } from "./menu-
|
|
4
|
+
import { A as renderHelp, C as loadProductConfig, D as catalog, M as probeEnvironment, O as helpPath, T as isProductMode, _ as explain, a as recordEnteredTier, b as ProductConfigError, c as nonEmpty, d as sessionLabel, g as errorMessage, h as bail, i as beginInvocation, j as describeEnvironment, k as renderCommandList, l as resolveDbConnection, n as bareGroupStartPath, o as recordResolved, r as runMenu, s as reproducibleCommand, t as bareGroupStartFlags, u as sessionIsLocal, v as explainError, w as activeCatalog, x as currentProduct, y as unwrap } from "./menu-Ctqnvduz.js";
|
|
5
5
|
import { a as isVersionRequest, c as cliName, m as positionals, o as ownPackageDir, p as flagValue, s as ownVersion } from "./failure-log-MSdCgvLc.js";
|
|
6
6
|
import { f as hasYes, n as captureDevtoolsError, o as initDevtoolsTelemetry, p as isNonInteractive } from "./telemetry-BDOwjuoC.js";
|
|
7
7
|
import { t as runStackCommand } from "./stack-CzNe6rxl.js";
|
|
@@ -7058,7 +7058,7 @@ function commandLine(command, args) {
|
|
|
7058
7058
|
function cliLine(cli, args) {
|
|
7059
7059
|
return commandLine(cli.command, [...cli.prefix, ...args]);
|
|
7060
7060
|
}
|
|
7061
|
-
function versionOf(output) {
|
|
7061
|
+
function versionOf$1(output) {
|
|
7062
7062
|
return /\b(\d+\.\d+\.\d+)\b/.exec(output)?.[1] ?? null;
|
|
7063
7063
|
}
|
|
7064
7064
|
async function resolveSupabaseCli(root, pin, run, fileExists = existsSync) {
|
|
@@ -7076,7 +7076,7 @@ async function resolveSupabaseCli(root, pin, run, fileExists = existsSync) {
|
|
|
7076
7076
|
});
|
|
7077
7077
|
for (const candidate of candidates) try {
|
|
7078
7078
|
const result = await run(candidate.command, ["--version"], { cwd: root });
|
|
7079
|
-
if (result.code === 0 && versionOf(result.stdout) === pin) return candidate;
|
|
7079
|
+
if (result.code === 0 && versionOf$1(result.stdout) === pin) return candidate;
|
|
7080
7080
|
} catch {}
|
|
7081
7081
|
try {
|
|
7082
7082
|
if ((await run("pnpm", ["--version"], { cwd: root })).code === 0) return {
|
|
@@ -7303,6 +7303,43 @@ function postgresSession(dbUrl) {
|
|
|
7303
7303
|
const present = new Set(rows.map((r) => r.nspname));
|
|
7304
7304
|
return schemas.filter((name) => !present.has(name));
|
|
7305
7305
|
},
|
|
7306
|
+
async existingObjects(schemas) {
|
|
7307
|
+
const list = [...schemas];
|
|
7308
|
+
const found = /* @__PURE__ */ new Set();
|
|
7309
|
+
const functions = await sql`
|
|
7310
|
+
select n.nspname as s, p.proname as n,
|
|
7311
|
+
oidvectortypes(p.proargtypes) as sig
|
|
7312
|
+
from pg_proc p
|
|
7313
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
7314
|
+
where n.nspname = any(${list}) and p.prokind in ('f', 'p')`;
|
|
7315
|
+
for (const r of functions) {
|
|
7316
|
+
found.add(`function:${r.s}.${r.n}`);
|
|
7317
|
+
found.add(`signature:${r.s}.${r.n}(${r.sig})`);
|
|
7318
|
+
}
|
|
7319
|
+
const views = await sql`
|
|
7320
|
+
select n.nspname as s, c.relname as n from pg_class c
|
|
7321
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
7322
|
+
where n.nspname = any(${list}) and c.relkind in ('v', 'm')`;
|
|
7323
|
+
for (const r of views) found.add(`view:${r.s}.${r.n}`);
|
|
7324
|
+
const triggers = await sql`
|
|
7325
|
+
select n.nspname as s, c.relname as t, g.tgname as n from pg_trigger g
|
|
7326
|
+
join pg_class c on c.oid = g.tgrelid
|
|
7327
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
7328
|
+
where n.nspname = any(${list}) and not g.tgisinternal`;
|
|
7329
|
+
for (const r of triggers) found.add(`trigger:${r.s}.${r.t}:${r.n}`);
|
|
7330
|
+
return found;
|
|
7331
|
+
},
|
|
7332
|
+
async columnTypes(schemas) {
|
|
7333
|
+
const rows = await sql`
|
|
7334
|
+
select n.nspname as s, c.relname as t, a.attname as c,
|
|
7335
|
+
format_type(a.atttypid, a.atttypmod) as type
|
|
7336
|
+
from pg_attribute a
|
|
7337
|
+
join pg_class c on c.oid = a.attrelid
|
|
7338
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
7339
|
+
where n.nspname = any(${[...schemas]}) and c.relkind in ('r', 'p', 'f')
|
|
7340
|
+
and a.attnum > 0 and not a.attisdropped`;
|
|
7341
|
+
return new Map(rows.map((r) => [`${r.s}.${r.t}.${r.c}`, r.type]));
|
|
7342
|
+
},
|
|
7306
7343
|
async exec(text) {
|
|
7307
7344
|
await sql.begin(async (tx) => {
|
|
7308
7345
|
await tx.unsafe(text);
|
|
@@ -7563,6 +7600,467 @@ function typesAssetName(product, hash) {
|
|
|
7563
7600
|
return `${product}-types-${hash}.tgz`;
|
|
7564
7601
|
}
|
|
7565
7602
|
//#endregion
|
|
7603
|
+
//#region src/product/drift.ts
|
|
7604
|
+
/** Splits SQL at top-level semicolons, honoring quotes, dollar quotes and comments. Comments are dropped. */
|
|
7605
|
+
function splitStatements(sql) {
|
|
7606
|
+
const out = [];
|
|
7607
|
+
let current = "";
|
|
7608
|
+
let i = 0;
|
|
7609
|
+
const push = () => {
|
|
7610
|
+
const text = current.trim();
|
|
7611
|
+
if (text) out.push(text);
|
|
7612
|
+
current = "";
|
|
7613
|
+
};
|
|
7614
|
+
while (i < sql.length) {
|
|
7615
|
+
const ch = sql[i];
|
|
7616
|
+
const next = sql[i + 1];
|
|
7617
|
+
if (ch === "-" && next === "-") {
|
|
7618
|
+
while (i < sql.length && sql[i] !== "\n") i++;
|
|
7619
|
+
continue;
|
|
7620
|
+
}
|
|
7621
|
+
if (ch === "/" && next === "*") {
|
|
7622
|
+
const end = sql.indexOf("*/", i + 2);
|
|
7623
|
+
i = end === -1 ? sql.length : end + 2;
|
|
7624
|
+
current += " ";
|
|
7625
|
+
continue;
|
|
7626
|
+
}
|
|
7627
|
+
if (ch === "'" || ch === "\"") {
|
|
7628
|
+
let j = i + 1;
|
|
7629
|
+
while (j < sql.length) {
|
|
7630
|
+
if (sql[j] === ch) {
|
|
7631
|
+
if (sql[j + 1] === ch) {
|
|
7632
|
+
j += 2;
|
|
7633
|
+
continue;
|
|
7634
|
+
}
|
|
7635
|
+
break;
|
|
7636
|
+
}
|
|
7637
|
+
j++;
|
|
7638
|
+
}
|
|
7639
|
+
current += sql.slice(i, j + 1);
|
|
7640
|
+
i = j + 1;
|
|
7641
|
+
continue;
|
|
7642
|
+
}
|
|
7643
|
+
if (ch === "$") {
|
|
7644
|
+
const tag = /^\$[A-Za-z_]*\$/.exec(sql.slice(i))?.[0];
|
|
7645
|
+
if (tag) {
|
|
7646
|
+
const end = sql.indexOf(tag, i + tag.length);
|
|
7647
|
+
const stop = end === -1 ? sql.length : end + tag.length;
|
|
7648
|
+
current += sql.slice(i, stop);
|
|
7649
|
+
i = stop;
|
|
7650
|
+
continue;
|
|
7651
|
+
}
|
|
7652
|
+
}
|
|
7653
|
+
if (ch === ";") {
|
|
7654
|
+
push();
|
|
7655
|
+
i++;
|
|
7656
|
+
continue;
|
|
7657
|
+
}
|
|
7658
|
+
current += ch;
|
|
7659
|
+
i++;
|
|
7660
|
+
}
|
|
7661
|
+
push();
|
|
7662
|
+
return out;
|
|
7663
|
+
}
|
|
7664
|
+
/** Splits `text` at commas that are not inside parentheses or quotes. */
|
|
7665
|
+
function splitTopLevel(text) {
|
|
7666
|
+
const parts = [];
|
|
7667
|
+
let depth = 0;
|
|
7668
|
+
let quote = null;
|
|
7669
|
+
let start = 0;
|
|
7670
|
+
for (let i = 0; i < text.length; i++) {
|
|
7671
|
+
const ch = text[i];
|
|
7672
|
+
if (quote) {
|
|
7673
|
+
if (ch === quote) quote = null;
|
|
7674
|
+
} else if (ch === "'" || ch === "\"") quote = ch;
|
|
7675
|
+
else if (ch === "(") depth++;
|
|
7676
|
+
else if (ch === ")") depth--;
|
|
7677
|
+
else if (ch === "," && depth === 0) {
|
|
7678
|
+
parts.push(text.slice(start, i).trim());
|
|
7679
|
+
start = i + 1;
|
|
7680
|
+
}
|
|
7681
|
+
}
|
|
7682
|
+
parts.push(text.slice(start).trim());
|
|
7683
|
+
return parts.filter(Boolean);
|
|
7684
|
+
}
|
|
7685
|
+
const IDENT = String.raw`(?:"(?:[^"]|"")+"|[A-Za-z_][\w$]*)`;
|
|
7686
|
+
const QNAME = String.raw`${IDENT}(?:\s*\.\s*${IDENT})*`;
|
|
7687
|
+
/** `"a"."b"` -> ["a", "b"]; unquoted names fold to lower case like Postgres. */
|
|
7688
|
+
function parts(name) {
|
|
7689
|
+
return (name.match(new RegExp(IDENT, "g")) ?? []).map((part) => part.startsWith("\"") ? part.slice(1, -1).replace(/""/g, "\"") : part.toLowerCase());
|
|
7690
|
+
}
|
|
7691
|
+
function qualified(name) {
|
|
7692
|
+
return parts(name).join(".");
|
|
7693
|
+
}
|
|
7694
|
+
function shown(name) {
|
|
7695
|
+
return parts(name).map((part) => `"${part}"`).join(".");
|
|
7696
|
+
}
|
|
7697
|
+
const TYPE_ALIASES = {
|
|
7698
|
+
int: "integer",
|
|
7699
|
+
int4: "integer",
|
|
7700
|
+
int8: "bigint",
|
|
7701
|
+
int2: "smallint",
|
|
7702
|
+
bool: "boolean",
|
|
7703
|
+
float8: "double precision",
|
|
7704
|
+
float4: "real",
|
|
7705
|
+
varchar: "character varying",
|
|
7706
|
+
bpchar: "character",
|
|
7707
|
+
timestamptz: "timestamp with time zone",
|
|
7708
|
+
timetz: "time with time zone",
|
|
7709
|
+
decimal: "numeric"
|
|
7710
|
+
};
|
|
7711
|
+
/** `Int4[ ]`, `pg_catalog.text`, `"text"` -> a comparable spelling. */
|
|
7712
|
+
function normalizeType(type) {
|
|
7713
|
+
let t = type.trim().replace(/\s+/g, " ").toLowerCase();
|
|
7714
|
+
const array = /(\s*\[\d*\])+$/.exec(t)?.[0] ?? "";
|
|
7715
|
+
t = t.slice(0, t.length - array.length);
|
|
7716
|
+
t = t.replace(/^pg_catalog\./, "").replace(/"/g, "");
|
|
7717
|
+
t = t.replace(/\s*\(.*\)$/, (m) => m.replace(/\s+/g, ""));
|
|
7718
|
+
const base = /^([a-z0-9_ ]+?)(\(.*\))?$/.exec(t);
|
|
7719
|
+
const head = base?.[1]?.trim() ?? t;
|
|
7720
|
+
const tail = base?.[2] ?? "";
|
|
7721
|
+
return `${TYPE_ALIASES[head] ?? head}${tail}${array.replace(/\s/g, "")}`;
|
|
7722
|
+
}
|
|
7723
|
+
/** The text inside the first balanced parentheses of `text`, or null. */
|
|
7724
|
+
function balanced(text) {
|
|
7725
|
+
const start = text.indexOf("(");
|
|
7726
|
+
if (start === -1) return null;
|
|
7727
|
+
let depth = 0;
|
|
7728
|
+
let quote = null;
|
|
7729
|
+
for (let i = start; i < text.length; i++) {
|
|
7730
|
+
const ch = text[i];
|
|
7731
|
+
if (quote) {
|
|
7732
|
+
if (ch === quote) quote = null;
|
|
7733
|
+
} else if (ch === "'" || ch === "\"") quote = ch;
|
|
7734
|
+
else if (ch === "(") depth++;
|
|
7735
|
+
else if (ch === ")" && --depth === 0) return text.slice(start + 1, i);
|
|
7736
|
+
}
|
|
7737
|
+
return null;
|
|
7738
|
+
}
|
|
7739
|
+
const MULTIWORD_HEADS = /* @__PURE__ */ new Set([
|
|
7740
|
+
"double",
|
|
7741
|
+
"timestamp",
|
|
7742
|
+
"time",
|
|
7743
|
+
"character",
|
|
7744
|
+
"bit",
|
|
7745
|
+
"national",
|
|
7746
|
+
"interval"
|
|
7747
|
+
]);
|
|
7748
|
+
/**
|
|
7749
|
+
* The argument types of a function's parameter list, as the database spells
|
|
7750
|
+
* them: `a int, OUT b text, c text DEFAULT 'x'` -> `(integer, text)`.
|
|
7751
|
+
*/
|
|
7752
|
+
function signatureOf(argList) {
|
|
7753
|
+
const types = [];
|
|
7754
|
+
for (let arg of splitTopLevel(argList)) {
|
|
7755
|
+
if (/^(OUT|TABLE)\b/i.test(arg)) continue;
|
|
7756
|
+
arg = arg.replace(/^(?:IN\s+OUT|INOUT|IN|VARIADIC)\s+/i, "");
|
|
7757
|
+
const dflt = /\s+DEFAULT\s|\s*=\s*/i.exec(arg);
|
|
7758
|
+
if (dflt) arg = arg.slice(0, dflt.index);
|
|
7759
|
+
arg = arg.trim();
|
|
7760
|
+
const named = new RegExp(`^(${IDENT})\\s+(.+)$`, "s").exec(arg);
|
|
7761
|
+
let type = arg;
|
|
7762
|
+
if (named) {
|
|
7763
|
+
const head = named[1].toLowerCase();
|
|
7764
|
+
const second = named[2].split(/[\s(]/)[0].toLowerCase();
|
|
7765
|
+
if (!(MULTIWORD_HEADS.has(head) && (/^(precision|with|without|varying)$/.test(second) || named[2].startsWith("(")))) type = named[2];
|
|
7766
|
+
}
|
|
7767
|
+
types.push(normalizeType(type));
|
|
7768
|
+
}
|
|
7769
|
+
return `(${types.join(", ")})`;
|
|
7770
|
+
}
|
|
7771
|
+
/** Normalizes the `function:s.n(types)` spelling the database reports. */
|
|
7772
|
+
function normalizeKeySignature(types) {
|
|
7773
|
+
return signatureOf(types);
|
|
7774
|
+
}
|
|
7775
|
+
/** Reads what a GRANT or REVOKE is on. */
|
|
7776
|
+
function privilegeOf(text, revoke) {
|
|
7777
|
+
const on = new RegExp(String.raw`\bON\s+(?:(TABLE|SEQUENCE|FUNCTION|PROCEDURE|SCHEMA|TYPE|DOMAIN)\s+)?(${QNAME})(\s*\(.*\))?\s+(?:FROM|TO)\s+(.+)$`, "is").exec(text);
|
|
7778
|
+
const base = {
|
|
7779
|
+
sql: text,
|
|
7780
|
+
revoke,
|
|
7781
|
+
targets: [],
|
|
7782
|
+
base: "",
|
|
7783
|
+
object: null,
|
|
7784
|
+
from: ""
|
|
7785
|
+
};
|
|
7786
|
+
if (!on) return base;
|
|
7787
|
+
const kind = (on[1] ?? "").toUpperCase();
|
|
7788
|
+
const name = on[2];
|
|
7789
|
+
const isFunction = kind === "FUNCTION" || kind === "PROCEDURE";
|
|
7790
|
+
const sig = isFunction ? signatureOf(balanced(on[3] ?? "") ?? "") : "";
|
|
7791
|
+
const object = `${qualified(name)}${sig}`;
|
|
7792
|
+
const columns = isFunction ? [] : [...text.slice(0, on.index).matchAll(/\(([^)]*)\)/g)].flatMap((m) => m[1].split(",").map((c) => parts(c)[0]).filter((c) => Boolean(c)));
|
|
7793
|
+
return {
|
|
7794
|
+
...base,
|
|
7795
|
+
base: object,
|
|
7796
|
+
targets: columns.length > 0 ? columns.map((c) => `${object}.${c}`) : [object],
|
|
7797
|
+
object: `privileges on ${isFunction ? "function " : ""}${shown(name)}${sig}`,
|
|
7798
|
+
from: on[4].replace(/"/g, "")
|
|
7799
|
+
};
|
|
7800
|
+
}
|
|
7801
|
+
const CREATE_KINDS = {
|
|
7802
|
+
SCHEMA: "schema",
|
|
7803
|
+
TABLE: "table",
|
|
7804
|
+
"FOREIGN TABLE": "table",
|
|
7805
|
+
SEQUENCE: "sequence",
|
|
7806
|
+
TYPE: "type",
|
|
7807
|
+
DOMAIN: "type",
|
|
7808
|
+
VIEW: "view",
|
|
7809
|
+
"MATERIALIZED VIEW": "view",
|
|
7810
|
+
FUNCTION: "function",
|
|
7811
|
+
PROCEDURE: "function"
|
|
7812
|
+
};
|
|
7813
|
+
const WORD = (word) => new RegExp(`^${word}\\b`, "i");
|
|
7814
|
+
/**
|
|
7815
|
+
* Classifies the statements of a live diff. `existing` is what the live
|
|
7816
|
+
* database has (for `CREATE OR REPLACE`, which reads the same whether the
|
|
7817
|
+
* object is missing or merely different).
|
|
7818
|
+
*/
|
|
7819
|
+
function classifyDiff(sql, existing, columnTypes = /* @__PURE__ */ new Map()) {
|
|
7820
|
+
const signatures = /* @__PURE__ */ new Set();
|
|
7821
|
+
for (const key of existing) {
|
|
7822
|
+
const m = /^signature:(.*?)\((.*)\)$/.exec(key);
|
|
7823
|
+
if (m) signatures.add(`${m[1]}${normalizeKeySignature(m[2])}`);
|
|
7824
|
+
}
|
|
7825
|
+
const creates = [];
|
|
7826
|
+
const drops = [];
|
|
7827
|
+
const conflicts = /* @__PURE__ */ new Map();
|
|
7828
|
+
const extras = [];
|
|
7829
|
+
const preamble = [];
|
|
7830
|
+
const privileges = [];
|
|
7831
|
+
const conflict = (object, why) => {
|
|
7832
|
+
const set = conflicts.get(object) ?? /* @__PURE__ */ new Set();
|
|
7833
|
+
set.add(why);
|
|
7834
|
+
conflicts.set(object, set);
|
|
7835
|
+
};
|
|
7836
|
+
for (const statement of splitStatements(sql)) {
|
|
7837
|
+
const text = statement.replace(/\s+/g, " ");
|
|
7838
|
+
if (WORD("SET").test(text)) {
|
|
7839
|
+
preamble.push(statement);
|
|
7840
|
+
continue;
|
|
7841
|
+
}
|
|
7842
|
+
if (WORD("GRANT").test(text) || WORD("REVOKE").test(text)) {
|
|
7843
|
+
const p = privilegeOf(text, WORD("REVOKE").test(text));
|
|
7844
|
+
privileges.push({
|
|
7845
|
+
...p,
|
|
7846
|
+
sql: statement
|
|
7847
|
+
});
|
|
7848
|
+
continue;
|
|
7849
|
+
}
|
|
7850
|
+
const create = new RegExp(String.raw`^CREATE\s+(OR\s+REPLACE\s+)?(?:UNIQUE\s+)?(?:TEMP(?:ORARY)?\s+)?(SCHEMA|TABLE|FOREIGN TABLE|SEQUENCE|TYPE|DOMAIN|VIEW|MATERIALIZED VIEW|FUNCTION|PROCEDURE|INDEX|POLICY|TRIGGER|CONSTRAINT TRIGGER)\s+(?:IF\s+NOT\s+EXISTS\s+)?(${IDENT}(?:\s*\.\s*${IDENT})*)(.*)$`, "is").exec(text);
|
|
7851
|
+
if (create) {
|
|
7852
|
+
const replace = Boolean(create[1]);
|
|
7853
|
+
const kindWord = create[2].toUpperCase().replace(/\s+/g, " ");
|
|
7854
|
+
const name = create[3];
|
|
7855
|
+
const rest = create[4];
|
|
7856
|
+
let key;
|
|
7857
|
+
let label;
|
|
7858
|
+
if (kindWord === "INDEX") {
|
|
7859
|
+
key = `index:${parts(name).at(-1)}`;
|
|
7860
|
+
label = `index ${shown(name)}`;
|
|
7861
|
+
} else if (kindWord === "POLICY" || kindWord === "TRIGGER" || kindWord === "CONSTRAINT TRIGGER") {
|
|
7862
|
+
const on = new RegExp(String.raw`\bON\s+(${QNAME})`, "i").exec(rest);
|
|
7863
|
+
const table = on ? qualified(on[1]) : "?";
|
|
7864
|
+
const noun = kindWord === "POLICY" ? "policy" : "trigger";
|
|
7865
|
+
key = `${noun}:${table}:${parts(name).at(-1)}`;
|
|
7866
|
+
label = `${noun} ${shown(name)} on ${on ? shown(on[1]) : "?"}`;
|
|
7867
|
+
} else if (kindWord === "FUNCTION" || kindWord === "PROCEDURE") {
|
|
7868
|
+
const sig = signatureOf(balanced(rest) ?? "");
|
|
7869
|
+
key = `function:${qualified(name)}${sig}`;
|
|
7870
|
+
label = `function ${shown(name)}${sig}`;
|
|
7871
|
+
} else {
|
|
7872
|
+
const kind = CREATE_KINDS[kindWord] ?? kindWord.toLowerCase();
|
|
7873
|
+
key = `${kind}:${qualified(name)}`;
|
|
7874
|
+
label = `${kind} ${shown(name)}`;
|
|
7875
|
+
}
|
|
7876
|
+
const exists = key.startsWith("function:") ? signatures.has(key.slice(9)) || signatures.size === 0 && existing.has(`function:${qualified(name)}`) : existing.has(key);
|
|
7877
|
+
if (replace && exists) conflict(label, "its definition differs from supabase/schemas");
|
|
7878
|
+
else creates.push({
|
|
7879
|
+
key,
|
|
7880
|
+
sql: statement,
|
|
7881
|
+
label
|
|
7882
|
+
});
|
|
7883
|
+
continue;
|
|
7884
|
+
}
|
|
7885
|
+
const drop = new RegExp(String.raw`^DROP\s+(SCHEMA|TABLE|FOREIGN TABLE|SEQUENCE|TYPE|DOMAIN|VIEW|MATERIALIZED VIEW|FUNCTION|PROCEDURE|INDEX|POLICY|TRIGGER|EXTENSION)\s+(?:IF\s+EXISTS\s+)?(${QNAME})(.*)$`, "is").exec(text);
|
|
7886
|
+
if (drop) {
|
|
7887
|
+
const kindWord = drop[1].toUpperCase();
|
|
7888
|
+
const name = drop[2];
|
|
7889
|
+
const rest = drop[3];
|
|
7890
|
+
if (kindWord === "INDEX") drops.push({
|
|
7891
|
+
key: `index:${parts(name).at(-1)}`,
|
|
7892
|
+
finding: {
|
|
7893
|
+
object: `index ${shown(name)}`,
|
|
7894
|
+
detail: "not in supabase/schemas"
|
|
7895
|
+
}
|
|
7896
|
+
});
|
|
7897
|
+
else if (kindWord === "POLICY" || kindWord === "TRIGGER") {
|
|
7898
|
+
const on = new RegExp(String.raw`\bON\s+(${QNAME})`, "i").exec(rest);
|
|
7899
|
+
const noun = kindWord.toLowerCase();
|
|
7900
|
+
drops.push({
|
|
7901
|
+
key: `${noun}:${on ? qualified(on[1]) : "?"}:${parts(name).at(-1)}`,
|
|
7902
|
+
finding: {
|
|
7903
|
+
object: `${noun} ${shown(name)} on ${on ? shown(on[1]) : "?"}`,
|
|
7904
|
+
detail: "not in supabase/schemas"
|
|
7905
|
+
}
|
|
7906
|
+
});
|
|
7907
|
+
} else if (kindWord === "FUNCTION" || kindWord === "PROCEDURE") {
|
|
7908
|
+
const sig = signatureOf(balanced(rest) ?? "");
|
|
7909
|
+
drops.push({
|
|
7910
|
+
key: `function:${qualified(name)}${sig}`,
|
|
7911
|
+
finding: {
|
|
7912
|
+
object: `function ${shown(name)}${sig}`,
|
|
7913
|
+
detail: "not in supabase/schemas"
|
|
7914
|
+
}
|
|
7915
|
+
});
|
|
7916
|
+
} else {
|
|
7917
|
+
const kind = CREATE_KINDS[kindWord] ?? kindWord.toLowerCase();
|
|
7918
|
+
drops.push({
|
|
7919
|
+
key: `${kind}:${qualified(name)}`,
|
|
7920
|
+
finding: {
|
|
7921
|
+
object: `${kind} ${shown(name)}`,
|
|
7922
|
+
detail: "not in supabase/schemas"
|
|
7923
|
+
}
|
|
7924
|
+
});
|
|
7925
|
+
}
|
|
7926
|
+
continue;
|
|
7927
|
+
}
|
|
7928
|
+
const alterTable = new RegExp(String.raw`^ALTER\s+(?:FOREIGN\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?(?:ONLY\s+)?(${QNAME})\s+(.*)$`, "is").exec(text);
|
|
7929
|
+
if (alterTable) {
|
|
7930
|
+
const table = alterTable[1];
|
|
7931
|
+
const t = qualified(table);
|
|
7932
|
+
for (const action of splitTopLevel(alterTable[2])) classifyTableAction(table, t, action, {
|
|
7933
|
+
creates,
|
|
7934
|
+
drops,
|
|
7935
|
+
conflict
|
|
7936
|
+
});
|
|
7937
|
+
continue;
|
|
7938
|
+
}
|
|
7939
|
+
const alterType = new RegExp(String.raw`^ALTER\s+TYPE\s+(${QNAME})\s+ADD\s+VALUE\b`, "i").exec(text);
|
|
7940
|
+
if (alterType) {
|
|
7941
|
+
creates.push({
|
|
7942
|
+
key: `enumvalue:${statement}`,
|
|
7943
|
+
sql: statement,
|
|
7944
|
+
label: `a value of type ${shown(alterType[1])}`
|
|
7945
|
+
});
|
|
7946
|
+
continue;
|
|
7947
|
+
}
|
|
7948
|
+
conflict(text.slice(0, 80), "a difference devtools does not know how to classify, so it will not touch it");
|
|
7949
|
+
}
|
|
7950
|
+
const renamedCreates = /* @__PURE__ */ new Set();
|
|
7951
|
+
const renamedDrops = /* @__PURE__ */ new Set();
|
|
7952
|
+
for (const c of creates) {
|
|
7953
|
+
if (!c.column) continue;
|
|
7954
|
+
const from = drops.find((d) => {
|
|
7955
|
+
if (!d.column || renamedDrops.has(d)) return false;
|
|
7956
|
+
if (d.column.table !== c.column.table) return false;
|
|
7957
|
+
const type = columnTypes.get(`${d.column.table}.${d.column.name}`);
|
|
7958
|
+
return type !== void 0 && normalizeType(type) === c.column.type;
|
|
7959
|
+
});
|
|
7960
|
+
if (!from) continue;
|
|
7961
|
+
renamedCreates.add(c);
|
|
7962
|
+
renamedDrops.add(from);
|
|
7963
|
+
conflict(c.label, `supabase/schemas has it, and your database has ${from.finding.object} of the same type (${c.column.type}); that looks like a rename, so devtools will not add an empty column beside it. If you renamed it on purpose, rename it in supabase/schemas too; otherwise rename it back`);
|
|
7964
|
+
}
|
|
7965
|
+
const skipped = new Set([...renamedCreates].map((c) => c.key.slice(7)));
|
|
7966
|
+
const liveCreates = creates.filter((c) => !renamedCreates.has(c));
|
|
7967
|
+
const liveDrops = drops.filter((d) => !renamedDrops.has(d));
|
|
7968
|
+
const created = new Set(liveCreates.map((c) => c.key));
|
|
7969
|
+
const dropped = new Set(liveDrops.map((d) => d.key));
|
|
7970
|
+
const restoring = liveCreates.filter((c) => !dropped.has(c.key));
|
|
7971
|
+
for (const d of liveDrops) if (created.has(d.key)) conflict(d.finding.object, "it is defined differently from supabase/schemas");
|
|
7972
|
+
else extras.push(d.finding);
|
|
7973
|
+
const newNames = new Set(restoring.map((c) => c.key).filter((key) => !/^(index|policy|trigger|constraint|rls)/.test(key)).map((key) => key.slice(key.indexOf(":") + 1)));
|
|
7974
|
+
const tail = [];
|
|
7975
|
+
const regranted = /* @__PURE__ */ new Set();
|
|
7976
|
+
const revokes = [];
|
|
7977
|
+
for (const p of privileges) {
|
|
7978
|
+
if (p.targets.length > 0 && p.targets.every((t) => skipped.has(t))) continue;
|
|
7979
|
+
if (p.targets.length > 0 && (newNames.has(p.base) || p.targets.every((t) => newNames.has(t)))) tail.push(p.sql);
|
|
7980
|
+
else if (!p.revoke) {
|
|
7981
|
+
tail.push(p.sql);
|
|
7982
|
+
regranted.add(p.object ?? "privileges");
|
|
7983
|
+
} else revokes.push(p);
|
|
7984
|
+
}
|
|
7985
|
+
for (const p of revokes) {
|
|
7986
|
+
if (p.object && regranted.has(p.object)) continue;
|
|
7987
|
+
extras.push({
|
|
7988
|
+
object: p.object ?? "privileges",
|
|
7989
|
+
detail: p.object ? `granted to ${p.from} beyond (or instead of) what supabase/schemas grants` : "differ from what supabase/schemas grants"
|
|
7990
|
+
});
|
|
7991
|
+
}
|
|
7992
|
+
const restore = [];
|
|
7993
|
+
const restored = [...restoring.map((c) => c.label), ...regranted];
|
|
7994
|
+
if (restoring.length > 0 || tail.length > 0) restore.push(...preamble, ...restoring.map((c) => c.sql), ...tail);
|
|
7995
|
+
return {
|
|
7996
|
+
restore,
|
|
7997
|
+
restored,
|
|
7998
|
+
extras: dedupe(extras),
|
|
7999
|
+
conflicts: [...conflicts].map(([object, why]) => ({
|
|
8000
|
+
object,
|
|
8001
|
+
detail: [...why].join("; ")
|
|
8002
|
+
}))
|
|
8003
|
+
};
|
|
8004
|
+
}
|
|
8005
|
+
function dedupe(findings) {
|
|
8006
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8007
|
+
return findings.filter((f) => {
|
|
8008
|
+
const key = `${f.object}|${f.detail}`;
|
|
8009
|
+
if (seen.has(key)) return false;
|
|
8010
|
+
seen.add(key);
|
|
8011
|
+
return true;
|
|
8012
|
+
});
|
|
8013
|
+
}
|
|
8014
|
+
function classifyTableAction(table, t, action, ctx) {
|
|
8015
|
+
const sql = `ALTER TABLE ${table} ${action}`;
|
|
8016
|
+
let m;
|
|
8017
|
+
if (m = new RegExp(String.raw`^ADD\s+COLUMN\s+(?:IF\s+NOT\s+EXISTS\s+)?(${IDENT})`, "i").exec(action)) {
|
|
8018
|
+
const typeText = action.slice(m[0].length).trim().split(/\s+(?:DEFAULT|NOT\s+NULL|NULL|GENERATED|CONSTRAINT|REFERENCES|CHECK|COLLATE|UNIQUE|PRIMARY)\b/i)[0];
|
|
8019
|
+
ctx.creates.push({
|
|
8020
|
+
key: `column:${t}.${parts(m[1])[0]}`,
|
|
8021
|
+
sql,
|
|
8022
|
+
label: `column ${shown(table)}.${shown(m[1])}`,
|
|
8023
|
+
column: {
|
|
8024
|
+
table: t,
|
|
8025
|
+
name: parts(m[1])[0],
|
|
8026
|
+
type: normalizeType(typeText)
|
|
8027
|
+
}
|
|
8028
|
+
});
|
|
8029
|
+
} else if (m = new RegExp(String.raw`^ADD\s+CONSTRAINT\s+(${IDENT})`, "i").exec(action)) ctx.creates.push({
|
|
8030
|
+
key: `constraint:${t}.${parts(m[1])[0]}`,
|
|
8031
|
+
sql,
|
|
8032
|
+
label: `constraint ${shown(m[1])} on ${shown(table)}`
|
|
8033
|
+
});
|
|
8034
|
+
else if (m = new RegExp(String.raw`^DROP\s+COLUMN\s+(?:IF\s+EXISTS\s+)?(${IDENT})`, "i").exec(action)) ctx.drops.push({
|
|
8035
|
+
key: `column:${t}.${parts(m[1])[0]}`,
|
|
8036
|
+
finding: {
|
|
8037
|
+
object: `column ${shown(table)}.${shown(m[1])}`,
|
|
8038
|
+
detail: "not in supabase/schemas"
|
|
8039
|
+
},
|
|
8040
|
+
column: {
|
|
8041
|
+
table: t,
|
|
8042
|
+
name: parts(m[1])[0]
|
|
8043
|
+
}
|
|
8044
|
+
});
|
|
8045
|
+
else if (m = new RegExp(String.raw`^DROP\s+CONSTRAINT\s+(?:IF\s+EXISTS\s+)?(${IDENT})`, "i").exec(action)) ctx.drops.push({
|
|
8046
|
+
key: `constraint:${t}.${parts(m[1])[0]}`,
|
|
8047
|
+
finding: {
|
|
8048
|
+
object: `constraint ${shown(m[1])} on ${shown(table)}`,
|
|
8049
|
+
detail: "not in supabase/schemas"
|
|
8050
|
+
}
|
|
8051
|
+
});
|
|
8052
|
+
else if (m = new RegExp(String.raw`^ALTER\s+COLUMN\s+(${IDENT})\s+(.*)$`, "i").exec(action)) {
|
|
8053
|
+
const what = m[2];
|
|
8054
|
+
const aspect = /^(?:SET\s+DATA\s+)?TYPE\b/i.test(what) ? "type" : /\bDEFAULT\b/i.test(what) ? "default" : /\bNOT\s+NULL\b/i.test(what) ? "nullability" : "definition";
|
|
8055
|
+
ctx.conflict(`column ${shown(table)}.${shown(m[1])}`, `its ${aspect} differs from supabase/schemas`);
|
|
8056
|
+
} else if (/^(?:ENABLE|FORCE)\s+ROW\s+LEVEL\s+SECURITY\b/i.test(action)) ctx.creates.push({
|
|
8057
|
+
key: `rls:${t}:${action.toUpperCase()}`,
|
|
8058
|
+
sql,
|
|
8059
|
+
label: `row level security on ${shown(table)}`
|
|
8060
|
+
});
|
|
8061
|
+
else ctx.conflict(`table ${shown(table)}`, `a change devtools will not make for you (${action.slice(0, 60)})`);
|
|
8062
|
+
}
|
|
8063
|
+
//#endregion
|
|
7566
8064
|
//#region src/product/sync.ts
|
|
7567
8065
|
/**
|
|
7568
8066
|
* `db sync`: make the contributor's development database match
|
|
@@ -7586,6 +8084,26 @@ function typesAssetName(product, hash) {
|
|
|
7586
8084
|
* Without that, the diff would start from an empty database and write the whole
|
|
7587
8085
|
* schema as one migration, which `db push` rejects as already applied.
|
|
7588
8086
|
*
|
|
8087
|
+
* The record only says what *should* be in the database, so before the diff,
|
|
8088
|
+
* sync compares the live database with it (`supabase db diff --from <database>
|
|
8089
|
+
* --to migrations --use-pg-delta`, limited to the product's schemas) and sorts
|
|
8090
|
+
* every difference (`drift.ts`):
|
|
8091
|
+
*
|
|
8092
|
+
* - missing (a dropped table, schema, column, index, policy, function; a
|
|
8093
|
+
* missing migration ledger): recreated. Only CREATE and ADD statements are
|
|
8094
|
+
* ever run, and the ledger is rewritten from the local record.
|
|
8095
|
+
* - extra (a table, column, index or sequence a member added): listed as a
|
|
8096
|
+
* warning and left alone. `--strict` fails on them instead.
|
|
8097
|
+
* - conflicting (a column's type, a function body, ... defined differently
|
|
8098
|
+
* in the database and the schema files): refused. Nothing is altered
|
|
8099
|
+
* silently; the member edits the schema files or runs `--rebuild`.
|
|
8100
|
+
*
|
|
8101
|
+
* "Matches" is only ever said after that comparison ran. If it cannot run
|
|
8102
|
+
* (the database is unreachable, the password is wrong, the diff tool fails)
|
|
8103
|
+
* the sync fails and says so. A schema change the member pulled is a
|
|
8104
|
+
* different thing: it is the difference between the files and the record, and
|
|
8105
|
+
* keeps the declarative path below, including the `--yes` question for drops.
|
|
8106
|
+
*
|
|
7589
8107
|
* It never resets the database. A reset wipes `auth.custom_oauth_providers`,
|
|
7590
8108
|
* which is where "Sign in with DevDogs" is registered (Q83); `--rebuild` is
|
|
7591
8109
|
* the explicit escape hatch for a database that has drifted from its record,
|
|
@@ -7607,8 +8125,233 @@ function destructiveStatements(sql) {
|
|
|
7607
8125
|
function migrationFiles(dir) {
|
|
7608
8126
|
return existsSync(dir) ? readdirSync(dir).filter((name) => name.endsWith(".sql")).sort() : [];
|
|
7609
8127
|
}
|
|
8128
|
+
const NOTHING = {
|
|
8129
|
+
restored: [],
|
|
8130
|
+
extras: [],
|
|
8131
|
+
compared: false
|
|
8132
|
+
};
|
|
8133
|
+
function recordFile(product) {
|
|
8134
|
+
return join(product.root, ".devtools", "record.json");
|
|
8135
|
+
}
|
|
8136
|
+
function pendingDir(product) {
|
|
8137
|
+
return join(product.root, ".devtools", "pending");
|
|
8138
|
+
}
|
|
8139
|
+
function readRecord(product) {
|
|
8140
|
+
try {
|
|
8141
|
+
const raw = JSON.parse(readFileSync(recordFile(product), "utf8"));
|
|
8142
|
+
return typeof raw.target === "string" && Array.isArray(raw.applied) ? {
|
|
8143
|
+
target: raw.target,
|
|
8144
|
+
applied: raw.applied.map(String)
|
|
8145
|
+
} : null;
|
|
8146
|
+
} catch {
|
|
8147
|
+
return null;
|
|
8148
|
+
}
|
|
8149
|
+
}
|
|
8150
|
+
function writeRecord(product, target, files) {
|
|
8151
|
+
mkdirSync(dirname(recordFile(product)), { recursive: true });
|
|
8152
|
+
const record = {
|
|
8153
|
+
target: targetKey(target),
|
|
8154
|
+
applied: files.map(versionOf)
|
|
8155
|
+
};
|
|
8156
|
+
writeFileSync(recordFile(product), JSON.stringify(record, null, 2) + "\n");
|
|
8157
|
+
}
|
|
8158
|
+
/** `local`, or `hosted:<project ref>` (never the password). */
|
|
8159
|
+
function targetKey(target) {
|
|
8160
|
+
if (target.kind === "local") return "local";
|
|
8161
|
+
try {
|
|
8162
|
+
const url = new URL(target.dbUrl);
|
|
8163
|
+
const user = decodeURIComponent(url.username);
|
|
8164
|
+
return `hosted:${(user.includes(".") ? user.split(".").slice(1).join(".") : "") || url.hostname}`;
|
|
8165
|
+
} catch {
|
|
8166
|
+
return "hosted";
|
|
8167
|
+
}
|
|
8168
|
+
}
|
|
8169
|
+
/** The migrations the target has applied (null: no history table); throws if it cannot be reached. */
|
|
8170
|
+
async function probe(open, target, redact) {
|
|
8171
|
+
let session;
|
|
8172
|
+
try {
|
|
8173
|
+
session = open(target.dbUrl);
|
|
8174
|
+
return await session.appliedMigrations();
|
|
8175
|
+
} catch (err) {
|
|
8176
|
+
throw new SyncError("Could not connect to the database to compare it with supabase/schemas, so nothing was changed, and the database was not checked against supabase/schemas. Fix the connection (is the database running, is the password right?) and run `devtools db sync` again.", redact(err.message));
|
|
8177
|
+
} finally {
|
|
8178
|
+
await session?.close().catch(() => void 0);
|
|
8179
|
+
}
|
|
8180
|
+
}
|
|
8181
|
+
/** Puts back migrations a sync that was killed had set aside. */
|
|
8182
|
+
function restorePending(product) {
|
|
8183
|
+
const dir = pendingDir(product);
|
|
8184
|
+
if (!existsSync(dir)) return;
|
|
8185
|
+
mkdirSync(product.paths.migrationsDir, { recursive: true });
|
|
8186
|
+
for (const file of readdirSync(dir)) {
|
|
8187
|
+
const back = join(product.paths.migrationsDir, file);
|
|
8188
|
+
if (existsSync(back)) rmSync(join(dir, file));
|
|
8189
|
+
else renameSync(join(dir, file), back);
|
|
8190
|
+
}
|
|
8191
|
+
}
|
|
8192
|
+
/** `20261011120000` from `20261011120000_devtools_sync.sql`. */
|
|
8193
|
+
function versionOf(file) {
|
|
8194
|
+
return file.split("_")[0] ?? file;
|
|
8195
|
+
}
|
|
8196
|
+
/** The connection string and its password never reach the terminal. */
|
|
8197
|
+
function redactor(target) {
|
|
8198
|
+
const secrets = /* @__PURE__ */ new Set([target.dbUrl]);
|
|
8199
|
+
try {
|
|
8200
|
+
const url = new URL(target.dbUrl);
|
|
8201
|
+
for (const value of [url.password, decodeURIComponent(url.password)]) if (value) secrets.add(value);
|
|
8202
|
+
} catch {}
|
|
8203
|
+
return (text) => {
|
|
8204
|
+
let out = text;
|
|
8205
|
+
for (const secret of [...secrets].sort((a, b) => b.length - a.length)) out = out.split(secret).join(secret === target.dbUrl ? "<DB_URL>" : "<password>");
|
|
8206
|
+
return out;
|
|
8207
|
+
};
|
|
8208
|
+
}
|
|
8209
|
+
function liveDiffArgs(product, target) {
|
|
8210
|
+
return [
|
|
8211
|
+
"db",
|
|
8212
|
+
"diff",
|
|
8213
|
+
"--from",
|
|
8214
|
+
target.kind === "local" ? "local" : target.dbUrl,
|
|
8215
|
+
"--to",
|
|
8216
|
+
"migrations",
|
|
8217
|
+
"--use-pg-delta",
|
|
8218
|
+
"--schema",
|
|
8219
|
+
product.config.supabase.schemas.join(","),
|
|
8220
|
+
"--output-format",
|
|
8221
|
+
"json"
|
|
8222
|
+
];
|
|
8223
|
+
}
|
|
8224
|
+
/**
|
|
8225
|
+
* The SQL that would turn the live database into what the record describes
|
|
8226
|
+
* (pg-delta, database to migrations). Never applied as is. A diff that cannot
|
|
8227
|
+
* run is an error: there is no fallback to trusting the record.
|
|
8228
|
+
*/
|
|
8229
|
+
async function liveDiff(product, cli, target, run, log, redact) {
|
|
8230
|
+
const args = [...cli.prefix, ...liveDiffArgs(product, target)];
|
|
8231
|
+
let result = await run(cli.command, args, { cwd: product.root });
|
|
8232
|
+
if (result.code !== 0 && isContainerFailure(result)) {
|
|
8233
|
+
log("The shadow database did not start; trying once more.");
|
|
8234
|
+
result = await run(cli.command, args, { cwd: product.root });
|
|
8235
|
+
}
|
|
8236
|
+
const raw = result.stdout + "\n" + result.stderr;
|
|
8237
|
+
let parsed = null;
|
|
8238
|
+
for (const line of result.stdout.split("\n").reverse()) {
|
|
8239
|
+
const text = line.trim();
|
|
8240
|
+
if (!text.startsWith("{")) continue;
|
|
8241
|
+
try {
|
|
8242
|
+
parsed = JSON.parse(text);
|
|
8243
|
+
break;
|
|
8244
|
+
} catch {}
|
|
8245
|
+
}
|
|
8246
|
+
if (result.code === 0 && parsed && typeof parsed.diff === "string") return parsed.diff;
|
|
8247
|
+
const reason = typeof parsed?.error?.message === "string" ? parsed.error.message : result.stderr.trim() || result.stdout.trim();
|
|
8248
|
+
throw new SyncError(isContainerFailure({
|
|
8249
|
+
stdout: "",
|
|
8250
|
+
stderr: raw
|
|
8251
|
+
}) && !parsed?.error ? "Could not compare the database with supabase/schemas: Docker is needed for the shadow database. Start Docker and try again. Nothing was changed,, and the database was not checked against supabase/schemas." : "Could not compare the database with supabase/schemas, so nothing was changed, and the database was not checked against supabase/schemas. Fix the connection (is the database running, is the password right?) and run `devtools db sync` again.", redact(reason));
|
|
8252
|
+
}
|
|
8253
|
+
/** Lists the headline objects; their constraints, policies and the like are a count. */
|
|
8254
|
+
function summarize(labels) {
|
|
8255
|
+
const major = labels.filter((label) => /^(schema|table|function|view|sequence|type|column) /.test(label));
|
|
8256
|
+
if (major.length === 0 || major.length === labels.length) return labels.join(", ");
|
|
8257
|
+
const rest = labels.length - major.length;
|
|
8258
|
+
return `${major.join(", ")} (and ${rest} related ${rest === 1 ? "thing" : "things"}: constraints, policies, indexes)`;
|
|
8259
|
+
}
|
|
8260
|
+
/** How to clean up a database that has things the schema files do not define. */
|
|
8261
|
+
function cleanupHint(target) {
|
|
8262
|
+
return target.kind === "local" ? "To clean up: `devtools db sync --rebuild` (it wipes your development database), then `devtools dev-data` to load the sample data again." : "To clean up: reset the project from its dashboard (Database > Settings), then run `devtools db sync` and `devtools dev-data`.";
|
|
8263
|
+
}
|
|
8264
|
+
function listFindings(findings) {
|
|
8265
|
+
return findings.map((f) => `- ${f.object}: ${f.detail}`).join("\n");
|
|
8266
|
+
}
|
|
8267
|
+
/**
|
|
8268
|
+
* Compares the live database with the record and puts right what is safe.
|
|
8269
|
+
* See the header comment for the three kinds of difference.
|
|
8270
|
+
*/
|
|
8271
|
+
async function reconcileLive(product, cli, target, run, options, log, warn, redact) {
|
|
8272
|
+
const files = migrationFiles(product.paths.migrationsDir);
|
|
8273
|
+
if (!options.open || files.length === 0) return {
|
|
8274
|
+
...NOTHING,
|
|
8275
|
+
pending: []
|
|
8276
|
+
};
|
|
8277
|
+
const { schemas } = product.config.supabase;
|
|
8278
|
+
let session;
|
|
8279
|
+
let ledger;
|
|
8280
|
+
let existing;
|
|
8281
|
+
let columnTypes;
|
|
8282
|
+
try {
|
|
8283
|
+
session = options.open(target.dbUrl);
|
|
8284
|
+
ledger = await session.appliedMigrations();
|
|
8285
|
+
existing = await session.existingObjects(schemas);
|
|
8286
|
+
columnTypes = await session.columnTypes(schemas);
|
|
8287
|
+
} catch (err) {
|
|
8288
|
+
await session?.close().catch(() => void 0);
|
|
8289
|
+
throw new SyncError("Could not connect to the database to compare it with supabase/schemas, so nothing was changed, and the database was not checked against supabase/schemas. Fix the connection (is the database running, is the password right?) and run `devtools db sync` again.", redact(err.message));
|
|
8290
|
+
}
|
|
8291
|
+
const record = readRecord(product);
|
|
8292
|
+
const noted = record?.target === targetKey(target) ? record.applied : null;
|
|
8293
|
+
const known = /* @__PURE__ */ new Set([...ledger ?? [], ...noted ?? []]);
|
|
8294
|
+
const everythingApplied = ledger === null && noted === null;
|
|
8295
|
+
const isApplied = (file) => everythingApplied || known.has(versionOf(file));
|
|
8296
|
+
const appliedFiles = files.filter(isApplied);
|
|
8297
|
+
const pending = files.filter((file) => !isApplied(file));
|
|
8298
|
+
if (appliedFiles.length === 0) {
|
|
8299
|
+
await session.close().catch(() => void 0);
|
|
8300
|
+
return {
|
|
8301
|
+
...NOTHING,
|
|
8302
|
+
pending
|
|
8303
|
+
};
|
|
8304
|
+
}
|
|
8305
|
+
const aside = pendingDir(product);
|
|
8306
|
+
if (pending.length > 0) mkdirSync(aside, { recursive: true });
|
|
8307
|
+
for (const file of pending) renameSync(join(product.paths.migrationsDir, file), join(aside, file));
|
|
8308
|
+
try {
|
|
8309
|
+
const drift = classifyDiff(await liveDiff(product, cli, target, run, log, redact), existing, columnTypes);
|
|
8310
|
+
if (drift.extras.length > 0 && !options.strict) warn(`Your database has ${drift.extras.length} thing${drift.extras.length === 1 ? "" : "s"} that supabase/schemas does not define. They are left as they are:\n${listFindings(drift.extras)}\n${cleanupHint(target)}`);
|
|
8311
|
+
if (drift.conflicts.length > 0) throw new SyncError(`Your database defines ${drift.conflicts.length} thing${drift.conflicts.length === 1 ? "" : "s"} differently from supabase/schemas. devtools will not change them for you, and nothing was changed.`, `${listFindings(drift.conflicts)}\nIf the database is right, edit supabase/schemas to match it. If the schema files are right, ${target.kind === "local" ? "run `devtools db sync --rebuild` (it wipes your development database), then `devtools dev-data`" : "reset the project from its dashboard (Database > Settings), then run `devtools db sync` and `devtools dev-data`"}.`);
|
|
8312
|
+
if (options.strict && drift.extras.length > 0) throw new SyncError(`--strict: your database has ${drift.extras.length} thing${drift.extras.length === 1 ? "" : "s"} that supabase/schemas does not define. Nothing was changed or deleted.`, `${listFindings(drift.extras)}\n${cleanupHint(target)}`);
|
|
8313
|
+
if (drift.restore.length > 0) {
|
|
8314
|
+
if (drift.restored.length > 0) log(`Your database is missing ${summarize(drift.restored)}; creating ${drift.restored.length === 1 ? "it" : "them"} again from supabase/schemas. Nothing that already exists is changed.${drift.restored.some((label) => label.startsWith("privileges on")) ? " For the privileges, what supabase/schemas grants was granted again; privileges beyond that were not taken away." : ""}`);
|
|
8315
|
+
try {
|
|
8316
|
+
await session.exec(drift.restore.join(";\n") + ";");
|
|
8317
|
+
} catch (err) {
|
|
8318
|
+
throw new SyncError("Could not recreate what the database is missing; nothing was changed.", `${redact(err.message)}\n${target.kind === "local" ? "`devtools db sync --rebuild` starts over (it wipes your development database)." : "Reset the project from its dashboard (Database > Settings), then run `devtools db sync`."}`);
|
|
8319
|
+
}
|
|
8320
|
+
}
|
|
8321
|
+
const listed = new Set(ledger ?? []);
|
|
8322
|
+
const unlisted = appliedFiles.map(versionOf).filter((version) => !listed.has(version));
|
|
8323
|
+
if (unlisted.length > 0) {
|
|
8324
|
+
if (ledger === null) log("The database has no migration history table; recreating it from the local record.");
|
|
8325
|
+
const repaired = await run(cli.command, [
|
|
8326
|
+
...cli.prefix,
|
|
8327
|
+
"migration",
|
|
8328
|
+
"repair",
|
|
8329
|
+
"--status",
|
|
8330
|
+
"applied",
|
|
8331
|
+
"--db-url",
|
|
8332
|
+
target.dbUrl,
|
|
8333
|
+
...unlisted
|
|
8334
|
+
], { cwd: product.root });
|
|
8335
|
+
if (repaired.code !== 0) throw new SyncError("Could not record the applied migrations in the database's history.", redact(repaired.stderr || repaired.stdout));
|
|
8336
|
+
}
|
|
8337
|
+
return {
|
|
8338
|
+
restored: drift.restored,
|
|
8339
|
+
extras: drift.extras,
|
|
8340
|
+
compared: true,
|
|
8341
|
+
pending
|
|
8342
|
+
};
|
|
8343
|
+
} finally {
|
|
8344
|
+
for (const file of pending) {
|
|
8345
|
+
const from = join(aside, file);
|
|
8346
|
+
if (existsSync(from)) renameSync(from, join(product.paths.migrationsDir, file));
|
|
8347
|
+
}
|
|
8348
|
+
await session.close().catch(() => void 0);
|
|
8349
|
+
}
|
|
8350
|
+
}
|
|
7610
8351
|
async function dbSync(product, cli, target, run, options) {
|
|
7611
8352
|
const log = options.log ?? (() => void 0);
|
|
8353
|
+
const warn = options.warn ?? log;
|
|
8354
|
+
const redact = redactor(target);
|
|
7612
8355
|
const { migrationsDir, schemasDir } = product.paths;
|
|
7613
8356
|
if (!existsSync(schemasDir)) throw new SyncError(`${product.config.supabase.dir}/schemas does not exist; there is nothing to sync.`);
|
|
7614
8357
|
mkdirSync(migrationsDir, { recursive: true });
|
|
@@ -7626,17 +8369,24 @@ async function dbSync(product, cli, target, run, options) {
|
|
|
7626
8369
|
...cli.prefix,
|
|
7627
8370
|
"migration",
|
|
7628
8371
|
"fetch",
|
|
7629
|
-
"--db-url",
|
|
7630
|
-
target.dbUrl
|
|
8372
|
+
...target.kind === "local" ? ["--local"] : ["--db-url", target.dbUrl]
|
|
7631
8373
|
];
|
|
7632
|
-
const
|
|
8374
|
+
const key = targetKey(target);
|
|
8375
|
+
const owner = readRecord(product)?.target;
|
|
8376
|
+
const foreign = owner !== void 0 && owner !== key && migrationFiles(migrationsDir).length > 0;
|
|
8377
|
+
let needsRecord = migrationFiles(migrationsDir).length === 0 && (target.kind === "hosted" || owner !== void 0 && owner !== key) || foreign;
|
|
7633
8378
|
if (options.dryRun) {
|
|
7634
|
-
if (needsRecord) log(`Would run: ${cliLine(cli, [
|
|
8379
|
+
if (needsRecord) log(`Would run: ${cliLine(cli, target.kind === "local" ? [
|
|
8380
|
+
"migration",
|
|
8381
|
+
"fetch",
|
|
8382
|
+
"--local"
|
|
8383
|
+
] : [
|
|
7635
8384
|
"migration",
|
|
7636
8385
|
"fetch",
|
|
7637
8386
|
"--db-url",
|
|
7638
8387
|
"<DB_URL>"
|
|
7639
|
-
])} (this machine has no record of what the project has had applied)`);
|
|
8388
|
+
])} (this machine has no record of what ${target.kind === "local" ? "the local stack" : "the project"} has had applied)`);
|
|
8389
|
+
if (options.open) log(`Would run: ${cliLine(cli, liveDiffArgs(product, target).map((arg) => arg === target.dbUrl ? "<DB_URL>" : arg))} (and recreate anything the database is missing; extras are only listed)`);
|
|
7640
8390
|
log(`Would run: ${cliLine(cli, generateArgs.slice(cli.prefix.length))}`);
|
|
7641
8391
|
log(target.kind === "local" ? `Would run: ${cliLine(cli, [
|
|
7642
8392
|
"migration",
|
|
@@ -7652,32 +8402,51 @@ async function dbSync(product, cli, target, run, options) {
|
|
|
7652
8402
|
return {
|
|
7653
8403
|
migration: null,
|
|
7654
8404
|
applied: false,
|
|
7655
|
-
destructive: []
|
|
8405
|
+
destructive: [],
|
|
8406
|
+
...NOTHING
|
|
7656
8407
|
};
|
|
7657
8408
|
}
|
|
8409
|
+
restorePending(product);
|
|
8410
|
+
if (!needsRecord && target.kind === "local" && options.open && migrationFiles(migrationsDir).length === 0) needsRecord = (await probe(options.open, target, redact) ?? []).length > 0;
|
|
8411
|
+
if (foreign) {
|
|
8412
|
+
log(`supabase/migrations holds the record of another database (${owner}); this sync is for ${key}, so that record is set aside and this database's own history is used.`);
|
|
8413
|
+
forgetMigrations(product);
|
|
8414
|
+
}
|
|
7658
8415
|
if (needsRecord) {
|
|
7659
8416
|
const fetched = await run(cli.command, fetchArgs, { cwd: product.root });
|
|
7660
|
-
|
|
7661
|
-
|
|
8417
|
+
if (fetched.code !== 0 && options.open) await probe(options.open, target, redact);
|
|
8418
|
+
const restoredFiles = migrationFiles(migrationsDir).length;
|
|
8419
|
+
if (fetched.code === 0 && restoredFiles > 0) {
|
|
8420
|
+
log(`Restored ${restoredFiles} applied migration${restoredFiles === 1 ? "" : "s"} from ${target.kind === "local" ? "the local stack's" : "the project's"} history.`);
|
|
8421
|
+
writeRecord(product, target, migrationFiles(migrationsDir));
|
|
8422
|
+
}
|
|
7662
8423
|
}
|
|
8424
|
+
const { pending, ...live } = await reconcileLive(product, cli, target, run, options, log, warn, redact);
|
|
8425
|
+
const restoredSomething = live.restored.length > 0;
|
|
7663
8426
|
const before = new Set(migrationFiles(migrationsDir));
|
|
7664
8427
|
let generated = await run(cli.command, generateArgs, { cwd: product.root });
|
|
7665
8428
|
if (generated.code !== 0 && isContainerFailure(generated)) {
|
|
7666
8429
|
log("The shadow database did not start; trying once more.");
|
|
7667
8430
|
generated = await run(cli.command, generateArgs, { cwd: product.root });
|
|
7668
8431
|
}
|
|
7669
|
-
if (generated.code !== 0) throw new SyncError(isContainerFailure(generated) ? "Could not diff the schema: Docker is needed for the shadow database. Start Docker and try again." : "Could not diff the schema against what this database has had applied.", generated.stderr || generated.stdout);
|
|
8432
|
+
if (generated.code !== 0) throw new SyncError(isContainerFailure(generated) ? "Could not diff the schema: Docker is needed for the shadow database. Start Docker and try again." : "Could not diff the schema against what this database has had applied.", redact(generated.stderr || generated.stdout));
|
|
7670
8433
|
const created = migrationFiles(migrationsDir).filter((f) => !before.has(f));
|
|
7671
|
-
|
|
7672
|
-
|
|
8434
|
+
const toApply = [...pending, ...created].sort();
|
|
8435
|
+
if (toApply.length === 0) {
|
|
8436
|
+
if (live.compared && !restoredSomething) {
|
|
8437
|
+
const n = live.extras.length;
|
|
8438
|
+
log(n === 0 ? "The database already matches supabase/schemas." : `The database matches supabase/schemas, with ${n} extra thing${n === 1 ? "" : "s"} listed above that it does not define.`);
|
|
8439
|
+
}
|
|
8440
|
+
writeRecord(product, target, migrationFiles(migrationsDir));
|
|
7673
8441
|
return {
|
|
7674
8442
|
migration: null,
|
|
7675
|
-
applied:
|
|
7676
|
-
destructive: []
|
|
8443
|
+
applied: restoredSomething,
|
|
8444
|
+
destructive: [],
|
|
8445
|
+
...live
|
|
7677
8446
|
};
|
|
7678
8447
|
}
|
|
7679
|
-
const migration =
|
|
7680
|
-
const sql = readFileSync(join(migrationsDir,
|
|
8448
|
+
const migration = toApply[toApply.length - 1];
|
|
8449
|
+
const sql = toApply.map((file) => readFileSync(join(migrationsDir, file), "utf8")).join("\n");
|
|
7681
8450
|
const destructive = destructiveStatements(sql);
|
|
7682
8451
|
if (destructive.length > 0 && !options.yes) {
|
|
7683
8452
|
if (!(options.confirmDestructive ? await options.confirmDestructive(sql, destructive) : false)) {
|
|
@@ -7685,7 +8454,7 @@ async function dbSync(product, cli, target, run, options) {
|
|
|
7685
8454
|
throw new SyncError(`The change drops data in your development database (${destructive.length} statement${destructive.length === 1 ? "" : "s"}). Re-run with --yes to allow it.`, destructive.join(";\n"));
|
|
7686
8455
|
}
|
|
7687
8456
|
}
|
|
7688
|
-
log(`Applying ${
|
|
8457
|
+
log(`Applying ${toApply.join(", ")}`);
|
|
7689
8458
|
const applyArgs = target.kind === "local" ? [
|
|
7690
8459
|
...cli.prefix,
|
|
7691
8460
|
"migration",
|
|
@@ -7702,12 +8471,14 @@ async function dbSync(product, cli, target, run, options) {
|
|
|
7702
8471
|
const applied = await run(cli.command, applyArgs, { cwd: product.root });
|
|
7703
8472
|
if (applied.code !== 0) {
|
|
7704
8473
|
for (const file of created) rmSync(join(migrationsDir, file));
|
|
7705
|
-
throw new SyncError("The change did not apply. Your schema files may not be valid SQL, or the database has drifted from what devtools recorded (`devtools db sync --rebuild` starts over).", applied.stderr || applied.stdout);
|
|
8474
|
+
throw new SyncError("The change did not apply. Your schema files may not be valid SQL, or the database has drifted from what devtools recorded (`devtools db sync --rebuild` starts over).", redact(applied.stderr || applied.stdout));
|
|
7706
8475
|
}
|
|
8476
|
+
writeRecord(product, target, migrationFiles(migrationsDir));
|
|
7707
8477
|
return {
|
|
7708
8478
|
migration,
|
|
7709
8479
|
applied: true,
|
|
7710
|
-
destructive
|
|
8480
|
+
destructive,
|
|
8481
|
+
...live
|
|
7711
8482
|
};
|
|
7712
8483
|
}
|
|
7713
8484
|
function isContainerFailure(result) {
|
|
@@ -7717,6 +8488,11 @@ function isContainerFailure(result) {
|
|
|
7717
8488
|
function forgetMigrations(product) {
|
|
7718
8489
|
const files = migrationFiles(product.paths.migrationsDir);
|
|
7719
8490
|
for (const file of files) rmSync(join(product.paths.migrationsDir, file));
|
|
8491
|
+
rmSync(recordFile(product), { force: true });
|
|
8492
|
+
rmSync(pendingDir(product), {
|
|
8493
|
+
recursive: true,
|
|
8494
|
+
force: true
|
|
8495
|
+
});
|
|
7720
8496
|
return files;
|
|
7721
8497
|
}
|
|
7722
8498
|
//#endregion
|
|
@@ -7972,7 +8748,10 @@ async function runSync(product, deps, rest) {
|
|
|
7972
8748
|
yes: hasYes(rest),
|
|
7973
8749
|
confirmDestructive: deps.interactive() ? confirmDestructive : void 0,
|
|
7974
8750
|
dryRun,
|
|
7975
|
-
|
|
8751
|
+
strict: rest.includes("--strict"),
|
|
8752
|
+
log: (line) => log.info(line),
|
|
8753
|
+
warn: (line) => log.warn(line),
|
|
8754
|
+
open: deps.openSql
|
|
7976
8755
|
});
|
|
7977
8756
|
if (rest.includes("--rebuild") && !dryRun) try {
|
|
7978
8757
|
const row = await reRegisterSavedProvider(product.paths.toolingFile, target);
|
|
@@ -7997,7 +8776,7 @@ async function regenerateAfterSync(product, cli, target, result, deps) {
|
|
|
7997
8776
|
log.info(`Would regenerate ${product.config.types.out}.`);
|
|
7998
8777
|
return;
|
|
7999
8778
|
}
|
|
8000
|
-
if (!result.
|
|
8779
|
+
if (!result.applied && typesAreCurrent(product)) return;
|
|
8001
8780
|
try {
|
|
8002
8781
|
await generateTypes(product, cli, target, deps.run, (line) => log.info(line));
|
|
8003
8782
|
log.success(`Regenerated ${product.config.types.out}.`);
|
|
@@ -8043,7 +8822,7 @@ function createProductHandlers(deps = defaultDeps) {
|
|
|
8043
8822
|
switch (sub) {
|
|
8044
8823
|
case "sync": {
|
|
8045
8824
|
const { result, cli, target } = await runSync(product, deps, flags);
|
|
8046
|
-
if (result.
|
|
8825
|
+
if (result.applied) log.success(result.extras.length > 0 ? `Your database now matches supabase/schemas, apart from the ${result.extras.length} extra thing${result.extras.length === 1 ? "" : "s"} listed above.` : "Your database now matches supabase/schemas.");
|
|
8047
8826
|
await regenerateAfterSync(product, cli, target, result, deps);
|
|
8048
8827
|
return DONE;
|
|
8049
8828
|
}
|
|
@@ -8273,9 +9052,12 @@ async function runSetup(product, deps, rest) {
|
|
|
8273
9052
|
const result = await dbSync(product, cli, target, deps.run, {
|
|
8274
9053
|
yes: hasYes(rest),
|
|
8275
9054
|
confirmDestructive: deps.interactive() ? confirmDestructive : void 0,
|
|
8276
|
-
log: (line) => log.info(line)
|
|
9055
|
+
log: (line) => log.info(line),
|
|
9056
|
+
warn: (line) => log.warn(line),
|
|
9057
|
+
open: deps.openSql
|
|
8277
9058
|
});
|
|
8278
|
-
log.success(
|
|
9059
|
+
if (result.applied) log.success("Applied supabase/schemas to your database.");
|
|
9060
|
+
else if (result.compared) log.success("Your database already matched supabase/schemas.");
|
|
8279
9061
|
if (target.kind === "local") await waitForRest(target, deps, product);
|
|
8280
9062
|
if (rest.includes("--download-types")) try {
|
|
8281
9063
|
await downloadTypes(product, {
|
package/dist/launch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as markOutsideCheckout, n as discoverRepoRoot, r as findRepoRoot, t as RepoNotFoundError } from "./root-RaJ8ECCO.js";
|
|
2
2
|
import { b as setDryRun, d as loadEnvLoad, f as loadEnvSession, m as formatCommand, v as isDryRun, y as resolveDryRun } from "./run-D5iI9zve.js";
|
|
3
|
-
import { D as catalog, E as setProductMode, N as setEnvironmentRoot, O as helpPath, S as findProductRoot, c as nonEmpty, f as enterSessionEnvironment, g as errorMessage, h as bail, m as setMenuEnvHook, n as bareGroupStartPath, p as realEnvEntryDeps, y as unwrap } from "./menu-
|
|
3
|
+
import { D as catalog, E as setProductMode, N as setEnvironmentRoot, O as helpPath, S as findProductRoot, c as nonEmpty, f as enterSessionEnvironment, g as errorMessage, h as bail, m as setMenuEnvHook, n as bareGroupStartPath, p as realEnvEntryDeps, y as unwrap } from "./menu-Ctqnvduz.js";
|
|
4
4
|
import { a as isVersionRequest, c as cliName, t as installFailureLog } from "./failure-log-MSdCgvLc.js";
|
|
5
5
|
import { c as lastSentryEventId, f as hasYes, h as stripTierFlag, m as stripNoEnvFlag, n as captureDevtoolsError, o as initDevtoolsTelemetry, p as isNonInteractive } from "./telemetry-BDOwjuoC.js";
|
|
6
6
|
import { confirm, select } from "@clack/prompts";
|
|
@@ -183,7 +183,7 @@ async function startStack() {
|
|
|
183
183
|
* post-tier-resolution path.
|
|
184
184
|
*/
|
|
185
185
|
async function dispatch(argv) {
|
|
186
|
-
const { main } = await import("./cli-
|
|
186
|
+
const { main } = await import("./cli-DfHsw0fp.js");
|
|
187
187
|
try {
|
|
188
188
|
await main(argv);
|
|
189
189
|
} catch (err) {
|
|
@@ -946,7 +946,7 @@ const productDbCommand = {
|
|
|
946
946
|
{
|
|
947
947
|
name: "sync",
|
|
948
948
|
title: "Apply the schema to my database",
|
|
949
|
-
summary: "
|
|
949
|
+
summary: "Compare your database with supabase/schemas: recreate what it is missing, apply schema changes you pulled, warn about extras, and refuse to alter what is defined differently.",
|
|
950
950
|
envFree: true,
|
|
951
951
|
dryRun: "handled",
|
|
952
952
|
options: [
|
|
@@ -955,6 +955,10 @@ const productDbCommand = {
|
|
|
955
955
|
flag: "--rebuild",
|
|
956
956
|
summary: "Reset the local database and start over from the schema, then register sign in again."
|
|
957
957
|
},
|
|
958
|
+
{
|
|
959
|
+
flag: "--strict",
|
|
960
|
+
summary: "Fail (changing nothing) when the database has things supabase/schemas does not define, instead of warning."
|
|
961
|
+
},
|
|
958
962
|
{
|
|
959
963
|
flag: "--yes",
|
|
960
964
|
summary: "Allow changes that drop tables or columns."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devdogsuga/devtools",
|
|
3
|
-
"version": "0.1.51-next.
|
|
3
|
+
"version": "0.1.51-next.552",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Contributor CLI for DevDogs repositories (the platform monorepo, DogDays, DogPack): the real tools with the session's tier filled in, `setup`, `db sync`, `db types`, `dev-data`, a menu over them, the checks CI runs, and the `with-env` launcher. Ships built JS; run via `pnpm devtools` in a product repo, `pnpm dlx @devdogsuga/devtools` (or the `devtools` bin once installed) from inside a clone.",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"eslint": "^9.39.5",
|
|
47
47
|
"tsdown": "^0.23.0",
|
|
48
48
|
"vitest": "^4.1.11",
|
|
49
|
-
"@devdogsuga/cli-core": "0.0.0",
|
|
50
49
|
"@devdogsuga/config": "0.1.4-next.526",
|
|
50
|
+
"@devdogsuga/cli-core": "0.0.0",
|
|
51
51
|
"@devdogsuga/sdk": "0.1.0"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|