@supacloud/cli 0.12.0 → 0.12.2

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 +80 -17
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6455,13 +6455,47 @@ 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 migrationVersionFromFilename(file) {
6461
6466
  const match = basename(file).match(/^(\d{8,20})[_-]/);
6462
- if (match)
6463
- return Number(match[1]);
6464
- return Date.now() * 1000 + fallbackIndex;
6467
+ if (match) {
6468
+ const version = BigInt(match[1]);
6469
+ if (version < 1n || version > MAX_MIGRATION_VERSION) {
6470
+ throw new Error(`Invalid migration version '${match[1]}' in ${basename(file)}: expected 1..${MAX_MIGRATION_VERSION}`);
6471
+ }
6472
+ if (version >= FALLBACK_MIGRATION_VERSION_BASE && version < FALLBACK_MIGRATION_VERSION_LIMIT) {
6473
+ 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`);
6474
+ }
6475
+ return version.toString();
6476
+ }
6477
+ const digest = createHash("sha256").update(basename(file)).digest("hex");
6478
+ const offset = BigInt(`0x${digest}`) % FALLBACK_MIGRATION_VERSION_RANGE;
6479
+ return (FALLBACK_MIGRATION_VERSION_BASE + offset).toString();
6480
+ }
6481
+ function sortMigrationFiles(migrations) {
6482
+ const sorted = [...migrations].sort((a, b) => {
6483
+ const versionA = BigInt(a.version);
6484
+ const versionB = BigInt(b.version);
6485
+ if (versionA < versionB)
6486
+ return -1;
6487
+ if (versionA > versionB)
6488
+ return 1;
6489
+ return a.file.localeCompare(b.file);
6490
+ });
6491
+ for (let i = 1;i < sorted.length; i += 1) {
6492
+ const previous = sorted[i - 1];
6493
+ const current = sorted[i];
6494
+ if (previous.version === current.version && (previous.file !== current.file || previous.name !== current.name)) {
6495
+ throw new Error(`Migration version collision for ${current.version}: ${previous.file} and ${current.file}`);
6496
+ }
6497
+ }
6498
+ return sorted;
6465
6499
  }
6466
6500
  function appliedMigrationKeys(data) {
6467
6501
  const rows = Array.isArray(data) ? data : data?.rows || [];
@@ -6477,6 +6511,30 @@ function appliedMigrationKeys(data) {
6477
6511
  }
6478
6512
  return keys;
6479
6513
  }
6514
+ function migrationRows(data) {
6515
+ const rows = Array.isArray(data) ? data : data?.rows || [];
6516
+ return Array.isArray(rows) ? rows.filter((row) => Boolean(row && typeof row === "object")) : [];
6517
+ }
6518
+ function baselineMigrationKey(version, name) {
6519
+ return `${version}\x00${name}`;
6520
+ }
6521
+ function baselineMigrationKeys(data) {
6522
+ const keys = new Set;
6523
+ for (const row of migrationRows(data)) {
6524
+ if (row.version == null || row.name == null || !Array.isArray(row.statements))
6525
+ continue;
6526
+ if (row.statements.length !== 1 || row.statements[0] !== `baseline:${String(row.name)}`)
6527
+ continue;
6528
+ keys.add(baselineMigrationKey(String(row.version), String(row.name)));
6529
+ }
6530
+ return keys;
6531
+ }
6532
+ function isAlreadyAppliedMigrationResponse(response) {
6533
+ if (response.status !== 409 || !response.data || typeof response.data !== "object")
6534
+ return false;
6535
+ const body = response.data;
6536
+ return body.code === "409" && body.message === "Migration already applied";
6537
+ }
6480
6538
  function sqlReferencesVector(sql) {
6481
6539
  return /\bvector\s*\(\s*\d+\s*\)/i.test(sql) || /::\s*vector\b/i.test(sql) || /\bvector_(cosine|l2|ip)_ops\b/i.test(sql) || /<=>|<#>|<->/.test(sql);
6482
6540
  }
@@ -6689,18 +6747,18 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
6689
6747
  text = `No .sql migration files found in ${dir}`;
6690
6748
  break;
6691
6749
  }
6692
- const migrationFiles = files.map((file, i) => ({
6750
+ const migrationFiles = sortMigrationFiles(files.map((file) => ({
6693
6751
  file,
6694
6752
  name: basename(file, ".sql"),
6695
- version: migrationVersionFromFilename(file, i)
6696
- }));
6753
+ version: migrationVersionFromFilename(file)
6754
+ })));
6755
+ const migrationsResult = await http.get(`/v1/projects/${ref}/database/migrations`);
6756
+ if (!migrationsResult.ok) {
6757
+ text = `❌ Failed to load applied migrations (${migrationsResult.status}): ${JSON.stringify(migrationsResult.data)}`;
6758
+ break;
6759
+ }
6697
6760
  if (args.dry_run) {
6698
- const r = await http.get(`/v1/projects/${ref}/database/migrations`);
6699
- if (!r.ok) {
6700
- text = `❌ Failed to load applied migrations (${r.status}): ${JSON.stringify(r.data)}`;
6701
- break;
6702
- }
6703
- const appliedKeys = appliedMigrationKeys(r.data);
6761
+ const appliedKeys = appliedMigrationKeys(migrationsResult.data);
6704
6762
  const pending = migrationFiles.filter(({ name, version }) => !appliedKeys.has(name) && !appliedKeys.has(String(version)));
6705
6763
  const alreadyApplied = migrationFiles.filter(({ name, version }) => appliedKeys.has(name) || appliedKeys.has(String(version)));
6706
6764
  const pendingWithSql = pending.map((migration) => ({
@@ -6729,12 +6787,17 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
6729
6787
  }
6730
6788
  const applied = [];
6731
6789
  const skipped = [];
6790
+ const baselineKeys = baselineMigrationKeys(migrationsResult.data);
6732
6791
  for (const { file, name, version } of migrationFiles) {
6792
+ if (baselineKeys.has(baselineMigrationKey(version, name))) {
6793
+ skipped.push(file);
6794
+ continue;
6795
+ }
6733
6796
  const sql = readFileSync2(join(dir, file), "utf-8");
6734
6797
  const r = await http.post(`/v1/projects/${ref}/database/migrations`, { name, sql, version });
6735
6798
  if (r.ok) {
6736
6799
  applied.push(file);
6737
- } else if (r.status === 409) {
6800
+ } else if (isAlreadyAppliedMigrationResponse(r)) {
6738
6801
  skipped.push(file);
6739
6802
  } else {
6740
6803
  text = [
@@ -6768,11 +6831,11 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
6768
6831
  text = `No .sql migration files found in ${dir}`;
6769
6832
  break;
6770
6833
  }
6771
- const migrationFiles = files.map((file, i) => ({
6834
+ const migrationFiles = sortMigrationFiles(files.map((file) => ({
6772
6835
  file,
6773
6836
  name: basename(file, ".sql"),
6774
- version: migrationVersionFromFilename(file, i)
6775
- }));
6837
+ version: migrationVersionFromFilename(file)
6838
+ })));
6776
6839
  const r = await http.get(`/v1/projects/${ref}/database/migrations`);
6777
6840
  if (!r.ok) {
6778
6841
  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.0",
3
+ "version": "0.12.2",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",