@supacloud/cli 0.12.1 → 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 +73 -16
  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,24 @@ 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
+ }
6480
6532
  function isAlreadyAppliedMigrationResponse(response) {
6481
6533
  if (response.status !== 409 || !response.data || typeof response.data !== "object")
6482
6534
  return false;
@@ -6695,18 +6747,18 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
6695
6747
  text = `No .sql migration files found in ${dir}`;
6696
6748
  break;
6697
6749
  }
6698
- const migrationFiles = files.map((file, i) => ({
6750
+ const migrationFiles = sortMigrationFiles(files.map((file) => ({
6699
6751
  file,
6700
6752
  name: basename(file, ".sql"),
6701
- version: migrationVersionFromFilename(file, i)
6702
- }));
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
+ }
6703
6760
  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);
6761
+ const appliedKeys = appliedMigrationKeys(migrationsResult.data);
6710
6762
  const pending = migrationFiles.filter(({ name, version }) => !appliedKeys.has(name) && !appliedKeys.has(String(version)));
6711
6763
  const alreadyApplied = migrationFiles.filter(({ name, version }) => appliedKeys.has(name) || appliedKeys.has(String(version)));
6712
6764
  const pendingWithSql = pending.map((migration) => ({
@@ -6735,7 +6787,12 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
6735
6787
  }
6736
6788
  const applied = [];
6737
6789
  const skipped = [];
6790
+ const baselineKeys = baselineMigrationKeys(migrationsResult.data);
6738
6791
  for (const { file, name, version } of migrationFiles) {
6792
+ if (baselineKeys.has(baselineMigrationKey(version, name))) {
6793
+ skipped.push(file);
6794
+ continue;
6795
+ }
6739
6796
  const sql = readFileSync2(join(dir, file), "utf-8");
6740
6797
  const r = await http.post(`/v1/projects/${ref}/database/migrations`, { name, sql, version });
6741
6798
  if (r.ok) {
@@ -6774,11 +6831,11 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
6774
6831
  text = `No .sql migration files found in ${dir}`;
6775
6832
  break;
6776
6833
  }
6777
- const migrationFiles = files.map((file, i) => ({
6834
+ const migrationFiles = sortMigrationFiles(files.map((file) => ({
6778
6835
  file,
6779
6836
  name: basename(file, ".sql"),
6780
- version: migrationVersionFromFilename(file, i)
6781
- }));
6837
+ version: migrationVersionFromFilename(file)
6838
+ })));
6782
6839
  const r = await http.get(`/v1/projects/${ref}/database/migrations`);
6783
6840
  if (!r.ok) {
6784
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.1",
3
+ "version": "0.12.2",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",