@supacloud/cli 0.12.1 → 0.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +102 -26
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6455,13 +6455,57 @@ class HttpTransport {
6455
6455
  }
6456
6456
 
6457
6457
  // src/shared/tools/database-tools.ts
6458
+ import { createHash } from "node:crypto";
6458
6459
  import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
6459
6460
  import { basename, join } from "node:path";
6460
- function migrationVersionFromFilename(file, fallbackIndex = 0) {
6461
+ var MAX_MIGRATION_VERSION = 9223372036854775807n;
6462
+ var FALLBACK_MIGRATION_VERSION_BASE = 8000000000000000000n;
6463
+ var FALLBACK_MIGRATION_VERSION_RANGE = 1000000000000000000n;
6464
+ var FALLBACK_MIGRATION_VERSION_LIMIT = FALLBACK_MIGRATION_VERSION_BASE + FALLBACK_MIGRATION_VERSION_RANGE;
6465
+ function readMigrationFile(dir, file) {
6466
+ const rawBytes = readFileSync2(join(dir, file));
6467
+ return {
6468
+ file,
6469
+ name: basename(file, ".sql"),
6470
+ version: migrationVersionFromFilename(file),
6471
+ sql: Buffer.from(rawBytes).toString("utf8"),
6472
+ rawBytes
6473
+ };
6474
+ }
6475
+ function migrationVersionFromFilename(file) {
6461
6476
  const match = basename(file).match(/^(\d{8,20})[_-]/);
6462
- if (match)
6463
- return Number(match[1]);
6464
- return Date.now() * 1000 + fallbackIndex;
6477
+ if (match) {
6478
+ const version = BigInt(match[1]);
6479
+ if (version < 1n || version > MAX_MIGRATION_VERSION) {
6480
+ throw new Error(`Invalid migration version '${match[1]}' in ${basename(file)}: expected 1..${MAX_MIGRATION_VERSION}`);
6481
+ }
6482
+ if (version >= FALLBACK_MIGRATION_VERSION_BASE && version < FALLBACK_MIGRATION_VERSION_LIMIT) {
6483
+ throw new Error(`Invalid migration version '${match[1]}' in ${basename(file)}: version range ${FALLBACK_MIGRATION_VERSION_BASE}..${FALLBACK_MIGRATION_VERSION_LIMIT - 1n} is reserved for non-timestamp migrations`);
6484
+ }
6485
+ return version.toString();
6486
+ }
6487
+ const digest = createHash("sha256").update(basename(file)).digest("hex");
6488
+ const offset = BigInt(`0x${digest}`) % FALLBACK_MIGRATION_VERSION_RANGE;
6489
+ return (FALLBACK_MIGRATION_VERSION_BASE + offset).toString();
6490
+ }
6491
+ function sortMigrationFiles(migrations) {
6492
+ const sorted = [...migrations].sort((a, b) => {
6493
+ const versionA = BigInt(a.version);
6494
+ const versionB = BigInt(b.version);
6495
+ if (versionA < versionB)
6496
+ return -1;
6497
+ if (versionA > versionB)
6498
+ return 1;
6499
+ return a.file.localeCompare(b.file);
6500
+ });
6501
+ for (let i = 1;i < sorted.length; i += 1) {
6502
+ const previous = sorted[i - 1];
6503
+ const current = sorted[i];
6504
+ if (previous.version === current.version && (previous.file !== current.file || previous.name !== current.name)) {
6505
+ throw new Error(`Migration version collision for ${current.version}: ${previous.file} and ${current.file}`);
6506
+ }
6507
+ }
6508
+ return sorted;
6465
6509
  }
6466
6510
  function appliedMigrationKeys(data) {
6467
6511
  const rows = Array.isArray(data) ? data : data?.rows || [];
@@ -6477,6 +6521,43 @@ function appliedMigrationKeys(data) {
6477
6521
  }
6478
6522
  return keys;
6479
6523
  }
6524
+ function migrationRows(data) {
6525
+ const rows = Array.isArray(data) ? data : data?.rows || [];
6526
+ return Array.isArray(rows) ? rows.filter((row) => Boolean(row && typeof row === "object")) : [];
6527
+ }
6528
+ function baselineMigrationKey(version, name) {
6529
+ return `${version}\x00${name}`;
6530
+ }
6531
+ function baselineMigrationKeys(data) {
6532
+ const keys = new Set;
6533
+ for (const row of migrationRows(data)) {
6534
+ if (row.version == null || row.name == null || !Array.isArray(row.statements))
6535
+ continue;
6536
+ if (row.statements.length !== 1 || row.statements[0] !== `baseline:${String(row.name)}`)
6537
+ continue;
6538
+ keys.add(baselineMigrationKey(String(row.version), String(row.name)));
6539
+ }
6540
+ return keys;
6541
+ }
6542
+ function historicalChecksumMarkerKeys(data, migrationFiles) {
6543
+ const filesByKey = new Map(migrationFiles.map((migration) => [baselineMigrationKey(migration.version, migration.name), migration]));
6544
+ const keys = new Set;
6545
+ for (const row of migrationRows(data)) {
6546
+ if (row.version == null || row.name == null || !Array.isArray(row.statements) || row.statements.length !== 1)
6547
+ continue;
6548
+ const marker = row.statements[0];
6549
+ if (typeof marker !== "string" || !/^sha256:[0-9a-f]{64}$/.test(marker))
6550
+ continue;
6551
+ const key = baselineMigrationKey(String(row.version), String(row.name));
6552
+ const migration = filesByKey.get(key);
6553
+ if (!migration)
6554
+ continue;
6555
+ const checksum = createHash("sha256").update(migration.rawBytes).digest("hex");
6556
+ if (marker === `sha256:${checksum}`)
6557
+ keys.add(key);
6558
+ }
6559
+ return keys;
6560
+ }
6480
6561
  function isAlreadyAppliedMigrationResponse(response) {
6481
6562
  if (response.status !== 409 || !response.data || typeof response.data !== "object")
6482
6563
  return false;
@@ -6695,24 +6776,17 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
6695
6776
  text = `No .sql migration files found in ${dir}`;
6696
6777
  break;
6697
6778
  }
6698
- const migrationFiles = files.map((file, i) => ({
6699
- file,
6700
- name: basename(file, ".sql"),
6701
- version: migrationVersionFromFilename(file, i)
6702
- }));
6779
+ const migrationFiles = sortMigrationFiles(files.map((file) => readMigrationFile(dir, file)));
6780
+ const migrationsResult = await http.get(`/v1/projects/${ref}/database/migrations`);
6781
+ if (!migrationsResult.ok) {
6782
+ text = `❌ Failed to load applied migrations (${migrationsResult.status}): ${JSON.stringify(migrationsResult.data)}`;
6783
+ break;
6784
+ }
6703
6785
  if (args.dry_run) {
6704
- const r = await http.get(`/v1/projects/${ref}/database/migrations`);
6705
- if (!r.ok) {
6706
- text = `❌ Failed to load applied migrations (${r.status}): ${JSON.stringify(r.data)}`;
6707
- break;
6708
- }
6709
- const appliedKeys = appliedMigrationKeys(r.data);
6786
+ const appliedKeys = appliedMigrationKeys(migrationsResult.data);
6710
6787
  const pending = migrationFiles.filter(({ name, version }) => !appliedKeys.has(name) && !appliedKeys.has(String(version)));
6711
6788
  const alreadyApplied = migrationFiles.filter(({ name, version }) => appliedKeys.has(name) || appliedKeys.has(String(version)));
6712
- const pendingWithSql = pending.map((migration) => ({
6713
- ...migration,
6714
- sql: readFileSync2(join(dir, migration.file), "utf-8")
6715
- }));
6789
+ const pendingWithSql = pending;
6716
6790
  let vectorEnabled = null;
6717
6791
  if (pendingWithSql.some(({ sql }) => sqlReferencesVector(sql) || sqlCreatesVectorExtension(sql))) {
6718
6792
  const extResult = await execSql("SELECT extname AS name FROM pg_extension WHERE extname = 'vector';");
@@ -6735,8 +6809,14 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
6735
6809
  }
6736
6810
  const applied = [];
6737
6811
  const skipped = [];
6738
- for (const { file, name, version } of migrationFiles) {
6739
- const sql = readFileSync2(join(dir, file), "utf-8");
6812
+ const baselineKeys = baselineMigrationKeys(migrationsResult.data);
6813
+ const historicalChecksumKeys = historicalChecksumMarkerKeys(migrationsResult.data, migrationFiles);
6814
+ for (const { file, name, version, sql } of migrationFiles) {
6815
+ const migrationKey = baselineMigrationKey(version, name);
6816
+ if (baselineKeys.has(migrationKey) || historicalChecksumKeys.has(migrationKey)) {
6817
+ skipped.push(file);
6818
+ continue;
6819
+ }
6740
6820
  const r = await http.post(`/v1/projects/${ref}/database/migrations`, { name, sql, version });
6741
6821
  if (r.ok) {
6742
6822
  applied.push(file);
@@ -6774,11 +6854,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
6774
6854
  text = `No .sql migration files found in ${dir}`;
6775
6855
  break;
6776
6856
  }
6777
- const migrationFiles = files.map((file, i) => ({
6778
- file,
6779
- name: basename(file, ".sql"),
6780
- version: migrationVersionFromFilename(file, i)
6781
- }));
6857
+ const migrationFiles = sortMigrationFiles(files.map((file) => readMigrationFile(dir, file)));
6782
6858
  const r = await http.get(`/v1/projects/${ref}/database/migrations`);
6783
6859
  if (!r.ok) {
6784
6860
  text = `❌ Failed to load applied migrations (${r.status}): ${JSON.stringify(r.data)}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.12.1",
3
+ "version": "0.12.3",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",