@larc-iu/plaid-client 0.2.0-alpha.14 → 0.2.0-alpha.16
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 +91 -22
- package/package.json +1 -1
- package/src/http.js +112 -95
- package/src/index.js +244 -192
- package/src/pagination.js +3 -7
- package/src/serviceSchema.js +27 -1
- package/src/services.js +14 -22
package/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { transformRequest, transformResponse } from "./transforms.js";
|
|
6
6
|
import {
|
|
7
7
|
makeRequest,
|
|
8
|
+
queueRequest,
|
|
8
9
|
extractDocumentVersions,
|
|
9
10
|
parseErrorBody,
|
|
10
11
|
makeHttpError,
|
|
@@ -117,6 +118,55 @@ async function anonymousGet(baseUrl, path, options = {}) {
|
|
|
117
118
|
*/
|
|
118
119
|
const MAX_BATCH_OPS = 1000;
|
|
119
120
|
|
|
121
|
+
/**
|
|
122
|
+
* A batch is the client with a different `_request` (see `queueRequest`) and
|
|
123
|
+
* its own copy of the bundles. Everything else resolves to the client through
|
|
124
|
+
* the prototype: the token and base URL, strict mode and the document versions
|
|
125
|
+
* it tracks, the open logical operation.
|
|
126
|
+
*/
|
|
127
|
+
function openBatch(client) {
|
|
128
|
+
const batch = Object.create(client);
|
|
129
|
+
batch.client = client;
|
|
130
|
+
batch.operations = [];
|
|
131
|
+
batch.open = true;
|
|
132
|
+
batch._request = (method, path, options = {}) =>
|
|
133
|
+
queueRequest(batch, method, path, options);
|
|
134
|
+
batch.batch = () => {
|
|
135
|
+
throw new Error("A batch is not nestable: queue on the batch you have");
|
|
136
|
+
};
|
|
137
|
+
batch.batched = batch.batch;
|
|
138
|
+
batch.submit = () => submitBatch(batch);
|
|
139
|
+
batch.abort = () => {
|
|
140
|
+
batch.operations = [];
|
|
141
|
+
batch.open = false;
|
|
142
|
+
};
|
|
143
|
+
batch._installResources();
|
|
144
|
+
return batch;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function submitBatch(batch) {
|
|
148
|
+
if (!batch.open) {
|
|
149
|
+
throw new Error("This batch was already submitted or aborted");
|
|
150
|
+
}
|
|
151
|
+
batch.open = false;
|
|
152
|
+
const ops = batch.operations;
|
|
153
|
+
batch.operations = [];
|
|
154
|
+
if (ops.length === 0) return [];
|
|
155
|
+
const url = `${batch.client.baseUrl}/api/v1/batch`;
|
|
156
|
+
// The server caps a batch at MAX_BATCH_OPS so one transaction cannot hold
|
|
157
|
+
// the write lock without bound. A larger batch goes as consecutive
|
|
158
|
+
// requests, results concatenated in queue order: it could not have been one
|
|
159
|
+
// transaction anyway, and a repair or bulk edit over a big document must
|
|
160
|
+
// not fail on its size alone.
|
|
161
|
+
const results = [];
|
|
162
|
+
for (let i = 0; i < ops.length; i += MAX_BATCH_OPS) {
|
|
163
|
+
results.push(
|
|
164
|
+
...(await batch.client._postBatch(url, ops.slice(i, i + MAX_BATCH_OPS))),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return results;
|
|
168
|
+
}
|
|
169
|
+
|
|
120
170
|
class PlaidClient {
|
|
121
171
|
/**
|
|
122
172
|
* Create a new PlaidClient instance
|
|
@@ -142,8 +192,6 @@ class PlaidClient {
|
|
|
142
192
|
: options.timeout !== undefined
|
|
143
193
|
? options.timeout
|
|
144
194
|
: DEFAULT_BATCH_TIMEOUT_MS;
|
|
145
|
-
this.isBatching = false;
|
|
146
|
-
this.batchOperations = [];
|
|
147
195
|
this.documentVersions = {};
|
|
148
196
|
this.strictModeDocumentId = null;
|
|
149
197
|
// Set to a DocumentLockLost while a `documents.locked()` block's keep-alive
|
|
@@ -161,8 +209,16 @@ class PlaidClient {
|
|
|
161
209
|
this.onAuthError = options.onAuthError || null;
|
|
162
210
|
this._authErrorFired = false;
|
|
163
211
|
|
|
164
|
-
|
|
212
|
+
this._installResources();
|
|
213
|
+
}
|
|
165
214
|
|
|
215
|
+
/**
|
|
216
|
+
* The API bundles (`client.documents`, `client.tokens`, ...). Installed on
|
|
217
|
+
* the client, and again on every batch it opens (see `batch()`): a batch is
|
|
218
|
+
* a view of the client with its own `_request`, so the same bundles built
|
|
219
|
+
* on it queue instead of sending.
|
|
220
|
+
*/
|
|
221
|
+
_installResources() {
|
|
166
222
|
this.vocabLinks = {
|
|
167
223
|
/**
|
|
168
224
|
* Create a new vocab link between tokens and a vocab item.
|
|
@@ -234,7 +290,6 @@ class PlaidClient {
|
|
|
234
290
|
*/
|
|
235
291
|
get: (id, asOf) =>
|
|
236
292
|
this._request("GET", `/api/v1/vocab-links/${id}`, {
|
|
237
|
-
bypassBatch: true,
|
|
238
293
|
queryParams: { "as-of": asOf },
|
|
239
294
|
}),
|
|
240
295
|
/**
|
|
@@ -254,7 +309,6 @@ class PlaidClient {
|
|
|
254
309
|
*/
|
|
255
310
|
get: (id, includeItems, asOf) =>
|
|
256
311
|
this._request("GET", `/api/v1/vocab-layers/${id}`, {
|
|
257
|
-
bypassBatch: true,
|
|
258
312
|
queryParams: { "include-items": includeItems, "as-of": asOf },
|
|
259
313
|
}),
|
|
260
314
|
/**
|
|
@@ -413,7 +467,6 @@ class PlaidClient {
|
|
|
413
467
|
*/
|
|
414
468
|
get: (relationId, asOf) =>
|
|
415
469
|
this._request("GET", `/api/v1/relations/${relationId}`, {
|
|
416
|
-
bypassBatch: true,
|
|
417
470
|
queryParams: { "as-of": asOf },
|
|
418
471
|
}),
|
|
419
472
|
/**
|
|
@@ -532,7 +585,6 @@ class PlaidClient {
|
|
|
532
585
|
*/
|
|
533
586
|
get: (spanLayerId, asOf) =>
|
|
534
587
|
this._request("GET", `/api/v1/span-layers/${spanLayerId}`, {
|
|
535
|
-
bypassBatch: true,
|
|
536
588
|
queryParams: { "as-of": asOf },
|
|
537
589
|
}),
|
|
538
590
|
/**
|
|
@@ -611,7 +663,6 @@ class PlaidClient {
|
|
|
611
663
|
*/
|
|
612
664
|
get: (spanId, asOf) =>
|
|
613
665
|
this._request("GET", `/api/v1/spans/${spanId}`, {
|
|
614
|
-
bypassBatch: true,
|
|
615
666
|
queryParams: { "as-of": asOf },
|
|
616
667
|
}),
|
|
617
668
|
/**
|
|
@@ -698,11 +749,7 @@ class PlaidClient {
|
|
|
698
749
|
*/
|
|
699
750
|
info: () => {
|
|
700
751
|
if (!infoPromise) {
|
|
701
|
-
|
|
702
|
-
// whatever batch happens to be open on this shared client.
|
|
703
|
-
infoPromise = this._request("GET", "/api/v1/info", {
|
|
704
|
-
bypassBatch: true,
|
|
705
|
-
}).catch((err) => {
|
|
752
|
+
infoPromise = this._request("GET", "/api/v1/info").catch((err) => {
|
|
706
753
|
// A failure must not be cached: a client that starts before the
|
|
707
754
|
// server is up would never see the limits at all.
|
|
708
755
|
infoPromise = null;
|
|
@@ -722,20 +769,6 @@ class PlaidClient {
|
|
|
722
769
|
health: () => PlaidClient.health(this.baseUrl, { timeout: this.timeout }),
|
|
723
770
|
};
|
|
724
771
|
|
|
725
|
-
this.batch = {
|
|
726
|
-
/**
|
|
727
|
-
* Execute multiple API operations atomically. If any operation fails, all
|
|
728
|
-
* changes are rolled back.
|
|
729
|
-
* @param {Array} body - The request body
|
|
730
|
-
*/
|
|
731
|
-
submit: (body, auditMessage) =>
|
|
732
|
-
this._request("POST", "/api/v1/batch", {
|
|
733
|
-
auditMessage,
|
|
734
|
-
body,
|
|
735
|
-
noBatch: true,
|
|
736
|
-
}),
|
|
737
|
-
};
|
|
738
|
-
|
|
739
772
|
this.texts = {
|
|
740
773
|
/**
|
|
741
774
|
* Replace all metadata for a text.
|
|
@@ -793,7 +826,6 @@ class PlaidClient {
|
|
|
793
826
|
*/
|
|
794
827
|
get: (textId, asOf) =>
|
|
795
828
|
this._request("GET", `/api/v1/texts/${textId}`, {
|
|
796
|
-
bypassBatch: true,
|
|
797
829
|
queryParams: { "as-of": asOf },
|
|
798
830
|
}),
|
|
799
831
|
/**
|
|
@@ -931,7 +963,6 @@ class PlaidClient {
|
|
|
931
963
|
*/
|
|
932
964
|
get: (id, asOf) =>
|
|
933
965
|
this._request("GET", `/api/v1/users/${id}`, {
|
|
934
|
-
bypassBatch: true,
|
|
935
966
|
queryParams: { "as-of": asOf },
|
|
936
967
|
}),
|
|
937
968
|
/**
|
|
@@ -1001,7 +1032,6 @@ class PlaidClient {
|
|
|
1001
1032
|
*/
|
|
1002
1033
|
getAvatar: (id) =>
|
|
1003
1034
|
this._request("GET", `/api/v1/users/${id}/avatar`, {
|
|
1004
|
-
bypassBatch: true,
|
|
1005
1035
|
binaryResponse: true,
|
|
1006
1036
|
}),
|
|
1007
1037
|
/**
|
|
@@ -1059,13 +1089,6 @@ class PlaidClient {
|
|
|
1059
1089
|
* @param {number} [opts.pageSize=100] - Entries per request (1..1000)
|
|
1060
1090
|
*/
|
|
1061
1091
|
list: (userId, { prefix, pattern, includeValues, pageSize = 100 } = {}) =>
|
|
1062
|
-
// Every page carries bypassBatch (the pagination helpers set it): a
|
|
1063
|
-
// read belongs to whoever asked for it, not to whatever batch happens
|
|
1064
|
-
// to be open on this shared client. The assistant panel is app chrome
|
|
1065
|
-
// in both SPAs, so it reads this store while an import or a bulk edit
|
|
1066
|
-
// holds a batch open. Queued, the read answered `{batched: true}` (the
|
|
1067
|
-
// sidebar then threw on `.map`) and took a slot in the batch's
|
|
1068
|
-
// results, shifting every index the caller counted on.
|
|
1069
1092
|
listAll(this, `/api/v1/users/${userId}/data`, {
|
|
1070
1093
|
pageSize,
|
|
1071
1094
|
query: { prefix, pattern, "include-values": includeValues },
|
|
@@ -1113,11 +1136,9 @@ class PlaidClient {
|
|
|
1113
1136
|
* @param {string} key
|
|
1114
1137
|
*/
|
|
1115
1138
|
get: (userId, key) =>
|
|
1116
|
-
// bypassBatch for the same reason as list() above.
|
|
1117
1139
|
this._request(
|
|
1118
1140
|
"GET",
|
|
1119
1141
|
`/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
|
|
1120
|
-
{ bypassBatch: true },
|
|
1121
1142
|
),
|
|
1122
1143
|
/**
|
|
1123
1144
|
* Create or replace one private data entry. `value` is any JSON (up to
|
|
@@ -1318,8 +1339,7 @@ class PlaidClient {
|
|
|
1318
1339
|
* walks the media directory, so open it, do not poll it. Admin only.
|
|
1319
1340
|
* @returns {Promise<{version: string, jvm: object, database: object, media: object, backup: object, settings: object}>}
|
|
1320
1341
|
*/
|
|
1321
|
-
server: () =>
|
|
1322
|
-
this._request("GET", "/api/v1/admin/server", { bypassBatch: true }),
|
|
1342
|
+
server: () => this._request("GET", "/api/v1/admin/server"),
|
|
1323
1343
|
/**
|
|
1324
1344
|
* Take a database backup right now, outside the nightly schedule.
|
|
1325
1345
|
* Resolves to the backup block with `ok` reporting whether the snapshot
|
|
@@ -1327,20 +1347,17 @@ class PlaidClient {
|
|
|
1327
1347
|
* people are working. Admin only.
|
|
1328
1348
|
* @returns {Promise<{ok: boolean, directory: string, backups: Array}>}
|
|
1329
1349
|
*
|
|
1330
|
-
*
|
|
1331
|
-
*
|
|
1332
|
-
* backup taken inside somebody's open write transaction is not what the
|
|
1333
|
-
* caller asked for.
|
|
1350
|
+
* outOfBand, as every admin action on the server itself is: none of them
|
|
1351
|
+
* writes project data (see the note at the top of http.js).
|
|
1334
1352
|
*/
|
|
1335
1353
|
backup: () =>
|
|
1336
|
-
this._request("POST", "/api/v1/admin/backup", {
|
|
1354
|
+
this._request("POST", "/api/v1/admin/backup", { outOfBand: true }),
|
|
1337
1355
|
/**
|
|
1338
1356
|
* Documents currently held by an editing lock, with who holds each and
|
|
1339
1357
|
* when it expires on its own. Admin only.
|
|
1340
1358
|
* @returns {Promise<{entries: Array<{documentId: string, userId: string, expiresAt: number}>}>}
|
|
1341
1359
|
*/
|
|
1342
|
-
locks: () =>
|
|
1343
|
-
this._request("GET", "/api/v1/admin/locks", { bypassBatch: true }),
|
|
1360
|
+
locks: () => this._request("GET", "/api/v1/admin/locks"),
|
|
1344
1361
|
/**
|
|
1345
1362
|
* Drop the lock on a document whoever holds it. Idempotent. For a client
|
|
1346
1363
|
* that went away without releasing one. Admin only.
|
|
@@ -1348,7 +1365,7 @@ class PlaidClient {
|
|
|
1348
1365
|
*/
|
|
1349
1366
|
releaseLock: (documentId) =>
|
|
1350
1367
|
this._request("DELETE", `/api/v1/admin/locks/${documentId}`, {
|
|
1351
|
-
|
|
1368
|
+
outOfBand: true,
|
|
1352
1369
|
}),
|
|
1353
1370
|
/**
|
|
1354
1371
|
* Live login and invite rate-limit buckets: the address, the account
|
|
@@ -1356,10 +1373,7 @@ class PlaidClient {
|
|
|
1356
1373
|
* it is currently blocking. Admin only.
|
|
1357
1374
|
* @returns {Promise<{windowMs: number, logins: Array, ips: Array, invites: Array}>}
|
|
1358
1375
|
*/
|
|
1359
|
-
rateLimits: () =>
|
|
1360
|
-
this._request("GET", "/api/v1/admin/rate-limits", {
|
|
1361
|
-
bypassBatch: true,
|
|
1362
|
-
}),
|
|
1376
|
+
rateLimits: () => this._request("GET", "/api/v1/admin/rate-limits"),
|
|
1363
1377
|
/**
|
|
1364
1378
|
* Forget recorded rate-limit failures. With `ip`, clears that address,
|
|
1365
1379
|
* narrowed to one account with `userId`. With neither, clears every
|
|
@@ -1370,7 +1384,7 @@ class PlaidClient {
|
|
|
1370
1384
|
*/
|
|
1371
1385
|
clearRateLimits: ({ ip, userId } = {}) =>
|
|
1372
1386
|
this._request("DELETE", "/api/v1/admin/rate-limits", {
|
|
1373
|
-
|
|
1387
|
+
outOfBand: true,
|
|
1374
1388
|
queryParams: { ip, "user-id": userId },
|
|
1375
1389
|
}),
|
|
1376
1390
|
/**
|
|
@@ -1394,7 +1408,6 @@ class PlaidClient {
|
|
|
1394
1408
|
*/
|
|
1395
1409
|
logs: ({ limit, q, level, status, user, method } = {}) =>
|
|
1396
1410
|
this._request("GET", "/api/v1/admin/logs", {
|
|
1397
|
-
bypassBatch: true,
|
|
1398
1411
|
queryParams: { limit, q, level, status, user, method },
|
|
1399
1412
|
}),
|
|
1400
1413
|
/**
|
|
@@ -1408,7 +1421,6 @@ class PlaidClient {
|
|
|
1408
1421
|
*/
|
|
1409
1422
|
logFile: ({ lines } = {}) =>
|
|
1410
1423
|
this._request("GET", "/api/v1/admin/logs/file", {
|
|
1411
|
-
bypassBatch: true,
|
|
1412
1424
|
queryParams: { lines },
|
|
1413
1425
|
}),
|
|
1414
1426
|
/**
|
|
@@ -1536,7 +1548,6 @@ class PlaidClient {
|
|
|
1536
1548
|
? `/api/v1/projects/${projectId}/audit/tally`
|
|
1537
1549
|
: "/api/v1/audit/tally";
|
|
1538
1550
|
const result = await this._request("GET", path, {
|
|
1539
|
-
bypassBatch: true,
|
|
1540
1551
|
queryParams: {
|
|
1541
1552
|
"start-time": startTime,
|
|
1542
1553
|
"end-time": endTime,
|
|
@@ -1619,7 +1630,6 @@ class PlaidClient {
|
|
|
1619
1630
|
*/
|
|
1620
1631
|
get: (tokenLayerId, asOf) =>
|
|
1621
1632
|
this._request("GET", `/api/v1/token-layers/${tokenLayerId}`, {
|
|
1622
|
-
bypassBatch: true,
|
|
1623
1633
|
queryParams: { "as-of": asOf },
|
|
1624
1634
|
}),
|
|
1625
1635
|
/**
|
|
@@ -1650,34 +1660,33 @@ class PlaidClient {
|
|
|
1650
1660
|
*/
|
|
1651
1661
|
checkLock: (documentId, asOf) =>
|
|
1652
1662
|
this._request("GET", `/api/v1/documents/${documentId}/lock`, {
|
|
1653
|
-
bypassBatch: true,
|
|
1654
1663
|
queryParams: { "as-of": asOf },
|
|
1655
1664
|
}),
|
|
1656
1665
|
/**
|
|
1657
1666
|
* Acquire or refresh a document lock.
|
|
1658
1667
|
*
|
|
1659
|
-
*
|
|
1660
|
-
*
|
|
1661
|
-
*
|
|
1662
|
-
*
|
|
1663
|
-
*
|
|
1668
|
+
* outOfBand: the lock is a signal, not project data (see the note at
|
|
1669
|
+
* the top of http.js). Queued on a batch it would be taken only at
|
|
1670
|
+
* submit, after every write it was meant to guard, and until then answer
|
|
1671
|
+
* success to a caller that does not hold it and cannot see the 423
|
|
1672
|
+
* saying somebody else does.
|
|
1664
1673
|
* @param {string} documentId - The document ID
|
|
1665
1674
|
*/
|
|
1666
1675
|
acquireLock: (documentId, auditMessage) =>
|
|
1667
1676
|
this._request("POST", `/api/v1/documents/${documentId}/lock`, {
|
|
1668
1677
|
auditMessage,
|
|
1669
|
-
|
|
1678
|
+
outOfBand: true,
|
|
1670
1679
|
}),
|
|
1671
1680
|
/**
|
|
1672
|
-
* Release a document lock.
|
|
1673
|
-
*
|
|
1674
|
-
*
|
|
1681
|
+
* Release a document lock. outOfBand, for the same reason as
|
|
1682
|
+
* acquireLock: queued, the lock would be held until the batch submits,
|
|
1683
|
+
* and not released at all if it aborts.
|
|
1675
1684
|
* @param {string} documentId - The document ID
|
|
1676
1685
|
*/
|
|
1677
1686
|
releaseLock: (documentId, auditMessage) =>
|
|
1678
1687
|
this._request("DELETE", `/api/v1/documents/${documentId}/lock`, {
|
|
1679
1688
|
auditMessage,
|
|
1680
|
-
|
|
1689
|
+
outOfBand: true,
|
|
1681
1690
|
}),
|
|
1682
1691
|
/**
|
|
1683
1692
|
* Hold this document's server-enforced lock for the length of `fn`,
|
|
@@ -1724,7 +1733,6 @@ class PlaidClient {
|
|
|
1724
1733
|
*/
|
|
1725
1734
|
getMedia: (documentId) =>
|
|
1726
1735
|
this._request("GET", `/api/v1/documents/${documentId}/media`, {
|
|
1727
|
-
bypassBatch: true,
|
|
1728
1736
|
binaryResponse: true,
|
|
1729
1737
|
}),
|
|
1730
1738
|
/**
|
|
@@ -1749,8 +1757,8 @@ class PlaidClient {
|
|
|
1749
1757
|
* Delete media file for a document
|
|
1750
1758
|
* @param {string} documentId - The document ID
|
|
1751
1759
|
*/
|
|
1752
|
-
//
|
|
1753
|
-
//
|
|
1760
|
+
// The upload above is multipart and cannot be batched, but a DELETE
|
|
1761
|
+
// carries no blob, so the batch transport takes it. It is a write
|
|
1754
1762
|
// of the document's own data and queues like any other. Note that the
|
|
1755
1763
|
// file removal happens outside the server's transaction, so a batch that
|
|
1756
1764
|
// aborts after this op does not bring the file back.
|
|
@@ -1874,7 +1882,6 @@ class PlaidClient {
|
|
|
1874
1882
|
*/
|
|
1875
1883
|
get: (documentId, includeBody, asOf, layers) =>
|
|
1876
1884
|
this._request("GET", `/api/v1/documents/${documentId}`, {
|
|
1877
|
-
bypassBatch: true,
|
|
1878
1885
|
queryParams: {
|
|
1879
1886
|
"include-body": includeBody,
|
|
1880
1887
|
"as-of": asOf,
|
|
@@ -2071,7 +2078,6 @@ class PlaidClient {
|
|
|
2071
2078
|
*/
|
|
2072
2079
|
myLastEdits: (projectId) =>
|
|
2073
2080
|
this._request("GET", `/api/v1/projects/${projectId}/audit/last-edits`, {
|
|
2074
|
-
bypassBatch: true,
|
|
2075
2081
|
skipResponseTransform: true,
|
|
2076
2082
|
}),
|
|
2077
2083
|
/**
|
|
@@ -2100,7 +2106,6 @@ class PlaidClient {
|
|
|
2100
2106
|
*/
|
|
2101
2107
|
get: (id, asOf) =>
|
|
2102
2108
|
this._request("GET", `/api/v1/projects/${id}`, {
|
|
2103
|
-
bypassBatch: true,
|
|
2104
2109
|
queryParams: { "as-of": asOf },
|
|
2105
2110
|
}),
|
|
2106
2111
|
/**
|
|
@@ -2247,7 +2252,6 @@ class PlaidClient {
|
|
|
2247
2252
|
*/
|
|
2248
2253
|
get: (textLayerId, asOf) =>
|
|
2249
2254
|
this._request("GET", `/api/v1/text-layers/${textLayerId}`, {
|
|
2250
|
-
bypassBatch: true,
|
|
2251
2255
|
queryParams: { "as-of": asOf },
|
|
2252
2256
|
}),
|
|
2253
2257
|
/**
|
|
@@ -2343,9 +2347,23 @@ class PlaidClient {
|
|
|
2343
2347
|
auditMessage,
|
|
2344
2348
|
body,
|
|
2345
2349
|
}),
|
|
2350
|
+
/**
|
|
2351
|
+
* Update many vocab items in a single operation: set forms and/or patch
|
|
2352
|
+
* metadata.
|
|
2353
|
+
* @param {Array<{id: string, form?: string, metadata?: any}>} body - `form` is set only when the key is present; `metadata` is a patch, a null value deleting that key. The entries may lie in several vocab layers; the user must have write access to each. An unknown id refuses the whole update, and an id may appear only once. Only an entry whose form really changes restates the documents linking it, and a strict-mode client picks up their new versions from the response.
|
|
2354
|
+
* @returns {Promise<{count: number}>} How many vocab items were updated.
|
|
2355
|
+
*/
|
|
2356
|
+
bulkUpdate: (body, auditMessage) =>
|
|
2357
|
+
this._request("PATCH", "/api/v1/vocab-items/bulk", {
|
|
2358
|
+
auditMessage,
|
|
2359
|
+
body,
|
|
2360
|
+
}),
|
|
2346
2361
|
/**
|
|
2347
2362
|
* Delete multiple vocab items in a single operation. Each item's
|
|
2348
2363
|
* descendant vocab links are deleted too. Provide an array of IDs.
|
|
2364
|
+
*
|
|
2365
|
+
* Every document holding one of those links has its version bumped, and
|
|
2366
|
+
* a strict-mode client picks up their new versions from the response.
|
|
2349
2367
|
* @param {string[]} body - The vocab item IDs to delete
|
|
2350
2368
|
*/
|
|
2351
2369
|
bulkDelete: (body, auditMessage) =>
|
|
@@ -2360,17 +2378,21 @@ class PlaidClient {
|
|
|
2360
2378
|
*/
|
|
2361
2379
|
get: (id, asOf) =>
|
|
2362
2380
|
this._request("GET", `/api/v1/vocab-items/${id}`, {
|
|
2363
|
-
bypassBatch: true,
|
|
2364
2381
|
queryParams: { "as-of": asOf },
|
|
2365
2382
|
}),
|
|
2366
2383
|
/**
|
|
2367
|
-
* Delete a vocab item
|
|
2384
|
+
* Delete a vocab item, and every link to it. Every document holding one
|
|
2385
|
+
* of those links has its version bumped, and a strict-mode client picks
|
|
2386
|
+
* up their new versions from the response.
|
|
2368
2387
|
* @param {string} id - The resource ID
|
|
2369
2388
|
*/
|
|
2370
2389
|
delete: (id, auditMessage) =>
|
|
2371
2390
|
this._request("DELETE", `/api/v1/vocab-items/${id}`, { auditMessage }),
|
|
2372
2391
|
/**
|
|
2373
|
-
* Update a vocab item's form
|
|
2392
|
+
* Update a vocab item's form. A document read carries the entry's form on
|
|
2393
|
+
* every link to it, so a rename restates those documents: each has its
|
|
2394
|
+
* version bumped, and a strict-mode client picks up their new versions
|
|
2395
|
+
* from the response.
|
|
2374
2396
|
* @param {string} id - The resource ID
|
|
2375
2397
|
* @param {string} form - The vocab item form
|
|
2376
2398
|
*/
|
|
@@ -2441,7 +2463,6 @@ class PlaidClient {
|
|
|
2441
2463
|
*/
|
|
2442
2464
|
get: (relationLayerId, asOf) =>
|
|
2443
2465
|
this._request("GET", `/api/v1/relation-layers/${relationLayerId}`, {
|
|
2444
|
-
bypassBatch: true,
|
|
2445
2466
|
queryParams: { "as-of": asOf },
|
|
2446
2467
|
}),
|
|
2447
2468
|
/**
|
|
@@ -2510,7 +2531,6 @@ class PlaidClient {
|
|
|
2510
2531
|
*/
|
|
2511
2532
|
get: (tokenId, asOf) =>
|
|
2512
2533
|
this._request("GET", `/api/v1/tokens/${tokenId}`, {
|
|
2513
|
-
bypassBatch: true,
|
|
2514
2534
|
queryParams: { "as-of": asOf },
|
|
2515
2535
|
}),
|
|
2516
2536
|
/**
|
|
@@ -2661,8 +2681,7 @@ class PlaidClient {
|
|
|
2661
2681
|
/**
|
|
2662
2682
|
* Discover the services seen on a project (synchronous GET). Currently
|
|
2663
2683
|
* connected services carry `online: true`; previously-seen offline ones
|
|
2664
|
-
* carry `online: false` plus a `lastSeenAt` stamp.
|
|
2665
|
-
* even while a batch is open on the client.
|
|
2684
|
+
* carry `online: false` plus a `lastSeenAt` stamp.
|
|
2666
2685
|
* @param {string} projectId - The UUID of the project to query
|
|
2667
2686
|
* @returns {Promise<Array>} Array of discovered service information
|
|
2668
2687
|
*/
|
|
@@ -2811,17 +2830,14 @@ class PlaidClient {
|
|
|
2811
2830
|
* For 'count': {return: 'count', count}. Entity cells are full entity objects
|
|
2812
2831
|
* (same shape as the GET endpoints).
|
|
2813
2832
|
*/
|
|
2814
|
-
//
|
|
2815
|
-
//
|
|
2816
|
-
//
|
|
2817
|
-
// be queued into it: the caller got `{batched: true}` instead of results,
|
|
2818
|
-
// and the server ran the query against the batch's tx Connection, which
|
|
2819
|
-
// 500s and rolls back every write in that batch.
|
|
2833
|
+
// outOfBand: a query is a read that travels as a POST (see the note at
|
|
2834
|
+
// the top of http.js). Made on a batch it goes over the wire like any
|
|
2835
|
+
// read, and it never joins a logical operation.
|
|
2820
2836
|
this.query = (body, auditMessage) =>
|
|
2821
2837
|
this._request("POST", "/api/v1/query", {
|
|
2822
2838
|
auditMessage,
|
|
2823
2839
|
body,
|
|
2824
|
-
|
|
2840
|
+
outOfBand: true,
|
|
2825
2841
|
});
|
|
2826
2842
|
|
|
2827
2843
|
// Logical-operation groups (audit-log grouping). There is no create: a
|
|
@@ -2832,10 +2848,7 @@ class PlaidClient {
|
|
|
2832
2848
|
* Get a logical-operation group (its label + creator).
|
|
2833
2849
|
* @param {string} id - The group id
|
|
2834
2850
|
*/
|
|
2835
|
-
get: (id) =>
|
|
2836
|
-
this._request("GET", `/api/v1/operation-groups/${id}`, {
|
|
2837
|
-
bypassBatch: true,
|
|
2838
|
-
}),
|
|
2851
|
+
get: (id) => this._request("GET", `/api/v1/operation-groups/${id}`),
|
|
2839
2852
|
/**
|
|
2840
2853
|
* Relabel a logical-operation group after the fact. Owner or admin only.
|
|
2841
2854
|
* @param {string} id - The group id
|
|
@@ -2876,8 +2889,7 @@ class PlaidClient {
|
|
|
2876
2889
|
* Read one comment.
|
|
2877
2890
|
* @param {string} id - The comment id
|
|
2878
2891
|
*/
|
|
2879
|
-
get: (id) =>
|
|
2880
|
-
this._request("GET", `/api/v1/comments/${id}`, { bypassBatch: true }),
|
|
2892
|
+
get: (id) => this._request("GET", `/api/v1/comments/${id}`),
|
|
2881
2893
|
/**
|
|
2882
2894
|
* Edit a comment's body. Only the comment's AUTHOR may do this - not
|
|
2883
2895
|
* maintainers, not admins. Sets `edited` on the comment.
|
|
@@ -2969,7 +2981,6 @@ class PlaidClient {
|
|
|
2969
2981
|
*/
|
|
2970
2982
|
counts: (projectId, { documentId, entityType, entityId } = {}) =>
|
|
2971
2983
|
this._request("GET", `/api/v1/projects/${projectId}/comments/counts`, {
|
|
2972
|
-
bypassBatch: true,
|
|
2973
2984
|
queryParams: {
|
|
2974
2985
|
"document-id": documentId,
|
|
2975
2986
|
"entity-type": entityType,
|
|
@@ -3017,12 +3028,111 @@ class PlaidClient {
|
|
|
3017
3028
|
"GET",
|
|
3018
3029
|
`/api/v1/vocab-layers/${vocabId}/comments/counts`,
|
|
3019
3030
|
{
|
|
3020
|
-
bypassBatch: true,
|
|
3021
3031
|
queryParams: { "entity-id": entityId },
|
|
3022
3032
|
skipResponseTransform: true,
|
|
3023
3033
|
},
|
|
3024
3034
|
),
|
|
3025
3035
|
};
|
|
3036
|
+
|
|
3037
|
+
this.guidelines = {
|
|
3038
|
+
/**
|
|
3039
|
+
* Create a guideline in a project. `title` is the handle an assistant
|
|
3040
|
+
* asks for one by, and the only thing a person has to keep current. It is
|
|
3041
|
+
* NOT required to be unique, so a title already in use is written like
|
|
3042
|
+
* any other: warn about it, do not refuse it. A `pinned` guideline is one
|
|
3043
|
+
* the assistant is given in full on every turn.
|
|
3044
|
+
* @param {string} projectId - The project the guideline belongs to
|
|
3045
|
+
* @param {string} title - The handle an assistant asks for one by (1..100 characters)
|
|
3046
|
+
* @param {object} [opts]
|
|
3047
|
+
* @param {string} [opts.body] - The Markdown text (up to 20000 characters; may be empty)
|
|
3048
|
+
* @param {boolean} [opts.pinned] - Send this one to the assistant in full on every turn
|
|
3049
|
+
*/
|
|
3050
|
+
create: (projectId, title, { body, pinned } = {}, auditMessage) =>
|
|
3051
|
+
this._request("POST", `/api/v1/projects/${projectId}/guidelines`, {
|
|
3052
|
+
auditMessage,
|
|
3053
|
+
body: bodyOf({ title, body, pinned }),
|
|
3054
|
+
}),
|
|
3055
|
+
/**
|
|
3056
|
+
* Read one guideline, Markdown body included.
|
|
3057
|
+
* @param {string} id - The guideline id
|
|
3058
|
+
*/
|
|
3059
|
+
get: (id) => this._request("GET", `/api/v1/guidelines/${id}`),
|
|
3060
|
+
/**
|
|
3061
|
+
* Update a guideline. Every field is optional and an omitted one is
|
|
3062
|
+
* left alone, so an edit to the body need not restate the title.
|
|
3063
|
+
*
|
|
3064
|
+
* Pass `expectedUpdatedAt` (the `updatedAt` you last read) when a person
|
|
3065
|
+
* has been editing prose: the write then fails with 409 rather than
|
|
3066
|
+
* overwriting somebody who saved in between. Leave it off for a pin
|
|
3067
|
+
* toggle or a script, which have nothing of anyone's to lose.
|
|
3068
|
+
* @param {string} id - The guideline id
|
|
3069
|
+
* @param {object} [changes]
|
|
3070
|
+
* @param {string} [changes.title] - The new handle
|
|
3071
|
+
* @param {string} [changes.body] - The new Markdown text
|
|
3072
|
+
* @param {boolean} [changes.pinned] - Whether the assistant always gets it in full
|
|
3073
|
+
* @param {string} [changes.expectedUpdatedAt] - Write only if this is still the stored updatedAt
|
|
3074
|
+
*/
|
|
3075
|
+
update: (
|
|
3076
|
+
id,
|
|
3077
|
+
{ title, body, pinned, expectedUpdatedAt } = {},
|
|
3078
|
+
auditMessage,
|
|
3079
|
+
) =>
|
|
3080
|
+
this._request("PATCH", `/api/v1/guidelines/${id}`, {
|
|
3081
|
+
auditMessage,
|
|
3082
|
+
queryParams: { "updated-at": expectedUpdatedAt },
|
|
3083
|
+
body: bodyOf({ title, body, pinned }),
|
|
3084
|
+
}),
|
|
3085
|
+
/**
|
|
3086
|
+
* Delete a guideline.
|
|
3087
|
+
* @param {string} id - The guideline id
|
|
3088
|
+
*/
|
|
3089
|
+
delete: (id, auditMessage) =>
|
|
3090
|
+
this._request("DELETE", `/api/v1/guidelines/${id}`, { auditMessage }),
|
|
3091
|
+
/**
|
|
3092
|
+
* List a project's guidelines, by title. Transparently follows
|
|
3093
|
+
* pagination cursors and returns the full flat array.
|
|
3094
|
+
*
|
|
3095
|
+
* Without `includeBodies` each entry carries `bodyChars`, the length of
|
|
3096
|
+
* its body, so a caller can budget before fetching any. Pinned
|
|
3097
|
+
* guidelines are NOT sorted first: `pinned` is on every entry and
|
|
3098
|
+
* grouping is the caller's.
|
|
3099
|
+
* @param {string} projectId - The project to read
|
|
3100
|
+
* @param {object} [opts]
|
|
3101
|
+
* @param {boolean} [opts.includeBodies] - Return each body instead of its length
|
|
3102
|
+
*/
|
|
3103
|
+
list: (projectId, { includeBodies } = {}) =>
|
|
3104
|
+
listAll(this, `/api/v1/projects/${projectId}/guidelines`, {
|
|
3105
|
+
query: { "include-bodies": includeBodies },
|
|
3106
|
+
}),
|
|
3107
|
+
/**
|
|
3108
|
+
* Fetch a single page of a project's guidelines.
|
|
3109
|
+
* @param {string} projectId - The project to read
|
|
3110
|
+
* @param {object} [opts]
|
|
3111
|
+
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
3112
|
+
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
3113
|
+
* @param {boolean} [opts.includeBodies] - Return each body instead of its length
|
|
3114
|
+
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
3115
|
+
*/
|
|
3116
|
+
listPage: (projectId, { limit, cursor, includeBodies } = {}) =>
|
|
3117
|
+
listPage(this, `/api/v1/projects/${projectId}/guidelines`, {
|
|
3118
|
+
limit,
|
|
3119
|
+
cursor,
|
|
3120
|
+
query: { "include-bodies": includeBodies },
|
|
3121
|
+
}),
|
|
3122
|
+
/**
|
|
3123
|
+
* Async-iterate a project's guidelines page by page; yields each page's entries array.
|
|
3124
|
+
* @param {string} projectId - The project to read
|
|
3125
|
+
* @param {object} [opts]
|
|
3126
|
+
* @param {number} [opts.pageSize] - Per-request page size
|
|
3127
|
+
* @param {boolean} [opts.includeBodies] - Return each body instead of its length
|
|
3128
|
+
* @returns {AsyncGenerator<Array>}
|
|
3129
|
+
*/
|
|
3130
|
+
iterPages: (projectId, { pageSize, includeBodies } = {}) =>
|
|
3131
|
+
iterPages(this, `/api/v1/projects/${projectId}/guidelines`, {
|
|
3132
|
+
pageSize,
|
|
3133
|
+
query: { "include-bodies": includeBodies },
|
|
3134
|
+
}),
|
|
3135
|
+
};
|
|
3026
3136
|
}
|
|
3027
3137
|
|
|
3028
3138
|
// --- Core methods ---
|
|
@@ -3124,7 +3234,7 @@ class PlaidClient {
|
|
|
3124
3234
|
* callback to refine the label once the outcome is known.
|
|
3125
3235
|
*
|
|
3126
3236
|
* await client.withOperation('Merge morphemes', async (setMessage) => {
|
|
3127
|
-
* await client.batched(() => { ... });
|
|
3237
|
+
* await client.batched(async (b) => { ... });
|
|
3128
3238
|
* setMessage(`Merged ${n} morphemes`);
|
|
3129
3239
|
* });
|
|
3130
3240
|
*
|
|
@@ -3146,55 +3256,28 @@ class PlaidClient {
|
|
|
3146
3256
|
}
|
|
3147
3257
|
|
|
3148
3258
|
/**
|
|
3149
|
-
*
|
|
3150
|
-
*
|
|
3259
|
+
* Open a batch: a view of this client with the same bundles, on which every
|
|
3260
|
+
* write of project data queues instead of going out. `submit()` sends the
|
|
3261
|
+
* queued operations as ONE atomic request (larger than the server's cap,
|
|
3262
|
+
* as consecutive requests with the results concatenated in queue order) and
|
|
3263
|
+
* resolves to one result per operation; `abort()` drops them. A call made
|
|
3264
|
+
* on the client itself is never touched by an open batch, and a read or an
|
|
3265
|
+
* out-of-band signal made on the batch goes over the wire now (see the note
|
|
3266
|
+
* at the top of `http.js`).
|
|
3267
|
+
*
|
|
3268
|
+
* const b = client.batch();
|
|
3269
|
+
* b.tokens.bulkCreate(sentenceOps);
|
|
3270
|
+
* b.tokens.bulkCreate(wordOps);
|
|
3271
|
+
* const [sentRes, wordRes] = await b.submit();
|
|
3272
|
+
*
|
|
3273
|
+
* Server-side a batch runs sequentially in one transaction: a child op sees
|
|
3274
|
+
* parents created earlier in the same batch, and any op's failure rolls the
|
|
3275
|
+
* whole batch back. A batch is not nestable. Prefer `batched()`, which
|
|
3276
|
+
* submits or aborts for you.
|
|
3277
|
+
* @returns {PlaidClient} the batch view
|
|
3151
3278
|
*/
|
|
3152
|
-
|
|
3153
|
-
this
|
|
3154
|
-
this.batchOperations = [];
|
|
3155
|
-
// Strict mode stamps the expected document-version on the FIRST QUEUED
|
|
3156
|
-
// write of the batch only (see _request), so reset the marker per batch.
|
|
3157
|
-
this.batchVersionStamped = false;
|
|
3158
|
-
}
|
|
3159
|
-
|
|
3160
|
-
/**
|
|
3161
|
-
* Submit all queued batch operations as a single batch request, executed
|
|
3162
|
-
* atomically. If any operation fails, all changes are rolled back.
|
|
3163
|
-
* @returns {Promise<Array>} Array of results corresponding to each operation
|
|
3164
|
-
*/
|
|
3165
|
-
async submitBatch() {
|
|
3166
|
-
if (!this.isBatching) {
|
|
3167
|
-
throw new Error("No active batch. Call beginBatch() first.");
|
|
3168
|
-
}
|
|
3169
|
-
|
|
3170
|
-
if (this.batchOperations.length === 0) {
|
|
3171
|
-
this.isBatching = false;
|
|
3172
|
-
return [];
|
|
3173
|
-
}
|
|
3174
|
-
|
|
3175
|
-
try {
|
|
3176
|
-
const url = `${this.baseUrl}/api/v1/batch`;
|
|
3177
|
-
const ops = this.batchOperations.map((op) => ({
|
|
3178
|
-
path: op.path,
|
|
3179
|
-
method: op.method.toUpperCase(),
|
|
3180
|
-
...(op.body && { body: op.body }),
|
|
3181
|
-
}));
|
|
3182
|
-
// The server caps a batch at MAX_BATCH_OPS so one transaction cannot
|
|
3183
|
-
// hold the write lock without bound. A larger batch goes as consecutive
|
|
3184
|
-
// requests, results concatenated in queue order: it could not have been
|
|
3185
|
-
// one transaction anyway, and a repair or bulk edit over a big document
|
|
3186
|
-
// must not fail on its size alone.
|
|
3187
|
-
const results = [];
|
|
3188
|
-
for (let i = 0; i < ops.length; i += MAX_BATCH_OPS) {
|
|
3189
|
-
results.push(
|
|
3190
|
-
...(await this._postBatch(url, ops.slice(i, i + MAX_BATCH_OPS))),
|
|
3191
|
-
);
|
|
3192
|
-
}
|
|
3193
|
-
return results;
|
|
3194
|
-
} finally {
|
|
3195
|
-
this.isBatching = false;
|
|
3196
|
-
this.batchOperations = [];
|
|
3197
|
-
}
|
|
3279
|
+
batch() {
|
|
3280
|
+
return openBatch(this);
|
|
3198
3281
|
}
|
|
3199
3282
|
|
|
3200
3283
|
/** POST one batch request (at most MAX_BATCH_OPS operations); resolves to its transformed results. */
|
|
@@ -3256,62 +3339,31 @@ class PlaidClient {
|
|
|
3256
3339
|
}
|
|
3257
3340
|
}
|
|
3258
3341
|
|
|
3259
|
-
/** Abort the current batch without executing any operations. */
|
|
3260
|
-
abortBatch() {
|
|
3261
|
-
this.isBatching = false;
|
|
3262
|
-
this.batchOperations = [];
|
|
3263
|
-
}
|
|
3264
|
-
|
|
3265
3342
|
/**
|
|
3266
|
-
*
|
|
3267
|
-
*
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
return this.isBatching;
|
|
3271
|
-
}
|
|
3272
|
-
|
|
3273
|
-
/**
|
|
3274
|
-
* Run `fn` with a batch open, then submit all queued ops as ONE atomic
|
|
3275
|
-
* request — or abort the batch if `fn` throws, so a half-open batch can never
|
|
3276
|
-
* silently swallow later non-batch calls. `fn` makes the (queued) client
|
|
3277
|
-
* calls; it must NOT call submitBatch itself. Resolves to the batch results
|
|
3278
|
-
* array (`[]` if `fn` queued nothing).
|
|
3343
|
+
* Run `fn` with a batch, then submit all queued ops as ONE atomic request,
|
|
3344
|
+
* or abort the batch if `fn` throws. `fn` receives the batch and makes its
|
|
3345
|
+
* writes on it; it must NOT call `submit()` itself. Resolves to the batch
|
|
3346
|
+
* results array (`[]` if `fn` queued nothing).
|
|
3279
3347
|
*
|
|
3280
|
-
* const [sentRes, wordRes] = await client.batched(async () => {
|
|
3281
|
-
*
|
|
3282
|
-
*
|
|
3348
|
+
* const [sentRes, wordRes] = await client.batched(async (b) => {
|
|
3349
|
+
* b.tokens.bulkCreate(sentenceOps);
|
|
3350
|
+
* b.tokens.bulkCreate(wordOps);
|
|
3283
3351
|
* });
|
|
3284
3352
|
*
|
|
3285
|
-
*
|
|
3286
|
-
*
|
|
3287
|
-
* batch
|
|
3288
|
-
* low-level batch resource.
|
|
3289
|
-
*
|
|
3290
|
-
* WRITES are what a batch queues, and batch mode is one flag on the whole
|
|
3291
|
-
* client, so it catches every write made while it is open, including writes
|
|
3292
|
-
* made by code that knows nothing about this batch. READS are never caught:
|
|
3293
|
-
* every read method goes over the wire while a batch is open and is answered
|
|
3294
|
-
* from the state the batch has not committed yet, so a read cannot come back
|
|
3295
|
-
* as a batch marker and cannot take a slot in the results array. Neither are
|
|
3296
|
-
* the calls that signal something out of band rather than write project data:
|
|
3297
|
-
* stopping a service request, a service reporting its progress or result,
|
|
3298
|
-
* taking and dropping a document lock, and the admin actions on the server
|
|
3299
|
-
* itself. See the note at the top of `http.js` for which column a new
|
|
3300
|
-
* endpoint belongs in.
|
|
3301
|
-
* @param {() => (void | Promise<void>)} fn
|
|
3353
|
+
* A write made on `client` inside `fn` is not part of the batch: it goes
|
|
3354
|
+
* over the wire at once, as it would anywhere else. See `batch()`.
|
|
3355
|
+
* @param {(batch: PlaidClient) => (void | Promise<void>)} fn
|
|
3302
3356
|
* @returns {Promise<Array>}
|
|
3303
3357
|
*/
|
|
3304
3358
|
async batched(fn) {
|
|
3305
|
-
this.
|
|
3359
|
+
const batch = this.batch();
|
|
3306
3360
|
try {
|
|
3307
|
-
await fn();
|
|
3361
|
+
await fn(batch);
|
|
3308
3362
|
} catch (e) {
|
|
3309
|
-
|
|
3310
|
-
// later plain calls don't queue into a never-submitted batch.
|
|
3311
|
-
if (this.isBatchMode()) this.abortBatch();
|
|
3363
|
+
batch.abort();
|
|
3312
3364
|
throw e;
|
|
3313
3365
|
}
|
|
3314
|
-
return
|
|
3366
|
+
return batch.submit();
|
|
3315
3367
|
}
|
|
3316
3368
|
|
|
3317
3369
|
/**
|