@larc-iu/plaid-client 0.2.0-alpha.3 → 0.2.0-alpha.30
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 +39 -0
- package/package.json +13 -6
- package/src/apiAnnotations.d.ts +508 -0
- package/src/apiAnnotations.js +543 -0
- package/src/apiDocuments.d.ts +569 -0
- package/src/apiDocuments.js +551 -0
- package/src/apiLayers.d.ts +369 -0
- package/src/apiLayers.js +222 -0
- package/src/apiMessages.d.ts +293 -0
- package/src/apiMessages.js +265 -0
- package/src/apiPeople.d.ts +626 -0
- package/src/apiPeople.js +698 -0
- package/src/apiProject.d.ts +761 -0
- package/src/apiProject.js +774 -0
- package/src/apiServer.d.ts +455 -0
- package/src/apiServer.js +422 -0
- package/src/apiVocab.d.ts +456 -0
- package/src/apiVocab.js +425 -0
- package/src/batch.d.ts +54 -0
- package/src/batch.js +323 -0
- package/src/batchRef.d.ts +30 -0
- package/src/batchRef.js +128 -0
- package/src/client.d.ts +1321 -0
- package/src/client.js +743 -0
- package/src/codepoint.d.ts +94 -0
- package/src/codepoint.js +131 -1
- package/src/constraints.d.ts +85 -0
- package/src/constraints.js +112 -0
- package/src/created.d.ts +2 -0
- package/src/created.js +29 -0
- package/src/documentLock.d.ts +172 -0
- package/src/documentLock.js +384 -0
- package/src/entryForms.d.ts +63 -0
- package/src/entryForms.js +152 -0
- package/src/events.d.ts +60 -0
- package/src/events.js +271 -0
- package/src/families.d.ts +349 -0
- package/src/families.js +396 -0
- package/src/http.d.ts +233 -0
- package/src/http.js +809 -93
- package/src/ids.d.ts +26 -0
- package/src/ids.js +96 -0
- package/src/index.d.ts +101 -0
- package/src/index.js +183 -2105
- package/src/metadataOps.d.ts +15 -0
- package/src/metadataOps.js +124 -0
- package/src/operations.d.ts +36 -0
- package/src/operations.js +164 -0
- package/src/pagination.d.ts +49 -0
- package/src/pagination.js +20 -53
- package/src/provenance.d.ts +243 -0
- package/src/provenance.js +458 -36
- package/src/replayed.d.ts +3 -0
- package/src/replayed.js +31 -0
- package/src/requester.d.ts +60 -0
- package/src/requester.js +74 -0
- package/src/roles.d.ts +104 -0
- package/src/roles.js +64 -2
- package/src/serviceSchema.d.ts +337 -0
- package/src/serviceSchema.js +177 -60
- package/src/services.d.ts +319 -0
- package/src/services.js +742 -58
- package/src/sse.d.ts +30 -0
- package/src/sse.js +80 -3
- package/src/testing.d.ts +128 -0
- package/src/testing.js +379 -0
- package/src/textEdits.d.ts +59 -0
- package/src/textEdits.js +234 -0
- package/src/transforms.d.ts +25 -0
- package/src/transforms.js +11 -1
- package/src/types.d.ts +867 -0
- package/src/types.js +663 -0
- 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
|
|
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
|
-
|
|
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
|
|
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
|
|
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) {
|