@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.
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 +455 -0
  16. package/src/apiServer.js +422 -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 +63 -0
  34. package/src/entryForms.js +152 -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
package/src/batch.js ADDED
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Batches: the batch a client opens (a view of the client whose writes
3
+ * queue), how its queued operations go out in requests of at most
4
+ * MAX_BATCH_OPS, and the batch POST itself, on the same send, retries and
5
+ * version learning as every other request. Which calls a batch carries is
6
+ * said at the top of http.js, and the refs a later write uses for an id an
7
+ * earlier one creates are in batchRef.js. The twin of plaid-client-py's
8
+ * batch.py.
9
+ */
10
+
11
+ import { makeBatchRef, rebaseRefs, takeRefs } from "./batchRef.js";
12
+ import {
13
+ IDEMPOTENCY_HEADER,
14
+ REPLAYED_HEADER,
15
+ extractDocumentVersions,
16
+ fetchWithTimeout,
17
+ makeHttpError,
18
+ makeNetworkError,
19
+ noteAuthError,
20
+ noteServerClock,
21
+ parseErrorBody,
22
+ prepareRequest,
23
+ restampDocumentVersion,
24
+ withRetries,
25
+ writesLanded,
26
+ writesStarted,
27
+ } from "./http.js";
28
+ import { uuidv7 } from "./ids.js";
29
+ import { nextIdempotencyKey } from "./operations.js";
30
+ import { markReplayed, wasReplayed } from "./replayed.js";
31
+ import { transformResponse } from "./transforms.js";
32
+
33
+ /**
34
+ * The server's cap on operations per batch request (plaid.rest-api.v1.batch).
35
+ * A batch queued past it is submitted as consecutive requests.
36
+ */
37
+ export const MAX_BATCH_OPS = 1000;
38
+
39
+ /**
40
+ * A batch is the client with a different `_request` (see `queueRequest`) and
41
+ * its own copy of the bundles. Everything else resolves to the client through
42
+ * the prototype: the token and base URL, strict mode and the document versions
43
+ * it tracks, the open logical operation.
44
+ * @param {any} client - PlaidClient instance
45
+ * @param {{timeout?: number | null}} [options]
46
+ */
47
+ export function openBatch(client, { timeout } = {}) {
48
+ const batch = Object.create(client);
49
+ batch.client = client;
50
+ // This batch's own timeout per request, in place of the client's
51
+ // `batchTimeout` (undefined: the client's).
52
+ batch.submitTimeout = timeout;
53
+ batch.operations = [];
54
+ // The document each queued op's strict-mode stamp is for (null for none),
55
+ // and the logical operation it joined (null for none), index for index with
56
+ // `operations`.
57
+ batch.stampedDocuments = [];
58
+ batch.stampedGroups = [];
59
+ batch.open = true;
60
+ batch._request = (method, path, options = {}) =>
61
+ queueRequest(batch, method, path, options);
62
+ batch.batch = () => {
63
+ throw new Error("A batch is not nestable: queue on the batch you have");
64
+ };
65
+ batch.batched = batch.batch;
66
+ // A stand-in for the id op n will create, for a later op's body (see
67
+ // batchRef.js). The server fills it in inside the batch's transaction.
68
+ batch.ref = (opIndex = -1, index) => makeBatchRef(batch, opIndex, index);
69
+ batch.submit = () => submitBatch(batch);
70
+ batch.abort = () => {
71
+ batch.operations = [];
72
+ batch.stampedDocuments = [];
73
+ batch.stampedGroups = [];
74
+ batch.open = false;
75
+ };
76
+ batch._installResources();
77
+ return batch;
78
+ }
79
+
80
+ export async function submitBatch(batch) {
81
+ if (!batch.open) {
82
+ throw new Error("This batch was already submitted or aborted");
83
+ }
84
+ batch.open = false;
85
+ const ops = batch.operations;
86
+ const stamps = batch.stampedDocuments;
87
+ const groups = batch.stampedGroups;
88
+ batch.operations = [];
89
+ batch.stampedDocuments = [];
90
+ batch.stampedGroups = [];
91
+ if (ops.length === 0) return [];
92
+ const client = batch.client;
93
+ const url = `${client.baseUrl}/api/v1/batch`;
94
+ // The server caps a batch at MAX_BATCH_OPS so one transaction cannot hold
95
+ // the write lock without bound. A larger batch goes as consecutive
96
+ // requests, results concatenated in queue order: it could not have been one
97
+ // transaction anyway, and a repair or bulk edit over a big document must
98
+ // not fail on its size alone. Past the first request, a strict-mode stamp
99
+ // claims the version the requests before it left (learned from their
100
+ // results), not the one the document had when the op was queued, which the
101
+ // first request has already moved on.
102
+ //
103
+ // Each request is atomic, the whole is not: a failure leaves the requests
104
+ // before it saved. The error says so, as `committed` (how many operations
105
+ // were saved) and `committedResults` (their results, in queue order), so a
106
+ // caller can tell a partial write from a batch that saved nothing. The
107
+ // failed request itself counts as unsaved, even when its answer was lost.
108
+ //
109
+ // Each request's results are marked replayed when that request was (see
110
+ // replayed.js), and the combined results when any request was: then some of
111
+ // the batch stored nothing new, which is what a caller reading the mark
112
+ // needs to know. The same holds for `committedResults`.
113
+ const results = [];
114
+ try {
115
+ await sendChunks(
116
+ client,
117
+ url,
118
+ ops,
119
+ stamps,
120
+ results,
121
+ groups,
122
+ batch.submitTimeout,
123
+ );
124
+ } catch (error) {
125
+ if (error && typeof error === "object") {
126
+ error.committed = results.length;
127
+ error.committedResults = results;
128
+ }
129
+ throw error;
130
+ }
131
+ return results;
132
+ }
133
+
134
+ /** The document-version an op's path claims, as a number, or null. */
135
+ export function claimedVersion(path) {
136
+ const v = new URLSearchParams(path.split("?")[1] ?? "").get("document-version");
137
+ return v == null ? null : Number(v);
138
+ }
139
+
140
+ export async function sendChunks(
141
+ client,
142
+ url,
143
+ ops,
144
+ stamps,
145
+ results,
146
+ groups = [],
147
+ timeout = undefined,
148
+ ) {
149
+ // Every chunk's refs are checked before the first request goes, so a batch
150
+ // whose refs cannot resolve writes nothing.
151
+ const chunks = [];
152
+ for (let i = 0; i < ops.length; i += MAX_BATCH_OPS) {
153
+ chunks.push(
154
+ ops
155
+ .slice(i, i + MAX_BATCH_OPS)
156
+ .map((op) =>
157
+ i > 0 && op.refs ? { ...op, refs: rebaseRefs(op.refs, i) } : op,
158
+ ),
159
+ );
160
+ }
161
+ for (let c = 0; c < chunks.length; c++) {
162
+ const i = c * MAX_BATCH_OPS;
163
+ let chunk = chunks[c];
164
+ // One Idempotency-Key per request, minted as the request is formed, so a
165
+ // resend of this chunk is answered from its first send when that landed.
166
+ // Inside an operation opened with keys, a chunk whose ops joined it takes
167
+ // the operation's next key and claims the version its first run claimed.
168
+ const joins = groups
169
+ .slice(i, i + chunk.length)
170
+ .some((g) => g && g === client.operationGroup);
171
+ const { key, pin, record } = nextIdempotencyKey(client, joins);
172
+ const claim = (docId) =>
173
+ pin !== undefined ? pin : client.documentVersions[docId];
174
+ if (i > 0 || pin !== undefined) {
175
+ chunk = chunk.map((op, j) => {
176
+ const docId = stamps[i + j];
177
+ if (!docId) return op;
178
+ const path = restampDocumentVersion(op.path, claim(docId));
179
+ return { ...op, path };
180
+ });
181
+ }
182
+ // Pin what the chunk actually claims: the first stamped op's version as
183
+ // its path now carries it.
184
+ const j = stamps.slice(i, i + chunk.length).findIndex(Boolean);
185
+ record(j < 0 ? null : claimedVersion(chunk[j].path));
186
+ const answered = await client._postBatch(url, chunk, { key, timeout });
187
+ results.push(...answered);
188
+ if (wasReplayed(answered)) markReplayed(results);
189
+ // The server took the chunk, so each operation it joined exists.
190
+ for (let j = i; j < i + chunk.length; j++) {
191
+ if (groups[j]) groups[j].written = true;
192
+ }
193
+ }
194
+ }
195
+
196
+ /**
197
+ * A call made on a batch (see `PlaidClient#batch`). A write of project data is
198
+ * queued as one operation of the batch and answers `{ batched: true }`; its
199
+ * result is the matching entry of what `submit()` resolves to. A read, and a
200
+ * signal marked `outOfBand`, is the client's to make and goes over the wire
201
+ * now, exactly as if it had been made on the client.
202
+ */
203
+ export async function queueRequest(batch, method, path, options = {}) {
204
+ if (method === "GET" || options.outOfBand) {
205
+ return batch.client._request(method, path, options);
206
+ }
207
+ if (!batch.open) {
208
+ throw new Error(`This batch was already submitted or aborted: ${path}`);
209
+ }
210
+ if (options.noBatch) {
211
+ throw new Error(`This endpoint cannot be used in a batch: ${path}`);
212
+ }
213
+ const { url, requestBody, stampedDocument, stampedGroup } = prepareRequest(
214
+ batch.client,
215
+ method,
216
+ path,
217
+ options,
218
+ );
219
+ const operation = {
220
+ path: url.replace(batch.client.baseUrl, ""),
221
+ method: method.toUpperCase(),
222
+ };
223
+ if (requestBody !== undefined) {
224
+ // Each b.ref() in the body goes beside it, as the path to a null the
225
+ // server fills with the id (batchRef.js).
226
+ const { body, refs } = takeRefs(batch, requestBody);
227
+ operation.body = body;
228
+ if (refs) operation.refs = refs;
229
+ }
230
+ batch.operations.push(operation);
231
+ batch.stampedDocuments.push(stampedDocument);
232
+ batch.stampedGroups.push(stampedGroup);
233
+ return { batched: true };
234
+ }
235
+
236
+ /**
237
+ * POST one batch request (at most MAX_BATCH_OPS operations) through the same
238
+ * send, retries and version learning as every other request. Resolves to one
239
+ * `{ status, headers, body }` per operation, with only the body recased: the
240
+ * headers keep the server's spelling. `key` is the request's Idempotency-Key:
241
+ * a batch whose answer is lost (no response, 502, 504) is sent again under
242
+ * it, and answered from the first send if that landed. A refused batch wrote
243
+ * nothing (it is atomic), so a 503 is retried too. `timeout` is this
244
+ * request's, in place of `batchTimeout` when given.
245
+ *
246
+ * Each sub-response's `X-Document-Versions` is learned, and the batch's own,
247
+ * which holds each document's version after the whole batch, the layer
248
+ * rules' remedies at its end included (a split that deletes a relation
249
+ * crossing the new boundary moves the version once more). The batch's own
250
+ * `X-Document-Versions-Omitted` is not taken up: each sub-response keeps its
251
+ * own list. Every answer notes the server's clock, as a single request's
252
+ * does.
253
+ * @param {object} client - PlaidClient instance
254
+ * @param {string} url
255
+ * @param {object[]} body - The operations
256
+ * @param {{key?: string, timeout?: number | null}} [options]
257
+ */
258
+ export async function postBatch(client, url, body, { key = uuidv7(), timeout } = {}) {
259
+ const fetchOptions = {
260
+ method: "POST",
261
+ headers: {
262
+ Authorization: `Bearer ${client.token}`,
263
+ "Content-Type": "application/json",
264
+ [IDEMPOTENCY_HEADER]: key,
265
+ },
266
+ body: JSON.stringify(body),
267
+ };
268
+
269
+ const started = writesStarted(client);
270
+ try {
271
+ const attempt = async () => {
272
+ let res;
273
+ try {
274
+ res = await fetchWithTimeout(
275
+ url,
276
+ fetchOptions,
277
+ timeout !== undefined ? timeout : client.batchTimeout,
278
+ );
279
+ } catch (e) {
280
+ throw makeNetworkError(e, url, "POST");
281
+ }
282
+ noteServerClock(client, res.headers);
283
+ if (!res.ok) {
284
+ throw makeHttpError(res, await parseErrorBody(res), url, "POST");
285
+ }
286
+ return res;
287
+ };
288
+ const response = await withRetries(client, attempt, { keyed: true }).catch((error) => {
289
+ error.idempotencyKey = key;
290
+ noteAuthError(client, error);
291
+ throw error;
292
+ });
293
+
294
+ const results = await response.json();
295
+ // A replayed batch carries the versions right after its first send.
296
+ const replayed = response.headers?.get?.(REPLAYED_HEADER) === "true";
297
+ const replayedHeader = replayed ? "true" : null;
298
+ for (const result of results) {
299
+ const versions = result.headers?.["X-Document-Versions"] ?? null;
300
+ extractDocumentVersions(client, {
301
+ get: (name) =>
302
+ name === "X-Document-Versions" ? versions : name === REPLAYED_HEADER ? replayedHeader : null,
303
+ });
304
+ }
305
+ extractDocumentVersions(client, {
306
+ get: (name) =>
307
+ name === "X-Document-Versions-Omitted" ? null : (response.headers?.get?.(name) ?? null),
308
+ });
309
+
310
+ writesLanded(client, started, replayed);
311
+
312
+ const out = results.map((result) => ({
313
+ ...result,
314
+ body: transformResponse(result.body),
315
+ }));
316
+ return replayed ? markReplayed(out) : out;
317
+ } catch (error) {
318
+ if (error.status !== undefined) throw error;
319
+ throw Object.assign(makeNetworkError(error, url, "POST"), {
320
+ idempotencyKey: key,
321
+ });
322
+ }
323
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * A BatchRef for op `opIndex` of `batch` (negative counts from the end, -1
3
+ * is the op queued last), fixed when it is called.
4
+ */
5
+ export function makeBatchRef(batch: any, opIndex: any, index: any): BatchRef;
6
+ /**
7
+ * `body` with each BatchRef in it replaced by null, and the `refs` that say
8
+ * where they were, or `refs` null when it holds none (the body is then
9
+ * returned as it was).
10
+ */
11
+ export function takeRefs(batch: any, body: any): {
12
+ body: any;
13
+ refs: any[];
14
+ };
15
+ /**
16
+ * `refs` counted from `start`, for an op in a batch sent in several requests
17
+ * (the server counts within one request). A ref to an op of an earlier
18
+ * request cannot resolve, so it throws before anything goes.
19
+ */
20
+ export function rebaseRefs(refs: any, start: any): any;
21
+ export class BatchRef {
22
+ constructor(batch: any, op: any, index: any);
23
+ /** The op, counted from 0 in the batch, whose id this stands for. */
24
+ $ref: any;
25
+ index: any;
26
+ belongsTo(batch: any): boolean;
27
+ toJSON(): void;
28
+ [Symbol.toPrimitive](): void;
29
+ #private;
30
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Batch refs: a write in a batch that uses the id an earlier write in the
3
+ * same batch creates.
4
+ *
5
+ * `b.ref()` returns a BatchRef, which the caller puts in a later write's
6
+ * body where the id goes. When that write is queued, each BatchRef is taken
7
+ * out of the body (null is left in its place) and recorded beside it as
8
+ * `refs: [{ at: [key or index, ...], op: n, index?: k }]`, which the server
9
+ * fills in. The server never searches a body, so user data of any shape,
10
+ * `{ $ref: 0 }` included, is stored as sent. A BatchRef is a class instance
11
+ * no JSON value can be, so data can never be taken for one here either.
12
+ */
13
+
14
+ const MISPLACED =
15
+ "b.ref() stands for an id only in the body of a later write on the same batch";
16
+
17
+ export class BatchRef {
18
+ #batch;
19
+
20
+ constructor(batch, op, index) {
21
+ this.#batch = batch;
22
+ /** The op, counted from 0 in the batch, whose id this stands for. */
23
+ this.$ref = op;
24
+ /** For a bulk create, which of its ids. */
25
+ if (index !== undefined) this.index = index;
26
+ Object.freeze(this);
27
+ }
28
+
29
+ belongsTo(batch) {
30
+ return this.#batch === batch;
31
+ }
32
+
33
+ // Sent anywhere but a queued body (a call made on the client, a path, a
34
+ // query), a ref would go out as something the server takes for data.
35
+ toJSON() {
36
+ throw new Error(MISPLACED);
37
+ }
38
+
39
+ [Symbol.toPrimitive]() {
40
+ throw new Error(MISPLACED);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * A BatchRef for op `opIndex` of `batch` (negative counts from the end, -1
46
+ * is the op queued last), fixed when it is called.
47
+ */
48
+ export function makeBatchRef(batch, opIndex, index) {
49
+ const n = opIndex < 0 ? batch.operations.length + opIndex : opIndex;
50
+ if (!Number.isInteger(n) || n < 0 || n >= batch.operations.length) {
51
+ throw new Error(`No operation ${opIndex} has been queued on this batch`);
52
+ }
53
+ if (index !== undefined && !(Number.isInteger(index) && index >= 0)) {
54
+ throw new Error(`A ref's index must be a whole number, not ${index}`);
55
+ }
56
+ return new BatchRef(batch, n, index);
57
+ }
58
+
59
+ const isPlainObject = (v) => {
60
+ if (v === null || typeof v !== "object") return false;
61
+ const proto = Object.getPrototypeOf(v);
62
+ return proto === Object.prototype || proto === null;
63
+ };
64
+
65
+ function holdsRef(v) {
66
+ if (v instanceof BatchRef) return true;
67
+ if (Array.isArray(v)) return v.some(holdsRef);
68
+ if (isPlainObject(v)) {
69
+ for (const k in v) if (holdsRef(v[k])) return true;
70
+ }
71
+ return false;
72
+ }
73
+
74
+ /**
75
+ * `body` with each BatchRef in it replaced by null, and the `refs` that say
76
+ * where they were, or `refs` null when it holds none (the body is then
77
+ * returned as it was).
78
+ */
79
+ export function takeRefs(batch, body) {
80
+ if (!holdsRef(body)) return { body, refs: null };
81
+ const refs = [];
82
+ const at = [];
83
+ const take = (v) => {
84
+ if (v instanceof BatchRef) {
85
+ if (!v.belongsTo(batch)) throw new Error(MISPLACED);
86
+ refs.push(
87
+ v.index === undefined
88
+ ? { at: [...at], op: v.$ref }
89
+ : { at: [...at], op: v.$ref, index: v.index },
90
+ );
91
+ return null;
92
+ }
93
+ if (!holdsRef(v)) return v;
94
+ if (Array.isArray(v)) {
95
+ return v.map((x, i) => {
96
+ at.push(i);
97
+ const out = take(x);
98
+ at.pop();
99
+ return out;
100
+ });
101
+ }
102
+ const out = {};
103
+ for (const k of Object.keys(v)) {
104
+ at.push(k);
105
+ out[k] = take(v[k]);
106
+ at.pop();
107
+ }
108
+ return out;
109
+ };
110
+ return { body: take(body), refs };
111
+ }
112
+
113
+ /**
114
+ * `refs` counted from `start`, for an op in a batch sent in several requests
115
+ * (the server counts within one request). A ref to an op of an earlier
116
+ * request cannot resolve, so it throws before anything goes.
117
+ */
118
+ export function rebaseRefs(refs, start) {
119
+ return refs.map((r) => {
120
+ if (r.op < start) {
121
+ throw new Error(
122
+ `Operation ${start} and later are sent in a request of their own, ` +
123
+ `so an operation among them cannot use the id operation ${r.op} creates`,
124
+ );
125
+ }
126
+ return { ...r, op: r.op - start };
127
+ });
128
+ }