feat(syncs): Expose listRecords for use in syncs#5676
Conversation
There was a problem hiding this comment.
Suggested change to correct model-name validation for listRecords argument handling.
Status: Changes Suggested | Risk: Medium
Issues Identified & Suggestions
- Fix model-name validation to check first argument for listRecords:
packages/cli/lib/services/parser.service.ts
Review Details
📁 9 files reviewed | 💬 1 comments
Instruction Files
└── .claude/
├── agents/
│ └── nango-docs-migrator.md
└── skills/
├── agent-builder-skill/
│ ├── EXAMPLES.md
│ └── SKILL.md
├── creating-integration-docs/
│ └── SKILL.md
└── creating-skills-skill/
└── SKILL.md
👍 / 👎 individual comments to help improve reviews for you
| 'deleteRecordsFromPreviousExecutions', | ||
| 'trackDeletesStart', | ||
| 'getRecordsByIds', | ||
| 'listRecords', |
There was a problem hiding this comment.
[Logic] The model-name validation below only runs when args.length > 1 and always checks the last argument. For listRecords(model, options?), the model is the first arg, so invalid model names will never be flagged (both when options is omitted and when it’s present).
Fix: handle listRecords explicitly (or map method → model-arg index) so the model name check uses args[0] and runs when args.length >= 1.
Context for Agents
The model-name validation below only runs when `args.length > 1` and always checks the **last** argument. For `listRecords(model, options?)`, the model is the **first** arg, so invalid model names will never be flagged (both when `options` is omitted and when it’s present).
Fix: handle `listRecords` explicitly (or map method → model-arg index) so the model name check uses `args[0]` and runs when `args.length >= 1`.
File: packages/cli/lib/services/parser.service.ts
Line: 95There was a problem hiding this comment.
Changed the call signature to have model be the final argument like all other functions. Will update validation if the team requests it instead. Assuming the model is the final argument does feel a bit hacky 🤷
There was a problem hiding this comment.
I must admit it's a weird convention. Also, it's the only required argument out of the 3 which feels weird to have to potentially have to call with (undefined, undefined, 'FooModel'). I'd prefer for it to be the first argument
There was a problem hiding this comment.
Per @kaposke, this class is actually deprecated and only used for the zero yaml migration script. I've reverted the call signature back to the orginal (model: string, options?: {...}), added an @deprecated comment to this file, and skipped updating the model arg validation.
| listRecords<T extends object = any>( | ||
| cursor: string | undefined, | ||
| limit: number | undefined, | ||
| model: string |
There was a problem hiding this comment.
I'd pass an object like the node client equivalent function.
limit should have a default value when not provided. Maybe quite a small number to discourage any heavy processing
There was a problem hiding this comment.
I originally had it that way, but there was a check in parser.service.ts that looks for the model arg in the last position per this AI note:
const modelArg = args[args.length - 1];
Want me to revert the signature to:
public async listRecords<T = any>( model: string, options?: { cursor?: string; limit?: number; } )
And update the parser to allow this?
There was a problem hiding this comment.
optional cursor/limit before the model looks weird. I think we should modify the parser to allow optional params after the model.
| throw res.error; | ||
| } | ||
|
|
||
| const raw = res.unwrap(); |
There was a problem hiding this comment.
unwrap is not to be used outside of tests.
since you have an isErr() guard you can get the value with res.value.
| }; | ||
| if (cursor !== undefined) { | ||
| pageOptions.cursor = cursor; | ||
| } |
There was a problem hiding this comment.
you can do
const pageOptions = {
externalIds: Array.from(externalIdMap.keys()),
...(cursor ? { cursor } : {} )
}
|
|
||
| for (const record of records) { | ||
| const stringId = String(record.id); | ||
| const stringId = String((record as Record<string, unknown>)['id']); |
There was a problem hiding this comment.
It was because records were getting type T from the cast in fetchRecordsPage instead of carrying the type NangoRecord from the call to the persist client.
I've adjusted the types to be more explicit (from T[] to NangoRecord<T>[] and T = any to T=Record<string, any>) so it's no longer required. Let me know if I'm missing some issue with the new types though.
| } | ||
| if (limit !== undefined) { | ||
| pageOptions.limit = limit; | ||
| } |
There was a problem hiding this comment.
same comment about using the spread operator.
| 'setMetadata', | ||
| 'proxy', | ||
| 'get', | ||
| 'listRecords', // 1. Allow await nango.listRecords() at compile time |
There was a problem hiding this comment.
I think listRecords needs to be added to disallowedActionCalls. below
| cursor: string | undefined, | ||
| limit: number | undefined, | ||
| model: string | ||
| ): Promise<{ records: T[]; next_cursor: string | null }>; |
There was a problem hiding this comment.
slightly unrelated question. Should we deprecate getRecordsByIds and make ids and optional field of listRecords. Since they are basically the same thing under the hood.
There was a problem hiding this comment.
Yeah, I think that made sense, but wasn't sure what the process for deprecating looks like. Is there any action we want to take now, like an @deprecated tag on the function?
There was a problem hiding this comment.
yes a @deprecated tag to start with. don't worry in this PR you can do it later
| mockPersistClient.getRecords = vi.fn().mockResolvedValueOnce(Ok({ records, nextCursor: null })); | ||
|
|
||
| const nango = new NangoSyncRunner({ ...nangoProps }, { persistClient: mockPersistClient, locks }); | ||
| const result = await nango.listRecords(undefined, undefined, 'SomeModel'); |
There was a problem hiding this comment.
Yeah I'll have to agree with the other comments, this signature doesn't look good
| ): Promise<{ records: T[]; next_cursor: string | null }>; | ||
| listRecords<T extends Record<string, any> = Record<string, any>>( | ||
| model: string, | ||
| options?: { cursor?: string; } |
There was a problem hiding this comment.
let's keep the limit options
There was a problem hiding this comment.
Hmm... I was going back and forth on this yesterday and removed it after discussing with Marcin and Agustin.
Can you explain how you would expect the limit to apply when returning an iterator? If there is some limit on the number of rows the iterator will yield, then we need to provide a nextCursor for the user to know to pickup the remaining records. If we return nextCursor, the user will have to paginate via cursor to get all records. At that point, what's the point having an iterator vs just paginating like it was originally?
| ...(cursor ? { cursor } : {}), | ||
| ...(limit ? { limit } : {}) | ||
| }; | ||
| let cursor: string | undefined = options?.cursor ?? undefined; |
There was a problem hiding this comment.
not sure I understand what this line is achieving?
There was a problem hiding this comment.
Just storing cursor from the param. ?? undefined was a rogue autocomplete I didn't catch I think. Removed, thanks!
|
|
||
| for (const record of records) { | ||
| yield record; | ||
| } |
There was a problem hiding this comment.
should the function generate records or page of records? wdyt?
how is listRecords going to be used in the webhook debouncing use case for instance?
There was a problem hiding this comment.
I thought returning a single record at a time made it easier to use the iterator. The webhook debouncing use case will want to get all records, de-dupe ids into a map, then fetch the data for every unique id in the map. I think this could work fine returning individual records or pages, but individual records felt cleaner.
|
|
||
| if (!next_cursor) { | ||
| break; | ||
| } |
There was a problem hiding this comment.
is this break necessary. isn't the while condition be enough to exit the loop?
There was a problem hiding this comment.
Yes, sorry. It's a relic from the original implementation of the loop. Changed it from while(true)...break to a do...while and forgot to remove. Fixed now.
There was a problem hiding this comment.
No issues found; the changes appear solid and aligned with the intended functionality.
Status: No Issues Found | Risk: Low
Review Details
📁 10 files reviewed | 💬 0 comments
Instruction Files
└── .claude/
├── agents/
│ └── nango-docs-migrator.md
└── skills
listRecords needs to be exposed in sync code to allow for two-layer sync for MobileFirst to reduce sync load
NAN-5073: Expose listRecords for use in syncs
In a sample integrations repo:
Expose
listRecordsasync iterator in sync runnersThis PR introduces a new
listRecordsasync iterator to sync execution paths, enabling scripts to iterate persisted records page-by-page. The change is wired through the runner SDK, CLI sync runner, type definitions, telemetry allowlists, and persist client request handling, with abort checks during iteration and batching.Supporting updates include a shared
fetchRecordsPagehelper forgetRecordsByIds, optionallimitpropagation to the persist API, parser/compile validations, mock updates, and expanded unit tests covering abort behavior and multi-page cursor flows.This summary was automatically generated by @propel-code-bot