@hasna/cloud 0.1.3 → 0.1.5
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/adapter.d.ts +2 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/auto-sync.d.ts +57 -0
- package/dist/auto-sync.d.ts.map +1 -0
- package/dist/cli/index.js +176 -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 +726 -63
- package/dist/mcp/index.js +173 -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 +1 -1
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) {
|
|
@@ -24430,6 +24430,9 @@ class SqliteAdapter {
|
|
|
24430
24430
|
exec(sql) {
|
|
24431
24431
|
this.db.exec(sql);
|
|
24432
24432
|
}
|
|
24433
|
+
query(sql) {
|
|
24434
|
+
return this.db.query(sql);
|
|
24435
|
+
}
|
|
24433
24436
|
prepare(sql) {
|
|
24434
24437
|
const stmt = this.db.prepare(sql);
|
|
24435
24438
|
return {
|
|
@@ -24656,17 +24659,88 @@ function createDatabase(options) {
|
|
|
24656
24659
|
}
|
|
24657
24660
|
|
|
24658
24661
|
// src/sync.ts
|
|
24659
|
-
function syncPush(local,
|
|
24660
|
-
|
|
24662
|
+
async function syncPush(local, remote, options) {
|
|
24663
|
+
const orderedTables = await getTableOrder(remote, options.tables);
|
|
24664
|
+
return syncTransfer(local, remote, { ...options, tables: orderedTables }, "push");
|
|
24665
|
+
}
|
|
24666
|
+
async function syncPull(remote, local, options) {
|
|
24667
|
+
const orderedTables = await getTableOrder(remote, options.tables);
|
|
24668
|
+
return syncTransfer(remote, local, { ...options, tables: orderedTables }, "pull");
|
|
24669
|
+
}
|
|
24670
|
+
async function getTableOrder(remote, tables) {
|
|
24671
|
+
if (tables.length <= 1)
|
|
24672
|
+
return tables;
|
|
24673
|
+
try {
|
|
24674
|
+
const fks = await remote.all(`
|
|
24675
|
+
SELECT DISTINCT
|
|
24676
|
+
tc.table_name AS source_table,
|
|
24677
|
+
ccu.table_name AS referenced_table
|
|
24678
|
+
FROM information_schema.table_constraints tc
|
|
24679
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
24680
|
+
ON tc.constraint_name = ccu.constraint_name
|
|
24681
|
+
AND tc.table_schema = ccu.table_schema
|
|
24682
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
24683
|
+
AND tc.table_schema = 'public'
|
|
24684
|
+
`);
|
|
24685
|
+
if (fks.length > 0) {
|
|
24686
|
+
return topoSort(tables, fks);
|
|
24687
|
+
}
|
|
24688
|
+
} catch {}
|
|
24689
|
+
return heuristicOrder(tables);
|
|
24661
24690
|
}
|
|
24662
|
-
function
|
|
24663
|
-
|
|
24691
|
+
function topoSort(tables, fks) {
|
|
24692
|
+
const tableSet = new Set(tables);
|
|
24693
|
+
const deps = new Map;
|
|
24694
|
+
for (const t of tables) {
|
|
24695
|
+
deps.set(t, new Set);
|
|
24696
|
+
}
|
|
24697
|
+
for (const fk of fks) {
|
|
24698
|
+
if (tableSet.has(fk.source_table) && tableSet.has(fk.referenced_table)) {
|
|
24699
|
+
deps.get(fk.source_table).add(fk.referenced_table);
|
|
24700
|
+
}
|
|
24701
|
+
}
|
|
24702
|
+
const sorted = [];
|
|
24703
|
+
const visited = new Set;
|
|
24704
|
+
const visiting = new Set;
|
|
24705
|
+
function visit(table) {
|
|
24706
|
+
if (visited.has(table))
|
|
24707
|
+
return;
|
|
24708
|
+
if (visiting.has(table)) {
|
|
24709
|
+
sorted.push(table);
|
|
24710
|
+
visited.add(table);
|
|
24711
|
+
return;
|
|
24712
|
+
}
|
|
24713
|
+
visiting.add(table);
|
|
24714
|
+
const tableDeps = deps.get(table) ?? new Set;
|
|
24715
|
+
for (const dep of tableDeps) {
|
|
24716
|
+
visit(dep);
|
|
24717
|
+
}
|
|
24718
|
+
visiting.delete(table);
|
|
24719
|
+
visited.add(table);
|
|
24720
|
+
sorted.push(table);
|
|
24721
|
+
}
|
|
24722
|
+
for (const t of tables) {
|
|
24723
|
+
visit(t);
|
|
24724
|
+
}
|
|
24725
|
+
return sorted;
|
|
24726
|
+
}
|
|
24727
|
+
function heuristicOrder(tables) {
|
|
24728
|
+
const sorted = [...tables].sort((a, b) => {
|
|
24729
|
+
const aIsChild = a.includes("_") && tables.some((t) => a.startsWith(t + "_") || a.endsWith("_" + t));
|
|
24730
|
+
const bIsChild = b.includes("_") && tables.some((t) => b.startsWith(t + "_") || b.endsWith("_" + t));
|
|
24731
|
+
if (aIsChild && !bIsChild)
|
|
24732
|
+
return 1;
|
|
24733
|
+
if (!aIsChild && bIsChild)
|
|
24734
|
+
return -1;
|
|
24735
|
+
return a.localeCompare(b);
|
|
24736
|
+
});
|
|
24737
|
+
return sorted;
|
|
24664
24738
|
}
|
|
24665
|
-
function syncTransfer(source, target, options, _direction) {
|
|
24739
|
+
async function syncTransfer(source, target, options, _direction) {
|
|
24666
24740
|
const {
|
|
24667
24741
|
tables,
|
|
24668
24742
|
onProgress,
|
|
24669
|
-
batchSize =
|
|
24743
|
+
batchSize = 100,
|
|
24670
24744
|
conflictColumn = "updated_at",
|
|
24671
24745
|
primaryKey = "id"
|
|
24672
24746
|
} = options;
|
|
@@ -24689,7 +24763,7 @@ function syncTransfer(source, target, options, _direction) {
|
|
|
24689
24763
|
totalTables: tables.length,
|
|
24690
24764
|
currentTableIndex: i
|
|
24691
24765
|
});
|
|
24692
|
-
const rows = source
|
|
24766
|
+
const rows = await readAll(source, `SELECT * FROM "${table}"`);
|
|
24693
24767
|
result.rowsRead = rows.length;
|
|
24694
24768
|
if (rows.length === 0) {
|
|
24695
24769
|
onProgress?.({
|
|
@@ -24704,7 +24778,6 @@ function syncTransfer(source, target, options, _direction) {
|
|
|
24704
24778
|
continue;
|
|
24705
24779
|
}
|
|
24706
24780
|
const columns = Object.keys(rows[0]);
|
|
24707
|
-
const hasConflictCol = columns.includes(conflictColumn);
|
|
24708
24781
|
const hasPrimaryKey = columns.includes(primaryKey);
|
|
24709
24782
|
if (!hasPrimaryKey) {
|
|
24710
24783
|
result.errors.push(`Table "${table}" has no "${primaryKey}" column — skipping`);
|
|
@@ -24719,34 +24792,18 @@ function syncTransfer(source, target, options, _direction) {
|
|
|
24719
24792
|
totalTables: tables.length,
|
|
24720
24793
|
currentTableIndex: i
|
|
24721
24794
|
});
|
|
24795
|
+
const updateCols = columns.filter((c) => c !== primaryKey);
|
|
24722
24796
|
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
24723
24797
|
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)}`);
|
|
24798
|
+
try {
|
|
24799
|
+
if (isAsyncAdapter(target)) {
|
|
24800
|
+
await batchUpsertPg(target, table, columns, updateCols, primaryKey, batch);
|
|
24801
|
+
} else {
|
|
24802
|
+
batchUpsertSqlite(target, table, columns, updateCols, primaryKey, batch);
|
|
24749
24803
|
}
|
|
24804
|
+
result.rowsWritten += batch.length;
|
|
24805
|
+
} catch (err) {
|
|
24806
|
+
result.errors.push(`Batch at offset ${offset}: ${err?.message ?? String(err)}`);
|
|
24750
24807
|
}
|
|
24751
24808
|
onProgress?.({
|
|
24752
24809
|
table,
|
|
@@ -24772,10 +24829,46 @@ function syncTransfer(source, target, options, _direction) {
|
|
|
24772
24829
|
}
|
|
24773
24830
|
return results;
|
|
24774
24831
|
}
|
|
24832
|
+
async function batchUpsertPg(target, table, columns, updateCols, primaryKey, batch) {
|
|
24833
|
+
if (batch.length === 0)
|
|
24834
|
+
return;
|
|
24835
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
24836
|
+
const valuePlaceholders = batch.map((_, rowIdx) => {
|
|
24837
|
+
const offset = rowIdx * columns.length;
|
|
24838
|
+
return `(${columns.map((_2, colIdx) => `$${offset + colIdx + 1}`).join(", ")})`;
|
|
24839
|
+
}).join(", ");
|
|
24840
|
+
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKey}" = EXCLUDED."${primaryKey}"`;
|
|
24841
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
24842
|
+
ON CONFLICT ("${primaryKey}") DO UPDATE SET ${setClause}`;
|
|
24843
|
+
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
24844
|
+
await target.run(sql, ...params);
|
|
24845
|
+
}
|
|
24846
|
+
function batchUpsertSqlite(target, table, columns, updateCols, primaryKey, batch) {
|
|
24847
|
+
if (batch.length === 0)
|
|
24848
|
+
return;
|
|
24849
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
24850
|
+
const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
24851
|
+
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKey}" = EXCLUDED."${primaryKey}"`;
|
|
24852
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
24853
|
+
ON CONFLICT ("${primaryKey}") DO UPDATE SET ${setClause}`;
|
|
24854
|
+
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
24855
|
+
target.run(sql, ...params);
|
|
24856
|
+
}
|
|
24857
|
+
function isAsyncAdapter(adapter) {
|
|
24858
|
+
return adapter.constructor.name === "PgAdapterAsync" || typeof adapter.raw?.connect === "function";
|
|
24859
|
+
}
|
|
24860
|
+
async function readAll(adapter, sql) {
|
|
24861
|
+
const result = adapter.all(sql);
|
|
24862
|
+
return result instanceof Promise ? await result : result;
|
|
24863
|
+
}
|
|
24775
24864
|
function listSqliteTables(db) {
|
|
24776
24865
|
const rows = db.all(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`);
|
|
24777
24866
|
return rows.map((r) => r.name);
|
|
24778
24867
|
}
|
|
24868
|
+
async function listPgTables(db) {
|
|
24869
|
+
const rows = await db.all(`SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename`);
|
|
24870
|
+
return rows.map((r) => r.tablename);
|
|
24871
|
+
}
|
|
24779
24872
|
|
|
24780
24873
|
// src/feedback.ts
|
|
24781
24874
|
import { hostname as hostname2 } from "os";
|
|
@@ -24922,6 +25015,9 @@ class SqliteAdapter2 {
|
|
|
24922
25015
|
exec(sql) {
|
|
24923
25016
|
this.db.exec(sql);
|
|
24924
25017
|
}
|
|
25018
|
+
query(sql) {
|
|
25019
|
+
return this.db.query(sql);
|
|
25020
|
+
}
|
|
24925
25021
|
prepare(sql) {
|
|
24926
25022
|
const stmt = this.db.prepare(sql);
|
|
24927
25023
|
return {
|
|
@@ -24951,10 +25047,8 @@ class SqliteAdapter2 {
|
|
|
24951
25047
|
return this.db;
|
|
24952
25048
|
}
|
|
24953
25049
|
}
|
|
24954
|
-
|
|
24955
|
-
class PgAdapter2 {
|
|
25050
|
+
class PgAdapterAsync {
|
|
24956
25051
|
pool;
|
|
24957
|
-
_client = null;
|
|
24958
25052
|
constructor(arg) {
|
|
24959
25053
|
if (typeof arg === "string") {
|
|
24960
25054
|
this.pool = new esm_default.Pool({ connectionString: arg });
|
|
@@ -24962,118 +25056,47 @@ class PgAdapter2 {
|
|
|
24962
25056
|
this.pool = arg;
|
|
24963
25057
|
}
|
|
24964
25058
|
}
|
|
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) {
|
|
25059
|
+
async run(sql, ...params) {
|
|
24987
25060
|
const pgSql = translateSql(sql, "pg");
|
|
24988
25061
|
const pgParams = translateParams(params);
|
|
24989
|
-
|
|
24990
|
-
|
|
24991
|
-
|
|
24992
|
-
|
|
24993
|
-
|
|
24994
|
-
};
|
|
24995
|
-
});
|
|
25062
|
+
const res = await this.pool.query(pgSql, pgParams);
|
|
25063
|
+
return {
|
|
25064
|
+
changes: res.rowCount ?? 0,
|
|
25065
|
+
lastInsertRowid: res.rows?.[0]?.id ?? 0
|
|
25066
|
+
};
|
|
24996
25067
|
}
|
|
24997
|
-
get(sql, ...params) {
|
|
25068
|
+
async get(sql, ...params) {
|
|
24998
25069
|
const pgSql = translateSql(sql, "pg");
|
|
24999
25070
|
const pgParams = translateParams(params);
|
|
25000
|
-
|
|
25001
|
-
|
|
25002
|
-
return res.rows[0] ?? null;
|
|
25003
|
-
});
|
|
25071
|
+
const res = await this.pool.query(pgSql, pgParams);
|
|
25072
|
+
return res.rows[0] ?? null;
|
|
25004
25073
|
}
|
|
25005
|
-
all(sql, ...params) {
|
|
25074
|
+
async all(sql, ...params) {
|
|
25006
25075
|
const pgSql = translateSql(sql, "pg");
|
|
25007
25076
|
const pgParams = translateParams(params);
|
|
25008
|
-
|
|
25009
|
-
|
|
25010
|
-
return res.rows;
|
|
25011
|
-
});
|
|
25012
|
-
}
|
|
25013
|
-
exec(sql) {
|
|
25014
|
-
const pgSql = translateSql(sql, "pg");
|
|
25015
|
-
this.runSync(async () => {
|
|
25016
|
-
await this.pool.query(pgSql);
|
|
25017
|
-
});
|
|
25077
|
+
const res = await this.pool.query(pgSql, pgParams);
|
|
25078
|
+
return res.rows;
|
|
25018
25079
|
}
|
|
25019
|
-
|
|
25080
|
+
async exec(sql) {
|
|
25020
25081
|
const pgSql = translateSql(sql, "pg");
|
|
25021
|
-
|
|
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
|
-
};
|
|
25082
|
+
await this.pool.query(pgSql);
|
|
25049
25083
|
}
|
|
25050
|
-
close() {
|
|
25051
|
-
this.
|
|
25052
|
-
await this.pool.end();
|
|
25053
|
-
});
|
|
25084
|
+
async close() {
|
|
25085
|
+
await this.pool.end();
|
|
25054
25086
|
}
|
|
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
|
-
});
|
|
25087
|
+
async transaction(fn) {
|
|
25088
|
+
const client = await this.pool.connect();
|
|
25089
|
+
try {
|
|
25090
|
+
await client.query("BEGIN");
|
|
25091
|
+
const result = await fn(client);
|
|
25092
|
+
await client.query("COMMIT");
|
|
25093
|
+
return result;
|
|
25094
|
+
} catch (err) {
|
|
25095
|
+
await client.query("ROLLBACK");
|
|
25096
|
+
throw err;
|
|
25097
|
+
} finally {
|
|
25098
|
+
client.release();
|
|
25099
|
+
}
|
|
25077
25100
|
}
|
|
25078
25101
|
get raw() {
|
|
25079
25102
|
return this.pool;
|
|
@@ -25098,10 +25121,10 @@ server.tool("cloud_status", "Show cloud configuration and connection health", {}
|
|
|
25098
25121
|
if (config2.rds.host && config2.rds.username) {
|
|
25099
25122
|
try {
|
|
25100
25123
|
const connStr = getConnectionString("postgres");
|
|
25101
|
-
const pg2 = new
|
|
25102
|
-
pg2.get("SELECT 1 as ok");
|
|
25124
|
+
const pg2 = new PgAdapterAsync(connStr);
|
|
25125
|
+
await pg2.get("SELECT 1 as ok");
|
|
25103
25126
|
lines.push("PostgreSQL: connected");
|
|
25104
|
-
pg2.close();
|
|
25127
|
+
await pg2.close();
|
|
25105
25128
|
} catch (err) {
|
|
25106
25129
|
lines.push(`PostgreSQL: connection failed \u2014 ${err?.message}`);
|
|
25107
25130
|
}
|
|
@@ -25128,16 +25151,16 @@ server.tool("sync_push", "Push local SQLite data to cloud PostgreSQL", {
|
|
|
25128
25151
|
const dbPath = getDbPath2(service);
|
|
25129
25152
|
const local = new SqliteAdapter2(dbPath);
|
|
25130
25153
|
const connStr = getConnectionString(service);
|
|
25131
|
-
const cloud = new
|
|
25154
|
+
const cloud = new PgAdapterAsync(connStr);
|
|
25132
25155
|
let tableList;
|
|
25133
25156
|
if (tablesStr) {
|
|
25134
25157
|
tableList = tablesStr.split(",").map((t) => t.trim());
|
|
25135
25158
|
} else {
|
|
25136
25159
|
tableList = listSqliteTables(local);
|
|
25137
25160
|
}
|
|
25138
|
-
const results = syncPush(local, cloud, { tables: tableList });
|
|
25161
|
+
const results = await syncPush(local, cloud, { tables: tableList });
|
|
25139
25162
|
local.close();
|
|
25140
|
-
cloud.close();
|
|
25163
|
+
await cloud.close();
|
|
25141
25164
|
const totalWritten = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
25142
25165
|
const totalErrors = results.reduce((s, r) => s + r.errors.length, 0);
|
|
25143
25166
|
const lines = [
|
|
@@ -25171,17 +25194,16 @@ server.tool("sync_pull", "Pull cloud PostgreSQL data to local SQLite", {
|
|
|
25171
25194
|
const dbPath = getDbPath2(service);
|
|
25172
25195
|
const local = new SqliteAdapter2(dbPath);
|
|
25173
25196
|
const connStr = getConnectionString(service);
|
|
25174
|
-
const cloud = new
|
|
25197
|
+
const cloud = new PgAdapterAsync(connStr);
|
|
25175
25198
|
let tableList;
|
|
25176
25199
|
if (tablesStr) {
|
|
25177
25200
|
tableList = tablesStr.split(",").map((t) => t.trim());
|
|
25178
25201
|
} else {
|
|
25179
25202
|
try {
|
|
25180
|
-
|
|
25181
|
-
tableList = rows.map((r) => r.tablename);
|
|
25203
|
+
tableList = await listPgTables(cloud);
|
|
25182
25204
|
} catch {
|
|
25183
25205
|
local.close();
|
|
25184
|
-
cloud.close();
|
|
25206
|
+
await cloud.close();
|
|
25185
25207
|
return {
|
|
25186
25208
|
content: [
|
|
25187
25209
|
{ type: "text", text: "Error: failed to list tables from cloud." }
|
|
@@ -25190,9 +25212,9 @@ server.tool("sync_pull", "Pull cloud PostgreSQL data to local SQLite", {
|
|
|
25190
25212
|
};
|
|
25191
25213
|
}
|
|
25192
25214
|
}
|
|
25193
|
-
const results = syncPull(
|
|
25215
|
+
const results = await syncPull(cloud, local, { tables: tableList });
|
|
25194
25216
|
local.close();
|
|
25195
|
-
cloud.close();
|
|
25217
|
+
await cloud.close();
|
|
25196
25218
|
const totalWritten = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
25197
25219
|
const totalErrors = results.reduce((s, r) => s + r.errors.length, 0);
|
|
25198
25220
|
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"}
|