@withone/cli 1.37.3 → 1.38.0
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 +1 -1
- package/dist/index.js +95 -23
- package/package.json +3 -2
- package/profiles/hacker-news/topStories.json +11 -0
package/README.md
CHANGED
|
@@ -343,7 +343,7 @@ one sync run stripe --full-refresh
|
|
|
343
343
|
| `schedule add/list/status/remove/repair` | Cron-backed scheduled syncs with drift detection |
|
|
344
344
|
| `remove <platform>` | Delete local data (`--dry-run` to preview) |
|
|
345
345
|
|
|
346
|
-
Change hooks (`onInsert`, `onUpdate`, `onChange`) fire per-page during sync — pipe to a shell command, a flow, or an event log. Run `one guide sync` for the full reference.
|
|
346
|
+
Change hooks (`onInsert`, `onUpdate`, `onChange`) fire per-page during sync — pipe to a shell command, a flow, or an event log. Root-array responses (e.g. Hacker News `/v0/topstories.json` → `[9129911, 9129199, ...]`) are supported by setting `resultsPath` to `""`, `"$"`, or `"."`; primitive elements are auto-wrapped as `{ [idField]: value }`. Run `one guide sync` for the full reference.
|
|
347
347
|
|
|
348
348
|
### `one guide [topic]`
|
|
349
349
|
|
package/dist/index.js
CHANGED
|
@@ -4727,12 +4727,15 @@ function readProfile(platform, model) {
|
|
|
4727
4727
|
}
|
|
4728
4728
|
}
|
|
4729
4729
|
function writeProfile(profile) {
|
|
4730
|
-
const required = ["platform", "model", "connectionKey", "actionId", "
|
|
4730
|
+
const required = ["platform", "model", "connectionKey", "actionId", "idField", "pagination"];
|
|
4731
4731
|
for (const field of required) {
|
|
4732
4732
|
if (!profile[field]) {
|
|
4733
4733
|
throw new Error(`Missing required field: ${field}`);
|
|
4734
4734
|
}
|
|
4735
4735
|
}
|
|
4736
|
+
if (profile.resultsPath === void 0) {
|
|
4737
|
+
throw new Error('Missing required field: resultsPath (use "" or "$" for root-array responses)');
|
|
4738
|
+
}
|
|
4736
4739
|
if (!profile.pagination.type) {
|
|
4737
4740
|
throw new Error("Missing required field: pagination.type");
|
|
4738
4741
|
}
|
|
@@ -5694,6 +5697,50 @@ function detectColumnType2(value) {
|
|
|
5694
5697
|
return "TEXT";
|
|
5695
5698
|
}
|
|
5696
5699
|
|
|
5700
|
+
// src/lib/sync/extract.ts
|
|
5701
|
+
var ROOT_PATH_TOKENS = /* @__PURE__ */ new Set(["", "$", "."]);
|
|
5702
|
+
function isRootPath(resultsPath) {
|
|
5703
|
+
return resultsPath === void 0 || resultsPath === null || ROOT_PATH_TOKENS.has(resultsPath);
|
|
5704
|
+
}
|
|
5705
|
+
function isPrimitive(value) {
|
|
5706
|
+
const t = typeof value;
|
|
5707
|
+
return t === "string" || t === "number" || t === "boolean";
|
|
5708
|
+
}
|
|
5709
|
+
function describeType(value) {
|
|
5710
|
+
if (value === null) return "null";
|
|
5711
|
+
if (Array.isArray(value)) return "array";
|
|
5712
|
+
return typeof value;
|
|
5713
|
+
}
|
|
5714
|
+
function extractRecords(responseData, resultsPath, idField, profileLabel) {
|
|
5715
|
+
let candidate;
|
|
5716
|
+
if (isRootPath(resultsPath)) {
|
|
5717
|
+
candidate = responseData;
|
|
5718
|
+
} else {
|
|
5719
|
+
candidate = getByDotPath(responseData, resultsPath);
|
|
5720
|
+
}
|
|
5721
|
+
if (!Array.isArray(candidate)) {
|
|
5722
|
+
const topKeys = typeof responseData === "object" && responseData !== null && !Array.isArray(responseData) ? Object.keys(responseData).slice(0, 10) : [];
|
|
5723
|
+
const pathLabel = isRootPath(resultsPath) ? "<root>" : `'${resultsPath}'`;
|
|
5724
|
+
const typeLabel = describeType(responseData);
|
|
5725
|
+
const keyHint = topKeys.length > 0 ? ` Top-level keys: [${topKeys.join(", ")}].` : "";
|
|
5726
|
+
throw new Error(
|
|
5727
|
+
`Could not find results at path ${pathLabel} for profile ${profileLabel}. Response top-level type is ${typeLabel}.${keyHint} Set resultsPath to the array field, or use "" / "$" / "." for root arrays.`
|
|
5728
|
+
);
|
|
5729
|
+
}
|
|
5730
|
+
if (candidate.length === 0) {
|
|
5731
|
+
return { records: [], wrappedPrimitives: false };
|
|
5732
|
+
}
|
|
5733
|
+
if (isPrimitive(candidate[0])) {
|
|
5734
|
+
const wrapped = [];
|
|
5735
|
+
for (const value of candidate) {
|
|
5736
|
+
if (!isPrimitive(value)) continue;
|
|
5737
|
+
wrapped.push({ [idField]: String(value) });
|
|
5738
|
+
}
|
|
5739
|
+
return { records: wrapped, wrappedPrimitives: true };
|
|
5740
|
+
}
|
|
5741
|
+
return { records: candidate, wrappedPrimitives: false };
|
|
5742
|
+
}
|
|
5743
|
+
|
|
5697
5744
|
// src/lib/sync/runner.ts
|
|
5698
5745
|
var MAX_RETRIES_PER_PAGE = 3;
|
|
5699
5746
|
var DEFAULT_SINCE_DAYS = 90;
|
|
@@ -5938,13 +5985,13 @@ async function syncModel(api, profile, options) {
|
|
|
5938
5985
|
throw err;
|
|
5939
5986
|
}
|
|
5940
5987
|
}
|
|
5941
|
-
const
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5988
|
+
const extraction = extractRecords(
|
|
5989
|
+
responseData,
|
|
5990
|
+
profile.resultsPath,
|
|
5991
|
+
profile.idField,
|
|
5992
|
+
`${platform}/${model}`
|
|
5993
|
+
);
|
|
5994
|
+
const records = extraction.records;
|
|
5948
5995
|
if (records.length === 0 && page === 0) {
|
|
5949
5996
|
if (!isAgentMode()) {
|
|
5950
5997
|
process.stderr.write(`No records found for ${model} with the given filters.
|
|
@@ -6243,23 +6290,36 @@ async function testSyncProfile(api, profile) {
|
|
|
6243
6290
|
return report;
|
|
6244
6291
|
}
|
|
6245
6292
|
let resolvedResultsPath = profile.resultsPath;
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
records = topObj[arrayKey];
|
|
6293
|
+
const rawCandidate = isRootPath(resolvedResultsPath) ? responseData : getByDotPath(responseData, resolvedResultsPath);
|
|
6294
|
+
let records = Array.isArray(rawCandidate) ? rawCandidate : null;
|
|
6295
|
+
if (records === null) {
|
|
6296
|
+
if (Array.isArray(responseData)) {
|
|
6297
|
+
resolvedResultsPath = "";
|
|
6298
|
+
records = responseData;
|
|
6253
6299
|
report.autoFixed = report.autoFixed ?? {};
|
|
6254
|
-
report.autoFixed.resultsPath =
|
|
6300
|
+
report.autoFixed.resultsPath = "";
|
|
6255
6301
|
checks.push({
|
|
6256
|
-
name: `resultsPath auto-discovered \u2192
|
|
6302
|
+
name: `resultsPath auto-discovered \u2192 <root>`,
|
|
6257
6303
|
ok: true,
|
|
6258
|
-
detail: `
|
|
6304
|
+
detail: `Response is a root-level array \u2014 profile had "${profile.resultsPath}"`
|
|
6259
6305
|
});
|
|
6306
|
+
} else if (typeof responseData === "object" && responseData !== null) {
|
|
6307
|
+
const topObj = responseData;
|
|
6308
|
+
const arrayKey = Object.keys(topObj).find((k) => Array.isArray(topObj[k]));
|
|
6309
|
+
if (arrayKey) {
|
|
6310
|
+
resolvedResultsPath = arrayKey;
|
|
6311
|
+
records = topObj[arrayKey];
|
|
6312
|
+
report.autoFixed = report.autoFixed ?? {};
|
|
6313
|
+
report.autoFixed.resultsPath = arrayKey;
|
|
6314
|
+
checks.push({
|
|
6315
|
+
name: `resultsPath auto-discovered \u2192 "${arrayKey}"`,
|
|
6316
|
+
ok: true,
|
|
6317
|
+
detail: `Profile had "${profile.resultsPath}" which didn't resolve; found "${arrayKey}" in response`
|
|
6318
|
+
});
|
|
6319
|
+
}
|
|
6260
6320
|
}
|
|
6261
6321
|
}
|
|
6262
|
-
if (
|
|
6322
|
+
if (records === null) {
|
|
6263
6323
|
const topKeys = typeof responseData === "object" && responseData !== null ? Object.keys(responseData) : [];
|
|
6264
6324
|
checks.push({
|
|
6265
6325
|
name: `resultsPath "${resolvedResultsPath}" \u2192 array`,
|
|
@@ -6268,10 +6328,22 @@ async function testSyncProfile(api, profile) {
|
|
|
6268
6328
|
});
|
|
6269
6329
|
return report;
|
|
6270
6330
|
}
|
|
6331
|
+
let wrappedPrimitives = false;
|
|
6332
|
+
if (records.length > 0 && typeof records[0] !== "object") {
|
|
6333
|
+
const idField = profile.idField || "id";
|
|
6334
|
+
const wrapped = [];
|
|
6335
|
+
for (const v of records) {
|
|
6336
|
+
if (typeof v === "object") continue;
|
|
6337
|
+
wrapped.push({ [idField]: String(v) });
|
|
6338
|
+
}
|
|
6339
|
+
records = wrapped;
|
|
6340
|
+
wrappedPrimitives = true;
|
|
6341
|
+
}
|
|
6342
|
+
const pathLabel = isRootPath(resolvedResultsPath) ? "<root>" : `"${resolvedResultsPath}"`;
|
|
6271
6343
|
checks.push({
|
|
6272
|
-
name: `resultsPath
|
|
6344
|
+
name: `resultsPath ${pathLabel} \u2192 array`,
|
|
6273
6345
|
ok: true,
|
|
6274
|
-
detail: `${records.length} records`
|
|
6346
|
+
detail: wrappedPrimitives ? `${records.length} primitive records (wrapped as { ${profile.idField || "id"}: value })` : `${records.length} records`
|
|
6275
6347
|
});
|
|
6276
6348
|
if (records.length === 0) {
|
|
6277
6349
|
checks.push({ name: "sample record available", ok: false, detail: "empty result set" });
|
|
@@ -8558,7 +8630,7 @@ one --agent sync sql stripe "SELECT count(*) FROM balanceTransactions"
|
|
|
8558
8630
|
\`sync init\` without \`--config\` does all of this automatically:
|
|
8559
8631
|
- **connectionKey** \u2014 auto-resolved when there's exactly one connection for the platform
|
|
8560
8632
|
- **Pagination** \u2014 Stripe id-pagination, Notion body-cursor, HubSpot/Google token, offset, link. Inapplicable fields stripped (no nextPath for offset, no passAs for none)
|
|
8561
|
-
- **resultsPath** \u2014 generic keys (data, results, items) + platform-specific (model name stripped of platform prefix: attioCompanies \u2192 companies)
|
|
8633
|
+
- **resultsPath** \u2014 generic keys (data, results, items) + platform-specific (model name stripped of platform prefix: attioCompanies \u2192 companies). Use \`""\`, \`"$"\`, or \`"."\` for responses that return a bare array at the root (e.g. Hacker News \`/v0/topstories.json\`); primitive array elements are auto-wrapped as \`{ [idField]: value }\`.
|
|
8562
8634
|
- **idField** \u2014 id, _id, uuid
|
|
8563
8635
|
- **pathVars** \u2014 extracted from URL template with smart defaults (calendarId="primary", userId="me"). Internal keys (INTERNAL_SIGNING_KEY) and record-level IDs (record_id) are stripped automatically
|
|
8564
8636
|
- **dateFilter** \u2014 updated_since, created_after, etc.
|
|
@@ -8723,7 +8795,7 @@ Fetches ALL records and deletes local rows whose IDs are no longer in the source
|
|
|
8723
8795
|
|-------|----------|-------------|
|
|
8724
8796
|
| connectionKey | yes | From \`one list\` |
|
|
8725
8797
|
| actionId | yes | Auto-resolved by \`sync init\` |
|
|
8726
|
-
| resultsPath | yes | Auto-inferred or auto-discovered by \`sync test\` |
|
|
8798
|
+
| resultsPath | yes | Auto-inferred or auto-discovered by \`sync test\`. Use \`""\` / \`"$"\` / \`"."\` for root-array responses |
|
|
8727
8799
|
| idField | yes | Auto-inferred or auto-discovered by \`sync test\` |
|
|
8728
8800
|
| pagination | yes | Auto-inferred (cursor/token/offset/id/link/none) |
|
|
8729
8801
|
| pathVars | no | Auto-extracted from URL template |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withone/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.38.0",
|
|
4
4
|
"description": "CLI for managing One",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"build": "tsup",
|
|
17
17
|
"dev": "tsup --watch",
|
|
18
18
|
"start": "node bin/cli.js",
|
|
19
|
-
"typecheck": "tsc --noEmit"
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "tsx --test \"src/**/*.test.ts\""
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
23
|
"@clack/prompts": "^0.9.1",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Hacker News top story IDs — up to 500 item IDs at /v0/topstories.json (root-array response, primitives wrapped as { id })",
|
|
3
|
+
"platform": "hacker-news",
|
|
4
|
+
"model": "topStories",
|
|
5
|
+
"connectionKey": "AUTO",
|
|
6
|
+
"actionId": "conn_mod_def::GJ3108Dwmm4::avAMAq7HQtW6PT8JhPg5vA",
|
|
7
|
+
"resultsPath": "",
|
|
8
|
+
"idField": "id",
|
|
9
|
+
"pagination": { "type": "none" },
|
|
10
|
+
"limitParam": ""
|
|
11
|
+
}
|