@larc-iu/plaid-client 0.2.0-alpha.3 → 0.2.0-alpha.32

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 (73) hide show
  1. package/README.md +39 -0
  2. package/package.json +13 -6
  3. package/src/apiAnnotations.d.ts +508 -0
  4. package/src/apiAnnotations.js +543 -0
  5. package/src/apiDocuments.d.ts +569 -0
  6. package/src/apiDocuments.js +551 -0
  7. package/src/apiLayers.d.ts +369 -0
  8. package/src/apiLayers.js +222 -0
  9. package/src/apiMessages.d.ts +293 -0
  10. package/src/apiMessages.js +265 -0
  11. package/src/apiPeople.d.ts +626 -0
  12. package/src/apiPeople.js +698 -0
  13. package/src/apiProject.d.ts +761 -0
  14. package/src/apiProject.js +774 -0
  15. package/src/apiServer.d.ts +457 -0
  16. package/src/apiServer.js +424 -0
  17. package/src/apiVocab.d.ts +456 -0
  18. package/src/apiVocab.js +425 -0
  19. package/src/batch.d.ts +54 -0
  20. package/src/batch.js +323 -0
  21. package/src/batchRef.d.ts +30 -0
  22. package/src/batchRef.js +128 -0
  23. package/src/client.d.ts +1321 -0
  24. package/src/client.js +743 -0
  25. package/src/codepoint.d.ts +94 -0
  26. package/src/codepoint.js +131 -1
  27. package/src/constraints.d.ts +85 -0
  28. package/src/constraints.js +112 -0
  29. package/src/created.d.ts +2 -0
  30. package/src/created.js +29 -0
  31. package/src/documentLock.d.ts +172 -0
  32. package/src/documentLock.js +384 -0
  33. package/src/entryForms.d.ts +68 -0
  34. package/src/entryForms.js +164 -0
  35. package/src/events.d.ts +60 -0
  36. package/src/events.js +271 -0
  37. package/src/families.d.ts +349 -0
  38. package/src/families.js +396 -0
  39. package/src/http.d.ts +233 -0
  40. package/src/http.js +809 -93
  41. package/src/ids.d.ts +26 -0
  42. package/src/ids.js +96 -0
  43. package/src/index.d.ts +101 -0
  44. package/src/index.js +183 -2105
  45. package/src/metadataOps.d.ts +15 -0
  46. package/src/metadataOps.js +124 -0
  47. package/src/operations.d.ts +36 -0
  48. package/src/operations.js +164 -0
  49. package/src/pagination.d.ts +49 -0
  50. package/src/pagination.js +20 -53
  51. package/src/provenance.d.ts +243 -0
  52. package/src/provenance.js +458 -36
  53. package/src/replayed.d.ts +3 -0
  54. package/src/replayed.js +31 -0
  55. package/src/requester.d.ts +60 -0
  56. package/src/requester.js +74 -0
  57. package/src/roles.d.ts +104 -0
  58. package/src/roles.js +64 -2
  59. package/src/serviceSchema.d.ts +337 -0
  60. package/src/serviceSchema.js +177 -60
  61. package/src/services.d.ts +319 -0
  62. package/src/services.js +742 -58
  63. package/src/sse.d.ts +30 -0
  64. package/src/sse.js +80 -3
  65. package/src/testing.d.ts +128 -0
  66. package/src/testing.js +379 -0
  67. package/src/textEdits.d.ts +59 -0
  68. package/src/textEdits.js +234 -0
  69. package/src/transforms.d.ts +25 -0
  70. package/src/transforms.js +11 -1
  71. package/src/types.d.ts +867 -0
  72. package/src/types.js +663 -0
  73. package/index.d.ts +0 -532
@@ -0,0 +1,15 @@
1
+ export function isReservedMetadataKey(key: string): boolean;
2
+ export function metadataOps(fragment: object | null | undefined): MetadataOp[];
3
+ export function applyMetadataOps(metadata: object | null | undefined, ops: MetadataOp[]): Record<string, any>;
4
+ export function mergeMetadata(metadata: object | null | undefined, fragment: object | null | undefined): Record<string, any>;
5
+ /**
6
+ * One metadata edit: the body of a metadata PATCH is a list of these.
7
+ */
8
+ export type MetadataOp = {
9
+ op: "set";
10
+ path: string[];
11
+ value: any;
12
+ } | {
13
+ op: "delete";
14
+ path: string[];
15
+ };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Metadata ops: the body of every metadata PATCH, and the `metadata` of a
3
+ * bulk update entry. An op is `{op: 'set', path, value}` or
4
+ * `{op: 'delete', path}`, where `path` is a non-empty array of keys into the
5
+ * nested metadata, the first a top-level key. See the manual, "Metadata".
6
+ */
7
+
8
+ import { PLAID_NAMESPACE } from './roles.js';
9
+ import { PROVENANCE_KEYS } from './provenance.js';
10
+
11
+ /**
12
+ * One metadata edit: the body of a metadata PATCH is a list of these.
13
+ * @typedef {{op: "set", path: string[], value: any} | {op: "delete", path: string[]}} MetadataOp
14
+ */
15
+
16
+ const RESERVED_KEYS = new Set([PLAID_NAMESPACE, ...PROVENANCE_KEYS]);
17
+
18
+ /**
19
+ * True for a top-level metadata key Plaid keeps for itself: the shared `plaid`
20
+ * namespace (settings every app reads, such as the text direction) and the
21
+ * provenance keys. Neither is ever a user's metadata field, so an app neither
22
+ * lists it as one nor writes a field's value to it.
23
+ * @param {string} key
24
+ * @returns {boolean}
25
+ */
26
+ export const isReservedMetadataKey = (key) => RESERVED_KEYS.has(key);
27
+
28
+ // The longest top-level key the server takes, in UTF-16 code units (Java's
29
+ // String length, which is what it counts).
30
+ const MAX_KEY_LENGTH = 200;
31
+
32
+ // Java's Character.isWhitespace, which the server's blank check uses: the
33
+ // Unicode space separators except the three no-break spaces, and the ASCII
34
+ // whitespace controls.
35
+ const isJavaWhitespace = (c) =>
36
+ (/[\p{Zs}\p{Zl}\p{Zp}]/u.test(c) && c !== '\u00A0' && c !== '\u2007' && c !== '\u202F') ||
37
+ /[\t\n\u000B\f\r\u001C-\u001F]/.test(c);
38
+
39
+ /**
40
+ * True when the server accepts `k` as a top-level metadata key: not blank,
41
+ * at most 200 UTF-16 code units, and no ASCII control character.
42
+ * @param {string} k
43
+ * @returns {boolean}
44
+ */
45
+ const validMetadataKey = (k) =>
46
+ typeof k === 'string' &&
47
+ k.length <= MAX_KEY_LENGTH &&
48
+ !/[\u0000-\u001F\u007F]/.test(k) &&
49
+ ![...k].every(isJavaWhitespace);
50
+
51
+ const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
52
+
53
+ /**
54
+ * The ops that set each top-level key of `fragment`, deleting a key whose
55
+ * value is null. This is how a provenance fragment (verifyOnEdit,
56
+ * contributeOnEdit, ...) is sent as a patch.
57
+ * @param {object|null|undefined} fragment
58
+ * @returns {MetadataOp[]}
59
+ */
60
+ export const metadataOps = (fragment) =>
61
+ Object.entries(fragment || {}).map(([k, v]) =>
62
+ v === null ? { op: 'delete', path: [k] } : { op: 'set', path: [k], value: v },
63
+ );
64
+
65
+ const applyOp = (node, path, depth, o) => {
66
+ const [k, ...more] = path.slice(depth);
67
+ if (more.length === 0) {
68
+ const out = { ...node };
69
+ if (o.op === 'set') out[k] = o.value;
70
+ else delete out[k];
71
+ return out;
72
+ }
73
+ const child = Object.prototype.hasOwnProperty.call(node, k) ? node[k] : undefined;
74
+ if (child === undefined) {
75
+ return o.op === 'set' ? { ...node, [k]: applyOp({}, path, depth + 1, o) } : node;
76
+ }
77
+ if (!isObject(child)) {
78
+ throw new Error(
79
+ `Metadata path ${JSON.stringify(path.slice(0, depth + 1))} holds a value that is not an object`,
80
+ );
81
+ }
82
+ return { ...node, [k]: applyOp(child, path, depth + 1, o) };
83
+ };
84
+
85
+ /**
86
+ * Apply ops to a local copy of an entity's metadata the way the server does,
87
+ * for an optimistic update. Returns a new object and never mutates its input.
88
+ * Throws on part of what the server refuses: an empty path, an op other than
89
+ * set or delete, a path through a non-object, or a first key that is blank,
90
+ * over 200 characters or holds a control character. The server's caps on
91
+ * depth, key count, string length and size are not checked here.
92
+ * @param {object|null|undefined} metadata
93
+ * @param {MetadataOp[]} ops
94
+ * @returns {Record<string, any>}
95
+ */
96
+ export const applyMetadataOps = (metadata, ops) =>
97
+ (ops || []).reduce((m, o) => {
98
+ if (!Array.isArray(o.path) || o.path.length === 0) {
99
+ throw new Error('A metadata op needs a non-empty path');
100
+ }
101
+ if (!validMetadataKey(o.path[0])) {
102
+ throw new Error('Invalid metadata key');
103
+ }
104
+ if (o.op !== 'set' && o.op !== 'delete') {
105
+ throw new Error(`Unknown metadata op '${o.op}': expected set or delete`);
106
+ }
107
+ if (o.op === 'set' && !('value' in o)) {
108
+ throw new Error('A set op needs a value');
109
+ }
110
+ return applyOp(m, o.path, 0, o);
111
+ }, { ...(metadata || {}) });
112
+
113
+ /**
114
+ * Merge a top-level fragment into a local copy, a null value deleting the
115
+ * key: applyMetadataOps over metadataOps(fragment), so the same result as
116
+ * sending those ops, refused where the server would refuse them. How an app
117
+ * mirrors a provenance stamp (verifyOnEdit, contributeOnEdit, ...) on the row
118
+ * it shows. Returns a new object.
119
+ * @param {object|null|undefined} metadata
120
+ * @param {object|null|undefined} fragment
121
+ * @returns {Record<string, any>}
122
+ */
123
+ export const mergeMetadata = (metadata, fragment) =>
124
+ applyMetadataOps(metadata, metadataOps(fragment));
@@ -0,0 +1,36 @@
1
+ /**
2
+ * End the key frame `frame` of `group`, wherever it is among the frames open:
3
+ * the frame under it, or the one open over it, numbers from then on. A plain
4
+ * function, so a caller that borrows withOperation onto another object
5
+ * (test doubles do) keeps it.
6
+ */
7
+ export function dropFrame(group: any, frame: any): void;
8
+ /**
9
+ * The key for the next keyed request, and the strict-mode stamp it must
10
+ * claim when an earlier run of its operation already sent it. `joins` says
11
+ * whether the request joins the open logical operation.
12
+ * Returns `{ key, pin, record }`: `pin` is undefined when nothing is pinned,
13
+ * and `record(version)` pins what this request claimed, the first time.
14
+ */
15
+ export function nextIdempotencyKey(client: any, joins: any): {
16
+ key: string;
17
+ pin: any;
18
+ record: (version: any) => void;
19
+ };
20
+ /** See PlaidClient.beginOperation. */
21
+ export function beginOperation(client: any, message: any, { groupId, kind, ref, keys, minted }?: {}): any;
22
+ /** See PlaidClient.keySeed. */
23
+ export function keySeed(): {
24
+ seed: string;
25
+ stamps: Map<any, any>;
26
+ };
27
+ /** See PlaidClient.endOperation. */
28
+ export function endOperation(client: any, message: any): Promise<void>;
29
+ /** See PlaidClient.withOperation. */
30
+ export function withOperation(client: any, message: any, fn: any, { kind, ref, groupId, keys, minted }?: {}): Promise<any>;
31
+ /**
32
+ * The kinds an operation may say it is (`?group-kind=`), the server's closed
33
+ * list (plaid.sql.operation-group/kinds). An operation of any other kind is
34
+ * refused when it begins, before it writes anything.
35
+ */
36
+ export const OPERATION_KINDS: string[];
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Logical operations: the audit-log group a run of writes is labelled with,
3
+ * its kind and reference, and the Idempotency-Keys that make a rerun send the
4
+ * same requests. The client's beginOperation, keySeed, endOperation and
5
+ * withOperation (whose notes say what each does) call these, with the client
6
+ * as `client`. The twin of plaid-client-py's operations.py.
7
+ */
8
+
9
+ import { uuidv4, uuidv7 } from "./ids.js";
10
+
11
+ /**
12
+ * The kinds an operation may say it is (`?group-kind=`), the server's closed
13
+ * list (plaid.sql.operation-group/kinds). An operation of any other kind is
14
+ * refused when it begins, before it writes anything.
15
+ */
16
+ export const OPERATION_KINDS = [
17
+ "assistant-plan",
18
+ "service-run",
19
+ "import",
20
+ "bulk-edit",
21
+ "guess-adoption",
22
+ "repair",
23
+ "review",
24
+ ];
25
+
26
+ /**
27
+ * End the key frame `frame` of `group`, wherever it is among the frames open:
28
+ * the frame under it, or the one open over it, numbers from then on. A plain
29
+ * function, so a caller that borrows withOperation onto another object
30
+ * (test doubles do) keeps it.
31
+ */
32
+ export function dropFrame(group, frame) {
33
+ if (!group || !frame || group.frames[0] === frame) return;
34
+ group.frames = group.frames.filter((f) => f !== frame);
35
+ }
36
+
37
+ /**
38
+ * The key for the next keyed request, and the strict-mode stamp it must
39
+ * claim when an earlier run of its operation already sent it. `joins` says
40
+ * whether the request joins the open logical operation.
41
+ * Returns `{ key, pin, record }`: `pin` is undefined when nothing is pinned,
42
+ * and `record(version)` pins what this request claimed, the first time.
43
+ */
44
+ export function nextIdempotencyKey(client, joins) {
45
+ const frame = joins ? client.operationGroup?.frames.at(-1) : null;
46
+ const keys = frame?.keys;
47
+ if (!keys) return { key: uuidv7(), pin: undefined, record: () => {} };
48
+ const n = frame.count;
49
+ frame.count += 1;
50
+ const pinned = keys.stamps.has(n);
51
+ return {
52
+ key: `${keys.seed}.${n}`,
53
+ pin: pinned ? keys.stamps.get(n) : undefined,
54
+ record: (version) => {
55
+ if (!pinned) keys.stamps.set(n, version ?? null);
56
+ },
57
+ };
58
+ }
59
+
60
+ /** See PlaidClient.beginOperation. */
61
+ export function beginOperation(client, message, { groupId, kind, ref, keys, minted } = {}) {
62
+ if (kind != null && !OPERATION_KINDS.includes(String(kind))) {
63
+ throw new Error(
64
+ `Unknown operation kind "${kind}". It is one of: ${OPERATION_KINDS.join(", ")}.`,
65
+ );
66
+ }
67
+ const frame = {
68
+ keys: keys || null,
69
+ count: 0,
70
+ // Any iterable of ids (a Set, an array).
71
+ minted: minted ? new Set(minted) : null,
72
+ depth: 1,
73
+ };
74
+ const open = client.operationGroup;
75
+ client._openedFrame = null;
76
+ if (open) {
77
+ // A nested operation joins the outer one. When it brings its own key
78
+ // seed or ids (a queued edit run inside a longer operation), it numbers
79
+ // the keys in a frame of its own until it ends, then the frame under it
80
+ // numbers again.
81
+ open.depth += 1;
82
+ if (keys || minted) {
83
+ frame.depth = open.depth;
84
+ open.frames.push(frame);
85
+ client._openedFrame = frame;
86
+ }
87
+ return open.id;
88
+ }
89
+ client.operationGroup = {
90
+ id: groupId ? String(groupId) : uuidv4(),
91
+ message: message == null ? null : String(message),
92
+ kind: kind == null ? null : String(kind),
93
+ ref: ref == null ? null : String(ref),
94
+ depth: 1,
95
+ written: false,
96
+ refined: undefined,
97
+ frames: [frame],
98
+ };
99
+ client._openedFrame = frame;
100
+ return client.operationGroup.id;
101
+ }
102
+
103
+ /** See PlaidClient.keySeed. */
104
+ export function keySeed() {
105
+ return { seed: uuidv7(), stamps: new Map() };
106
+ }
107
+
108
+ /** See PlaidClient.endOperation. */
109
+ export async function endOperation(client, message) {
110
+ const group = client.operationGroup;
111
+ if (!group) return;
112
+ if (group.depth > 1) {
113
+ // A frame begun at client depth by hand (beginOperation) ends here. One
114
+ // begun by withOperation is ended by that call itself (_dropFrame).
115
+ const top = group.frames.at(-1);
116
+ if (group.frames.length > 1 && !top.owned && top.depth === group.depth) {
117
+ group.frames.pop();
118
+ }
119
+ group.depth -= 1;
120
+ return;
121
+ }
122
+ client.operationGroup = null;
123
+ const refined = message !== undefined ? message : group.refined;
124
+ if (refined !== undefined && group.written) {
125
+ try {
126
+ await client.operationGroups.update(group.id, refined);
127
+ } catch (e) {
128
+ // 404: the group never materialized server-side (every tagged write
129
+ // failed or a batch was aborted). Nothing to relabel — not an error.
130
+ if (e.status !== 404) throw e;
131
+ }
132
+ }
133
+ }
134
+
135
+ /** See PlaidClient.withOperation. */
136
+ export async function withOperation(client, message, fn, { kind, ref, groupId, keys, minted } = {}) {
137
+ client.beginOperation(message, { kind, ref, groupId, keys, minted });
138
+ const group = client.operationGroup;
139
+ // The key frame client call opened is ended by client call, whatever else
140
+ // began or ended meanwhile, not by whichever end comes at its depth.
141
+ const frame = client._openedFrame;
142
+ if (frame) frame.owned = true;
143
+ const setMessage = (msg) => {
144
+ if (group.depth === 1) group.refined = msg;
145
+ };
146
+ let result;
147
+ try {
148
+ result = await fn(setMessage);
149
+ } catch (e) {
150
+ // A relabel the server refuses (a token scoped to projects may not
151
+ // relabel a group, which names none) must not take the place of the
152
+ // error `fn` threw: the caller answers for that one.
153
+ dropFrame(group, frame);
154
+ try {
155
+ await client.endOperation();
156
+ } catch (relabel) {
157
+ console.warn('The operation was not relabelled:', relabel);
158
+ }
159
+ throw e;
160
+ }
161
+ dropFrame(group, frame);
162
+ await client.endOperation();
163
+ return result;
164
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Fetch every page and return the full flat array of entries, transparently
3
+ * following `nextCursor` until it is null. This is what `.list()` calls.
4
+ *
5
+ * @param {object} client - PlaidClient instance
6
+ * @param {string} path - API path, e.g. '/api/v1/projects'
7
+ * @param {object} [opts]
8
+ * @param {number} [opts.pageSize=1000] - Per-request page size (limit)
9
+ * @param {object} [opts.query={}] - Extra query params (e.g. { 'as-of': asOf })
10
+ * @returns {Promise<Array>} The concatenated entries across all pages
11
+ */
12
+ export function listAll(client: object, path: string, { pageSize, query }?: {
13
+ pageSize?: number;
14
+ query?: object;
15
+ }): Promise<any[]>;
16
+ /**
17
+ * Fetch a single page and return the raw envelope.
18
+ *
19
+ * @param {object} client - PlaidClient instance
20
+ * @param {string} path - API path
21
+ * @param {object} [opts]
22
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
23
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
24
+ * @param {object} [opts.query={}] - Extra query params
25
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
26
+ */
27
+ export function listPage(client: object, path: string, { limit, cursor, query }?: {
28
+ limit?: number;
29
+ cursor?: string;
30
+ query?: object;
31
+ }): Promise<{
32
+ entries: any[];
33
+ nextCursor: (string | null);
34
+ }>;
35
+ /**
36
+ * Async generator yielding each page's entries array in turn, following
37
+ * `nextCursor` until it is null.
38
+ *
39
+ * @param {object} client - PlaidClient instance
40
+ * @param {string} path - API path
41
+ * @param {object} [opts]
42
+ * @param {number} [opts.pageSize=1000] - Per-request page size (limit)
43
+ * @param {object} [opts.query={}] - Extra query params
44
+ * @yields {Array} The entries array for each page
45
+ */
46
+ export function iterPages(client: object, path: string, { pageSize, query }?: {
47
+ pageSize?: number;
48
+ query?: object;
49
+ }): AsyncGenerator<any, void, unknown>;
package/src/pagination.js CHANGED
@@ -10,6 +10,10 @@
10
10
  * Each helper threads the client through to `client._request`, so auth
11
11
  * headers, base URL, and response transforms all apply exactly as for any
12
12
  * other request.
13
+ *
14
+ * A page is a read, so it goes over the wire even when the helper is handed a
15
+ * batch (see the note at the top of http.js): each page is answered from the
16
+ * pool, in order, against the state no batch has committed yet.
13
17
  */
14
18
 
15
19
  // Merge caller query params with paging params, dropping undefined/null so we
@@ -21,15 +25,21 @@ function buildQueryParams(query, limit, cursor) {
21
25
  return params;
22
26
  }
23
27
 
28
+ // Every collection endpoint answers with the envelope. A bare array or any
29
+ // other shape is a broken endpoint, so name it and stop rather than return a
30
+ // silently truncated result.
31
+ function entriesOf(response, path) {
32
+ if (!response || !Array.isArray(response.entries)) {
33
+ throw new Error(
34
+ `GET ${path} did not return a paginated envelope (no 'entries' array)`,
35
+ );
36
+ }
37
+ return response.entries;
38
+ }
39
+
24
40
  /**
25
41
  * Fetch every page and return the full flat array of entries, transparently
26
- * following `nextCursor` until it is null. This is what `.list()` calls so it
27
- * stays backward compatible with the pre-pagination (bare-array) contract.
28
- *
29
- * NOTE: This auto-paginates and therefore CANNOT be used inside a batch — each
30
- * page's request needs the previous page's `nextCursor`, which doesn't exist
31
- * until the batch executes. It throws immediately when `client.isBatching` is
32
- * true. Use `listPage` for a single page inside a batch.
42
+ * following `nextCursor` until it is null. This is what `.list()` calls.
33
43
  *
34
44
  * @param {object} client - PlaidClient instance
35
45
  * @param {string} path - API path, e.g. '/api/v1/projects'
@@ -39,41 +49,8 @@ function buildQueryParams(query, limit, cursor) {
39
49
  * @returns {Promise<Array>} The concatenated entries across all pages
40
50
  */
41
51
  export async function listAll(client, path, { pageSize = 1000, query = {} } = {}) {
42
- if (client.isBatching) {
43
- throw new Error(
44
- `Cannot auto-paginate ${path} inside a batch: list methods follow cursors across multiple requests, which a batch cannot do. Use listPage() for a single page inside a batch, or call list() outside the batch.`,
45
- );
46
- }
47
52
  const all = [];
48
- let cursor = null;
49
- let prevCursor;
50
- do {
51
- const response = await client._request('GET', path, {
52
- queryParams: buildQueryParams(query, pageSize, cursor),
53
- });
54
- // Compatibility shim: a non-paginated server (or proxy) may return a bare
55
- // array. Treat it as a terminal full result with no further paging.
56
- if (Array.isArray(response)) {
57
- all.push(...response);
58
- break;
59
- }
60
- if (response && Array.isArray(response.entries)) {
61
- all.push(...response.entries);
62
- } else {
63
- throw new Error(
64
- "Unexpected list response shape (no 'entries'); server may be incompatible.",
65
- );
66
- }
67
- prevCursor = cursor;
68
- cursor = response.nextCursor;
69
- // Guard against a buggy server/proxy that returns a constant non-null
70
- // cursor, which would otherwise loop forever.
71
- if (cursor !== null && cursor !== undefined && cursor === prevCursor) {
72
- throw new Error(
73
- 'Pagination cursor did not advance; aborting to avoid an infinite loop.',
74
- );
75
- }
76
- } while (cursor !== null && cursor !== undefined);
53
+ for await (const entries of iterPages(client, path, { pageSize, query })) all.push(...entries);
77
54
  return all;
78
55
  }
79
56
 
@@ -98,11 +75,6 @@ export async function listPage(client, path, { limit, cursor, query = {} } = {})
98
75
  * Async generator yielding each page's entries array in turn, following
99
76
  * `nextCursor` until it is null.
100
77
  *
101
- * NOTE: This auto-paginates and therefore CANNOT be used inside a batch — each
102
- * page's request needs the previous page's `nextCursor`, which doesn't exist
103
- * until the batch executes. It throws on first iteration when
104
- * `client.isBatching` is true. Use `listPage` for a single page inside a batch.
105
- *
106
78
  * @param {object} client - PlaidClient instance
107
79
  * @param {string} path - API path
108
80
  * @param {object} [opts]
@@ -111,18 +83,13 @@ export async function listPage(client, path, { limit, cursor, query = {} } = {})
111
83
  * @yields {Array} The entries array for each page
112
84
  */
113
85
  export async function* iterPages(client, path, { pageSize = 1000, query = {} } = {}) {
114
- if (client.isBatching) {
115
- throw new Error(
116
- `Cannot auto-paginate ${path} inside a batch: list methods follow cursors across multiple requests, which a batch cannot do. Use listPage() for a single page inside a batch, or call list() outside the batch.`,
117
- );
118
- }
119
86
  let cursor = null;
120
87
  let prevCursor;
121
88
  do {
122
89
  const response = await client._request('GET', path, {
123
90
  queryParams: buildQueryParams(query, pageSize, cursor),
124
91
  });
125
- const entries = (response && Array.isArray(response.entries)) ? response.entries : [];
92
+ const entries = entriesOf(response, path);
126
93
  // Suppress the trailing empty page that the server emits when a collection's
127
94
  // size is an exact multiple of the page size (a final full page with a
128
95
  // non-null cursor, then an empty page). Still follow the cursor below.
@@ -130,7 +97,7 @@ export async function* iterPages(client, path, { pageSize = 1000, query = {} } =
130
97
  yield entries;
131
98
  }
132
99
  prevCursor = cursor;
133
- cursor = response ? response.nextCursor : null;
100
+ cursor = response.nextCursor;
134
101
  // Guard against a buggy server/proxy that returns a constant non-null
135
102
  // cursor, which would otherwise loop forever.
136
103
  if (cursor !== null && cursor !== undefined && cursor === prevCursor) {