@hasna/cloud 0.1.2 → 0.1.4
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/dist/auto-sync.d.ts +57 -0
- package/dist/auto-sync.d.ts.map +1 -0
- package/dist/cli/index.js +170 -154
- package/dist/cli-helpers.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +723 -63
- package/dist/mcp/index.js +167 -151
- package/dist/mcp-helpers.d.ts.map +1 -1
- package/dist/sync-conflicts.d.ts +76 -0
- package/dist/sync-conflicts.d.ts.map +1 -0
- package/dist/sync-incremental.d.ts +62 -0
- package/dist/sync-incremental.d.ts.map +1 -0
- package/dist/sync-progress.d.ts +68 -0
- package/dist/sync-progress.d.ts.map +1 -0
- package/dist/sync.d.ts +10 -9
- package/dist/sync.d.ts.map +1 -1
- package/package.json +5 -4
package/dist/mcp/index.js
CHANGED
|
@@ -6629,7 +6629,7 @@ var require_arrayParser = __commonJS((exports, module) => {
|
|
|
6629
6629
|
};
|
|
6630
6630
|
});
|
|
6631
6631
|
|
|
6632
|
-
// node_modules/postgres-date/index.js
|
|
6632
|
+
// node_modules/pg-types/node_modules/postgres-date/index.js
|
|
6633
6633
|
var require_postgres_date = __commonJS((exports, module) => {
|
|
6634
6634
|
var DATE_TIME = /(\d{1,})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(\.\d{1,})?.*?( BC)?$/;
|
|
6635
6635
|
var DATE = /^(\d{1,})-(\d{2})-(\d{2})( BC)?$/;
|
|
@@ -6731,7 +6731,7 @@ var require_mutable = __commonJS((exports, module) => {
|
|
|
6731
6731
|
}
|
|
6732
6732
|
});
|
|
6733
6733
|
|
|
6734
|
-
// node_modules/postgres-interval/index.js
|
|
6734
|
+
// node_modules/pg-types/node_modules/postgres-interval/index.js
|
|
6735
6735
|
var require_postgres_interval = __commonJS((exports, module) => {
|
|
6736
6736
|
var extend2 = require_mutable();
|
|
6737
6737
|
module.exports = PostgresInterval;
|
|
@@ -6823,7 +6823,7 @@ var require_postgres_interval = __commonJS((exports, module) => {
|
|
|
6823
6823
|
}
|
|
6824
6824
|
});
|
|
6825
6825
|
|
|
6826
|
-
// node_modules/postgres-bytea/index.js
|
|
6826
|
+
// node_modules/pg-types/node_modules/postgres-bytea/index.js
|
|
6827
6827
|
var require_postgres_bytea = __commonJS((exports, module) => {
|
|
6828
6828
|
var bufferFrom = Buffer.from || Buffer;
|
|
6829
6829
|
module.exports = function parseBytea(input) {
|
|
@@ -24656,17 +24656,88 @@ function createDatabase(options) {
|
|
|
24656
24656
|
}
|
|
24657
24657
|
|
|
24658
24658
|
// src/sync.ts
|
|
24659
|
-
function syncPush(local,
|
|
24660
|
-
|
|
24659
|
+
async function syncPush(local, remote, options) {
|
|
24660
|
+
const orderedTables = await getTableOrder(remote, options.tables);
|
|
24661
|
+
return syncTransfer(local, remote, { ...options, tables: orderedTables }, "push");
|
|
24661
24662
|
}
|
|
24662
|
-
function syncPull(
|
|
24663
|
-
|
|
24663
|
+
async function syncPull(remote, local, options) {
|
|
24664
|
+
const orderedTables = await getTableOrder(remote, options.tables);
|
|
24665
|
+
return syncTransfer(remote, local, { ...options, tables: orderedTables }, "pull");
|
|
24664
24666
|
}
|
|
24665
|
-
function
|
|
24667
|
+
async function getTableOrder(remote, tables) {
|
|
24668
|
+
if (tables.length <= 1)
|
|
24669
|
+
return tables;
|
|
24670
|
+
try {
|
|
24671
|
+
const fks = await remote.all(`
|
|
24672
|
+
SELECT DISTINCT
|
|
24673
|
+
tc.table_name AS source_table,
|
|
24674
|
+
ccu.table_name AS referenced_table
|
|
24675
|
+
FROM information_schema.table_constraints tc
|
|
24676
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
24677
|
+
ON tc.constraint_name = ccu.constraint_name
|
|
24678
|
+
AND tc.table_schema = ccu.table_schema
|
|
24679
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
24680
|
+
AND tc.table_schema = 'public'
|
|
24681
|
+
`);
|
|
24682
|
+
if (fks.length > 0) {
|
|
24683
|
+
return topoSort(tables, fks);
|
|
24684
|
+
}
|
|
24685
|
+
} catch {}
|
|
24686
|
+
return heuristicOrder(tables);
|
|
24687
|
+
}
|
|
24688
|
+
function topoSort(tables, fks) {
|
|
24689
|
+
const tableSet = new Set(tables);
|
|
24690
|
+
const deps = new Map;
|
|
24691
|
+
for (const t of tables) {
|
|
24692
|
+
deps.set(t, new Set);
|
|
24693
|
+
}
|
|
24694
|
+
for (const fk of fks) {
|
|
24695
|
+
if (tableSet.has(fk.source_table) && tableSet.has(fk.referenced_table)) {
|
|
24696
|
+
deps.get(fk.source_table).add(fk.referenced_table);
|
|
24697
|
+
}
|
|
24698
|
+
}
|
|
24699
|
+
const sorted = [];
|
|
24700
|
+
const visited = new Set;
|
|
24701
|
+
const visiting = new Set;
|
|
24702
|
+
function visit(table) {
|
|
24703
|
+
if (visited.has(table))
|
|
24704
|
+
return;
|
|
24705
|
+
if (visiting.has(table)) {
|
|
24706
|
+
sorted.push(table);
|
|
24707
|
+
visited.add(table);
|
|
24708
|
+
return;
|
|
24709
|
+
}
|
|
24710
|
+
visiting.add(table);
|
|
24711
|
+
const tableDeps = deps.get(table) ?? new Set;
|
|
24712
|
+
for (const dep of tableDeps) {
|
|
24713
|
+
visit(dep);
|
|
24714
|
+
}
|
|
24715
|
+
visiting.delete(table);
|
|
24716
|
+
visited.add(table);
|
|
24717
|
+
sorted.push(table);
|
|
24718
|
+
}
|
|
24719
|
+
for (const t of tables) {
|
|
24720
|
+
visit(t);
|
|
24721
|
+
}
|
|
24722
|
+
return sorted;
|
|
24723
|
+
}
|
|
24724
|
+
function heuristicOrder(tables) {
|
|
24725
|
+
const sorted = [...tables].sort((a, b) => {
|
|
24726
|
+
const aIsChild = a.includes("_") && tables.some((t) => a.startsWith(t + "_") || a.endsWith("_" + t));
|
|
24727
|
+
const bIsChild = b.includes("_") && tables.some((t) => b.startsWith(t + "_") || b.endsWith("_" + t));
|
|
24728
|
+
if (aIsChild && !bIsChild)
|
|
24729
|
+
return 1;
|
|
24730
|
+
if (!aIsChild && bIsChild)
|
|
24731
|
+
return -1;
|
|
24732
|
+
return a.localeCompare(b);
|
|
24733
|
+
});
|
|
24734
|
+
return sorted;
|
|
24735
|
+
}
|
|
24736
|
+
async function syncTransfer(source, target, options, _direction) {
|
|
24666
24737
|
const {
|
|
24667
24738
|
tables,
|
|
24668
24739
|
onProgress,
|
|
24669
|
-
batchSize =
|
|
24740
|
+
batchSize = 100,
|
|
24670
24741
|
conflictColumn = "updated_at",
|
|
24671
24742
|
primaryKey = "id"
|
|
24672
24743
|
} = options;
|
|
@@ -24689,7 +24760,7 @@ function syncTransfer(source, target, options, _direction) {
|
|
|
24689
24760
|
totalTables: tables.length,
|
|
24690
24761
|
currentTableIndex: i
|
|
24691
24762
|
});
|
|
24692
|
-
const rows = source
|
|
24763
|
+
const rows = await readAll(source, `SELECT * FROM "${table}"`);
|
|
24693
24764
|
result.rowsRead = rows.length;
|
|
24694
24765
|
if (rows.length === 0) {
|
|
24695
24766
|
onProgress?.({
|
|
@@ -24704,7 +24775,6 @@ function syncTransfer(source, target, options, _direction) {
|
|
|
24704
24775
|
continue;
|
|
24705
24776
|
}
|
|
24706
24777
|
const columns = Object.keys(rows[0]);
|
|
24707
|
-
const hasConflictCol = columns.includes(conflictColumn);
|
|
24708
24778
|
const hasPrimaryKey = columns.includes(primaryKey);
|
|
24709
24779
|
if (!hasPrimaryKey) {
|
|
24710
24780
|
result.errors.push(`Table "${table}" has no "${primaryKey}" column — skipping`);
|
|
@@ -24719,34 +24789,18 @@ function syncTransfer(source, target, options, _direction) {
|
|
|
24719
24789
|
totalTables: tables.length,
|
|
24720
24790
|
currentTableIndex: i
|
|
24721
24791
|
});
|
|
24792
|
+
const updateCols = columns.filter((c) => c !== primaryKey);
|
|
24722
24793
|
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
24723
24794
|
const batch = rows.slice(offset, offset + batchSize);
|
|
24724
|
-
|
|
24725
|
-
|
|
24726
|
-
|
|
24727
|
-
|
|
24728
|
-
|
|
24729
|
-
const existingTime = new Date(existing[conflictColumn]).getTime();
|
|
24730
|
-
const incomingTime = new Date(row[conflictColumn]).getTime();
|
|
24731
|
-
if (existingTime >= incomingTime) {
|
|
24732
|
-
result.rowsSkipped++;
|
|
24733
|
-
continue;
|
|
24734
|
-
}
|
|
24735
|
-
}
|
|
24736
|
-
const setClauses = columns.filter((c) => c !== primaryKey).map((c) => `"${c}" = ?`).join(", ");
|
|
24737
|
-
const values = columns.filter((c) => c !== primaryKey).map((c) => row[c]);
|
|
24738
|
-
values.push(row[primaryKey]);
|
|
24739
|
-
target.run(`UPDATE "${table}" SET ${setClauses} WHERE "${primaryKey}" = ?`, ...values);
|
|
24740
|
-
} else {
|
|
24741
|
-
const placeholders = columns.map(() => "?").join(", ");
|
|
24742
|
-
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
24743
|
-
const values = columns.map((c) => row[c]);
|
|
24744
|
-
target.run(`INSERT INTO "${table}" (${colList}) VALUES (${placeholders})`, ...values);
|
|
24745
|
-
}
|
|
24746
|
-
result.rowsWritten++;
|
|
24747
|
-
} catch (err) {
|
|
24748
|
-
result.errors.push(`Row ${row[primaryKey]}: ${err?.message ?? String(err)}`);
|
|
24795
|
+
try {
|
|
24796
|
+
if (isAsyncAdapter(target)) {
|
|
24797
|
+
await batchUpsertPg(target, table, columns, updateCols, primaryKey, batch);
|
|
24798
|
+
} else {
|
|
24799
|
+
batchUpsertSqlite(target, table, columns, updateCols, primaryKey, batch);
|
|
24749
24800
|
}
|
|
24801
|
+
result.rowsWritten += batch.length;
|
|
24802
|
+
} catch (err) {
|
|
24803
|
+
result.errors.push(`Batch at offset ${offset}: ${err?.message ?? String(err)}`);
|
|
24750
24804
|
}
|
|
24751
24805
|
onProgress?.({
|
|
24752
24806
|
table,
|
|
@@ -24772,10 +24826,46 @@ function syncTransfer(source, target, options, _direction) {
|
|
|
24772
24826
|
}
|
|
24773
24827
|
return results;
|
|
24774
24828
|
}
|
|
24829
|
+
async function batchUpsertPg(target, table, columns, updateCols, primaryKey, batch) {
|
|
24830
|
+
if (batch.length === 0)
|
|
24831
|
+
return;
|
|
24832
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
24833
|
+
const valuePlaceholders = batch.map((_, rowIdx) => {
|
|
24834
|
+
const offset = rowIdx * columns.length;
|
|
24835
|
+
return `(${columns.map((_2, colIdx) => `$${offset + colIdx + 1}`).join(", ")})`;
|
|
24836
|
+
}).join(", ");
|
|
24837
|
+
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKey}" = EXCLUDED."${primaryKey}"`;
|
|
24838
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
24839
|
+
ON CONFLICT ("${primaryKey}") DO UPDATE SET ${setClause}`;
|
|
24840
|
+
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
24841
|
+
await target.run(sql, ...params);
|
|
24842
|
+
}
|
|
24843
|
+
function batchUpsertSqlite(target, table, columns, updateCols, primaryKey, batch) {
|
|
24844
|
+
if (batch.length === 0)
|
|
24845
|
+
return;
|
|
24846
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
24847
|
+
const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
24848
|
+
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKey}" = EXCLUDED."${primaryKey}"`;
|
|
24849
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
24850
|
+
ON CONFLICT ("${primaryKey}") DO UPDATE SET ${setClause}`;
|
|
24851
|
+
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
24852
|
+
target.run(sql, ...params);
|
|
24853
|
+
}
|
|
24854
|
+
function isAsyncAdapter(adapter) {
|
|
24855
|
+
return adapter.constructor.name === "PgAdapterAsync" || typeof adapter.raw?.connect === "function";
|
|
24856
|
+
}
|
|
24857
|
+
async function readAll(adapter, sql) {
|
|
24858
|
+
const result = adapter.all(sql);
|
|
24859
|
+
return result instanceof Promise ? await result : result;
|
|
24860
|
+
}
|
|
24775
24861
|
function listSqliteTables(db) {
|
|
24776
24862
|
const rows = db.all(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`);
|
|
24777
24863
|
return rows.map((r) => r.name);
|
|
24778
24864
|
}
|
|
24865
|
+
async function listPgTables(db) {
|
|
24866
|
+
const rows = await db.all(`SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename`);
|
|
24867
|
+
return rows.map((r) => r.tablename);
|
|
24868
|
+
}
|
|
24779
24869
|
|
|
24780
24870
|
// src/feedback.ts
|
|
24781
24871
|
import { hostname as hostname2 } from "os";
|
|
@@ -24951,10 +25041,8 @@ class SqliteAdapter2 {
|
|
|
24951
25041
|
return this.db;
|
|
24952
25042
|
}
|
|
24953
25043
|
}
|
|
24954
|
-
|
|
24955
|
-
class PgAdapter2 {
|
|
25044
|
+
class PgAdapterAsync {
|
|
24956
25045
|
pool;
|
|
24957
|
-
_client = null;
|
|
24958
25046
|
constructor(arg) {
|
|
24959
25047
|
if (typeof arg === "string") {
|
|
24960
25048
|
this.pool = new esm_default.Pool({ connectionString: arg });
|
|
@@ -24962,118 +25050,47 @@ class PgAdapter2 {
|
|
|
24962
25050
|
this.pool = arg;
|
|
24963
25051
|
}
|
|
24964
25052
|
}
|
|
24965
|
-
|
|
24966
|
-
let result;
|
|
24967
|
-
let error2;
|
|
24968
|
-
let done = false;
|
|
24969
|
-
fn().then((r) => {
|
|
24970
|
-
result = r;
|
|
24971
|
-
done = true;
|
|
24972
|
-
}).catch((e) => {
|
|
24973
|
-
error2 = e;
|
|
24974
|
-
done = true;
|
|
24975
|
-
});
|
|
24976
|
-
const deadline = Date.now() + 30000;
|
|
24977
|
-
while (!done && Date.now() < deadline) {
|
|
24978
|
-
Bun.sleepSync(1);
|
|
24979
|
-
}
|
|
24980
|
-
if (error2)
|
|
24981
|
-
throw error2;
|
|
24982
|
-
if (!done)
|
|
24983
|
-
throw new Error("PgAdapter: query timed out (30s)");
|
|
24984
|
-
return result;
|
|
24985
|
-
}
|
|
24986
|
-
run(sql, ...params) {
|
|
25053
|
+
async run(sql, ...params) {
|
|
24987
25054
|
const pgSql = translateSql(sql, "pg");
|
|
24988
25055
|
const pgParams = translateParams(params);
|
|
24989
|
-
|
|
24990
|
-
|
|
24991
|
-
|
|
24992
|
-
|
|
24993
|
-
|
|
24994
|
-
};
|
|
24995
|
-
});
|
|
25056
|
+
const res = await this.pool.query(pgSql, pgParams);
|
|
25057
|
+
return {
|
|
25058
|
+
changes: res.rowCount ?? 0,
|
|
25059
|
+
lastInsertRowid: res.rows?.[0]?.id ?? 0
|
|
25060
|
+
};
|
|
24996
25061
|
}
|
|
24997
|
-
get(sql, ...params) {
|
|
25062
|
+
async get(sql, ...params) {
|
|
24998
25063
|
const pgSql = translateSql(sql, "pg");
|
|
24999
25064
|
const pgParams = translateParams(params);
|
|
25000
|
-
|
|
25001
|
-
|
|
25002
|
-
return res.rows[0] ?? null;
|
|
25003
|
-
});
|
|
25065
|
+
const res = await this.pool.query(pgSql, pgParams);
|
|
25066
|
+
return res.rows[0] ?? null;
|
|
25004
25067
|
}
|
|
25005
|
-
all(sql, ...params) {
|
|
25068
|
+
async all(sql, ...params) {
|
|
25006
25069
|
const pgSql = translateSql(sql, "pg");
|
|
25007
25070
|
const pgParams = translateParams(params);
|
|
25008
|
-
|
|
25009
|
-
|
|
25010
|
-
return res.rows;
|
|
25011
|
-
});
|
|
25071
|
+
const res = await this.pool.query(pgSql, pgParams);
|
|
25072
|
+
return res.rows;
|
|
25012
25073
|
}
|
|
25013
|
-
exec(sql) {
|
|
25074
|
+
async exec(sql) {
|
|
25014
25075
|
const pgSql = translateSql(sql, "pg");
|
|
25015
|
-
this.
|
|
25016
|
-
await this.pool.query(pgSql);
|
|
25017
|
-
});
|
|
25076
|
+
await this.pool.query(pgSql);
|
|
25018
25077
|
}
|
|
25019
|
-
|
|
25020
|
-
|
|
25021
|
-
const adapter = this;
|
|
25022
|
-
return {
|
|
25023
|
-
run(...params) {
|
|
25024
|
-
const pgParams = translateParams(params);
|
|
25025
|
-
return adapter.runSync(async () => {
|
|
25026
|
-
const res = await adapter.pool.query(pgSql, pgParams);
|
|
25027
|
-
return {
|
|
25028
|
-
changes: res.rowCount ?? 0,
|
|
25029
|
-
lastInsertRowid: res.rows?.[0]?.id ?? 0
|
|
25030
|
-
};
|
|
25031
|
-
});
|
|
25032
|
-
},
|
|
25033
|
-
get(...params) {
|
|
25034
|
-
const pgParams = translateParams(params);
|
|
25035
|
-
return adapter.runSync(async () => {
|
|
25036
|
-
const res = await adapter.pool.query(pgSql, pgParams);
|
|
25037
|
-
return res.rows[0] ?? null;
|
|
25038
|
-
});
|
|
25039
|
-
},
|
|
25040
|
-
all(...params) {
|
|
25041
|
-
const pgParams = translateParams(params);
|
|
25042
|
-
return adapter.runSync(async () => {
|
|
25043
|
-
const res = await adapter.pool.query(pgSql, pgParams);
|
|
25044
|
-
return res.rows;
|
|
25045
|
-
});
|
|
25046
|
-
},
|
|
25047
|
-
finalize() {}
|
|
25048
|
-
};
|
|
25049
|
-
}
|
|
25050
|
-
close() {
|
|
25051
|
-
this.runSync(async () => {
|
|
25052
|
-
await this.pool.end();
|
|
25053
|
-
});
|
|
25078
|
+
async close() {
|
|
25079
|
+
await this.pool.end();
|
|
25054
25080
|
}
|
|
25055
|
-
transaction(fn) {
|
|
25056
|
-
|
|
25057
|
-
|
|
25058
|
-
|
|
25059
|
-
|
|
25060
|
-
|
|
25061
|
-
|
|
25062
|
-
|
|
25063
|
-
|
|
25064
|
-
|
|
25065
|
-
|
|
25066
|
-
|
|
25067
|
-
|
|
25068
|
-
await client.query("COMMIT");
|
|
25069
|
-
return result;
|
|
25070
|
-
} catch (err) {
|
|
25071
|
-
await client.query("ROLLBACK");
|
|
25072
|
-
throw err;
|
|
25073
|
-
} finally {
|
|
25074
|
-
client.release();
|
|
25075
|
-
}
|
|
25076
|
-
});
|
|
25081
|
+
async transaction(fn) {
|
|
25082
|
+
const client = await this.pool.connect();
|
|
25083
|
+
try {
|
|
25084
|
+
await client.query("BEGIN");
|
|
25085
|
+
const result = await fn(client);
|
|
25086
|
+
await client.query("COMMIT");
|
|
25087
|
+
return result;
|
|
25088
|
+
} catch (err) {
|
|
25089
|
+
await client.query("ROLLBACK");
|
|
25090
|
+
throw err;
|
|
25091
|
+
} finally {
|
|
25092
|
+
client.release();
|
|
25093
|
+
}
|
|
25077
25094
|
}
|
|
25078
25095
|
get raw() {
|
|
25079
25096
|
return this.pool;
|
|
@@ -25098,10 +25115,10 @@ server.tool("cloud_status", "Show cloud configuration and connection health", {}
|
|
|
25098
25115
|
if (config2.rds.host && config2.rds.username) {
|
|
25099
25116
|
try {
|
|
25100
25117
|
const connStr = getConnectionString("postgres");
|
|
25101
|
-
const pg2 = new
|
|
25102
|
-
pg2.get("SELECT 1 as ok");
|
|
25118
|
+
const pg2 = new PgAdapterAsync(connStr);
|
|
25119
|
+
await pg2.get("SELECT 1 as ok");
|
|
25103
25120
|
lines.push("PostgreSQL: connected");
|
|
25104
|
-
pg2.close();
|
|
25121
|
+
await pg2.close();
|
|
25105
25122
|
} catch (err) {
|
|
25106
25123
|
lines.push(`PostgreSQL: connection failed \u2014 ${err?.message}`);
|
|
25107
25124
|
}
|
|
@@ -25128,16 +25145,16 @@ server.tool("sync_push", "Push local SQLite data to cloud PostgreSQL", {
|
|
|
25128
25145
|
const dbPath = getDbPath2(service);
|
|
25129
25146
|
const local = new SqliteAdapter2(dbPath);
|
|
25130
25147
|
const connStr = getConnectionString(service);
|
|
25131
|
-
const cloud = new
|
|
25148
|
+
const cloud = new PgAdapterAsync(connStr);
|
|
25132
25149
|
let tableList;
|
|
25133
25150
|
if (tablesStr) {
|
|
25134
25151
|
tableList = tablesStr.split(",").map((t) => t.trim());
|
|
25135
25152
|
} else {
|
|
25136
25153
|
tableList = listSqliteTables(local);
|
|
25137
25154
|
}
|
|
25138
|
-
const results = syncPush(local, cloud, { tables: tableList });
|
|
25155
|
+
const results = await syncPush(local, cloud, { tables: tableList });
|
|
25139
25156
|
local.close();
|
|
25140
|
-
cloud.close();
|
|
25157
|
+
await cloud.close();
|
|
25141
25158
|
const totalWritten = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
25142
25159
|
const totalErrors = results.reduce((s, r) => s + r.errors.length, 0);
|
|
25143
25160
|
const lines = [
|
|
@@ -25171,17 +25188,16 @@ server.tool("sync_pull", "Pull cloud PostgreSQL data to local SQLite", {
|
|
|
25171
25188
|
const dbPath = getDbPath2(service);
|
|
25172
25189
|
const local = new SqliteAdapter2(dbPath);
|
|
25173
25190
|
const connStr = getConnectionString(service);
|
|
25174
|
-
const cloud = new
|
|
25191
|
+
const cloud = new PgAdapterAsync(connStr);
|
|
25175
25192
|
let tableList;
|
|
25176
25193
|
if (tablesStr) {
|
|
25177
25194
|
tableList = tablesStr.split(",").map((t) => t.trim());
|
|
25178
25195
|
} else {
|
|
25179
25196
|
try {
|
|
25180
|
-
|
|
25181
|
-
tableList = rows.map((r) => r.tablename);
|
|
25197
|
+
tableList = await listPgTables(cloud);
|
|
25182
25198
|
} catch {
|
|
25183
25199
|
local.close();
|
|
25184
|
-
cloud.close();
|
|
25200
|
+
await cloud.close();
|
|
25185
25201
|
return {
|
|
25186
25202
|
content: [
|
|
25187
25203
|
{ type: "text", text: "Error: failed to list tables from cloud." }
|
|
@@ -25190,9 +25206,9 @@ server.tool("sync_pull", "Pull cloud PostgreSQL data to local SQLite", {
|
|
|
25190
25206
|
};
|
|
25191
25207
|
}
|
|
25192
25208
|
}
|
|
25193
|
-
const results = syncPull(
|
|
25209
|
+
const results = await syncPull(cloud, local, { tables: tableList });
|
|
25194
25210
|
local.close();
|
|
25195
|
-
cloud.close();
|
|
25211
|
+
await cloud.close();
|
|
25196
25212
|
const totalWritten = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
25197
25213
|
const totalErrors = results.reduce((s, r) => s + r.errors.length, 0);
|
|
25198
25214
|
const lines = [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-helpers.d.ts","sourceRoot":"","sources":["../src/mcp-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAYzE;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,EACjB,WAAW,EAAE,MAAM,GAClB,IAAI,
|
|
1
|
+
{"version":3,"file":"mcp-helpers.d.ts","sourceRoot":"","sources":["../src/mcp-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAYzE;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,SAAS,EACjB,WAAW,EAAE,MAAM,GAClB,IAAI,CAoJN"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { DbAdapter } from "./adapter.js";
|
|
2
|
+
export interface SyncConflict {
|
|
3
|
+
table: string;
|
|
4
|
+
row_id: string;
|
|
5
|
+
local_updated_at: string;
|
|
6
|
+
remote_updated_at: string;
|
|
7
|
+
local_data: Record<string, any>;
|
|
8
|
+
remote_data: Record<string, any>;
|
|
9
|
+
resolved: boolean;
|
|
10
|
+
resolution?: "local-wins" | "remote-wins" | "newest-wins" | "manual";
|
|
11
|
+
}
|
|
12
|
+
export type ConflictStrategy = "local-wins" | "remote-wins" | "newest-wins";
|
|
13
|
+
export interface StoredConflict {
|
|
14
|
+
id: string;
|
|
15
|
+
table_name: string;
|
|
16
|
+
row_id: string;
|
|
17
|
+
local_data: string;
|
|
18
|
+
remote_data: string;
|
|
19
|
+
local_updated_at: string;
|
|
20
|
+
remote_updated_at: string;
|
|
21
|
+
resolution: string | null;
|
|
22
|
+
resolved_at: string | null;
|
|
23
|
+
created_at: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Find rows that exist in BOTH local and remote datasets with DIFFERENT
|
|
27
|
+
* `updated_at` values. Returns a list of SyncConflict objects.
|
|
28
|
+
*
|
|
29
|
+
* @param local - Array of rows from the local database
|
|
30
|
+
* @param remote - Array of rows from the remote database
|
|
31
|
+
* @param table - The table name these rows belong to
|
|
32
|
+
* @param primaryKey - Column used as primary key (default: "id")
|
|
33
|
+
* @param conflictColumn - Column used for timestamp comparison (default: "updated_at")
|
|
34
|
+
*/
|
|
35
|
+
export declare function detectConflicts(local: Record<string, any>[], remote: Record<string, any>[], table: string, primaryKey?: string, conflictColumn?: string): SyncConflict[];
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a list of conflicts using the given strategy.
|
|
38
|
+
* Returns the winning row data for each conflict.
|
|
39
|
+
*
|
|
40
|
+
* @param conflicts - The conflicts to resolve
|
|
41
|
+
* @param strategy - Resolution strategy (default: "newest-wins")
|
|
42
|
+
* @returns Array of resolved conflicts with `resolved: true` and `resolution` set
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveConflicts(conflicts: SyncConflict[], strategy?: ConflictStrategy): SyncConflict[];
|
|
45
|
+
/**
|
|
46
|
+
* Get the winning data for a resolved conflict.
|
|
47
|
+
*/
|
|
48
|
+
export declare function getWinningData(conflict: SyncConflict): Record<string, any>;
|
|
49
|
+
/**
|
|
50
|
+
* Ensure the _sync_conflicts table exists.
|
|
51
|
+
*/
|
|
52
|
+
export declare function ensureConflictsTable(db: DbAdapter): void;
|
|
53
|
+
/**
|
|
54
|
+
* Store unresolved conflicts in the database for later review.
|
|
55
|
+
*/
|
|
56
|
+
export declare function storeConflicts(db: DbAdapter, conflicts: SyncConflict[]): void;
|
|
57
|
+
/**
|
|
58
|
+
* List all stored conflicts, optionally filtered by resolved status.
|
|
59
|
+
*/
|
|
60
|
+
export declare function listConflicts(db: DbAdapter, opts?: {
|
|
61
|
+
resolved?: boolean;
|
|
62
|
+
table?: string;
|
|
63
|
+
}): StoredConflict[];
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a stored conflict by ID using the given strategy.
|
|
66
|
+
*/
|
|
67
|
+
export declare function resolveConflict(db: DbAdapter, conflictId: string, strategy: ConflictStrategy | "manual"): StoredConflict | null;
|
|
68
|
+
/**
|
|
69
|
+
* Get a single stored conflict by ID.
|
|
70
|
+
*/
|
|
71
|
+
export declare function getConflict(db: DbAdapter, conflictId: string): StoredConflict | null;
|
|
72
|
+
/**
|
|
73
|
+
* Delete all resolved conflicts (cleanup).
|
|
74
|
+
*/
|
|
75
|
+
export declare function purgeResolvedConflicts(db: DbAdapter): number;
|
|
76
|
+
//# sourceMappingURL=sync-conflicts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-conflicts.d.ts","sourceRoot":"","sources":["../src/sync-conflicts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAM9C,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAChC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,YAAY,GAAG,aAAa,GAAG,aAAa,GAAG,QAAQ,CAAC;CACtE;AAED,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,aAAa,GAAG,aAAa,CAAC;AAE5E,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;CACpB;AAMD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EAC7B,KAAK,EAAE,MAAM,EACb,UAAU,SAAO,EACjB,cAAc,SAAe,GAC5B,YAAY,EAAE,CAkChB;AAMD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,YAAY,EAAE,EACzB,QAAQ,GAAE,gBAAgC,GACzC,YAAY,EAAE,CA4BhB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAyB1E;AAMD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI,CAexD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,GAAG,IAAI,CAmB7E;AAMD;;GAEG;AACH,wBAAgB,aAAa,CAC3B,EAAE,EAAE,SAAS,EACb,IAAI,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5C,cAAc,EAAE,CAsBlB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,EAAE,EAAE,SAAS,EACb,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,gBAAgB,GAAG,QAAQ,GACpC,cAAc,GAAG,IAAI,CAoBvB;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAMpF;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,SAAS,GAAG,MAAM,CAM5D"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { DbAdapter } from "./adapter.js";
|
|
2
|
+
export interface IncrementalSyncStats {
|
|
3
|
+
table: string;
|
|
4
|
+
total_rows: number;
|
|
5
|
+
synced_rows: number;
|
|
6
|
+
skipped_rows: number;
|
|
7
|
+
errors: string[];
|
|
8
|
+
first_sync: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface SyncMeta {
|
|
11
|
+
table_name: string;
|
|
12
|
+
last_synced_at: string;
|
|
13
|
+
last_synced_row_count: number;
|
|
14
|
+
direction: "push" | "pull";
|
|
15
|
+
}
|
|
16
|
+
export interface IncrementalSyncOptions {
|
|
17
|
+
/** Primary key column name (default: "id"). */
|
|
18
|
+
primaryKey?: string;
|
|
19
|
+
/** Conflict resolution column (default: "updated_at"). */
|
|
20
|
+
conflictColumn?: string;
|
|
21
|
+
/** Batch size for writes (default: 500). */
|
|
22
|
+
batchSize?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Ensure the `_sync_meta` table exists in the given database.
|
|
26
|
+
*/
|
|
27
|
+
export declare function ensureSyncMetaTable(db: DbAdapter): void;
|
|
28
|
+
/**
|
|
29
|
+
* Push only changed rows (since last sync) from local to remote.
|
|
30
|
+
*
|
|
31
|
+
* - Checks `_sync_meta` in the local DB for `last_synced_at`.
|
|
32
|
+
* - If found: only selects rows where `updated_at > last_synced_at`.
|
|
33
|
+
* - If not found: full push (first-time sync).
|
|
34
|
+
* - After push, updates `_sync_meta` with current timestamp and row count.
|
|
35
|
+
*/
|
|
36
|
+
export declare function incrementalSyncPush(local: DbAdapter, remote: DbAdapter, tables: string[], options?: IncrementalSyncOptions): IncrementalSyncStats[];
|
|
37
|
+
/**
|
|
38
|
+
* Pull only changed rows (since last sync) from remote to local.
|
|
39
|
+
*
|
|
40
|
+
* - Checks `_sync_meta` in the local DB for `last_synced_at`.
|
|
41
|
+
* - If found: only selects rows where `updated_at > last_synced_at`.
|
|
42
|
+
* - If not found: full pull (first-time sync).
|
|
43
|
+
* - After pull, updates `_sync_meta` with current timestamp and row count.
|
|
44
|
+
*/
|
|
45
|
+
export declare function incrementalSyncPull(remote: DbAdapter, local: DbAdapter, tables: string[], options?: IncrementalSyncOptions): IncrementalSyncStats[];
|
|
46
|
+
/**
|
|
47
|
+
* Get the sync metadata for all tables or a specific table.
|
|
48
|
+
*/
|
|
49
|
+
export declare function getSyncMetaAll(db: DbAdapter): SyncMeta[];
|
|
50
|
+
/**
|
|
51
|
+
* Get sync metadata for a specific table.
|
|
52
|
+
*/
|
|
53
|
+
export declare function getSyncMetaForTable(db: DbAdapter, table: string): SyncMeta | null;
|
|
54
|
+
/**
|
|
55
|
+
* Reset sync metadata for a table (forces full re-sync on next run).
|
|
56
|
+
*/
|
|
57
|
+
export declare function resetSyncMeta(db: DbAdapter, table: string): void;
|
|
58
|
+
/**
|
|
59
|
+
* Reset all sync metadata (forces full re-sync for all tables).
|
|
60
|
+
*/
|
|
61
|
+
export declare function resetAllSyncMeta(db: DbAdapter): void;
|
|
62
|
+
//# sourceMappingURL=sync-incremental.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-incremental.d.ts","sourceRoot":"","sources":["../src/sync-incremental.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAM9C,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,QAAQ;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,sBAAsB;IACrC,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAcD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI,CAEvD;AAkID;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,GAAE,sBAA2B,GACnC,oBAAoB,EAAE,CAyExB;AAMD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,GAAE,sBAA2B,GACnC,oBAAoB,EAAE,CAyExB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,SAAS,GAAG,QAAQ,EAAE,CAKxD;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,SAAS,EACb,KAAK,EAAE,MAAM,GACZ,QAAQ,GAAG,IAAI,CAEjB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAGhE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,SAAS,GAAG,IAAI,CAGpD"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { DbAdapter } from "./adapter.js";
|
|
2
|
+
export interface SyncProgressInfo {
|
|
3
|
+
table: string;
|
|
4
|
+
total: number;
|
|
5
|
+
done: number;
|
|
6
|
+
percent: number;
|
|
7
|
+
elapsed_ms: number;
|
|
8
|
+
eta_ms: number;
|
|
9
|
+
status: "pending" | "in_progress" | "completed" | "failed" | "resumed";
|
|
10
|
+
}
|
|
11
|
+
export type ProgressCallback = (progress: SyncProgressInfo) => void;
|
|
12
|
+
export interface ResumePoint {
|
|
13
|
+
table_name: string;
|
|
14
|
+
last_row_id: string;
|
|
15
|
+
direction: string;
|
|
16
|
+
started_at: string;
|
|
17
|
+
status: string;
|
|
18
|
+
}
|
|
19
|
+
export declare class SyncProgressTracker {
|
|
20
|
+
private db;
|
|
21
|
+
private progress;
|
|
22
|
+
private startTimes;
|
|
23
|
+
private callback?;
|
|
24
|
+
constructor(db: DbAdapter, callback?: ProgressCallback);
|
|
25
|
+
private ensureResumeTable;
|
|
26
|
+
/**
|
|
27
|
+
* Start tracking a table sync. Sets status to in_progress or resumed.
|
|
28
|
+
*/
|
|
29
|
+
start(table: string, total: number, direction: string): void;
|
|
30
|
+
/**
|
|
31
|
+
* Update progress for a table after processing rows.
|
|
32
|
+
*/
|
|
33
|
+
update(table: string, done: number, lastRowId: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* Mark a table sync as completed.
|
|
36
|
+
*/
|
|
37
|
+
markComplete(table: string): void;
|
|
38
|
+
/**
|
|
39
|
+
* Mark a table sync as failed.
|
|
40
|
+
*/
|
|
41
|
+
markFailed(table: string, _error: string): void;
|
|
42
|
+
/**
|
|
43
|
+
* Check if a previous sync was interrupted (status is 'in_progress' or 'resumed').
|
|
44
|
+
*/
|
|
45
|
+
canResume(table: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Returns the last successfully synced row ID for a table, or null.
|
|
48
|
+
*/
|
|
49
|
+
getResumePoint(table: string): ResumePoint | null;
|
|
50
|
+
/**
|
|
51
|
+
* Clear resume state for a table (e.g., after a fresh sync starts).
|
|
52
|
+
*/
|
|
53
|
+
clearResume(table: string): void;
|
|
54
|
+
/**
|
|
55
|
+
* Get current progress info for a table.
|
|
56
|
+
*/
|
|
57
|
+
getProgress(table: string): SyncProgressInfo | null;
|
|
58
|
+
/**
|
|
59
|
+
* Get progress info for all tracked tables.
|
|
60
|
+
*/
|
|
61
|
+
getAllProgress(): SyncProgressInfo[];
|
|
62
|
+
/**
|
|
63
|
+
* List all resume records from the database (including historical).
|
|
64
|
+
*/
|
|
65
|
+
listResumeRecords(): ResumePoint[];
|
|
66
|
+
private notify;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=sync-progress.d.ts.map
|