@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 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
+ [![npm](https://img.shields.io/npm/v/@hasna/cloud)](https://www.npmjs.com/package/@hasna/cloud)
6
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](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 = "id"
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
- const hasPrimaryKey = columns.includes(primaryKey);
11383
- if (!hasPrimaryKey) {
11384
- result.errors.push(`Table "${table}" has no "${primaryKey}" column — skipping`);
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 !== primaryKey);
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, primaryKey, batch);
11479
+ await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch);
11402
11480
  } else {
11403
- batchUpsertSqlite(target, table, columns, updateCols, primaryKey, batch);
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, primaryKey, batch) {
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 setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKey}" = EXCLUDED."${primaryKey}"`;
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 ("${primaryKey}") DO UPDATE SET ${setClause}`;
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, primaryKey, batch) {
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 setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKey}" = EXCLUDED."${primaryKey}"`;
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 ("${primaryKey}") DO UPDATE SET ${setClause}`;
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.0");
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 = "id"
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
- const hasPrimaryKey = columns.includes(primaryKey);
9435
- if (!hasPrimaryKey) {
9436
- result.errors.push(`Table "${table}" has no "${primaryKey}" column — skipping`);
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 !== primaryKey);
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, primaryKey, batch);
9531
+ await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch);
9454
9532
  } else {
9455
- batchUpsertSqlite(target, table, columns, updateCols, primaryKey, batch);
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, primaryKey, batch) {
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 setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKey}" = EXCLUDED."${primaryKey}"`;
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 ("${primaryKey}") DO UPDATE SET ${setClause}`;
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, primaryKey, batch) {
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 setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKey}" = EXCLUDED."${primaryKey}"`;
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 ("${primaryKey}") DO UPDATE SET ${setClause}`;
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 = "id"
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
- const hasPrimaryKey = columns.includes(primaryKey);
24782
- if (!hasPrimaryKey) {
24783
- result.errors.push(`Table "${table}" has no "${primaryKey}" column — skipping`);
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 !== primaryKey);
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, primaryKey, batch);
24878
+ await batchUpsertPg(target, table, columns, updateCols, pkColumns, batch);
24801
24879
  } else {
24802
- batchUpsertSqlite(target, table, columns, updateCols, primaryKey, batch);
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, primaryKey, batch) {
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 setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKey}" = EXCLUDED."${primaryKey}"`;
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 ("${primaryKey}") DO UPDATE SET ${setClause}`;
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, primaryKey, batch) {
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 setClause = updateCols.length > 0 ? updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(", ") : `"${primaryKey}" = EXCLUDED."${primaryKey}"`;
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 ("${primaryKey}") DO UPDATE SET ${setClause}`;
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
- /** Primary key column name (default: "id"). */
22
- primaryKey?: string;
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;
@@ -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,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;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;AAmWD;;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"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/cloud",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Shared cloud infrastructure \u2014 database adapter (SQLite + PostgreSQL), sync engine, feedback system, unified dotfile config",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",