@withone/cli 1.37.0 → 1.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +147 -98
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5267,6 +5267,80 @@ function runShellHook(command, events) {
5267
5267
  });
5268
5268
  }
5269
5269
 
5270
+ // src/lib/sync/transform.ts
5271
+ import { spawn as spawn3 } from "child_process";
5272
+ var TRANSFORM_TIMEOUT_MS = 6e4;
5273
+ async function transformRecords(command, records) {
5274
+ const input = JSON.stringify(records);
5275
+ return new Promise((resolve) => {
5276
+ const child = spawn3("sh", ["-c", command], {
5277
+ stdio: ["pipe", "pipe", "pipe"]
5278
+ });
5279
+ let stdout = "";
5280
+ let stderr = "";
5281
+ child.stdout.on("data", (chunk) => {
5282
+ stdout += chunk.toString();
5283
+ });
5284
+ child.stderr.on("data", (chunk) => {
5285
+ stderr += chunk.toString();
5286
+ });
5287
+ const timer = setTimeout(() => {
5288
+ try {
5289
+ child.kill();
5290
+ } catch {
5291
+ }
5292
+ if (!isAgentMode()) {
5293
+ process.stderr.write(` Transform timed out after ${TRANSFORM_TIMEOUT_MS / 1e3}s \u2014 using original records
5294
+ `);
5295
+ }
5296
+ resolve(null);
5297
+ }, TRANSFORM_TIMEOUT_MS);
5298
+ child.on("exit", (code) => {
5299
+ clearTimeout(timer);
5300
+ if (stderr.trim() && !isAgentMode()) {
5301
+ process.stderr.write(` Transform stderr: ${stderr.trim()}
5302
+ `);
5303
+ }
5304
+ if (code !== 0) {
5305
+ if (!isAgentMode()) {
5306
+ process.stderr.write(` Transform exited with code ${code} \u2014 using original records
5307
+ `);
5308
+ }
5309
+ resolve(null);
5310
+ return;
5311
+ }
5312
+ try {
5313
+ const parsed = JSON.parse(stdout.trim());
5314
+ if (!Array.isArray(parsed)) {
5315
+ if (!isAgentMode()) {
5316
+ process.stderr.write(` Transform returned non-array JSON \u2014 using original records
5317
+ `);
5318
+ }
5319
+ resolve(null);
5320
+ return;
5321
+ }
5322
+ resolve(parsed);
5323
+ } catch {
5324
+ if (!isAgentMode()) {
5325
+ process.stderr.write(` Transform returned invalid JSON \u2014 using original records
5326
+ `);
5327
+ }
5328
+ resolve(null);
5329
+ }
5330
+ });
5331
+ child.on("error", (err) => {
5332
+ clearTimeout(timer);
5333
+ if (!isAgentMode()) {
5334
+ process.stderr.write(` Transform failed to start: ${err.message} \u2014 using original records
5335
+ `);
5336
+ }
5337
+ resolve(null);
5338
+ });
5339
+ child.stdin.write(input);
5340
+ child.stdin.end();
5341
+ });
5342
+ }
5343
+
5270
5344
  // src/lib/sync/enrich.ts
5271
5345
  var DEFAULT_CONCURRENCY = 5;
5272
5346
  var MAX_RETRIES = 3;
@@ -5356,7 +5430,7 @@ function pickFields(obj, fields) {
5356
5430
  }
5357
5431
  return result;
5358
5432
  }
5359
- async function enrichPhase(api, db, config2, model, idField, connectionKey, platform) {
5433
+ async function enrichPhase(api, db, config2, model, idField, connectionKey, platform, ctx = {}) {
5360
5434
  const startTime = Date.now();
5361
5435
  const tsField = config2.timestampField ?? "_enriched_at";
5362
5436
  const safeTable = model.replace(/[^a-zA-Z0-9_]/g, "_");
@@ -5404,6 +5478,7 @@ async function enrichPhase(api, db, config2, model, idField, connectionKey, plat
5404
5478
  );
5405
5479
  let batchHitRateLimit = false;
5406
5480
  const now = (/* @__PURE__ */ new Date()).toISOString();
5481
+ const pending = [];
5407
5482
  for (let j = 0; j < results.length; j++) {
5408
5483
  const result = results[j];
5409
5484
  const row = batch[j];
@@ -5418,28 +5493,7 @@ async function enrichPhase(api, db, config2, model, idField, connectionKey, plat
5418
5493
  }
5419
5494
  const merged = config2.merge !== false ? deepMerge(row, enrichedData) : { ...enrichedData, [idField]: id };
5420
5495
  merged[tsField] = now;
5421
- const setClauses = [];
5422
- const values = [];
5423
- for (const [key, val] of Object.entries(merged)) {
5424
- if (key === idField) continue;
5425
- setClauses.push(`"${key}" = ?`);
5426
- values.push(prepareValue2(val));
5427
- }
5428
- values.push(prepareValue2(id));
5429
- if (setClauses.length > 0) {
5430
- const existingCols = new Set(db.prepare(`PRAGMA table_info("${safeTable}")`).all().map((c) => c.name));
5431
- for (const [key, val] of Object.entries(merged)) {
5432
- if (!existingCols.has(key)) {
5433
- const colType = detectColumnType2(val);
5434
- db.exec(`ALTER TABLE "${safeTable}" ADD COLUMN "${key}" ${colType}`);
5435
- existingCols.add(key);
5436
- }
5437
- }
5438
- db.prepare(
5439
- `UPDATE "${safeTable}" SET ${setClauses.join(", ")} WHERE "${safeIdField}" = ?`
5440
- ).run(...values);
5441
- }
5442
- enriched++;
5496
+ pending.push({ merged, id });
5443
5497
  } else if (result.status === "fulfilled" && result.value === null) {
5444
5498
  rateLimited++;
5445
5499
  skipped++;
@@ -5448,6 +5502,62 @@ async function enrichPhase(api, db, config2, model, idField, connectionKey, plat
5448
5502
  skipped++;
5449
5503
  }
5450
5504
  }
5505
+ let writes = pending;
5506
+ if (ctx.transform && pending.length > 0) {
5507
+ const transformed = await transformRecords(ctx.transform, pending.map((p10) => p10.merged));
5508
+ if (transformed) {
5509
+ const byId = /* @__PURE__ */ new Map();
5510
+ for (const r of transformed) byId.set(r[idField], r);
5511
+ writes = [];
5512
+ for (const p10 of pending) {
5513
+ const t = byId.get(p10.id);
5514
+ if (!t) continue;
5515
+ if (!t[tsField]) t[tsField] = now;
5516
+ writes.push({ merged: t, id: p10.id });
5517
+ }
5518
+ }
5519
+ }
5520
+ const hookEvents = [];
5521
+ for (const { merged, id } of writes) {
5522
+ if (ctx.exclude && ctx.exclude.length > 0) {
5523
+ stripExcludedFields(merged, ctx.exclude);
5524
+ }
5525
+ if (ctx.identityKey) {
5526
+ const raw = getByDotPath2(merged, ctx.identityKey);
5527
+ if (raw !== null && raw !== void 0) {
5528
+ merged._identity = String(raw).toLowerCase().trim();
5529
+ }
5530
+ }
5531
+ const setClauses = [];
5532
+ const values = [];
5533
+ for (const [key, val] of Object.entries(merged)) {
5534
+ if (key === idField) continue;
5535
+ setClauses.push(`"${key}" = ?`);
5536
+ values.push(prepareValue2(val));
5537
+ }
5538
+ values.push(prepareValue2(id));
5539
+ if (setClauses.length > 0) {
5540
+ const existingCols = new Set(db.prepare(`PRAGMA table_info("${safeTable}")`).all().map((c) => c.name));
5541
+ for (const [key, val] of Object.entries(merged)) {
5542
+ if (!existingCols.has(key)) {
5543
+ const colType = detectColumnType2(val);
5544
+ db.exec(`ALTER TABLE "${safeTable}" ADD COLUMN "${key}" ${colType}`);
5545
+ existingCols.add(key);
5546
+ }
5547
+ }
5548
+ db.prepare(
5549
+ `UPDATE "${safeTable}" SET ${setClauses.join(", ")} WHERE "${safeIdField}" = ?`
5550
+ ).run(...values);
5551
+ }
5552
+ enriched++;
5553
+ if (ctx.onUpdate || ctx.onChange) {
5554
+ hookEvents.push({ type: "update", platform, model, record: merged, timestamp: now });
5555
+ }
5556
+ }
5557
+ if (hookEvents.length > 0) {
5558
+ const hook = ctx.onUpdate || ctx.onChange;
5559
+ if (hook) await fireHooks(hook, hookEvents);
5560
+ }
5451
5561
  if (batchHitRateLimit) {
5452
5562
  concurrency = Math.max(1, Math.floor(concurrency / 2));
5453
5563
  if (!isAgentMode()) {
@@ -5527,80 +5637,6 @@ function detectColumnType2(value) {
5527
5637
  return "TEXT";
5528
5638
  }
5529
5639
 
5530
- // src/lib/sync/transform.ts
5531
- import { spawn as spawn3 } from "child_process";
5532
- var TRANSFORM_TIMEOUT_MS = 6e4;
5533
- async function transformRecords(command, records) {
5534
- const input = JSON.stringify(records);
5535
- return new Promise((resolve) => {
5536
- const child = spawn3("sh", ["-c", command], {
5537
- stdio: ["pipe", "pipe", "pipe"]
5538
- });
5539
- let stdout = "";
5540
- let stderr = "";
5541
- child.stdout.on("data", (chunk) => {
5542
- stdout += chunk.toString();
5543
- });
5544
- child.stderr.on("data", (chunk) => {
5545
- stderr += chunk.toString();
5546
- });
5547
- const timer = setTimeout(() => {
5548
- try {
5549
- child.kill();
5550
- } catch {
5551
- }
5552
- if (!isAgentMode()) {
5553
- process.stderr.write(` Transform timed out after ${TRANSFORM_TIMEOUT_MS / 1e3}s \u2014 using original records
5554
- `);
5555
- }
5556
- resolve(null);
5557
- }, TRANSFORM_TIMEOUT_MS);
5558
- child.on("exit", (code) => {
5559
- clearTimeout(timer);
5560
- if (stderr.trim() && !isAgentMode()) {
5561
- process.stderr.write(` Transform stderr: ${stderr.trim()}
5562
- `);
5563
- }
5564
- if (code !== 0) {
5565
- if (!isAgentMode()) {
5566
- process.stderr.write(` Transform exited with code ${code} \u2014 using original records
5567
- `);
5568
- }
5569
- resolve(null);
5570
- return;
5571
- }
5572
- try {
5573
- const parsed = JSON.parse(stdout.trim());
5574
- if (!Array.isArray(parsed)) {
5575
- if (!isAgentMode()) {
5576
- process.stderr.write(` Transform returned non-array JSON \u2014 using original records
5577
- `);
5578
- }
5579
- resolve(null);
5580
- return;
5581
- }
5582
- resolve(parsed);
5583
- } catch {
5584
- if (!isAgentMode()) {
5585
- process.stderr.write(` Transform returned invalid JSON \u2014 using original records
5586
- `);
5587
- }
5588
- resolve(null);
5589
- }
5590
- });
5591
- child.on("error", (err) => {
5592
- clearTimeout(timer);
5593
- if (!isAgentMode()) {
5594
- process.stderr.write(` Transform failed to start: ${err.message} \u2014 using original records
5595
- `);
5596
- }
5597
- resolve(null);
5598
- });
5599
- child.stdin.write(input);
5600
- child.stdin.end();
5601
- });
5602
- }
5603
-
5604
5640
  // src/lib/sync/runner.ts
5605
5641
  var MAX_RETRIES_PER_PAGE = 3;
5606
5642
  var DEFAULT_SINCE_DAYS = 90;
@@ -6000,11 +6036,22 @@ async function syncModel(api, profile, options) {
6000
6036
  model,
6001
6037
  profile.idField,
6002
6038
  profile.connectionKey,
6003
- platform
6039
+ platform,
6040
+ {
6041
+ transform: profile.transform,
6042
+ exclude: profile.exclude,
6043
+ identityKey: profile.identityKey,
6044
+ onInsert: profile.onInsert,
6045
+ onUpdate: profile.onUpdate,
6046
+ onChange: profile.onChange
6047
+ }
6004
6048
  );
6005
6049
  enrichedTotal = enrichResult.enriched;
6006
6050
  enrichSkipped = enrichResult.skipped;
6007
6051
  enrichRateLimited = enrichResult.rateLimited;
6052
+ if (hasHooks && (profile.onUpdate || profile.onChange)) {
6053
+ hooksUpdated += enrichResult.enriched;
6054
+ }
6008
6055
  if (enrichResult.enriched > 0) {
6009
6056
  rebuildFtsIndex(db, model);
6010
6057
  }
@@ -8509,6 +8556,8 @@ The transform can be any command: \`jq\`, \`python3\`, a bash script, or \`one f
8509
8556
 
8510
8557
  **Pipeline order:** fetch \u2192 enrich \u2192 transform \u2192 **exclude** \u2192 create table \u2192 schema evolution \u2192 upsert \u2192 hooks
8511
8558
 
8559
+ Transform, exclude, identityKey, and hooks all fire in **both** phases. In Phase 1 they run on the raw list page; in Phase 2 they run again on the merged (list + enriched) record so that transforms can extract columns from fields that only appear after enrichment. Phase 2 fires \`onUpdate\`/\`onChange\` for every row it writes \u2014 \`onInsert\` is Phase-1-only because the row already exists in SQL by the time enrichment runs.
8560
+
8512
8561
  ## Cross-Platform Identity
8513
8562
 
8514
8563
  Add \`identityKey\` to a sync profile to extract a stable cross-platform identifier (e.g. email) into a normalized \`_identity\` column:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withone/cli",
3
- "version": "1.37.0",
3
+ "version": "1.37.1",
4
4
  "description": "CLI for managing One",
5
5
  "type": "module",
6
6
  "files": [