@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.
- package/dist/index.js +147 -98
- 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
|
-
|
|
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:
|