@larc-iu/plaid-client 0.2.0-alpha.29 → 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/package.json +3 -2
- 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/client.d.ts +1321 -0
- package/src/client.js +743 -0
- package/src/constraints.d.ts +8 -0
- package/src/constraints.js +14 -0
- package/src/documentLock.d.ts +18 -4
- package/src/documentLock.js +43 -9
- package/src/entryForms.d.ts +63 -0
- package/src/entryForms.js +152 -0
- package/src/events.js +2 -1
- package/src/families.d.ts +349 -0
- package/src/families.js +396 -0
- package/src/http.d.ts +39 -18
- package/src/http.js +127 -87
- package/src/index.d.ts +90 -4557
- package/src/index.js +113 -5172
- package/src/operations.d.ts +36 -0
- package/src/operations.js +164 -0
- package/src/provenance.d.ts +47 -4
- package/src/provenance.js +168 -12
- package/src/roles.d.ts +39 -0
- package/src/roles.js +44 -0
- package/src/serviceSchema.d.ts +19 -13
- package/src/serviceSchema.js +30 -15
- package/src/services.d.ts +20 -3
- package/src/services.js +45 -8
- package/src/sse.js +10 -1
- package/src/testing.d.ts +128 -0
- package/src/testing.js +379 -0
- package/src/types.d.ts +867 -0
- package/src/types.js +663 -0
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
|
+
}
|