@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 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", "resultsPath", "idField", "pagination"];
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 records = getByDotPath(responseData, profile.resultsPath);
5942
- if (!Array.isArray(records)) {
5943
- const topKeys = typeof responseData === "object" && responseData !== null ? Object.keys(responseData) : [];
5944
- throw new Error(
5945
- `Could not find results at path '${profile.resultsPath}' in API response. Check your sync profile. Response keys: [${topKeys.join(", ")}]`
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
- let records = getByDotPath(responseData, resolvedResultsPath);
6247
- if (!Array.isArray(records) && typeof responseData === "object" && responseData !== null) {
6248
- const topObj = responseData;
6249
- const arrayKey = Object.keys(topObj).find((k) => Array.isArray(topObj[k]));
6250
- if (arrayKey) {
6251
- resolvedResultsPath = arrayKey;
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 = arrayKey;
6300
+ report.autoFixed.resultsPath = "";
6255
6301
  checks.push({
6256
- name: `resultsPath auto-discovered \u2192 "${arrayKey}"`,
6302
+ name: `resultsPath auto-discovered \u2192 <root>`,
6257
6303
  ok: true,
6258
- detail: `Profile had "${profile.resultsPath}" which didn't resolve; found "${arrayKey}" in response`
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 (!Array.isArray(records)) {
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 "${resolvedResultsPath}" \u2192 array`,
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.37.3",
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
+ }