@hasna/cloud 0.1.5 → 0.1.7
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/README.md +37 -0
- package/dist/cli/index.js +115 -14
- package/dist/index.js +114 -13
- package/dist/mcp/index.js +114 -13
- package/dist/sync.d.ts +6 -2
- package/dist/sync.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @hasna/cloud
|
|
2
|
+
|
|
3
|
+
Shared cloud infrastructure — database adapter (SQLite + PostgreSQL), sync engine, feedback system, unified dotfile config
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@hasna/cloud)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install -g @hasna/cloud
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## CLI Usage
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
cloud --help
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- `cloud setup`
|
|
21
|
+
- `cloud status`
|
|
22
|
+
- `cloud sync push`
|
|
23
|
+
- `cloud sync pull`
|
|
24
|
+
- `cloud feedback`
|
|
25
|
+
- `cloud migrate`
|
|
26
|
+
|
|
27
|
+
## MCP Server
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
cloud-mcp
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
4 tools available.
|
|
34
|
+
|
|
35
|
+
## License
|
|
36
|
+
|
|
37
|
+
Apache-2.0 -- see [LICENSE](LICENSE)
|
package/dist/cli/index.js
CHANGED
|
@@ -11337,13 +11337,56 @@ function heuristicOrder(tables) {
|
|
|
11337
11337
|
});
|
|
11338
11338
|
return sorted;
|
|
11339
11339
|
}
|
|
11340
|
+
function getSqlitePrimaryKeys(adapter, table) {
|
|
11341
|
+
try {
|
|
11342
|
+
const cols = adapter.all(`PRAGMA table_info("${table}")`);
|
|
11343
|
+
const pkCols = cols.filter((c) => c.pk > 0).sort((a, b) => a.pk - b.pk).map((c) => c.name);
|
|
11344
|
+
return pkCols;
|
|
11345
|
+
} catch {
|
|
11346
|
+
return [];
|
|
11347
|
+
}
|
|
11348
|
+
}
|
|
11349
|
+
async function getPgPrimaryKeys(adapter, table) {
|
|
11350
|
+
try {
|
|
11351
|
+
const rows = await adapter.all(`
|
|
11352
|
+
SELECT kcu.column_name, kcu.ordinal_position
|
|
11353
|
+
FROM information_schema.table_constraints tc
|
|
11354
|
+
JOIN information_schema.key_column_usage kcu
|
|
11355
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
11356
|
+
AND tc.table_schema = kcu.table_schema
|
|
11357
|
+
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
11358
|
+
AND tc.table_schema = 'public'
|
|
11359
|
+
AND tc.table_name = '${table}'
|
|
11360
|
+
ORDER BY kcu.ordinal_position
|
|
11361
|
+
`);
|
|
11362
|
+
return rows.map((r) => r.column_name);
|
|
11363
|
+
} catch {
|
|
11364
|
+
return [];
|
|
11365
|
+
}
|
|
11366
|
+
}
|
|
11367
|
+
async function detectPrimaryKeys(adapter, table) {
|
|
11368
|
+
if (isAsyncAdapter(adapter)) {
|
|
11369
|
+
return getPgPrimaryKeys(adapter, table);
|
|
11370
|
+
}
|
|
11371
|
+
return getSqlitePrimaryKeys(adapter, table);
|
|
11372
|
+
}
|
|
11373
|
+
async function resolvePrimaryKeys(source, target, table, pkOption) {
|
|
11374
|
+
if (pkOption) {
|
|
11375
|
+
return Array.isArray(pkOption) ? pkOption : [pkOption];
|
|
11376
|
+
}
|
|
11377
|
+
let pks = await detectPrimaryKeys(source, table);
|
|
11378
|
+
if (pks.length === 0) {
|
|
11379
|
+
pks = await detectPrimaryKeys(target, table);
|
|
11380
|
+
}
|
|
11381
|
+
return pks;
|
|
11382
|
+
}
|
|
11340
11383
|
async function syncTransfer(source, target, options, _direction) {
|
|
11341
11384
|
const {
|
|
11342
11385
|
tables,
|
|
11343
11386
|
onProgress,
|
|
11344
11387
|
batchSize = 100,
|
|
11345
11388
|
conflictColumn = "updated_at",
|
|
11346
|
-
primaryKey
|
|
11389
|
+
primaryKey: pkOption
|
|
11347
11390
|
} = options;
|
|
11348
11391
|
const results = [];
|
|
11349
11392
|
for (let i = 0;i < tables.length; i++) {
|
|
@@ -11378,10 +11421,45 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
11378
11421
|
results.push(result);
|
|
11379
11422
|
continue;
|
|
11380
11423
|
}
|
|
11424
|
+
const pkColumns = await resolvePrimaryKeys(source, target, table, pkOption);
|
|
11381
11425
|
const columns = Object.keys(rows[0]);
|
|
11382
|
-
|
|
11383
|
-
|
|
11384
|
-
|
|
11426
|
+
if (pkColumns.length === 0) {
|
|
11427
|
+
result.errors.push(`Table "${table}" has no primary key — inserting without conflict handling`);
|
|
11428
|
+
onProgress?.({
|
|
11429
|
+
table,
|
|
11430
|
+
phase: "writing",
|
|
11431
|
+
rowsRead: result.rowsRead,
|
|
11432
|
+
rowsWritten: 0,
|
|
11433
|
+
totalTables: tables.length,
|
|
11434
|
+
currentTableIndex: i
|
|
11435
|
+
});
|
|
11436
|
+
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
11437
|
+
const batch = rows.slice(offset, offset + batchSize);
|
|
11438
|
+
try {
|
|
11439
|
+
if (isAsyncAdapter(target)) {
|
|
11440
|
+
await batchInsertPg(target, table, columns, batch);
|
|
11441
|
+
} else {
|
|
11442
|
+
batchInsertSqlite(target, table, columns, batch);
|
|
11443
|
+
}
|
|
11444
|
+
result.rowsWritten += batch.length;
|
|
11445
|
+
} catch (err) {
|
|
11446
|
+
result.errors.push(`Batch at offset ${offset}: ${err?.message ?? String(err)}`);
|
|
11447
|
+
}
|
|
11448
|
+
}
|
|
11449
|
+
onProgress?.({
|
|
11450
|
+
table,
|
|
11451
|
+
phase: "done",
|
|
11452
|
+
rowsRead: result.rowsRead,
|
|
11453
|
+
rowsWritten: result.rowsWritten,
|
|
11454
|
+
totalTables: tables.length,
|
|
11455
|
+
currentTableIndex: i
|
|
11456
|
+
});
|
|
11457
|
+
results.push(result);
|
|
11458
|
+
continue;
|
|
11459
|
+
}
|
|
11460
|
+
const missingPks = pkColumns.filter((pk) => !columns.includes(pk));
|
|
11461
|
+
if (missingPks.length > 0) {
|
|
11462
|
+
result.errors.push(`Table "${table}" missing PK columns in data: ${missingPks.join(", ")} — skipping`);
|
|
11385
11463
|
results.push(result);
|
|
11386
11464
|
continue;
|
|
11387
11465
|
}
|
|
@@ -11393,14 +11471,14 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
11393
11471
|
totalTables: tables.length,
|
|
11394
11472
|
currentTableIndex: i
|
|
11395
11473
|
});
|
|
11396
|
-
const updateCols = columns.filter((c) => c
|
|
11474
|
+
const updateCols = columns.filter((c) => !pkColumns.includes(c));
|
|
11397
11475
|
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
11398
11476
|
const batch = rows.slice(offset, offset + batchSize);
|
|
11399
11477
|
try {
|
|
11400
11478
|
if (isAsyncAdapter(target)) {
|
|
11401
|
-
await batchUpsertPg(target, table, columns, updateCols,
|
|
11479
|
+
await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch);
|
|
11402
11480
|
} else {
|
|
11403
|
-
batchUpsertSqlite(target, table, columns, updateCols,
|
|
11481
|
+
batchUpsertSqlite(target, table, columns, updateCols, pkColumns, batch);
|
|
11404
11482
|
}
|
|
11405
11483
|
result.rowsWritten += batch.length;
|
|
11406
11484
|
} catch (err) {
|
|
@@ -11430,7 +11508,7 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
11430
11508
|
}
|
|
11431
11509
|
return results;
|
|
11432
11510
|
}
|
|
11433
|
-
async function batchUpsertPg(target, table, columns, updateCols,
|
|
11511
|
+
async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch) {
|
|
11434
11512
|
if (batch.length === 0)
|
|
11435
11513
|
return;
|
|
11436
11514
|
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
@@ -11438,20 +11516,43 @@ async function batchUpsertPg(target, table, columns, updateCols, primaryKey, bat
|
|
|
11438
11516
|
const offset = rowIdx * columns.length;
|
|
11439
11517
|
return `(${columns.map((_2, colIdx) => `$${offset + colIdx + 1}`).join(", ")})`;
|
|
11440
11518
|
}).join(", ");
|
|
11441
|
-
const
|
|
11519
|
+
const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
|
|
11520
|
+
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
|
|
11442
11521
|
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
11443
|
-
ON CONFLICT (
|
|
11522
|
+
ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}`;
|
|
11444
11523
|
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
11445
11524
|
await target.run(sql, ...params);
|
|
11446
11525
|
}
|
|
11447
|
-
function batchUpsertSqlite(target, table, columns, updateCols,
|
|
11526
|
+
function batchUpsertSqlite(target, table, columns, updateCols, primaryKeys, batch) {
|
|
11448
11527
|
if (batch.length === 0)
|
|
11449
11528
|
return;
|
|
11450
11529
|
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
11451
11530
|
const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
11452
|
-
const
|
|
11531
|
+
const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
|
|
11532
|
+
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
|
|
11453
11533
|
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
11454
|
-
ON CONFLICT (
|
|
11534
|
+
ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}`;
|
|
11535
|
+
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
11536
|
+
target.run(sql, ...params);
|
|
11537
|
+
}
|
|
11538
|
+
async function batchInsertPg(target, table, columns, batch) {
|
|
11539
|
+
if (batch.length === 0)
|
|
11540
|
+
return;
|
|
11541
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
11542
|
+
const valuePlaceholders = batch.map((_, rowIdx) => {
|
|
11543
|
+
const offset = rowIdx * columns.length;
|
|
11544
|
+
return `(${columns.map((_2, colIdx) => `$${offset + colIdx + 1}`).join(", ")})`;
|
|
11545
|
+
}).join(", ");
|
|
11546
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}`;
|
|
11547
|
+
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
11548
|
+
await target.run(sql, ...params);
|
|
11549
|
+
}
|
|
11550
|
+
function batchInsertSqlite(target, table, columns, batch) {
|
|
11551
|
+
if (batch.length === 0)
|
|
11552
|
+
return;
|
|
11553
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
11554
|
+
const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
11555
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}`;
|
|
11455
11556
|
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
11456
11557
|
target.run(sql, ...params);
|
|
11457
11558
|
}
|
|
@@ -11732,7 +11833,7 @@ class PgAdapterAsync {
|
|
|
11732
11833
|
|
|
11733
11834
|
// src/cli/index.ts
|
|
11734
11835
|
var program2 = new Command;
|
|
11735
|
-
program2.name("cloud").description("Shared cloud infrastructure \u2014 database adapter, sync engine, feedback, dotfile migration").version("0.1.
|
|
11836
|
+
program2.name("cloud").description("Shared cloud infrastructure \u2014 database adapter, sync engine, feedback, dotfile migration").version("0.1.7");
|
|
11736
11837
|
program2.command("setup").description("Configure cloud settings").option("--host <host>", "RDS hostname").option("--port <port>", "RDS port", "5432").option("--username <user>", "RDS username").option("--password-env <env>", "Env var for RDS password", "HASNA_RDS_PASSWORD").option("--ssl", "Enable SSL", true).option("--no-ssl", "Disable SSL").option("--mode <mode>", "Mode: local, cloud, or hybrid", "local").option("--sync-interval <minutes>", "Auto-sync interval in minutes", "0").action((opts) => {
|
|
11737
11838
|
const config = getCloudConfig();
|
|
11738
11839
|
if (opts.host)
|
package/dist/index.js
CHANGED
|
@@ -9389,13 +9389,56 @@ function heuristicOrder(tables) {
|
|
|
9389
9389
|
});
|
|
9390
9390
|
return sorted;
|
|
9391
9391
|
}
|
|
9392
|
+
function getSqlitePrimaryKeys(adapter, table) {
|
|
9393
|
+
try {
|
|
9394
|
+
const cols = adapter.all(`PRAGMA table_info("${table}")`);
|
|
9395
|
+
const pkCols = cols.filter((c) => c.pk > 0).sort((a, b) => a.pk - b.pk).map((c) => c.name);
|
|
9396
|
+
return pkCols;
|
|
9397
|
+
} catch {
|
|
9398
|
+
return [];
|
|
9399
|
+
}
|
|
9400
|
+
}
|
|
9401
|
+
async function getPgPrimaryKeys(adapter, table) {
|
|
9402
|
+
try {
|
|
9403
|
+
const rows = await adapter.all(`
|
|
9404
|
+
SELECT kcu.column_name, kcu.ordinal_position
|
|
9405
|
+
FROM information_schema.table_constraints tc
|
|
9406
|
+
JOIN information_schema.key_column_usage kcu
|
|
9407
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
9408
|
+
AND tc.table_schema = kcu.table_schema
|
|
9409
|
+
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
9410
|
+
AND tc.table_schema = 'public'
|
|
9411
|
+
AND tc.table_name = '${table}'
|
|
9412
|
+
ORDER BY kcu.ordinal_position
|
|
9413
|
+
`);
|
|
9414
|
+
return rows.map((r) => r.column_name);
|
|
9415
|
+
} catch {
|
|
9416
|
+
return [];
|
|
9417
|
+
}
|
|
9418
|
+
}
|
|
9419
|
+
async function detectPrimaryKeys(adapter, table) {
|
|
9420
|
+
if (isAsyncAdapter(adapter)) {
|
|
9421
|
+
return getPgPrimaryKeys(adapter, table);
|
|
9422
|
+
}
|
|
9423
|
+
return getSqlitePrimaryKeys(adapter, table);
|
|
9424
|
+
}
|
|
9425
|
+
async function resolvePrimaryKeys(source, target, table, pkOption) {
|
|
9426
|
+
if (pkOption) {
|
|
9427
|
+
return Array.isArray(pkOption) ? pkOption : [pkOption];
|
|
9428
|
+
}
|
|
9429
|
+
let pks = await detectPrimaryKeys(source, table);
|
|
9430
|
+
if (pks.length === 0) {
|
|
9431
|
+
pks = await detectPrimaryKeys(target, table);
|
|
9432
|
+
}
|
|
9433
|
+
return pks;
|
|
9434
|
+
}
|
|
9392
9435
|
async function syncTransfer(source, target, options, _direction) {
|
|
9393
9436
|
const {
|
|
9394
9437
|
tables,
|
|
9395
9438
|
onProgress,
|
|
9396
9439
|
batchSize = 100,
|
|
9397
9440
|
conflictColumn = "updated_at",
|
|
9398
|
-
primaryKey
|
|
9441
|
+
primaryKey: pkOption
|
|
9399
9442
|
} = options;
|
|
9400
9443
|
const results = [];
|
|
9401
9444
|
for (let i = 0;i < tables.length; i++) {
|
|
@@ -9430,10 +9473,45 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
9430
9473
|
results.push(result);
|
|
9431
9474
|
continue;
|
|
9432
9475
|
}
|
|
9476
|
+
const pkColumns = await resolvePrimaryKeys(source, target, table, pkOption);
|
|
9433
9477
|
const columns = Object.keys(rows[0]);
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9478
|
+
if (pkColumns.length === 0) {
|
|
9479
|
+
result.errors.push(`Table "${table}" has no primary key — inserting without conflict handling`);
|
|
9480
|
+
onProgress?.({
|
|
9481
|
+
table,
|
|
9482
|
+
phase: "writing",
|
|
9483
|
+
rowsRead: result.rowsRead,
|
|
9484
|
+
rowsWritten: 0,
|
|
9485
|
+
totalTables: tables.length,
|
|
9486
|
+
currentTableIndex: i
|
|
9487
|
+
});
|
|
9488
|
+
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
9489
|
+
const batch = rows.slice(offset, offset + batchSize);
|
|
9490
|
+
try {
|
|
9491
|
+
if (isAsyncAdapter(target)) {
|
|
9492
|
+
await batchInsertPg(target, table, columns, batch);
|
|
9493
|
+
} else {
|
|
9494
|
+
batchInsertSqlite(target, table, columns, batch);
|
|
9495
|
+
}
|
|
9496
|
+
result.rowsWritten += batch.length;
|
|
9497
|
+
} catch (err) {
|
|
9498
|
+
result.errors.push(`Batch at offset ${offset}: ${err?.message ?? String(err)}`);
|
|
9499
|
+
}
|
|
9500
|
+
}
|
|
9501
|
+
onProgress?.({
|
|
9502
|
+
table,
|
|
9503
|
+
phase: "done",
|
|
9504
|
+
rowsRead: result.rowsRead,
|
|
9505
|
+
rowsWritten: result.rowsWritten,
|
|
9506
|
+
totalTables: tables.length,
|
|
9507
|
+
currentTableIndex: i
|
|
9508
|
+
});
|
|
9509
|
+
results.push(result);
|
|
9510
|
+
continue;
|
|
9511
|
+
}
|
|
9512
|
+
const missingPks = pkColumns.filter((pk) => !columns.includes(pk));
|
|
9513
|
+
if (missingPks.length > 0) {
|
|
9514
|
+
result.errors.push(`Table "${table}" missing PK columns in data: ${missingPks.join(", ")} — skipping`);
|
|
9437
9515
|
results.push(result);
|
|
9438
9516
|
continue;
|
|
9439
9517
|
}
|
|
@@ -9445,14 +9523,14 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
9445
9523
|
totalTables: tables.length,
|
|
9446
9524
|
currentTableIndex: i
|
|
9447
9525
|
});
|
|
9448
|
-
const updateCols = columns.filter((c) => c
|
|
9526
|
+
const updateCols = columns.filter((c) => !pkColumns.includes(c));
|
|
9449
9527
|
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
9450
9528
|
const batch = rows.slice(offset, offset + batchSize);
|
|
9451
9529
|
try {
|
|
9452
9530
|
if (isAsyncAdapter(target)) {
|
|
9453
|
-
await batchUpsertPg(target, table, columns, updateCols,
|
|
9531
|
+
await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch);
|
|
9454
9532
|
} else {
|
|
9455
|
-
batchUpsertSqlite(target, table, columns, updateCols,
|
|
9533
|
+
batchUpsertSqlite(target, table, columns, updateCols, pkColumns, batch);
|
|
9456
9534
|
}
|
|
9457
9535
|
result.rowsWritten += batch.length;
|
|
9458
9536
|
} catch (err) {
|
|
@@ -9482,7 +9560,7 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
9482
9560
|
}
|
|
9483
9561
|
return results;
|
|
9484
9562
|
}
|
|
9485
|
-
async function batchUpsertPg(target, table, columns, updateCols,
|
|
9563
|
+
async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch) {
|
|
9486
9564
|
if (batch.length === 0)
|
|
9487
9565
|
return;
|
|
9488
9566
|
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
@@ -9490,20 +9568,43 @@ async function batchUpsertPg(target, table, columns, updateCols, primaryKey, bat
|
|
|
9490
9568
|
const offset = rowIdx * columns.length;
|
|
9491
9569
|
return `(${columns.map((_2, colIdx) => `$${offset + colIdx + 1}`).join(", ")})`;
|
|
9492
9570
|
}).join(", ");
|
|
9493
|
-
const
|
|
9571
|
+
const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
|
|
9572
|
+
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
|
|
9494
9573
|
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
9495
|
-
ON CONFLICT (
|
|
9574
|
+
ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}`;
|
|
9496
9575
|
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
9497
9576
|
await target.run(sql, ...params);
|
|
9498
9577
|
}
|
|
9499
|
-
function batchUpsertSqlite(target, table, columns, updateCols,
|
|
9578
|
+
function batchUpsertSqlite(target, table, columns, updateCols, primaryKeys, batch) {
|
|
9500
9579
|
if (batch.length === 0)
|
|
9501
9580
|
return;
|
|
9502
9581
|
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
9503
9582
|
const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
9504
|
-
const
|
|
9583
|
+
const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
|
|
9584
|
+
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
|
|
9505
9585
|
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
9506
|
-
ON CONFLICT (
|
|
9586
|
+
ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}`;
|
|
9587
|
+
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
9588
|
+
target.run(sql, ...params);
|
|
9589
|
+
}
|
|
9590
|
+
async function batchInsertPg(target, table, columns, batch) {
|
|
9591
|
+
if (batch.length === 0)
|
|
9592
|
+
return;
|
|
9593
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
9594
|
+
const valuePlaceholders = batch.map((_, rowIdx) => {
|
|
9595
|
+
const offset = rowIdx * columns.length;
|
|
9596
|
+
return `(${columns.map((_2, colIdx) => `$${offset + colIdx + 1}`).join(", ")})`;
|
|
9597
|
+
}).join(", ");
|
|
9598
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}`;
|
|
9599
|
+
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
9600
|
+
await target.run(sql, ...params);
|
|
9601
|
+
}
|
|
9602
|
+
function batchInsertSqlite(target, table, columns, batch) {
|
|
9603
|
+
if (batch.length === 0)
|
|
9604
|
+
return;
|
|
9605
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
9606
|
+
const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
9607
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}`;
|
|
9507
9608
|
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
9508
9609
|
target.run(sql, ...params);
|
|
9509
9610
|
}
|
package/dist/mcp/index.js
CHANGED
|
@@ -24736,13 +24736,56 @@ function heuristicOrder(tables) {
|
|
|
24736
24736
|
});
|
|
24737
24737
|
return sorted;
|
|
24738
24738
|
}
|
|
24739
|
+
function getSqlitePrimaryKeys(adapter, table) {
|
|
24740
|
+
try {
|
|
24741
|
+
const cols = adapter.all(`PRAGMA table_info("${table}")`);
|
|
24742
|
+
const pkCols = cols.filter((c) => c.pk > 0).sort((a, b) => a.pk - b.pk).map((c) => c.name);
|
|
24743
|
+
return pkCols;
|
|
24744
|
+
} catch {
|
|
24745
|
+
return [];
|
|
24746
|
+
}
|
|
24747
|
+
}
|
|
24748
|
+
async function getPgPrimaryKeys(adapter, table) {
|
|
24749
|
+
try {
|
|
24750
|
+
const rows = await adapter.all(`
|
|
24751
|
+
SELECT kcu.column_name, kcu.ordinal_position
|
|
24752
|
+
FROM information_schema.table_constraints tc
|
|
24753
|
+
JOIN information_schema.key_column_usage kcu
|
|
24754
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
24755
|
+
AND tc.table_schema = kcu.table_schema
|
|
24756
|
+
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
24757
|
+
AND tc.table_schema = 'public'
|
|
24758
|
+
AND tc.table_name = '${table}'
|
|
24759
|
+
ORDER BY kcu.ordinal_position
|
|
24760
|
+
`);
|
|
24761
|
+
return rows.map((r) => r.column_name);
|
|
24762
|
+
} catch {
|
|
24763
|
+
return [];
|
|
24764
|
+
}
|
|
24765
|
+
}
|
|
24766
|
+
async function detectPrimaryKeys(adapter, table) {
|
|
24767
|
+
if (isAsyncAdapter(adapter)) {
|
|
24768
|
+
return getPgPrimaryKeys(adapter, table);
|
|
24769
|
+
}
|
|
24770
|
+
return getSqlitePrimaryKeys(adapter, table);
|
|
24771
|
+
}
|
|
24772
|
+
async function resolvePrimaryKeys(source, target, table, pkOption) {
|
|
24773
|
+
if (pkOption) {
|
|
24774
|
+
return Array.isArray(pkOption) ? pkOption : [pkOption];
|
|
24775
|
+
}
|
|
24776
|
+
let pks = await detectPrimaryKeys(source, table);
|
|
24777
|
+
if (pks.length === 0) {
|
|
24778
|
+
pks = await detectPrimaryKeys(target, table);
|
|
24779
|
+
}
|
|
24780
|
+
return pks;
|
|
24781
|
+
}
|
|
24739
24782
|
async function syncTransfer(source, target, options, _direction) {
|
|
24740
24783
|
const {
|
|
24741
24784
|
tables,
|
|
24742
24785
|
onProgress,
|
|
24743
24786
|
batchSize = 100,
|
|
24744
24787
|
conflictColumn = "updated_at",
|
|
24745
|
-
primaryKey
|
|
24788
|
+
primaryKey: pkOption
|
|
24746
24789
|
} = options;
|
|
24747
24790
|
const results = [];
|
|
24748
24791
|
for (let i = 0;i < tables.length; i++) {
|
|
@@ -24777,10 +24820,45 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
24777
24820
|
results.push(result);
|
|
24778
24821
|
continue;
|
|
24779
24822
|
}
|
|
24823
|
+
const pkColumns = await resolvePrimaryKeys(source, target, table, pkOption);
|
|
24780
24824
|
const columns = Object.keys(rows[0]);
|
|
24781
|
-
|
|
24782
|
-
|
|
24783
|
-
|
|
24825
|
+
if (pkColumns.length === 0) {
|
|
24826
|
+
result.errors.push(`Table "${table}" has no primary key — inserting without conflict handling`);
|
|
24827
|
+
onProgress?.({
|
|
24828
|
+
table,
|
|
24829
|
+
phase: "writing",
|
|
24830
|
+
rowsRead: result.rowsRead,
|
|
24831
|
+
rowsWritten: 0,
|
|
24832
|
+
totalTables: tables.length,
|
|
24833
|
+
currentTableIndex: i
|
|
24834
|
+
});
|
|
24835
|
+
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
24836
|
+
const batch = rows.slice(offset, offset + batchSize);
|
|
24837
|
+
try {
|
|
24838
|
+
if (isAsyncAdapter(target)) {
|
|
24839
|
+
await batchInsertPg(target, table, columns, batch);
|
|
24840
|
+
} else {
|
|
24841
|
+
batchInsertSqlite(target, table, columns, batch);
|
|
24842
|
+
}
|
|
24843
|
+
result.rowsWritten += batch.length;
|
|
24844
|
+
} catch (err) {
|
|
24845
|
+
result.errors.push(`Batch at offset ${offset}: ${err?.message ?? String(err)}`);
|
|
24846
|
+
}
|
|
24847
|
+
}
|
|
24848
|
+
onProgress?.({
|
|
24849
|
+
table,
|
|
24850
|
+
phase: "done",
|
|
24851
|
+
rowsRead: result.rowsRead,
|
|
24852
|
+
rowsWritten: result.rowsWritten,
|
|
24853
|
+
totalTables: tables.length,
|
|
24854
|
+
currentTableIndex: i
|
|
24855
|
+
});
|
|
24856
|
+
results.push(result);
|
|
24857
|
+
continue;
|
|
24858
|
+
}
|
|
24859
|
+
const missingPks = pkColumns.filter((pk) => !columns.includes(pk));
|
|
24860
|
+
if (missingPks.length > 0) {
|
|
24861
|
+
result.errors.push(`Table "${table}" missing PK columns in data: ${missingPks.join(", ")} — skipping`);
|
|
24784
24862
|
results.push(result);
|
|
24785
24863
|
continue;
|
|
24786
24864
|
}
|
|
@@ -24792,14 +24870,14 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
24792
24870
|
totalTables: tables.length,
|
|
24793
24871
|
currentTableIndex: i
|
|
24794
24872
|
});
|
|
24795
|
-
const updateCols = columns.filter((c) => c
|
|
24873
|
+
const updateCols = columns.filter((c) => !pkColumns.includes(c));
|
|
24796
24874
|
for (let offset = 0;offset < rows.length; offset += batchSize) {
|
|
24797
24875
|
const batch = rows.slice(offset, offset + batchSize);
|
|
24798
24876
|
try {
|
|
24799
24877
|
if (isAsyncAdapter(target)) {
|
|
24800
|
-
await batchUpsertPg(target, table, columns, updateCols,
|
|
24878
|
+
await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch);
|
|
24801
24879
|
} else {
|
|
24802
|
-
batchUpsertSqlite(target, table, columns, updateCols,
|
|
24880
|
+
batchUpsertSqlite(target, table, columns, updateCols, pkColumns, batch);
|
|
24803
24881
|
}
|
|
24804
24882
|
result.rowsWritten += batch.length;
|
|
24805
24883
|
} catch (err) {
|
|
@@ -24829,7 +24907,7 @@ async function syncTransfer(source, target, options, _direction) {
|
|
|
24829
24907
|
}
|
|
24830
24908
|
return results;
|
|
24831
24909
|
}
|
|
24832
|
-
async function batchUpsertPg(target, table, columns, updateCols,
|
|
24910
|
+
async function batchUpsertPg(target, table, columns, updateCols, primaryKeys, batch) {
|
|
24833
24911
|
if (batch.length === 0)
|
|
24834
24912
|
return;
|
|
24835
24913
|
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
@@ -24837,20 +24915,43 @@ async function batchUpsertPg(target, table, columns, updateCols, primaryKey, bat
|
|
|
24837
24915
|
const offset = rowIdx * columns.length;
|
|
24838
24916
|
return `(${columns.map((_2, colIdx) => `$${offset + colIdx + 1}`).join(", ")})`;
|
|
24839
24917
|
}).join(", ");
|
|
24840
|
-
const
|
|
24918
|
+
const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
|
|
24919
|
+
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
|
|
24841
24920
|
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
24842
|
-
ON CONFLICT (
|
|
24921
|
+
ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}`;
|
|
24843
24922
|
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
24844
24923
|
await target.run(sql, ...params);
|
|
24845
24924
|
}
|
|
24846
|
-
function batchUpsertSqlite(target, table, columns, updateCols,
|
|
24925
|
+
function batchUpsertSqlite(target, table, columns, updateCols, primaryKeys, batch) {
|
|
24847
24926
|
if (batch.length === 0)
|
|
24848
24927
|
return;
|
|
24849
24928
|
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
24850
24929
|
const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
24851
|
-
const
|
|
24930
|
+
const pkList = primaryKeys.map((c) => `"${c}"`).join(", ");
|
|
24931
|
+
const setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKeys[0]}" = EXCLUDED."${primaryKeys[0]}"`;
|
|
24852
24932
|
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}
|
|
24853
|
-
ON CONFLICT (
|
|
24933
|
+
ON CONFLICT (${pkList}) DO UPDATE SET ${setClause}`;
|
|
24934
|
+
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
24935
|
+
target.run(sql, ...params);
|
|
24936
|
+
}
|
|
24937
|
+
async function batchInsertPg(target, table, columns, batch) {
|
|
24938
|
+
if (batch.length === 0)
|
|
24939
|
+
return;
|
|
24940
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
24941
|
+
const valuePlaceholders = batch.map((_, rowIdx) => {
|
|
24942
|
+
const offset = rowIdx * columns.length;
|
|
24943
|
+
return `(${columns.map((_2, colIdx) => `$${offset + colIdx + 1}`).join(", ")})`;
|
|
24944
|
+
}).join(", ");
|
|
24945
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}`;
|
|
24946
|
+
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
24947
|
+
await target.run(sql, ...params);
|
|
24948
|
+
}
|
|
24949
|
+
function batchInsertSqlite(target, table, columns, batch) {
|
|
24950
|
+
if (batch.length === 0)
|
|
24951
|
+
return;
|
|
24952
|
+
const colList = columns.map((c) => `"${c}"`).join(", ");
|
|
24953
|
+
const valuePlaceholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
24954
|
+
const sql = `INSERT INTO "${table}" (${colList}) VALUES ${valuePlaceholders}`;
|
|
24854
24955
|
const params = batch.flatMap((row) => columns.map((c) => row[c] ?? null));
|
|
24855
24956
|
target.run(sql, ...params);
|
|
24856
24957
|
}
|
package/dist/sync.d.ts
CHANGED
|
@@ -18,8 +18,12 @@ export interface SyncOptions {
|
|
|
18
18
|
batchSize?: number;
|
|
19
19
|
/** Conflict resolution column (default: "updated_at"). Newest wins. */
|
|
20
20
|
conflictColumn?: string;
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Primary key column name(s). Can be a single column string or an array
|
|
23
|
+
* for composite primary keys (default: auto-detected from the database).
|
|
24
|
+
* If not provided and auto-detection fails, falls back to "id".
|
|
25
|
+
*/
|
|
26
|
+
primaryKey?: string | string[];
|
|
23
27
|
}
|
|
24
28
|
export interface SyncResult {
|
|
25
29
|
table: string;
|
package/dist/sync.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAMnD,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;AAEpE,MAAM,WAAW,WAAW;IAC1B,sBAAsB;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,kCAAkC;IAClC,UAAU,CAAC,EAAE,oBAAoB,CAAC;IAClC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAMnD,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;AAEpE,MAAM,WAAW,WAAW;IAC1B,sBAAsB;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,kCAAkC;IAClC,UAAU,CAAC,EAAE,oBAAoB,CAAC;IAClC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAMD;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,EAAE,CAAC,CAGvB;AAMD;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,SAAS,EAChB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,EAAE,CAAC,CAGvB;AAoiBD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,SAAS,GAAG,MAAM,EAAE,CAKxD;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAKxE"}
|
package/package.json
CHANGED