@larc-iu/plaid-client 0.2.0-alpha.20 → 0.2.0-alpha.22
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 +28 -27
- package/package.json +1 -1
- package/src/constraints.js +0 -11
- package/src/documentLock.js +54 -71
- package/src/http.js +59 -19
- package/src/index.js +188 -44
- package/src/metadataOps.js +4 -2
- package/src/provenance.js +0 -3
- package/src/roles.js +4 -13
- package/src/serviceSchema.js +5 -23
- package/src/services.js +88 -8
- package/src/textEdits.js +18 -2
package/src/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
REPLAYED_HEADER,
|
|
21
21
|
DEFAULT_TIMEOUT_MS,
|
|
22
22
|
DEFAULT_BATCH_TIMEOUT_MS,
|
|
23
|
+
queryTimeout,
|
|
23
24
|
} from "./http.js";
|
|
24
25
|
import { uuidv7 } from "./ids.js";
|
|
25
26
|
import { constraintsBody } from "./constraints.js";
|
|
@@ -133,6 +134,27 @@ async function anonymousGet(baseUrl, path, options = {}) {
|
|
|
133
134
|
*/
|
|
134
135
|
const MAX_BATCH_OPS = 1000;
|
|
135
136
|
|
|
137
|
+
/**
|
|
138
|
+
* The kinds an operation may say it is (`?group-kind=`), the server's closed
|
|
139
|
+
* list (plaid.sql.operation-group/kinds). An operation of any other kind is
|
|
140
|
+
* refused when it begins, before it writes anything.
|
|
141
|
+
*/
|
|
142
|
+
const OPERATION_KINDS = [
|
|
143
|
+
"assistant-plan",
|
|
144
|
+
"service-run",
|
|
145
|
+
"import",
|
|
146
|
+
"bulk-edit",
|
|
147
|
+
"guess-adoption",
|
|
148
|
+
"repair",
|
|
149
|
+
"review",
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* How long `admin.backup()` waits for the server to finish writing a backup:
|
|
154
|
+
* minutes on a large database, past the usual per-request timeout.
|
|
155
|
+
*/
|
|
156
|
+
export const BACKUP_TIMEOUT_MS = 30 * 60 * 1000;
|
|
157
|
+
|
|
136
158
|
/**
|
|
137
159
|
* A batch is the client with a different `_request` (see `queueRequest`) and
|
|
138
160
|
* its own copy of the bundles. Everything else resolves to the client through
|
|
@@ -337,10 +359,37 @@ class PlaidClient {
|
|
|
337
359
|
// the stored token and route back to login. See makeRequest in http.js.
|
|
338
360
|
this.onAuthError = options.onAuthError || null;
|
|
339
361
|
this._authErrorFired = false;
|
|
362
|
+
// Who hears that a request is being sent again (onRetry).
|
|
363
|
+
this._retryListeners = new Set();
|
|
340
364
|
|
|
341
365
|
this._installResources();
|
|
342
366
|
}
|
|
343
367
|
|
|
368
|
+
/**
|
|
369
|
+
* Hear every time a request is sent again: after a 503 (the database was
|
|
370
|
+
* busy), or for a keyed write after its answer was lost (no response, 502,
|
|
371
|
+
* 504). The listener gets `{ attempt, retries, delay, error }` before the
|
|
372
|
+
* wait. A screen that writes through the client directly can say it is
|
|
373
|
+
* retrying while the server does not answer, though the browser is online.
|
|
374
|
+
* @param {(info: {attempt: number, retries: number, delay: number, error: Error}) => void} listener
|
|
375
|
+
* @returns {() => void} the unsubscribe
|
|
376
|
+
*/
|
|
377
|
+
onRetry(listener) {
|
|
378
|
+
this._retryListeners.add(listener);
|
|
379
|
+
return () => this._retryListeners.delete(listener);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Tell every `onRetry` listener. A listener that throws is passed over. */
|
|
383
|
+
_noteRetry(info) {
|
|
384
|
+
for (const listener of this._retryListeners) {
|
|
385
|
+
try {
|
|
386
|
+
listener(info);
|
|
387
|
+
} catch (_) {
|
|
388
|
+
/* a listener must not stop the retry */
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
344
393
|
/**
|
|
345
394
|
* The server's time now, as its last response's Date header put it (to the
|
|
346
395
|
* second), else this machine's. Judge a time the server stamped, such as an
|
|
@@ -495,13 +544,17 @@ class PlaidClient {
|
|
|
495
544
|
* @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
|
|
496
545
|
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
497
546
|
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
547
|
+
* @param {number} [opts.opsLimit] - Keep only each entry's oldest N
|
|
548
|
+
* operations in `ops` (1..1000). `opCount` on every entry says how
|
|
549
|
+
* many it has
|
|
550
|
+
* @param {string} [opts.entryId] - Read only this entry, not a page
|
|
498
551
|
* @param {string} [opts.itemId] - Only the changes that wrote this one
|
|
499
552
|
* entry, each with only its operations that did
|
|
500
553
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
501
554
|
*/
|
|
502
555
|
auditPage: (
|
|
503
556
|
id,
|
|
504
|
-
{ startTime, endTime, opTypes, kinds, order, limit, cursor, itemId } = {},
|
|
557
|
+
{ startTime, endTime, opTypes, kinds, order, limit, cursor, opsLimit, entryId, itemId } = {},
|
|
505
558
|
) =>
|
|
506
559
|
listPage(this, `/api/v1/vocab-layers/${id}/audit`, {
|
|
507
560
|
limit,
|
|
@@ -512,6 +565,8 @@ class PlaidClient {
|
|
|
512
565
|
"op-types": opTypesParam(opTypes),
|
|
513
566
|
kinds: kindsParam(kinds),
|
|
514
567
|
order,
|
|
568
|
+
"ops-limit": opsLimit,
|
|
569
|
+
"entry-id": entryId,
|
|
515
570
|
"item-id": itemId,
|
|
516
571
|
},
|
|
517
572
|
}),
|
|
@@ -1140,14 +1195,14 @@ class PlaidClient {
|
|
|
1140
1195
|
* List (or search) users. Transparently follows pagination cursors and
|
|
1141
1196
|
* returns the full flat array. Admin-or-maintainer only.
|
|
1142
1197
|
* @param {object} [opts]
|
|
1143
|
-
* @param {string} [opts.q] - Filter to users whose display name or email contains
|
|
1198
|
+
* @param {string} [opts.q] - Filter to users whose display name contains this text, or whose email starts with it (contains it, when the text has an @), case-insensitive
|
|
1144
1199
|
*/
|
|
1145
1200
|
list: ({ q } = {}) =>
|
|
1146
1201
|
listAll(this, "/api/v1/users", { query: { q } }),
|
|
1147
1202
|
/**
|
|
1148
1203
|
* Fetch a single page of users (optionally filtered by `q`).
|
|
1149
1204
|
* @param {object} [opts]
|
|
1150
|
-
* @param {string} [opts.q] - Filter to users whose display name or email contains
|
|
1205
|
+
* @param {string} [opts.q] - Filter to users whose display name contains this text, or whose email starts with it (contains it, when the text has an @), case-insensitive
|
|
1151
1206
|
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
1152
1207
|
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
1153
1208
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
@@ -1161,7 +1216,7 @@ class PlaidClient {
|
|
|
1161
1216
|
/**
|
|
1162
1217
|
* Async-iterate users page by page; yields each page's entries array.
|
|
1163
1218
|
* @param {object} [opts]
|
|
1164
|
-
* @param {string} [opts.q] - Filter to users whose display name or email contains
|
|
1219
|
+
* @param {string} [opts.q] - Filter to users whose display name contains this text, or whose email starts with it (contains it, when the text has an @), case-insensitive
|
|
1165
1220
|
* @param {number} [opts.pageSize] - Per-request page size
|
|
1166
1221
|
* @returns {AsyncGenerator<Array>}
|
|
1167
1222
|
*/
|
|
@@ -1171,7 +1226,8 @@ class PlaidClient {
|
|
|
1171
1226
|
query: { q },
|
|
1172
1227
|
}),
|
|
1173
1228
|
/**
|
|
1174
|
-
* Create a new user.
|
|
1229
|
+
* Create a new user. Admin only, and not with a named API token (403):
|
|
1230
|
+
* it needs a sign-in token.
|
|
1175
1231
|
* @param {string} email - The account's email address. It becomes the
|
|
1176
1232
|
* user's id and is what they log in with; it can never be changed.
|
|
1177
1233
|
* @param {string} password - The password
|
|
@@ -1222,11 +1278,15 @@ class PlaidClient {
|
|
|
1222
1278
|
* @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
|
|
1223
1279
|
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
1224
1280
|
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
1281
|
+
* @param {number} [opts.opsLimit] - Keep only each entry's oldest N
|
|
1282
|
+
* operations in `ops` (1..1000). `opCount` on every entry says how
|
|
1283
|
+
* many it has
|
|
1284
|
+
* @param {string} [opts.entryId] - Read only this entry, not a page
|
|
1225
1285
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
1226
1286
|
*/
|
|
1227
1287
|
auditPage: (
|
|
1228
1288
|
userId,
|
|
1229
|
-
{ startTime, endTime, opTypes, kinds, order, limit, cursor } = {},
|
|
1289
|
+
{ startTime, endTime, opTypes, kinds, order, limit, cursor, opsLimit, entryId } = {},
|
|
1230
1290
|
) =>
|
|
1231
1291
|
listPage(this, `/api/v1/users/${userId}/audit`, {
|
|
1232
1292
|
limit,
|
|
@@ -1237,6 +1297,8 @@ class PlaidClient {
|
|
|
1237
1297
|
"op-types": opTypesParam(opTypes),
|
|
1238
1298
|
kinds: kindsParam(kinds),
|
|
1239
1299
|
order,
|
|
1300
|
+
"ops-limit": opsLimit,
|
|
1301
|
+
"entry-id": entryId,
|
|
1240
1302
|
},
|
|
1241
1303
|
}),
|
|
1242
1304
|
/**
|
|
@@ -1266,7 +1328,8 @@ class PlaidClient {
|
|
|
1266
1328
|
/**
|
|
1267
1329
|
* Modify a user. Admins may change the display name, password, and admin
|
|
1268
1330
|
* status of any user. All other users may only modify their own display
|
|
1269
|
-
* name or password.
|
|
1331
|
+
* name or password. A password change cannot be made with a named API
|
|
1332
|
+
* token (403): it needs a sign-in token.
|
|
1270
1333
|
*
|
|
1271
1334
|
* A user's id is their email address and is fixed for the life of the
|
|
1272
1335
|
* account — it is what they log in with, so nothing can change it.
|
|
@@ -1489,6 +1552,8 @@ class PlaidClient {
|
|
|
1489
1552
|
* Mint a named API token for a user. The returned `token` is the signed
|
|
1490
1553
|
* credential and is shown ONLY here — store it immediately. API tokens
|
|
1491
1554
|
* do not expire and survive password changes / logout; revoke to kill.
|
|
1555
|
+
* A client signed in with a named API token cannot do this (403): it
|
|
1556
|
+
* needs a sign-in token.
|
|
1492
1557
|
* @param {string} userId - The user ID who will own the token
|
|
1493
1558
|
* @param {string} name - A human label, e.g. "Stanza parser"
|
|
1494
1559
|
* @returns {Promise<{id: string, name: string, token: string}>}
|
|
@@ -1497,11 +1562,14 @@ class PlaidClient {
|
|
|
1497
1562
|
this._request("POST", `/api/v1/users/${userId}/tokens`, {
|
|
1498
1563
|
auditMessage,
|
|
1499
1564
|
body: bodyOf({ name }),
|
|
1500
|
-
// The answer is the secret, which the server never keeps
|
|
1501
|
-
|
|
1565
|
+
// The answer is the secret, which the server never keeps, so it
|
|
1566
|
+
// takes no Idempotency-Key and a batch cannot carry it.
|
|
1567
|
+
noBatch: true,
|
|
1502
1568
|
}),
|
|
1503
1569
|
/**
|
|
1504
1570
|
* Revoke a named API token (soft-revoke; idempotent).
|
|
1571
|
+
* A client signed in with a named API token cannot do this (403): it
|
|
1572
|
+
* needs a sign-in token.
|
|
1505
1573
|
* @param {string} userId - The user ID who owns the token
|
|
1506
1574
|
* @param {string} tokenId - The token ID to revoke
|
|
1507
1575
|
*/
|
|
@@ -1564,6 +1632,9 @@ class PlaidClient {
|
|
|
1564
1632
|
* invite, no password resets — so for a non-admin, projectId/projectRole
|
|
1565
1633
|
* are required in practice (403 without them).
|
|
1566
1634
|
*
|
|
1635
|
+
* Needs a signed-in session: a client signed in with a named API token
|
|
1636
|
+
* cannot mint any invite (403).
|
|
1637
|
+
*
|
|
1567
1638
|
* EVERY option is optional; `create()` with no arguments mints a
|
|
1568
1639
|
* single-use signup link granting nothing but an account. Two pairing
|
|
1569
1640
|
* rules the server enforces with a 400: projectId and projectRole must be
|
|
@@ -1593,8 +1664,9 @@ class PlaidClient {
|
|
|
1593
1664
|
) =>
|
|
1594
1665
|
this._request("POST", "/api/v1/invites", {
|
|
1595
1666
|
auditMessage,
|
|
1596
|
-
// The answer is the code, which the server never keeps
|
|
1597
|
-
|
|
1667
|
+
// The answer is the code, which the server never keeps, so it
|
|
1668
|
+
// takes no Idempotency-Key and a batch cannot carry it.
|
|
1669
|
+
noBatch: true,
|
|
1598
1670
|
body: bodyOf({
|
|
1599
1671
|
"project-id": projectId,
|
|
1600
1672
|
"project-role": projectRole,
|
|
@@ -1629,13 +1701,22 @@ class PlaidClient {
|
|
|
1629
1701
|
* Resolves to the backup block with `ok` reporting whether the snapshot
|
|
1630
1702
|
* succeeded. Uses VACUUM INTO, which only reads, so it is safe while
|
|
1631
1703
|
* people are working. Admin only.
|
|
1704
|
+
*
|
|
1705
|
+
* The server answers once the backup is written, which takes minutes on
|
|
1706
|
+
* a large database, so this waits up to `BACKUP_TIMEOUT_MS` (30 minutes)
|
|
1707
|
+
* rather than the client's usual timeout. A proxy in front of the server
|
|
1708
|
+
* may still give up first (a 504): the backup goes on, and its file
|
|
1709
|
+
* shows in `server().backup.backups` once it is written.
|
|
1632
1710
|
* @returns {Promise<{ok: boolean, directory: string, backups: Array}>}
|
|
1633
1711
|
*
|
|
1634
1712
|
* outOfBand, as every admin action on the server itself is: none of them
|
|
1635
1713
|
* writes project data (see the note at the top of http.js).
|
|
1636
1714
|
*/
|
|
1637
1715
|
backup: () =>
|
|
1638
|
-
this._request("POST", "/api/v1/admin/backup", {
|
|
1716
|
+
this._request("POST", "/api/v1/admin/backup", {
|
|
1717
|
+
outOfBand: true,
|
|
1718
|
+
timeout: BACKUP_TIMEOUT_MS,
|
|
1719
|
+
}),
|
|
1639
1720
|
/**
|
|
1640
1721
|
* Documents currently held by an editing lock, with who holds each and
|
|
1641
1722
|
* when it expires on its own. Admin only.
|
|
@@ -1786,9 +1867,13 @@ class PlaidClient {
|
|
|
1786
1867
|
* is what a feed wants. A cursor belongs to the direction that made it.
|
|
1787
1868
|
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
1788
1869
|
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
1870
|
+
* @param {number} [opts.opsLimit] - Keep only each entry's oldest N
|
|
1871
|
+
* operations in `ops` (1..1000). `opCount` on every entry says how
|
|
1872
|
+
* many it has
|
|
1873
|
+
* @param {string} [opts.entryId] - Read only this entry, not a page
|
|
1789
1874
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
1790
1875
|
*/
|
|
1791
|
-
listPage: ({ startTime, endTime, opTypes, kinds, order, limit, cursor } = {}) =>
|
|
1876
|
+
listPage: ({ startTime, endTime, opTypes, kinds, order, limit, cursor, opsLimit, entryId } = {}) =>
|
|
1792
1877
|
listPage(this, "/api/v1/audit", {
|
|
1793
1878
|
limit,
|
|
1794
1879
|
cursor,
|
|
@@ -1798,6 +1883,8 @@ class PlaidClient {
|
|
|
1798
1883
|
"op-types": opTypesParam(opTypes),
|
|
1799
1884
|
kinds: kindsParam(kinds),
|
|
1800
1885
|
order,
|
|
1886
|
+
"ops-limit": opsLimit,
|
|
1887
|
+
"entry-id": entryId,
|
|
1801
1888
|
},
|
|
1802
1889
|
}),
|
|
1803
1890
|
/**
|
|
@@ -2059,6 +2146,11 @@ class PlaidClient {
|
|
|
2059
2146
|
}),
|
|
2060
2147
|
/**
|
|
2061
2148
|
* Upload a media file for a document. Uses Apache Tika for content validation.
|
|
2149
|
+
* A document that already has a recording refuses it with a 409
|
|
2150
|
+
* (`media-exists`, and the current `media-url`): delete that one first.
|
|
2151
|
+
* A WAV whose samples are not PCM or float (IMA or MS ADPCM, GSM, A-law
|
|
2152
|
+
* and the rest), which browsers cannot play, is refused with a 415
|
|
2153
|
+
* whose `error` names its coding.
|
|
2062
2154
|
* @param {string} documentId - The document ID
|
|
2063
2155
|
* @param {File} file - The file to upload
|
|
2064
2156
|
* @param {string} [auditMessage] - Custom audit-log message for this write
|
|
@@ -2078,15 +2170,21 @@ class PlaidClient {
|
|
|
2078
2170
|
/**
|
|
2079
2171
|
* Delete media file for a document
|
|
2080
2172
|
* @param {string} documentId - The document ID
|
|
2173
|
+
* @param {string} [auditMessage] - Audit message for this write
|
|
2174
|
+
* @param {{mediaVersion?: string}} [options] - `mediaVersion`: the
|
|
2175
|
+
* recording meant, the `?v=` of the document's `mediaUrl`. The delete
|
|
2176
|
+
* is refused with a 409 (`media-changed`, and the current `media-url`)
|
|
2177
|
+
* when the stored recording is another one.
|
|
2081
2178
|
*/
|
|
2082
2179
|
// The upload above is multipart and cannot be batched, but a DELETE
|
|
2083
2180
|
// carries no blob, so the batch transport takes it. It is a write
|
|
2084
2181
|
// of the document's own data and queues like any other. Note that the
|
|
2085
2182
|
// file removal happens outside the server's transaction, so a batch that
|
|
2086
2183
|
// aborts after this op does not bring the file back.
|
|
2087
|
-
deleteMedia: (documentId, auditMessage) =>
|
|
2184
|
+
deleteMedia: (documentId, auditMessage, { mediaVersion } = {}) =>
|
|
2088
2185
|
this._request("DELETE", `/api/v1/documents/${documentId}/media`, {
|
|
2089
2186
|
auditMessage,
|
|
2187
|
+
queryParams: { "media-version": mediaVersion },
|
|
2090
2188
|
}),
|
|
2091
2189
|
/**
|
|
2092
2190
|
* Replace all metadata for a document.
|
|
@@ -2147,11 +2245,15 @@ class PlaidClient {
|
|
|
2147
2245
|
* @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
|
|
2148
2246
|
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
2149
2247
|
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
2248
|
+
* @param {number} [opts.opsLimit] - Keep only each entry's oldest N
|
|
2249
|
+
* operations in `ops` (1..1000). `opCount` on every entry says how
|
|
2250
|
+
* many it has
|
|
2251
|
+
* @param {string} [opts.entryId] - Read only this entry, not a page
|
|
2150
2252
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
2151
2253
|
*/
|
|
2152
2254
|
auditPage: (
|
|
2153
2255
|
documentId,
|
|
2154
|
-
{ startTime, endTime, opTypes, kinds, order, limit, cursor } = {},
|
|
2256
|
+
{ startTime, endTime, opTypes, kinds, order, limit, cursor, opsLimit, entryId } = {},
|
|
2155
2257
|
) =>
|
|
2156
2258
|
listPage(this, `/api/v1/documents/${documentId}/audit`, {
|
|
2157
2259
|
limit,
|
|
@@ -2162,6 +2264,8 @@ class PlaidClient {
|
|
|
2162
2264
|
"op-types": opTypesParam(opTypes),
|
|
2163
2265
|
kinds: kindsParam(kinds),
|
|
2164
2266
|
order,
|
|
2267
|
+
"ops-limit": opsLimit,
|
|
2268
|
+
"entry-id": entryId,
|
|
2165
2269
|
},
|
|
2166
2270
|
}),
|
|
2167
2271
|
/**
|
|
@@ -2377,11 +2481,15 @@ class PlaidClient {
|
|
|
2377
2481
|
* @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
|
|
2378
2482
|
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
2379
2483
|
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
2484
|
+
* @param {number} [opts.opsLimit] - Keep only each entry's oldest N
|
|
2485
|
+
* operations in `ops` (1..1000). `opCount` on every entry says how
|
|
2486
|
+
* many it has
|
|
2487
|
+
* @param {string} [opts.entryId] - Read only this entry, not a page
|
|
2380
2488
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
2381
2489
|
*/
|
|
2382
2490
|
auditPage: (
|
|
2383
2491
|
projectId,
|
|
2384
|
-
{ startTime, endTime, opTypes, kinds, order, limit, cursor } = {},
|
|
2492
|
+
{ startTime, endTime, opTypes, kinds, order, limit, cursor, opsLimit, entryId } = {},
|
|
2385
2493
|
) =>
|
|
2386
2494
|
listPage(this, `/api/v1/projects/${projectId}/audit`, {
|
|
2387
2495
|
limit,
|
|
@@ -2392,6 +2500,8 @@ class PlaidClient {
|
|
|
2392
2500
|
"op-types": opTypesParam(opTypes),
|
|
2393
2501
|
kinds: kindsParam(kinds),
|
|
2394
2502
|
order,
|
|
2503
|
+
"ops-limit": opsLimit,
|
|
2504
|
+
"entry-id": entryId,
|
|
2395
2505
|
},
|
|
2396
2506
|
}),
|
|
2397
2507
|
/**
|
|
@@ -2709,8 +2819,11 @@ class PlaidClient {
|
|
|
2709
2819
|
* losers are deleted. Links are read when the merge runs, so one made
|
|
2710
2820
|
* after the caller looked moves too. Every loser must be in the
|
|
2711
2821
|
* survivor's vocabulary, and a loser that is already gone is skipped, so
|
|
2712
|
-
* a repeated merge changes nothing.
|
|
2713
|
-
*
|
|
2822
|
+
* a repeated merge changes nothing. Metadata naming a loser by its id,
|
|
2823
|
+
* in the vocabulary's other entries and in the documents of every
|
|
2824
|
+
* project it is linked to, is rewritten to name the survivor. A
|
|
2825
|
+
* reference to a loser in the survivor's own metadata is the caller's to
|
|
2826
|
+
* rewrite, in the same batch.
|
|
2714
2827
|
* Needs maintainer rights on the vocabulary.
|
|
2715
2828
|
* @param {string} survivorId - The entry that stays
|
|
2716
2829
|
* @param {string[]} loserIds - The entries merged into it
|
|
@@ -2913,8 +3026,11 @@ class PlaidClient {
|
|
|
2913
3026
|
* @param {string} tokenId - The token ID
|
|
2914
3027
|
* @param {number} position - Code-point offset to split at (strictly between begin and end)
|
|
2915
3028
|
* @param {string} [auditMessage] - Audit message for this write
|
|
2916
|
-
* @param {{id?: string}} [options] - `id`: the
|
|
2917
|
-
*
|
|
3029
|
+
* @param {{id?: string, keep?: "left"|"right"}} [options] - `id`: the new
|
|
3030
|
+
* token's id, a UUIDv7 this client minted. `keep`: the half the original token
|
|
3031
|
+
* (its id, spans, vocab-links, comments and metadata) stays on, `"left"` by
|
|
3032
|
+
* default. With `"right"` the new token, whose id is answered, is the left
|
|
3033
|
+
* half. A relation layer whose relations must stay inside one token
|
|
2918
3034
|
* of this layer declares a same-ancestor constraint, and the server deletes
|
|
2919
3035
|
* the relations the split leaves crossing in the same transaction.
|
|
2920
3036
|
*/
|
|
@@ -2922,11 +3038,11 @@ class PlaidClient {
|
|
|
2922
3038
|
tokenId,
|
|
2923
3039
|
position,
|
|
2924
3040
|
auditMessage,
|
|
2925
|
-
{ id } = {},
|
|
3041
|
+
{ id, keep } = {},
|
|
2926
3042
|
) =>
|
|
2927
3043
|
this._request("POST", `/api/v1/tokens/${tokenId}/split`, {
|
|
2928
3044
|
auditMessage,
|
|
2929
|
-
body: bodyOf({ id, position }),
|
|
3045
|
+
body: bodyOf({ id, keep, position }),
|
|
2930
3046
|
}),
|
|
2931
3047
|
/**
|
|
2932
3048
|
* Merge two tokens. The left token (smaller begin) survives with the combined
|
|
@@ -3175,9 +3291,16 @@ class PlaidClient {
|
|
|
3175
3291
|
*/
|
|
3176
3292
|
// outOfBand: a query is a read that travels as a POST (see the note at
|
|
3177
3293
|
// the top of http.js). Made on a batch it goes over the wire like any
|
|
3178
|
-
// read, and it never joins a logical operation.
|
|
3294
|
+
// read, and it never joins a logical operation. It waits past core's own
|
|
3295
|
+
// query limit, so a query too broad answers with core's 408, and a 503
|
|
3296
|
+
// from core's full queue of large queries is not retried.
|
|
3179
3297
|
this.query = (body) =>
|
|
3180
|
-
this._request("POST", "/api/v1/query", {
|
|
3298
|
+
this._request("POST", "/api/v1/query", {
|
|
3299
|
+
body,
|
|
3300
|
+
outOfBand: true,
|
|
3301
|
+
timeout: queryTimeout(this),
|
|
3302
|
+
noBusyRetry: true,
|
|
3303
|
+
});
|
|
3181
3304
|
|
|
3182
3305
|
// Logical-operation groups (audit-log grouping). There is no create: a
|
|
3183
3306
|
// group row is made lazily by the first write carrying `?group-id=`
|
|
@@ -3638,6 +3761,7 @@ class PlaidClient {
|
|
|
3638
3761
|
* `kind` says what kind of operation this is, for a program reading the
|
|
3639
3762
|
* log: one of `assistant-plan`, `service-run`, `import`, `bulk-edit`,
|
|
3640
3763
|
* `guess-adoption`, `repair` or `review` (the server refuses any other).
|
|
3764
|
+
* Any other kind throws here, before anything is written.
|
|
3641
3765
|
* `ref` is a short string naming what the operation came from, in the
|
|
3642
3766
|
* shape its kind documents (the core manual, "Kinds of operation"). Both
|
|
3643
3767
|
* are recorded from the first write like the label, and a nested operation
|
|
@@ -3653,10 +3777,15 @@ class PlaidClient {
|
|
|
3653
3777
|
* matching end, and the outer numbering resumes after it.
|
|
3654
3778
|
*
|
|
3655
3779
|
* @param {string} message - Human label for the operation.
|
|
3656
|
-
* @param {object} [opts] - Optional `{
|
|
3780
|
+
* @param {object} [opts] - Optional `{ groupId, kind, ref, keys, minted }`. `groupId` adopts an existing group id instead of minting one (a service joining the requester's operation; `requestService` propagates an open operation to the service automatically). Python's is `group_id`. `kind`, `ref` and `keys` are described above. `minted` is the ids the operation mints for what it creates: a create refused 409 id-taken for one of them was made by an earlier send of the operation, and answers as made.
|
|
3657
3781
|
* @returns {string} The operation's group id.
|
|
3658
3782
|
*/
|
|
3659
|
-
beginOperation(message, {
|
|
3783
|
+
beginOperation(message, { groupId, kind, ref, keys, minted } = {}) {
|
|
3784
|
+
if (kind != null && !OPERATION_KINDS.includes(String(kind))) {
|
|
3785
|
+
throw new Error(
|
|
3786
|
+
`Unknown operation kind "${kind}". It is one of: ${OPERATION_KINDS.join(", ")}.`,
|
|
3787
|
+
);
|
|
3788
|
+
}
|
|
3660
3789
|
const frame = {
|
|
3661
3790
|
keys: keys || null,
|
|
3662
3791
|
count: 0,
|
|
@@ -3680,7 +3809,7 @@ class PlaidClient {
|
|
|
3680
3809
|
return open.id;
|
|
3681
3810
|
}
|
|
3682
3811
|
this.operationGroup = {
|
|
3683
|
-
id:
|
|
3812
|
+
id: groupId ? String(groupId) : crypto.randomUUID(),
|
|
3684
3813
|
message: message == null ? null : String(message),
|
|
3685
3814
|
kind: kind == null ? null : String(kind),
|
|
3686
3815
|
ref: ref == null ? null : String(ref),
|
|
@@ -3754,11 +3883,11 @@ class PlaidClient {
|
|
|
3754
3883
|
*
|
|
3755
3884
|
* @param {string} message - Human label for the operation.
|
|
3756
3885
|
* @param {function} fn - The work to run; receives `setMessage(msg)` to refine the label once the outcome is known.
|
|
3757
|
-
* @param {object} [opts] - Optional `{ kind, ref,
|
|
3886
|
+
* @param {object} [opts] - Optional `{ kind, ref, groupId, keys, minted }`, as for beginOperation. `kind` is one of `assistant-plan`, `service-run`, `import`, `bulk-edit`, `guess-adoption`, `repair` or `review`, and any other throws before `fn` runs.
|
|
3758
3887
|
* @returns {Promise<any>} Whatever `fn` resolves to.
|
|
3759
3888
|
*/
|
|
3760
|
-
async withOperation(message, fn, { kind, ref,
|
|
3761
|
-
this.beginOperation(message, { kind, ref,
|
|
3889
|
+
async withOperation(message, fn, { kind, ref, groupId, keys, minted } = {}) {
|
|
3890
|
+
this.beginOperation(message, { kind, ref, groupId, keys, minted });
|
|
3762
3891
|
const group = this.operationGroup;
|
|
3763
3892
|
// The key frame this call opened is ended by this call, whatever else
|
|
3764
3893
|
// began or ended meanwhile, not by whichever end comes at its depth.
|
|
@@ -3854,8 +3983,10 @@ class PlaidClient {
|
|
|
3854
3983
|
}
|
|
3855
3984
|
return res;
|
|
3856
3985
|
};
|
|
3857
|
-
const
|
|
3986
|
+
const onRetry = (info) => this._noteRetry(info);
|
|
3987
|
+
const response = await retryUnknown(() => retryWhileBusy(attempt, { onRetry }), {
|
|
3858
3988
|
delaysMs: this.retryDelaysMs,
|
|
3989
|
+
onRetry,
|
|
3859
3990
|
}).catch((error) => {
|
|
3860
3991
|
error.idempotencyKey = key;
|
|
3861
3992
|
// 401 means the token is missing, expired or invalid: fire the app's
|
|
@@ -3902,6 +4033,28 @@ class PlaidClient {
|
|
|
3902
4033
|
}
|
|
3903
4034
|
}
|
|
3904
4035
|
}
|
|
4036
|
+
// The batch's own header holds each document's version after the whole
|
|
4037
|
+
// batch, the layer rules' remedies at its end included, which no single
|
|
4038
|
+
// operation's answer does: a split that deletes a relation crossing
|
|
4039
|
+
// the new boundary moves the version once more.
|
|
4040
|
+
const outerVersions = response.headers?.get?.("X-Document-Versions");
|
|
4041
|
+
if (outerVersions) {
|
|
4042
|
+
try {
|
|
4043
|
+
const versionsMap = JSON.parse(outerVersions);
|
|
4044
|
+
if (typeof versionsMap === "object" && versionsMap !== null) {
|
|
4045
|
+
this.documentVersions = mergeVersions(
|
|
4046
|
+
this.documentVersions,
|
|
4047
|
+
versionsMap,
|
|
4048
|
+
replayed,
|
|
4049
|
+
);
|
|
4050
|
+
}
|
|
4051
|
+
} catch (e) {
|
|
4052
|
+
console.warn(
|
|
4053
|
+
"Failed to parse document versions header from batch response:",
|
|
4054
|
+
e,
|
|
4055
|
+
);
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
3905
4058
|
|
|
3906
4059
|
const out = results.map((result) => ({
|
|
3907
4060
|
...result,
|
|
@@ -3982,7 +4135,8 @@ class PlaidClient {
|
|
|
3982
4135
|
* Describe an invite code, with NO authentication — this is what a signup
|
|
3983
4136
|
* page calls before the redeemer has an account. Returns the kind of link
|
|
3984
4137
|
* ("signup" or "password-reset"), its status ("active", "used", "expired",
|
|
3985
|
-
* "revoked"
|
|
4138
|
+
* "revoked", or "inactive" when its creator can no longer grant what it
|
|
4139
|
+
* grants), and the project it grants access to, if any.
|
|
3986
4140
|
*
|
|
3987
4141
|
* Throws a 404-shaped error if the code is unknown. A known-but-dead code
|
|
3988
4142
|
* resolves normally with a non-"active" status, so the page can say why.
|
|
@@ -4176,7 +4330,7 @@ function constraintMethods(client, kind) {
|
|
|
4176
4330
|
|
|
4177
4331
|
export default PlaidClient;
|
|
4178
4332
|
export { PlaidClient };
|
|
4179
|
-
export {
|
|
4333
|
+
export { valueSetAllows, violationsOf } from "./constraints.js";
|
|
4180
4334
|
|
|
4181
4335
|
// Unicode code-point helpers for text offsets (token begin/end are code-point
|
|
4182
4336
|
// indices). See ./codepoint.js.
|
|
@@ -4185,7 +4339,6 @@ export {
|
|
|
4185
4339
|
cpSlice,
|
|
4186
4340
|
cpSlicer,
|
|
4187
4341
|
utf16ToCp,
|
|
4188
|
-
cpToUtf16,
|
|
4189
4342
|
cpIndexOf,
|
|
4190
4343
|
} from "./codepoint.js";
|
|
4191
4344
|
export { composeTextEdits, gapsToOps, applyTextOps } from "./textEdits.js";
|
|
@@ -4193,7 +4346,6 @@ export {
|
|
|
4193
4346
|
PLAID_NAMESPACE,
|
|
4194
4347
|
ROLE_KEY,
|
|
4195
4348
|
PRESERVE_ON_SPLIT_KEY,
|
|
4196
|
-
PLAIN_EDITS_KEY,
|
|
4197
4349
|
SPLIT_ON_SPACE_KEY,
|
|
4198
4350
|
ROLES,
|
|
4199
4351
|
readRole,
|
|
@@ -4222,11 +4374,7 @@ export { REQUESTED_BY, makeRequester, requesterOf } from "./requester.js";
|
|
|
4222
4374
|
// Document locks: `client.documents.locked()` holds one for a block of work and
|
|
4223
4375
|
// renews it while the block runs. DocumentLockLost is what a write throws once
|
|
4224
4376
|
// a renewal has failed. See ./documentLock.js.
|
|
4225
|
-
export {
|
|
4226
|
-
DOCUMENT_LOCK_TTL_MS,
|
|
4227
|
-
DocumentLock,
|
|
4228
|
-
DocumentLockLost,
|
|
4229
|
-
} from "./documentLock.js";
|
|
4377
|
+
export { DocumentLock, DocumentLockLost } from "./documentLock.js";
|
|
4230
4378
|
// Provenance: the cross-app convention for who made an annotation (flat
|
|
4231
4379
|
// prov/provSource/provConfirmed metadata; absence = a verifier; 'inferred' =
|
|
4232
4380
|
// a machine; 'contributed' = a person whose work is reviewed), plus the
|
|
@@ -4234,7 +4382,6 @@ export {
|
|
|
4234
4382
|
export {
|
|
4235
4383
|
PROV,
|
|
4236
4384
|
PROV_STATES,
|
|
4237
|
-
PROV_CONFIRMED,
|
|
4238
4385
|
PROVENANCE_KEYS,
|
|
4239
4386
|
stampInferred,
|
|
4240
4387
|
confirmedInferred,
|
|
@@ -4244,12 +4391,9 @@ export {
|
|
|
4244
4391
|
isMachine,
|
|
4245
4392
|
isProtected,
|
|
4246
4393
|
needsReview,
|
|
4247
|
-
verifyOnEdit,
|
|
4248
4394
|
contributeOnEdit,
|
|
4249
4395
|
serviceSource,
|
|
4250
|
-
userSource,
|
|
4251
4396
|
REVIEW_KEY,
|
|
4252
|
-
PROJECT_ROLES,
|
|
4253
4397
|
readReview,
|
|
4254
4398
|
projectRole,
|
|
4255
4399
|
isReviewed,
|
package/src/metadataOps.js
CHANGED
|
@@ -80,8 +80,10 @@ const applyOp = (node, path, depth, o) => {
|
|
|
80
80
|
/**
|
|
81
81
|
* Apply ops to a local copy of an entity's metadata the way the server does,
|
|
82
82
|
* for an optimistic update. Returns a new object and never mutates its input.
|
|
83
|
-
* Throws
|
|
84
|
-
*
|
|
83
|
+
* Throws on part of what the server refuses: an empty path, an op other than
|
|
84
|
+
* set or delete, a path through a non-object, or a first key that is blank,
|
|
85
|
+
* over 200 characters or holds a control character. The server's caps on
|
|
86
|
+
* depth, key count, string length and size are not checked here.
|
|
85
87
|
* @param {Object|null|undefined} metadata
|
|
86
88
|
* @param {Array<{op: string, path: string[], value?: any}>} ops
|
|
87
89
|
* @returns {Object}
|
package/src/provenance.js
CHANGED
|
@@ -257,9 +257,6 @@ export const userSource = (userId) => `user:${userId}`;
|
|
|
257
257
|
/** The config key, under the `plaid` namespace, holding the review lists. */
|
|
258
258
|
export const REVIEW_KEY = 'review';
|
|
259
259
|
|
|
260
|
-
/** The project roles a review list may name. */
|
|
261
|
-
export const PROJECT_ROLES = Object.freeze(['reader', 'writer', 'maintainer']);
|
|
262
|
-
|
|
263
260
|
/**
|
|
264
261
|
* The project's review lists, normalized: { users: string[], roles: string[] }.
|
|
265
262
|
* @param {Object|null|undefined} config - a project's `config`
|
package/src/roles.js
CHANGED
|
@@ -35,19 +35,10 @@ export const ROLE_KEY = 'role';
|
|
|
35
35
|
export const PRESERVE_ON_SPLIT_KEY = 'preserveOnSplit';
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Layer config key (true)
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* manual's `Changing a text's body`.
|
|
43
|
-
*/
|
|
44
|
-
export const PLAIN_EDITS_KEY = 'plainEdits';
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Layer config key (true), beside PLAIN_EDITS_KEY: a space typed inside a
|
|
48
|
-
* token of the text's words splits it, the token going on the half sharing
|
|
49
|
-
* more letters with it. Declared on a word layer another app shares, it holds
|
|
50
|
-
* for the whole text.
|
|
38
|
+
* Layer config key (true): a space typed inside a token of the text's words
|
|
39
|
+
* splits it, the token going on the half sharing more letters with it.
|
|
40
|
+
* Declared on a word layer another app shares, it holds for the whole text.
|
|
41
|
+
* See the manual's `Changing a text's body`.
|
|
51
42
|
*/
|
|
52
43
|
export const SPLIT_ON_SPACE_KEY = 'splitOnSpace';
|
|
53
44
|
|