@larc-iu/plaid-client 0.2.0-alpha.7 → 0.2.0-alpha.9
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/index.d.ts +42 -4
- package/package.json +1 -1
- package/src/http.js +193 -39
- package/src/index.js +220 -72
- package/src/provenance.js +248 -27
- package/src/services.js +145 -32
package/src/index.js
CHANGED
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
discardService,
|
|
22
22
|
serve,
|
|
23
23
|
requestService,
|
|
24
|
+
attachServiceRequest,
|
|
25
|
+
cancelServiceRequest,
|
|
24
26
|
} from "./services.js";
|
|
25
27
|
|
|
26
28
|
// Helper: normalize a document-read `layers` filter to the wire's
|
|
@@ -83,6 +85,12 @@ async function anonymousPost(baseUrl, path, body, options = {}) {
|
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
/**
|
|
89
|
+
* The server's cap on operations per batch request (plaid.rest-api.v1.batch).
|
|
90
|
+
* A batch queued past it is submitted as consecutive requests.
|
|
91
|
+
*/
|
|
92
|
+
const MAX_BATCH_OPS = 1000;
|
|
93
|
+
|
|
86
94
|
class PlaidClient {
|
|
87
95
|
/**
|
|
88
96
|
* Create a new PlaidClient instance
|
|
@@ -1247,13 +1255,13 @@ class PlaidClient {
|
|
|
1247
1255
|
auditMessage,
|
|
1248
1256
|
}),
|
|
1249
1257
|
/**
|
|
1250
|
-
* Get media file for a document
|
|
1258
|
+
* Get the media file for a document. Media is not versioned, so there is
|
|
1259
|
+
* no as-of form: the route refuses the parameter. Prefer the document's
|
|
1260
|
+
* own `mediaUrl`, which carries the file's version for caching.
|
|
1251
1261
|
* @param {string} documentId - The document ID
|
|
1252
|
-
* @param {string} [asOf] - Temporal query timestamp
|
|
1253
1262
|
*/
|
|
1254
|
-
getMedia: (documentId
|
|
1263
|
+
getMedia: (documentId) =>
|
|
1255
1264
|
this._request("GET", `/api/v1/documents/${documentId}/media`, {
|
|
1256
|
-
queryParams: { "as-of": asOf },
|
|
1257
1265
|
noBatch: true,
|
|
1258
1266
|
binaryResponse: true,
|
|
1259
1267
|
}),
|
|
@@ -1261,8 +1269,10 @@ class PlaidClient {
|
|
|
1261
1269
|
* Upload a media file for a document. Uses Apache Tika for content validation.
|
|
1262
1270
|
* @param {string} documentId - The document ID
|
|
1263
1271
|
* @param {File} file - The file to upload
|
|
1272
|
+
* @param {string} [auditMessage] - Custom audit-log message for this write
|
|
1273
|
+
* @param {object} [options] - `{ onProgress }`: called with `{ loaded, total }` (bytes) as the file goes up. In a browser the upload then travels by XMLHttpRequest, and the request timeout only fires when the upload stalls, not on total time.
|
|
1264
1274
|
*/
|
|
1265
|
-
uploadMedia: (documentId, file, auditMessage) => {
|
|
1275
|
+
uploadMedia: (documentId, file, auditMessage, { onProgress } = {}) => {
|
|
1266
1276
|
const fd = new FormData();
|
|
1267
1277
|
fd.append("file", file);
|
|
1268
1278
|
return this._request("PUT", `/api/v1/documents/${documentId}/media`, {
|
|
@@ -1270,6 +1280,7 @@ class PlaidClient {
|
|
|
1270
1280
|
body: fd,
|
|
1271
1281
|
formData: true,
|
|
1272
1282
|
noBatch: true,
|
|
1283
|
+
onUploadProgress: onProgress,
|
|
1273
1284
|
});
|
|
1274
1285
|
},
|
|
1275
1286
|
/**
|
|
@@ -1334,6 +1345,25 @@ class PlaidClient {
|
|
|
1334
1345
|
"op-types": opTypesParam(opTypes),
|
|
1335
1346
|
},
|
|
1336
1347
|
}),
|
|
1348
|
+
/**
|
|
1349
|
+
* Restore a document to its state at an earlier time, as one
|
|
1350
|
+
* operation: what was deleted since then comes back under its original
|
|
1351
|
+
* id, what was added since is removed, and what changed is set back,
|
|
1352
|
+
* across every layer. A layer deleted since then, or a vocabulary entry
|
|
1353
|
+
* that no longer exists, is skipped and reported under `skipped`.
|
|
1354
|
+
* Resolves to a summary of the changes. Maintainers only.
|
|
1355
|
+
* @param {string} documentId - The document ID
|
|
1356
|
+
* @param {string} asOf - The moment to go back to (ISO-8601 instant),
|
|
1357
|
+
* typically a history entry's `endTime`
|
|
1358
|
+
* @param {object} [options] - `{ dryRun }`: with `dryRun` true nothing
|
|
1359
|
+
* is written and the summary says what would change
|
|
1360
|
+
* @param {string} [auditMessage] - Custom audit message for this operation
|
|
1361
|
+
*/
|
|
1362
|
+
restore: (documentId, asOf, { dryRun } = {}, auditMessage) =>
|
|
1363
|
+
this._request("POST", `/api/v1/documents/${documentId}/restore`, {
|
|
1364
|
+
queryParams: { "as-of": asOf, "dry-run": dryRun ? "true" : undefined },
|
|
1365
|
+
auditMessage,
|
|
1366
|
+
}),
|
|
1337
1367
|
/**
|
|
1338
1368
|
* Get a document. Set `includeBody` to true to include all data.
|
|
1339
1369
|
*
|
|
@@ -2102,16 +2132,54 @@ class PlaidClient {
|
|
|
2102
2132
|
|
|
2103
2133
|
/**
|
|
2104
2134
|
* Request a service to perform work and await its result.
|
|
2135
|
+
*
|
|
2136
|
+
* The request outlives this call: after a timeout, an abort, or a
|
|
2137
|
+
* dropped connection the service goes on, and `attachServiceRequest`
|
|
2138
|
+
* collects the result given the request id, which `opts.onAccepted`
|
|
2139
|
+
* receives as soon as the server has taken the request. Pass
|
|
2140
|
+
* `opts.requestId` (a UUID you mint) to know the id before submitting;
|
|
2141
|
+
* submitting an id that names a request you already made rejoins it
|
|
2142
|
+
* instead of starting another.
|
|
2105
2143
|
* @param {string} projectId - The UUID of the project
|
|
2106
2144
|
* @param {string} serviceId - The ID of the service to request
|
|
2107
2145
|
* @param {any} data - The request data
|
|
2108
|
-
* @param {number} [timeout] - Timeout in
|
|
2146
|
+
* @param {number} [timeout=10000] - Timeout in ms
|
|
2109
2147
|
* @param {function} [onProgress] - Called with each progress payload {percent, message}
|
|
2110
2148
|
* @param {AbortSignal} [signal] - Abort to stop waiting; rejects with an AbortError
|
|
2111
|
-
* @
|
|
2149
|
+
* @param {Object} [opts] - {requestId, onAccepted}
|
|
2150
|
+
* @returns {Promise<any>} The service's result
|
|
2151
|
+
*/
|
|
2152
|
+
requestService: (projectId, serviceId, data, timeout, onProgress, signal, opts) =>
|
|
2153
|
+
requestService(this, projectId, serviceId, data, timeout, onProgress, signal, opts),
|
|
2154
|
+
|
|
2155
|
+
/**
|
|
2156
|
+
* Rejoin a service request made earlier and await its result: the
|
|
2157
|
+
* latest progress is replayed, then the result comes, or at once if the
|
|
2158
|
+
* request already finished. Only the user who submitted it (or an
|
|
2159
|
+
* admin). Rejects with an error whose `status` is 404 when the request
|
|
2160
|
+
* is unknown or expired (a finished request's result is kept for a
|
|
2161
|
+
* while, not forever).
|
|
2162
|
+
* @param {string} projectId - The UUID of the project
|
|
2163
|
+
* @param {string} requestId - The request id
|
|
2164
|
+
* @param {number} [timeout=10000] - Timeout in ms
|
|
2165
|
+
* @param {function} [onProgress] - Called with each progress payload
|
|
2166
|
+
* @param {AbortSignal} [signal] - Abort to stop waiting
|
|
2167
|
+
* @returns {Promise<any>} The service's result
|
|
2168
|
+
*/
|
|
2169
|
+
attachServiceRequest: (projectId, requestId, timeout, onProgress, signal) =>
|
|
2170
|
+
attachServiceRequest(this, projectId, requestId, timeout, onProgress, signal),
|
|
2171
|
+
|
|
2172
|
+
/**
|
|
2173
|
+
* Ask the service to stop a request made earlier. The request still
|
|
2174
|
+
* ends with whatever the service then reports, on the stream of whoever
|
|
2175
|
+
* is awaiting it. Rejects with 404 if unknown or expired, 409 once
|
|
2176
|
+
* finished.
|
|
2177
|
+
* @param {string} projectId - The UUID of the project
|
|
2178
|
+
* @param {string} requestId - The request id
|
|
2179
|
+
* @returns {Promise<void>}
|
|
2112
2180
|
*/
|
|
2113
|
-
|
|
2114
|
-
|
|
2181
|
+
cancelServiceRequest: (projectId, requestId) =>
|
|
2182
|
+
cancelServiceRequest(this, projectId, requestId),
|
|
2115
2183
|
};
|
|
2116
2184
|
|
|
2117
2185
|
/**
|
|
@@ -2181,15 +2249,27 @@ class PlaidClient {
|
|
|
2181
2249
|
this.comments = {
|
|
2182
2250
|
/**
|
|
2183
2251
|
* Post a comment on an entity. Requires write access to the entity's
|
|
2184
|
-
* project
|
|
2185
|
-
* audited and do not bump the
|
|
2186
|
-
*
|
|
2252
|
+
* project, or to the vocabulary for a vocab-item; the author is the
|
|
2253
|
+
* authenticated caller. Comments are not audited and do not bump the
|
|
2254
|
+
* document version.
|
|
2255
|
+
*
|
|
2256
|
+
* A comment outlives its anchor: deleting the entity does not delete
|
|
2257
|
+
* the comment. `anchorLabel` is the caption shown once that happens
|
|
2258
|
+
* ("Gloss of ktab, sentence 4"), so pass what the comment is about.
|
|
2259
|
+
* @param {'document'|'text'|'token'|'span'|'relation'|'vocab-item'} entityType - What kind of thing is being commented on
|
|
2187
2260
|
* @param {string} entityId - The commented entity's id
|
|
2188
2261
|
* @param {string} body - The comment text (1..10000 characters)
|
|
2262
|
+
* @param {object} [opts]
|
|
2263
|
+
* @param {string} [opts.anchorLabel] - What the comment is about, in words (at most 200 characters)
|
|
2189
2264
|
*/
|
|
2190
|
-
create: (entityType, entityId, body) =>
|
|
2265
|
+
create: (entityType, entityId, body, { anchorLabel } = {}) =>
|
|
2191
2266
|
this._request("POST", "/api/v1/comments", {
|
|
2192
|
-
body: bodyOf({
|
|
2267
|
+
body: bodyOf({
|
|
2268
|
+
"entity-type": entityType,
|
|
2269
|
+
"entity-id": entityId,
|
|
2270
|
+
body,
|
|
2271
|
+
"anchor-label": anchorLabel,
|
|
2272
|
+
}),
|
|
2193
2273
|
}),
|
|
2194
2274
|
/**
|
|
2195
2275
|
* Read one comment.
|
|
@@ -2290,6 +2370,47 @@ class PlaidClient {
|
|
|
2290
2370
|
},
|
|
2291
2371
|
skipResponseTransform: true,
|
|
2292
2372
|
}),
|
|
2373
|
+
/**
|
|
2374
|
+
* List the comments on a vocabulary's entries, oldest first. Requires
|
|
2375
|
+
* read access to the vocabulary. Transparently follows pagination
|
|
2376
|
+
* cursors and returns the full flat array.
|
|
2377
|
+
* Cannot be used inside a batch (auto-paginates across requests); throws if called while batching - use listInVocabPage() for a single page in a batch.
|
|
2378
|
+
* @param {string} vocabId - The vocab layer to read
|
|
2379
|
+
* @param {object} [filters]
|
|
2380
|
+
* @param {string} [filters.entityId] - One entry's thread
|
|
2381
|
+
*/
|
|
2382
|
+
listInVocab: (vocabId, { entityId } = {}) =>
|
|
2383
|
+
listAll(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
|
|
2384
|
+
query: { "entity-id": entityId },
|
|
2385
|
+
}),
|
|
2386
|
+
/**
|
|
2387
|
+
* Fetch a single page of a vocabulary's comments.
|
|
2388
|
+
* @param {string} vocabId - The vocab layer to read
|
|
2389
|
+
* @param {object} [opts]
|
|
2390
|
+
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
2391
|
+
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
2392
|
+
* @param {string} [opts.entityId] - One entry's thread
|
|
2393
|
+
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
2394
|
+
*/
|
|
2395
|
+
listInVocabPage: (vocabId, { limit, cursor, entityId } = {}) =>
|
|
2396
|
+
listPage(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
|
|
2397
|
+
limit,
|
|
2398
|
+
cursor,
|
|
2399
|
+
query: { "entity-id": entityId },
|
|
2400
|
+
}),
|
|
2401
|
+
/**
|
|
2402
|
+
* Comment counts per entry of a vocabulary, as an `{entityId: n}` map.
|
|
2403
|
+
* The response is NOT key-transformed: its keys are entry ids.
|
|
2404
|
+
* @param {string} vocabId - The vocab layer to read
|
|
2405
|
+
* @param {object} [filters]
|
|
2406
|
+
* @param {string} [filters.entityId] - One entry only
|
|
2407
|
+
* @returns {Promise<Object<string, number>>}
|
|
2408
|
+
*/
|
|
2409
|
+
countsInVocab: (vocabId, { entityId } = {}) =>
|
|
2410
|
+
this._request("GET", `/api/v1/vocab-layers/${vocabId}/comments/counts`, {
|
|
2411
|
+
queryParams: { "entity-id": entityId },
|
|
2412
|
+
skipResponseTransform: true,
|
|
2413
|
+
}),
|
|
2293
2414
|
};
|
|
2294
2415
|
}
|
|
2295
2416
|
|
|
@@ -2432,71 +2553,84 @@ class PlaidClient {
|
|
|
2432
2553
|
}
|
|
2433
2554
|
|
|
2434
2555
|
try {
|
|
2435
|
-
|
|
2436
|
-
const
|
|
2556
|
+
const url = `${this.baseUrl}/api/v1/batch`;
|
|
2557
|
+
const ops = this.batchOperations.map((op) => ({
|
|
2437
2558
|
path: op.path,
|
|
2438
2559
|
method: op.method.toUpperCase(),
|
|
2439
2560
|
...(op.body && { body: op.body }),
|
|
2440
2561
|
}));
|
|
2562
|
+
// The server caps a batch at MAX_BATCH_OPS so one transaction cannot
|
|
2563
|
+
// hold the write lock without bound. A larger batch goes as consecutive
|
|
2564
|
+
// requests, results concatenated in queue order: it could not have been
|
|
2565
|
+
// one transaction anyway, and a repair or bulk edit over a big document
|
|
2566
|
+
// must not fail on its size alone.
|
|
2567
|
+
const results = [];
|
|
2568
|
+
for (let i = 0; i < ops.length; i += MAX_BATCH_OPS) {
|
|
2569
|
+
results.push(...(await this._postBatch(url, ops.slice(i, i + MAX_BATCH_OPS))));
|
|
2570
|
+
}
|
|
2571
|
+
return results;
|
|
2572
|
+
} finally {
|
|
2573
|
+
this.isBatching = false;
|
|
2574
|
+
this.batchOperations = [];
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2441
2577
|
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
|
-
}
|
|
2578
|
+
/** POST one batch request (at most MAX_BATCH_OPS operations); resolves to its transformed results. */
|
|
2579
|
+
async _postBatch(url, body) {
|
|
2580
|
+
const fetchOptions = {
|
|
2581
|
+
method: "POST",
|
|
2582
|
+
headers: {
|
|
2583
|
+
Authorization: `Bearer ${this.token}`,
|
|
2584
|
+
"Content-Type": "application/json",
|
|
2585
|
+
},
|
|
2586
|
+
body: JSON.stringify(body),
|
|
2587
|
+
};
|
|
2450
2588
|
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2589
|
+
try {
|
|
2590
|
+
// Retry a 503: the batch is atomic, so a refused one wrote nothing and
|
|
2591
|
+
// repeating it is safe. A fresh timeout signal per attempt — an
|
|
2592
|
+
// AbortSignal.timeout stays aborted once it fires.
|
|
2593
|
+
const response = await retryWhileBusy(async () => {
|
|
2594
|
+
const signal = timeoutSignal(this.batchTimeout);
|
|
2595
|
+
const res = await fetch(
|
|
2596
|
+
url,
|
|
2597
|
+
signal ? { ...fetchOptions, signal } : fetchOptions,
|
|
2598
|
+
);
|
|
2599
|
+
if (!res.ok) {
|
|
2600
|
+
throw makeHttpError(res, await parseErrorBody(res), url, "POST");
|
|
2601
|
+
}
|
|
2602
|
+
return res;
|
|
2603
|
+
});
|
|
2466
2604
|
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
}
|
|
2483
|
-
} catch (e) {
|
|
2484
|
-
console.warn(
|
|
2485
|
-
"Failed to parse document versions header from batch response:",
|
|
2486
|
-
e,
|
|
2487
|
-
);
|
|
2605
|
+
const results = await response.json();
|
|
2606
|
+
|
|
2607
|
+
// Extract document versions from each batch response
|
|
2608
|
+
for (const result of results) {
|
|
2609
|
+
if (result.headers && result.headers["X-Document-Versions"]) {
|
|
2610
|
+
try {
|
|
2611
|
+
const versionsMap = JSON.parse(
|
|
2612
|
+
result.headers["X-Document-Versions"],
|
|
2613
|
+
);
|
|
2614
|
+
if (typeof versionsMap === "object" && versionsMap !== null) {
|
|
2615
|
+
// Clone once per response, then merge — not once per entry.
|
|
2616
|
+
this.documentVersions = {
|
|
2617
|
+
...this.documentVersions,
|
|
2618
|
+
...versionsMap,
|
|
2619
|
+
};
|
|
2488
2620
|
}
|
|
2621
|
+
} catch (e) {
|
|
2622
|
+
console.warn(
|
|
2623
|
+
"Failed to parse document versions header from batch response:",
|
|
2624
|
+
e,
|
|
2625
|
+
);
|
|
2489
2626
|
}
|
|
2490
2627
|
}
|
|
2491
|
-
|
|
2492
|
-
return results.map((result) => transformResponse(result));
|
|
2493
|
-
} catch (error) {
|
|
2494
|
-
if (error.status) throw error;
|
|
2495
|
-
throw makeNetworkError(error, url, "POST");
|
|
2496
2628
|
}
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2629
|
+
|
|
2630
|
+
return results.map((result) => transformResponse(result));
|
|
2631
|
+
} catch (error) {
|
|
2632
|
+
if (error.status) throw error;
|
|
2633
|
+
throw makeNetworkError(error, url, "POST");
|
|
2500
2634
|
}
|
|
2501
2635
|
}
|
|
2502
2636
|
|
|
@@ -2699,19 +2833,33 @@ export {
|
|
|
2699
2833
|
buildDefaultValues,
|
|
2700
2834
|
coerceParamValues,
|
|
2701
2835
|
} from "./serviceSchema.js";
|
|
2702
|
-
// Provenance: the cross-app convention for
|
|
2703
|
-
//
|
|
2704
|
-
//
|
|
2705
|
-
// "Provenance".
|
|
2836
|
+
// Provenance: the cross-app convention for who made an annotation (flat
|
|
2837
|
+
// prov/provSource/provConfirmed metadata; absence = a verifier; 'inferred' =
|
|
2838
|
+
// a machine; 'contributed' = a person whose work is reviewed), plus the
|
|
2839
|
+
// machine-writer contract. See ./provenance.js and the manual, "Provenance".
|
|
2706
2840
|
export {
|
|
2707
2841
|
PROV,
|
|
2708
2842
|
PROV_STATES,
|
|
2709
2843
|
PROV_CONFIRMED,
|
|
2710
2844
|
stampInferred,
|
|
2711
2845
|
confirmedInferred,
|
|
2846
|
+
stampContributed,
|
|
2712
2847
|
provState,
|
|
2848
|
+
provOrigin,
|
|
2713
2849
|
isMachine,
|
|
2714
2850
|
isProtected,
|
|
2851
|
+
needsReview,
|
|
2715
2852
|
verifyOnEdit,
|
|
2853
|
+
contributeOnEdit,
|
|
2854
|
+
mergeMetadata,
|
|
2716
2855
|
serviceSource,
|
|
2856
|
+
userSource,
|
|
2857
|
+
REVIEW_KEY,
|
|
2858
|
+
PROJECT_ROLES,
|
|
2859
|
+
readReview,
|
|
2860
|
+
projectRole,
|
|
2861
|
+
isReviewed,
|
|
2862
|
+
withReviewedUser,
|
|
2863
|
+
writerPolicy,
|
|
2717
2864
|
} from "./provenance.js";
|
|
2865
|
+
export { MAX_BATCH_OPS };
|