@larc-iu/plaid-client 0.2.0-alpha.18 → 0.2.0-alpha.20
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 +262 -14
- package/package.json +1 -1
- package/src/batchRef.js +128 -0
- package/src/constraints.js +84 -0
- package/src/documentLock.js +67 -29
- package/src/events.js +21 -11
- package/src/http.js +249 -41
- package/src/ids.js +54 -0
- package/src/index.js +540 -135
- package/src/replayed.js +27 -0
- package/src/roles.js +17 -0
- package/src/services.js +49 -1
- package/src/textEdits.js +191 -0
- package/src/transforms.js +4 -0
package/src/index.js
CHANGED
|
@@ -13,13 +13,22 @@ import {
|
|
|
13
13
|
makeNetworkError,
|
|
14
14
|
timeoutSignal,
|
|
15
15
|
retryWhileBusy,
|
|
16
|
+
retryUnknown,
|
|
17
|
+
nextIdempotencyKey,
|
|
18
|
+
mergeVersions,
|
|
19
|
+
IDEMPOTENCY_HEADER,
|
|
20
|
+
REPLAYED_HEADER,
|
|
16
21
|
DEFAULT_TIMEOUT_MS,
|
|
17
22
|
DEFAULT_BATCH_TIMEOUT_MS,
|
|
18
23
|
} from "./http.js";
|
|
24
|
+
import { uuidv7 } from "./ids.js";
|
|
25
|
+
import { constraintsBody } from "./constraints.js";
|
|
19
26
|
import { listAll, listPage, iterPages } from "./pagination.js";
|
|
20
27
|
import { recorderFor } from "./events.js";
|
|
21
28
|
import { withDocumentLock } from "./documentLock.js";
|
|
22
29
|
import { createSSEConnection } from "./sse.js";
|
|
30
|
+
import { makeBatchRef, rebaseRefs } from "./batchRef.js";
|
|
31
|
+
import { markReplayed, wasReplayed } from "./replayed.js";
|
|
23
32
|
import {
|
|
24
33
|
discoverServices,
|
|
25
34
|
discardService,
|
|
@@ -47,6 +56,10 @@ function opTypesParam(opTypes) {
|
|
|
47
56
|
return joined.length > 0 ? joined : undefined;
|
|
48
57
|
}
|
|
49
58
|
|
|
59
|
+
// The same for an audit `kinds` filter (`?kinds=`): operation kinds, such as
|
|
60
|
+
// "review", in a list or a ready-made string.
|
|
61
|
+
const kindsParam = opTypesParam;
|
|
62
|
+
|
|
50
63
|
// Helper: build body object, filtering out undefined values
|
|
51
64
|
function bodyOf(obj) {
|
|
52
65
|
const result = {};
|
|
@@ -131,8 +144,10 @@ function openBatch(client) {
|
|
|
131
144
|
batch.client = client;
|
|
132
145
|
batch.operations = [];
|
|
133
146
|
// The document each queued op's strict-mode stamp is for (null for none),
|
|
134
|
-
// index for index with
|
|
147
|
+
// and the logical operation it joined (null for none), index for index with
|
|
148
|
+
// `operations`.
|
|
135
149
|
batch.stampedDocuments = [];
|
|
150
|
+
batch.stampedGroups = [];
|
|
136
151
|
batch.open = true;
|
|
137
152
|
batch._request = (method, path, options = {}) =>
|
|
138
153
|
queueRequest(batch, method, path, options);
|
|
@@ -140,10 +155,14 @@ function openBatch(client) {
|
|
|
140
155
|
throw new Error("A batch is not nestable: queue on the batch you have");
|
|
141
156
|
};
|
|
142
157
|
batch.batched = batch.batch;
|
|
158
|
+
// A stand-in for the id op n will create, for a later op's body (see
|
|
159
|
+
// batchRef.js). The server fills it in inside the batch's transaction.
|
|
160
|
+
batch.ref = (opIndex = -1, index) => makeBatchRef(batch, opIndex, index);
|
|
143
161
|
batch.submit = () => submitBatch(batch);
|
|
144
162
|
batch.abort = () => {
|
|
145
163
|
batch.operations = [];
|
|
146
164
|
batch.stampedDocuments = [];
|
|
165
|
+
batch.stampedGroups = [];
|
|
147
166
|
batch.open = false;
|
|
148
167
|
};
|
|
149
168
|
batch._installResources();
|
|
@@ -157,8 +176,10 @@ async function submitBatch(batch) {
|
|
|
157
176
|
batch.open = false;
|
|
158
177
|
const ops = batch.operations;
|
|
159
178
|
const stamps = batch.stampedDocuments;
|
|
179
|
+
const groups = batch.stampedGroups;
|
|
160
180
|
batch.operations = [];
|
|
161
181
|
batch.stampedDocuments = [];
|
|
182
|
+
batch.stampedGroups = [];
|
|
162
183
|
if (ops.length === 0) return [];
|
|
163
184
|
const client = batch.client;
|
|
164
185
|
const url = `${client.baseUrl}/api/v1/batch`;
|
|
@@ -176,9 +197,14 @@ async function submitBatch(batch) {
|
|
|
176
197
|
// were saved) and `committedResults` (their results, in queue order), so a
|
|
177
198
|
// caller can tell a partial write from a batch that saved nothing. The
|
|
178
199
|
// failed request itself counts as unsaved, even when its answer was lost.
|
|
200
|
+
//
|
|
201
|
+
// Each request's results are marked replayed when that request was (see
|
|
202
|
+
// replayed.js), and the combined results when any request was: then some of
|
|
203
|
+
// the batch stored nothing new, which is what a caller reading the mark
|
|
204
|
+
// needs to know. The same holds for `committedResults`.
|
|
179
205
|
const results = [];
|
|
180
206
|
try {
|
|
181
|
-
await sendChunks(client, url, ops, stamps, results);
|
|
207
|
+
await sendChunks(client, url, ops, stamps, results, groups);
|
|
182
208
|
} catch (error) {
|
|
183
209
|
if (error && typeof error === "object") {
|
|
184
210
|
error.committed = results.length;
|
|
@@ -189,24 +215,71 @@ async function submitBatch(batch) {
|
|
|
189
215
|
return results;
|
|
190
216
|
}
|
|
191
217
|
|
|
192
|
-
|
|
218
|
+
/** The document-version an op's path claims, as a number, or null. */
|
|
219
|
+
function claimedVersion(path) {
|
|
220
|
+
const v = new URLSearchParams(path.split("?")[1] ?? "").get("document-version");
|
|
221
|
+
return v == null ? null : Number(v);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function sendChunks(client, url, ops, stamps, results, groups = []) {
|
|
225
|
+
// Every chunk's refs are checked before the first request goes, so a batch
|
|
226
|
+
// whose refs cannot resolve writes nothing.
|
|
227
|
+
const chunks = [];
|
|
193
228
|
for (let i = 0; i < ops.length; i += MAX_BATCH_OPS) {
|
|
194
|
-
|
|
195
|
-
|
|
229
|
+
chunks.push(
|
|
230
|
+
ops
|
|
231
|
+
.slice(i, i + MAX_BATCH_OPS)
|
|
232
|
+
.map((op) =>
|
|
233
|
+
i > 0 && op.refs ? { ...op, refs: rebaseRefs(op.refs, i) } : op,
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
for (let c = 0; c < chunks.length; c++) {
|
|
238
|
+
const i = c * MAX_BATCH_OPS;
|
|
239
|
+
let chunk = chunks[c];
|
|
240
|
+
// One Idempotency-Key per request, minted as the request is formed, so a
|
|
241
|
+
// resend of this chunk is answered from its first send when that landed.
|
|
242
|
+
// Inside an operation opened with keys, a chunk whose ops joined it takes
|
|
243
|
+
// the operation's next key and claims the version its first run claimed.
|
|
244
|
+
const joins = groups
|
|
245
|
+
.slice(i, i + chunk.length)
|
|
246
|
+
.some((g) => g && g === client.operationGroup);
|
|
247
|
+
const { key, pin, record } = nextIdempotencyKey(client, joins);
|
|
248
|
+
const claim = (docId) =>
|
|
249
|
+
pin !== undefined ? pin : client.documentVersions[docId];
|
|
250
|
+
if (i > 0 || pin !== undefined) {
|
|
196
251
|
chunk = chunk.map((op, j) => {
|
|
197
252
|
const docId = stamps[i + j];
|
|
198
253
|
if (!docId) return op;
|
|
199
|
-
const path = restampDocumentVersion(
|
|
200
|
-
op.path,
|
|
201
|
-
client.documentVersions[docId],
|
|
202
|
-
);
|
|
254
|
+
const path = restampDocumentVersion(op.path, claim(docId));
|
|
203
255
|
return { ...op, path };
|
|
204
256
|
});
|
|
205
257
|
}
|
|
206
|
-
|
|
258
|
+
// Pin what the chunk actually claims: the first stamped op's version as
|
|
259
|
+
// its path now carries it.
|
|
260
|
+
const j = stamps.slice(i, i + chunk.length).findIndex(Boolean);
|
|
261
|
+
record(j < 0 ? null : claimedVersion(chunk[j].path));
|
|
262
|
+
const answered = await client._postBatch(url, chunk, { key });
|
|
263
|
+
results.push(...answered);
|
|
264
|
+
if (wasReplayed(answered)) markReplayed(results);
|
|
265
|
+
// The server took the chunk, so each operation it joined exists.
|
|
266
|
+
for (let j = i; j < i + chunk.length; j++) {
|
|
267
|
+
if (groups[j]) groups[j].written = true;
|
|
268
|
+
}
|
|
207
269
|
}
|
|
208
270
|
}
|
|
209
271
|
|
|
272
|
+
/**
|
|
273
|
+
* End the key frame `frame` of `group`, wherever it is among the frames open:
|
|
274
|
+
* the frame under it, or the one open over it, numbers from then on. A plain
|
|
275
|
+
* function, so a caller that borrows withOperation onto another object
|
|
276
|
+
* (test doubles do) keeps it.
|
|
277
|
+
*/
|
|
278
|
+
function dropFrame(group, frame) {
|
|
279
|
+
if (!group || !frame || group.frames[0] === frame) return;
|
|
280
|
+
group.frames = group.frames.filter((f) => f !== frame);
|
|
281
|
+
}
|
|
282
|
+
|
|
210
283
|
/** The query of an events read: `types` as the wire's comma-separated list. */
|
|
211
284
|
function eventQuery(types, startTime, endTime) {
|
|
212
285
|
return {
|
|
@@ -241,6 +314,9 @@ class PlaidClient {
|
|
|
241
314
|
: options.timeout !== undefined
|
|
242
315
|
? options.timeout
|
|
243
316
|
: DEFAULT_BATCH_TIMEOUT_MS;
|
|
317
|
+
// Delays (ms) before sending a keyed write again when its answer was lost
|
|
318
|
+
// (no response, 502, 504). See the Idempotency-Key note in http.js.
|
|
319
|
+
this.retryDelaysMs = options.retryDelaysMs;
|
|
244
320
|
this.documentVersions = {};
|
|
245
321
|
// The server's clock minus this machine's, in ms, from the last response
|
|
246
322
|
// with a Date header (null before one). See serverNow().
|
|
@@ -253,7 +329,8 @@ class PlaidClient {
|
|
|
253
329
|
// The open logical operation (audit-log group), or null. While set, every
|
|
254
330
|
// write is stamped with `?group-id=` (+ `group-message`) so the audit log
|
|
255
331
|
// folds them into ONE expandable entry. See beginOperation / withOperation.
|
|
256
|
-
// Shape: { id, message, depth, written, refined }
|
|
332
|
+
// Shape: { id, message, depth, written, refined, frames }, where each of
|
|
333
|
+
// `frames` is { keys, count, minted, depth, owned } and the last numbers.
|
|
257
334
|
this.operationGroup = null;
|
|
258
335
|
// Optional callback fired once (per client) when any request returns HTTP
|
|
259
336
|
// 401 — i.e. the token is missing/expired/invalid. Apps use it to discard
|
|
@@ -289,10 +366,10 @@ class PlaidClient {
|
|
|
289
366
|
* @param {Array} tokens - The tokens to link
|
|
290
367
|
* @param {any} [metadata] - Metadata for the link. Omit to leave unset; pass null to send JSON null.
|
|
291
368
|
*/
|
|
292
|
-
create: (vocabItem, tokens, metadata, auditMessage) =>
|
|
369
|
+
create: (vocabItem, tokens, metadata, auditMessage, { id } = {}) =>
|
|
293
370
|
this._request("POST", "/api/v1/vocab-links", {
|
|
294
371
|
auditMessage,
|
|
295
|
-
body: bodyOf({ "vocab-item": vocabItem, tokens, metadata }),
|
|
372
|
+
body: bodyOf({ id, "vocab-item": vocabItem, tokens, metadata }),
|
|
296
373
|
}),
|
|
297
374
|
/**
|
|
298
375
|
* Create multiple vocab links in a single operation. Entries may
|
|
@@ -396,14 +473,17 @@ class PlaidClient {
|
|
|
396
473
|
* `['vocab-item/delete', 'vocab-item/restore']`)
|
|
397
474
|
* @param {string} [itemId] - Only the changes that wrote this one entry,
|
|
398
475
|
* each with only its operations that did
|
|
476
|
+
* @param {string[]|string} [kinds] - Only the entries of operations of
|
|
477
|
+
* these kinds (e.g. `['review', 'guess-adoption']`), each whole
|
|
399
478
|
*/
|
|
400
|
-
audit: (id, startTime, endTime, opTypes, itemId) =>
|
|
479
|
+
audit: (id, startTime, endTime, opTypes, itemId, kinds) =>
|
|
401
480
|
listAll(this, `/api/v1/vocab-layers/${id}/audit`, {
|
|
402
481
|
query: {
|
|
403
482
|
"start-time": startTime,
|
|
404
483
|
"end-time": endTime,
|
|
405
484
|
"op-types": opTypesParam(opTypes),
|
|
406
485
|
"item-id": itemId,
|
|
486
|
+
kinds: kindsParam(kinds),
|
|
407
487
|
},
|
|
408
488
|
}),
|
|
409
489
|
/**
|
|
@@ -421,7 +501,7 @@ class PlaidClient {
|
|
|
421
501
|
*/
|
|
422
502
|
auditPage: (
|
|
423
503
|
id,
|
|
424
|
-
{ startTime, endTime, opTypes, order, limit, cursor, itemId } = {},
|
|
504
|
+
{ startTime, endTime, opTypes, kinds, order, limit, cursor, itemId } = {},
|
|
425
505
|
) =>
|
|
426
506
|
listPage(this, `/api/v1/vocab-layers/${id}/audit`, {
|
|
427
507
|
limit,
|
|
@@ -430,6 +510,7 @@ class PlaidClient {
|
|
|
430
510
|
"start-time": startTime,
|
|
431
511
|
"end-time": endTime,
|
|
432
512
|
"op-types": opTypesParam(opTypes),
|
|
513
|
+
kinds: kindsParam(kinds),
|
|
433
514
|
order,
|
|
434
515
|
"item-id": itemId,
|
|
435
516
|
},
|
|
@@ -488,24 +569,29 @@ class PlaidClient {
|
|
|
488
569
|
* @param {string} namespace - The config namespace
|
|
489
570
|
* @param {string} configKey - The config key
|
|
490
571
|
* @param {any} configValue - Configuration value to set
|
|
572
|
+
* @param {string} [auditMessage] - Audit message for this write
|
|
573
|
+
* @param {{expected?: any}} [options] - `expected`: the value read for this key (null or undefined when it
|
|
574
|
+
* was absent). The write is then refused with a 409 when the stored value differs.
|
|
491
575
|
*/
|
|
492
|
-
setConfig: (id, namespace, configKey, configValue, auditMessage) =>
|
|
576
|
+
setConfig: (id, namespace, configKey, configValue, auditMessage, options) =>
|
|
493
577
|
this._request(
|
|
494
578
|
"PUT",
|
|
495
579
|
`/api/v1/vocab-layers/${id}/config/${namespace}/${configKey}`,
|
|
496
|
-
|
|
580
|
+
configRequest(auditMessage, configValue, options),
|
|
497
581
|
),
|
|
498
582
|
/**
|
|
499
583
|
* Remove a configuration value for a layer.
|
|
500
584
|
* @param {string} id - The resource ID
|
|
501
585
|
* @param {string} namespace - The config namespace
|
|
502
586
|
* @param {string} configKey - The config key
|
|
587
|
+
* @param {string} [auditMessage] - Audit message for this write
|
|
588
|
+
* @param {{expected?: any}} [options] - `expected`, as on setConfig.
|
|
503
589
|
*/
|
|
504
|
-
deleteConfig: (id, namespace, configKey, auditMessage) =>
|
|
590
|
+
deleteConfig: (id, namespace, configKey, auditMessage, options) =>
|
|
505
591
|
this._request(
|
|
506
592
|
"DELETE",
|
|
507
593
|
`/api/v1/vocab-layers/${id}/config/${namespace}/${configKey}`,
|
|
508
|
-
|
|
594
|
+
configRequest(auditMessage, undefined, options, true),
|
|
509
595
|
),
|
|
510
596
|
/**
|
|
511
597
|
* List all vocab layers accessible to user. Transparently follows
|
|
@@ -538,10 +624,10 @@ class PlaidClient {
|
|
|
538
624
|
* Create a new vocab layer. Note: this also registers the user as a maintainer.
|
|
539
625
|
* @param {string} name - The name
|
|
540
626
|
*/
|
|
541
|
-
create: (name, auditMessage) =>
|
|
627
|
+
create: (name, auditMessage, { id } = {}) =>
|
|
542
628
|
this._request("POST", "/api/v1/vocab-layers", {
|
|
543
629
|
auditMessage,
|
|
544
|
-
body: bodyOf({ name }),
|
|
630
|
+
body: bodyOf({ id, name }),
|
|
545
631
|
}),
|
|
546
632
|
/**
|
|
547
633
|
* Assign a user as a maintainer for this vocab layer.
|
|
@@ -650,10 +736,19 @@ class PlaidClient {
|
|
|
650
736
|
* @param {any} value - The value
|
|
651
737
|
* @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
|
|
652
738
|
*/
|
|
653
|
-
create: (
|
|
739
|
+
create: (
|
|
740
|
+
layerId,
|
|
741
|
+
sourceId,
|
|
742
|
+
targetId,
|
|
743
|
+
value,
|
|
744
|
+
metadata,
|
|
745
|
+
auditMessage,
|
|
746
|
+
{ id } = {},
|
|
747
|
+
) =>
|
|
654
748
|
this._request("POST", "/api/v1/relations", {
|
|
655
749
|
auditMessage,
|
|
656
750
|
body: bodyOf({
|
|
751
|
+
id,
|
|
657
752
|
"layer-id": layerId,
|
|
658
753
|
"source-id": sourceId,
|
|
659
754
|
"target-id": targetId,
|
|
@@ -696,31 +791,31 @@ class PlaidClient {
|
|
|
696
791
|
* @param {string} namespace - The config namespace
|
|
697
792
|
* @param {string} configKey - The config key
|
|
698
793
|
* @param {any} configValue - Configuration value to set
|
|
794
|
+
* @param {string} [auditMessage] - Audit message for this write
|
|
795
|
+
* @param {{expected?: any}} [options] - `expected`: the value read for this key (null or undefined when it
|
|
796
|
+
* was absent). The write is then refused with a 409 when the stored value differs.
|
|
699
797
|
*/
|
|
700
|
-
setConfig: (
|
|
701
|
-
spanLayerId,
|
|
702
|
-
namespace,
|
|
703
|
-
configKey,
|
|
704
|
-
configValue,
|
|
705
|
-
auditMessage,
|
|
706
|
-
) =>
|
|
798
|
+
setConfig: (spanLayerId, namespace, configKey, configValue, auditMessage, options) =>
|
|
707
799
|
this._request(
|
|
708
800
|
"PUT",
|
|
709
801
|
`/api/v1/span-layers/${spanLayerId}/config/${namespace}/${configKey}`,
|
|
710
|
-
|
|
802
|
+
configRequest(auditMessage, configValue, options),
|
|
711
803
|
),
|
|
712
804
|
/**
|
|
713
805
|
* Remove a configuration value for a layer.
|
|
714
806
|
* @param {string} spanLayerId - The span layer ID
|
|
715
807
|
* @param {string} namespace - The config namespace
|
|
716
808
|
* @param {string} configKey - The config key
|
|
809
|
+
* @param {string} [auditMessage] - Audit message for this write
|
|
810
|
+
* @param {{expected?: any}} [options] - `expected`, as on setConfig.
|
|
717
811
|
*/
|
|
718
|
-
deleteConfig: (spanLayerId, namespace, configKey, auditMessage) =>
|
|
812
|
+
deleteConfig: (spanLayerId, namespace, configKey, auditMessage, options) =>
|
|
719
813
|
this._request(
|
|
720
814
|
"DELETE",
|
|
721
815
|
`/api/v1/span-layers/${spanLayerId}/config/${namespace}/${configKey}`,
|
|
722
|
-
|
|
816
|
+
configRequest(auditMessage, undefined, options, true),
|
|
723
817
|
),
|
|
818
|
+
...constraintMethods(this, "span-layers"),
|
|
724
819
|
/**
|
|
725
820
|
* Get a span layer by ID.
|
|
726
821
|
* @param {string} spanLayerId - The span layer ID
|
|
@@ -750,10 +845,10 @@ class PlaidClient {
|
|
|
750
845
|
* @param {string} tokenLayerId - The token layer ID
|
|
751
846
|
* @param {string} name - The name
|
|
752
847
|
*/
|
|
753
|
-
create: (tokenLayerId, name, auditMessage) =>
|
|
848
|
+
create: (tokenLayerId, name, auditMessage, { id } = {}) =>
|
|
754
849
|
this._request("POST", "/api/v1/span-layers", {
|
|
755
850
|
auditMessage,
|
|
756
|
-
body: bodyOf({ "token-layer-id": tokenLayerId, name }),
|
|
851
|
+
body: bodyOf({ id, "token-layer-id": tokenLayerId, name }),
|
|
757
852
|
}),
|
|
758
853
|
/**
|
|
759
854
|
* Shift a span layer's display order.
|
|
@@ -786,10 +881,11 @@ class PlaidClient {
|
|
|
786
881
|
* @param {any} value - The value
|
|
787
882
|
* @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
|
|
788
883
|
*/
|
|
789
|
-
create: (spanLayerId, tokens, value, metadata, auditMessage) =>
|
|
884
|
+
create: (spanLayerId, tokens, value, metadata, auditMessage, { id } = {}) =>
|
|
790
885
|
this._request("POST", "/api/v1/spans", {
|
|
791
886
|
auditMessage,
|
|
792
887
|
body: bodyOf({
|
|
888
|
+
id,
|
|
793
889
|
"span-layer-id": spanLayerId,
|
|
794
890
|
tokens,
|
|
795
891
|
value,
|
|
@@ -958,10 +1054,18 @@ class PlaidClient {
|
|
|
958
1054
|
* @param {string} body - The request body
|
|
959
1055
|
* @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
|
|
960
1056
|
*/
|
|
961
|
-
create: (
|
|
1057
|
+
create: (
|
|
1058
|
+
textLayerId,
|
|
1059
|
+
documentId,
|
|
1060
|
+
body,
|
|
1061
|
+
metadata,
|
|
1062
|
+
auditMessage,
|
|
1063
|
+
{ id } = {},
|
|
1064
|
+
) =>
|
|
962
1065
|
this._request("POST", "/api/v1/texts", {
|
|
963
1066
|
auditMessage,
|
|
964
1067
|
body: bodyOf({
|
|
1068
|
+
id,
|
|
965
1069
|
"text-layer-id": textLayerId,
|
|
966
1070
|
"document-id": documentId,
|
|
967
1071
|
body,
|
|
@@ -989,13 +1093,45 @@ class PlaidClient {
|
|
|
989
1093
|
* {type: 'replace', index, length, value: string} — unlike
|
|
990
1094
|
* delete+insert, a token covering the whole range is resized to keep
|
|
991
1095
|
* it (respell a word in place without losing its annotations).
|
|
1096
|
+
* A list is applied exactly as sent. With `base` (the `digest` of the
|
|
1097
|
+
* body the update was made on, as every read of a text gives it) the
|
|
1098
|
+
* update applies only to that body, and is refused with 409 and
|
|
1099
|
+
* `text-changed` otherwise. Strict mode then does not stamp it, unless
|
|
1100
|
+
* `versioned` is true (a batch whose later writes are stamped needs its
|
|
1101
|
+
* first write stamped too).
|
|
992
1102
|
* @param {string} textId - The text ID
|
|
993
1103
|
* @param {any} body - The request body
|
|
1104
|
+
* @param {string} [auditMessage]
|
|
1105
|
+
* @param {{base?: string, versioned?: boolean}} [options]
|
|
1106
|
+
*/
|
|
1107
|
+
update: (textId, body, auditMessage, { base, versioned } = {}) =>
|
|
1108
|
+
this._request("PATCH", `/api/v1/texts/${textId}`, {
|
|
1109
|
+
auditMessage,
|
|
1110
|
+
body: bodyOf({ body, base }),
|
|
1111
|
+
versioned: versioned ?? (base === undefined || base === null),
|
|
1112
|
+
}),
|
|
1113
|
+
/**
|
|
1114
|
+
* Change a text's body by the edits made at the caret: a list of edit
|
|
1115
|
+
* directives as `update` takes them (code-point indices, applied in
|
|
1116
|
+
* order, each index in the body the ones before it left). Only their
|
|
1117
|
+
* net change counts. An insert or a delete stays where it was made, and
|
|
1118
|
+
* a stretch deleted and typed over is read as a whole new body is. The
|
|
1119
|
+
* answer is the text with its new `digest` and `reshape` (the tokens
|
|
1120
|
+
* moved, the spans and vocab links trimmed, the rows deleted). With
|
|
1121
|
+
* `base`, the digest of the body the edits were made on, the edit
|
|
1122
|
+
* applies only to that body and is refused with 409 and `text-changed`
|
|
1123
|
+
* otherwise, and strict mode does not stamp it unless `versioned` is
|
|
1124
|
+
* true. See composeTextEdits and gapsToOps.
|
|
1125
|
+
* @param {string} textId - The text ID
|
|
1126
|
+
* @param {Array<object>} edits - The edit directives
|
|
1127
|
+
* @param {string} [auditMessage]
|
|
1128
|
+
* @param {{base?: string, versioned?: boolean}} [options]
|
|
994
1129
|
*/
|
|
995
|
-
|
|
1130
|
+
edit: (textId, edits, auditMessage, { base, versioned } = {}) =>
|
|
996
1131
|
this._request("PATCH", `/api/v1/texts/${textId}`, {
|
|
997
1132
|
auditMessage,
|
|
998
|
-
body: bodyOf({
|
|
1133
|
+
body: bodyOf({ edits, base }),
|
|
1134
|
+
versioned: versioned ?? (base === undefined || base === null),
|
|
999
1135
|
}),
|
|
1000
1136
|
};
|
|
1001
1137
|
|
|
@@ -1065,13 +1201,16 @@ class PlaidClient {
|
|
|
1065
1201
|
* types, spelled as in an entry's `op/type` (e.g.
|
|
1066
1202
|
* `['span-layer/create', 'span-layer/delete']`). An entry appears when
|
|
1067
1203
|
* one of its operations matches, carrying only the ones that did.
|
|
1204
|
+
* @param {string[]|string} [kinds] - Only the entries of operations of
|
|
1205
|
+
* these kinds (e.g. `['review', 'guess-adoption']`), each whole
|
|
1068
1206
|
*/
|
|
1069
|
-
audit: (userId, startTime, endTime, opTypes) =>
|
|
1207
|
+
audit: (userId, startTime, endTime, opTypes, kinds) =>
|
|
1070
1208
|
listAll(this, `/api/v1/users/${userId}/audit`, {
|
|
1071
1209
|
query: {
|
|
1072
1210
|
"start-time": startTime,
|
|
1073
1211
|
"end-time": endTime,
|
|
1074
1212
|
"op-types": opTypesParam(opTypes),
|
|
1213
|
+
kinds: kindsParam(kinds),
|
|
1075
1214
|
},
|
|
1076
1215
|
}),
|
|
1077
1216
|
/**
|
|
@@ -1087,7 +1226,7 @@ class PlaidClient {
|
|
|
1087
1226
|
*/
|
|
1088
1227
|
auditPage: (
|
|
1089
1228
|
userId,
|
|
1090
|
-
{ startTime, endTime, opTypes, order, limit, cursor } = {},
|
|
1229
|
+
{ startTime, endTime, opTypes, kinds, order, limit, cursor } = {},
|
|
1091
1230
|
) =>
|
|
1092
1231
|
listPage(this, `/api/v1/users/${userId}/audit`, {
|
|
1093
1232
|
limit,
|
|
@@ -1096,6 +1235,7 @@ class PlaidClient {
|
|
|
1096
1235
|
"start-time": startTime,
|
|
1097
1236
|
"end-time": endTime,
|
|
1098
1237
|
"op-types": opTypesParam(opTypes),
|
|
1238
|
+
kinds: kindsParam(kinds),
|
|
1099
1239
|
order,
|
|
1100
1240
|
},
|
|
1101
1241
|
}),
|
|
@@ -1357,6 +1497,8 @@ class PlaidClient {
|
|
|
1357
1497
|
this._request("POST", `/api/v1/users/${userId}/tokens`, {
|
|
1358
1498
|
auditMessage,
|
|
1359
1499
|
body: bodyOf({ name }),
|
|
1500
|
+
// The answer is the secret, which the server never keeps.
|
|
1501
|
+
noIdempotency: true,
|
|
1360
1502
|
}),
|
|
1361
1503
|
/**
|
|
1362
1504
|
* Revoke a named API token (soft-revoke; idempotent).
|
|
@@ -1451,6 +1593,8 @@ class PlaidClient {
|
|
|
1451
1593
|
) =>
|
|
1452
1594
|
this._request("POST", "/api/v1/invites", {
|
|
1453
1595
|
auditMessage,
|
|
1596
|
+
// The answer is the code, which the server never keeps.
|
|
1597
|
+
noIdempotency: true,
|
|
1454
1598
|
body: bodyOf({
|
|
1455
1599
|
"project-id": projectId,
|
|
1456
1600
|
"project-role": projectRole,
|
|
@@ -1623,13 +1767,16 @@ class PlaidClient {
|
|
|
1623
1767
|
* @param {string[]|string} [opts.opTypes] - Only these op types (e.g.
|
|
1624
1768
|
* `['span-layer/create']`). An entry appears when one of its operations
|
|
1625
1769
|
* matches, carrying only the ones that did.
|
|
1770
|
+
* @param {string[]|string} [opts.kinds] - Only the entries of operations
|
|
1771
|
+
* of these kinds (e.g. `['review']`), each whole
|
|
1626
1772
|
*/
|
|
1627
|
-
list: ({ startTime, endTime, opTypes } = {}) =>
|
|
1773
|
+
list: ({ startTime, endTime, opTypes, kinds } = {}) =>
|
|
1628
1774
|
listAll(this, "/api/v1/audit", {
|
|
1629
1775
|
query: {
|
|
1630
1776
|
"start-time": startTime,
|
|
1631
1777
|
"end-time": endTime,
|
|
1632
1778
|
"op-types": opTypesParam(opTypes),
|
|
1779
|
+
kinds: kindsParam(kinds),
|
|
1633
1780
|
},
|
|
1634
1781
|
}),
|
|
1635
1782
|
/**
|
|
@@ -1641,7 +1788,7 @@ class PlaidClient {
|
|
|
1641
1788
|
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
1642
1789
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
1643
1790
|
*/
|
|
1644
|
-
listPage: ({ startTime, endTime, opTypes, order, limit, cursor } = {}) =>
|
|
1791
|
+
listPage: ({ startTime, endTime, opTypes, kinds, order, limit, cursor } = {}) =>
|
|
1645
1792
|
listPage(this, "/api/v1/audit", {
|
|
1646
1793
|
limit,
|
|
1647
1794
|
cursor,
|
|
@@ -1649,6 +1796,7 @@ class PlaidClient {
|
|
|
1649
1796
|
"start-time": startTime,
|
|
1650
1797
|
"end-time": endTime,
|
|
1651
1798
|
"op-types": opTypesParam(opTypes),
|
|
1799
|
+
kinds: kindsParam(kinds),
|
|
1652
1800
|
order,
|
|
1653
1801
|
},
|
|
1654
1802
|
}),
|
|
@@ -1656,13 +1804,14 @@ class PlaidClient {
|
|
|
1656
1804
|
* Async-iterate the instance-wide audit log page by page. Admin only.
|
|
1657
1805
|
* @returns {AsyncGenerator<Array>}
|
|
1658
1806
|
*/
|
|
1659
|
-
iterPages: ({ startTime, endTime, opTypes, pageSize } = {}) =>
|
|
1807
|
+
iterPages: ({ startTime, endTime, opTypes, kinds, pageSize } = {}) =>
|
|
1660
1808
|
iterPages(this, "/api/v1/audit", {
|
|
1661
1809
|
pageSize,
|
|
1662
1810
|
query: {
|
|
1663
1811
|
"start-time": startTime,
|
|
1664
1812
|
"end-time": endTime,
|
|
1665
1813
|
"op-types": opTypesParam(opTypes),
|
|
1814
|
+
kinds: kindsParam(kinds),
|
|
1666
1815
|
},
|
|
1667
1816
|
}),
|
|
1668
1817
|
/**
|
|
@@ -1722,10 +1871,12 @@ class PlaidClient {
|
|
|
1722
1871
|
overlapMode,
|
|
1723
1872
|
parentTokenLayerId,
|
|
1724
1873
|
auditMessage,
|
|
1874
|
+
{ id } = {},
|
|
1725
1875
|
) =>
|
|
1726
1876
|
this._request("POST", "/api/v1/token-layers", {
|
|
1727
1877
|
auditMessage,
|
|
1728
1878
|
body: bodyOf({
|
|
1879
|
+
id,
|
|
1729
1880
|
"text-layer-id": textLayerId,
|
|
1730
1881
|
name,
|
|
1731
1882
|
"overlap-mode": overlapMode,
|
|
@@ -1738,31 +1889,31 @@ class PlaidClient {
|
|
|
1738
1889
|
* @param {string} namespace - The config namespace
|
|
1739
1890
|
* @param {string} configKey - The config key
|
|
1740
1891
|
* @param {any} configValue - Configuration value to set
|
|
1892
|
+
* @param {string} [auditMessage] - Audit message for this write
|
|
1893
|
+
* @param {{expected?: any}} [options] - `expected`: the value read for this key (null or undefined when it
|
|
1894
|
+
* was absent). The write is then refused with a 409 when the stored value differs.
|
|
1741
1895
|
*/
|
|
1742
|
-
setConfig: (
|
|
1743
|
-
tokenLayerId,
|
|
1744
|
-
namespace,
|
|
1745
|
-
configKey,
|
|
1746
|
-
configValue,
|
|
1747
|
-
auditMessage,
|
|
1748
|
-
) =>
|
|
1896
|
+
setConfig: (tokenLayerId, namespace, configKey, configValue, auditMessage, options) =>
|
|
1749
1897
|
this._request(
|
|
1750
1898
|
"PUT",
|
|
1751
1899
|
`/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`,
|
|
1752
|
-
|
|
1900
|
+
configRequest(auditMessage, configValue, options),
|
|
1753
1901
|
),
|
|
1754
1902
|
/**
|
|
1755
1903
|
* Remove a configuration value for a layer.
|
|
1756
1904
|
* @param {string} tokenLayerId - The token layer ID
|
|
1757
1905
|
* @param {string} namespace - The config namespace
|
|
1758
1906
|
* @param {string} configKey - The config key
|
|
1907
|
+
* @param {string} [auditMessage] - Audit message for this write
|
|
1908
|
+
* @param {{expected?: any}} [options] - `expected`, as on setConfig.
|
|
1759
1909
|
*/
|
|
1760
|
-
deleteConfig: (tokenLayerId, namespace, configKey, auditMessage) =>
|
|
1910
|
+
deleteConfig: (tokenLayerId, namespace, configKey, auditMessage, options) =>
|
|
1761
1911
|
this._request(
|
|
1762
1912
|
"DELETE",
|
|
1763
1913
|
`/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`,
|
|
1764
|
-
|
|
1914
|
+
configRequest(auditMessage, undefined, options, true),
|
|
1765
1915
|
),
|
|
1916
|
+
...constraintMethods(this, "token-layers"),
|
|
1766
1917
|
/**
|
|
1767
1918
|
* Get a token layer by ID.
|
|
1768
1919
|
* @param {string} tokenLayerId - The token layer ID
|
|
@@ -1803,15 +1954,26 @@ class PlaidClient {
|
|
|
1803
1954
|
* take it. While the lock is held, a second acquire is refused with
|
|
1804
1955
|
* HTTP 423, whoever makes it, this user included.
|
|
1805
1956
|
*
|
|
1957
|
+
* `newLockId` names the holder from the client's side (a fresh UUID). An
|
|
1958
|
+
* acquire whose answer never arrived can then be sent again, which
|
|
1959
|
+
* answers 200 while that holder has the lock, or released. Without it
|
|
1960
|
+
* the server mints the id, and a lost answer leaves a lock nobody can
|
|
1961
|
+
* release until it expires.
|
|
1962
|
+
*
|
|
1806
1963
|
* outOfBand: the lock is a signal, not project data (see the note at
|
|
1807
1964
|
* the top of http.js). Queued on a batch it would be taken only at
|
|
1808
1965
|
* submit, after every write it was meant to guard, and until then answer
|
|
1809
1966
|
* success to a caller that does not hold it and cannot see the 423
|
|
1810
1967
|
* saying somebody else does.
|
|
1811
1968
|
* @param {string} documentId - The document ID
|
|
1969
|
+
* @param {string} [auditMessage]
|
|
1970
|
+
* @param {string} [newLockId] - A holder id this client minted
|
|
1812
1971
|
*/
|
|
1813
|
-
acquireLock: (documentId, auditMessage) =>
|
|
1972
|
+
acquireLock: (documentId, auditMessage, newLockId) =>
|
|
1814
1973
|
this._request("POST", `/api/v1/documents/${documentId}/lock`, {
|
|
1974
|
+
...(newLockId !== undefined && newLockId !== null
|
|
1975
|
+
? { queryParams: { "new-lock-id": newLockId } }
|
|
1976
|
+
: {}),
|
|
1815
1977
|
auditMessage,
|
|
1816
1978
|
outOfBand: true,
|
|
1817
1979
|
}),
|
|
@@ -1872,7 +2034,10 @@ class PlaidClient {
|
|
|
1872
2034
|
* The lock is per HOLDER: each block is its own, named by
|
|
1873
2035
|
* `lock.lockId`, and only that id renews or releases it. NOT
|
|
1874
2036
|
* re-entrant: a nested `locked()` block on one document is a second
|
|
1875
|
-
* holder and gets the 423. Lock at exactly one level per call path.
|
|
2037
|
+
* holder and gets the 423. Lock at exactly one level per call path.
|
|
2038
|
+
* The block mints its holder id and sends it with the acquire, so an
|
|
2039
|
+
* acquire whose answer was lost is sent again (up to three tries) and,
|
|
2040
|
+
* if none is answered, released rather than left to expire. A
|
|
1876
2041
|
* lost lock is recorded on the CLIENT, like strict mode, so it stops
|
|
1877
2042
|
* every write the client makes (on any batch of it too) and not only the
|
|
1878
2043
|
* ones this block makes.
|
|
@@ -1961,13 +2126,16 @@ class PlaidClient {
|
|
|
1961
2126
|
* types, spelled as in an entry's `op/type` (e.g.
|
|
1962
2127
|
* `['span-layer/create', 'span-layer/delete']`). An entry appears when
|
|
1963
2128
|
* one of its operations matches, carrying only the ones that did.
|
|
2129
|
+
* @param {string[]|string} [kinds] - Only the entries of operations of
|
|
2130
|
+
* these kinds (e.g. `['review', 'guess-adoption']`), each whole
|
|
1964
2131
|
*/
|
|
1965
|
-
audit: (documentId, startTime, endTime, opTypes) =>
|
|
2132
|
+
audit: (documentId, startTime, endTime, opTypes, kinds) =>
|
|
1966
2133
|
listAll(this, `/api/v1/documents/${documentId}/audit`, {
|
|
1967
2134
|
query: {
|
|
1968
2135
|
"start-time": startTime,
|
|
1969
2136
|
"end-time": endTime,
|
|
1970
2137
|
"op-types": opTypesParam(opTypes),
|
|
2138
|
+
kinds: kindsParam(kinds),
|
|
1971
2139
|
},
|
|
1972
2140
|
}),
|
|
1973
2141
|
/**
|
|
@@ -1983,7 +2151,7 @@ class PlaidClient {
|
|
|
1983
2151
|
*/
|
|
1984
2152
|
auditPage: (
|
|
1985
2153
|
documentId,
|
|
1986
|
-
{ startTime, endTime, opTypes, order, limit, cursor } = {},
|
|
2154
|
+
{ startTime, endTime, opTypes, kinds, order, limit, cursor } = {},
|
|
1987
2155
|
) =>
|
|
1988
2156
|
listPage(this, `/api/v1/documents/${documentId}/audit`, {
|
|
1989
2157
|
limit,
|
|
@@ -1992,6 +2160,7 @@ class PlaidClient {
|
|
|
1992
2160
|
"start-time": startTime,
|
|
1993
2161
|
"end-time": endTime,
|
|
1994
2162
|
"op-types": opTypesParam(opTypes),
|
|
2163
|
+
kinds: kindsParam(kinds),
|
|
1995
2164
|
order,
|
|
1996
2165
|
},
|
|
1997
2166
|
}),
|
|
@@ -2064,10 +2233,10 @@ class PlaidClient {
|
|
|
2064
2233
|
* @param {string} name - The name
|
|
2065
2234
|
* @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
|
|
2066
2235
|
*/
|
|
2067
|
-
create: (projectId, name, metadata, auditMessage) =>
|
|
2236
|
+
create: (projectId, name, metadata, auditMessage, { id } = {}) =>
|
|
2068
2237
|
this._request("POST", "/api/v1/documents", {
|
|
2069
2238
|
auditMessage,
|
|
2070
|
-
body: bodyOf({ "project-id": projectId, name, metadata }),
|
|
2239
|
+
body: bodyOf({ id, "project-id": projectId, name, metadata }),
|
|
2071
2240
|
}),
|
|
2072
2241
|
/**
|
|
2073
2242
|
* Copy a document and everything in it into a new document of the same
|
|
@@ -2083,10 +2252,10 @@ class PlaidClient {
|
|
|
2083
2252
|
* file behind
|
|
2084
2253
|
* @param {string} [auditMessage] - Custom audit message for this operation
|
|
2085
2254
|
*/
|
|
2086
|
-
copy: (documentId, name, { includeMedia } = {}, auditMessage) =>
|
|
2255
|
+
copy: (documentId, name, { includeMedia, id } = {}, auditMessage) =>
|
|
2087
2256
|
this._request("POST", `/api/v1/documents/${documentId}/copy`, {
|
|
2088
2257
|
auditMessage,
|
|
2089
|
-
body: bodyOf({ name, "include-media": includeMedia }),
|
|
2258
|
+
body: bodyOf({ id, name, "include-media": includeMedia }),
|
|
2090
2259
|
}),
|
|
2091
2260
|
};
|
|
2092
2261
|
|
|
@@ -2133,24 +2302,29 @@ class PlaidClient {
|
|
|
2133
2302
|
* @param {string} namespace - The config namespace
|
|
2134
2303
|
* @param {string} configKey - The config key
|
|
2135
2304
|
* @param {any} configValue - Configuration value to set
|
|
2305
|
+
* @param {string} [auditMessage] - Audit message for this write
|
|
2306
|
+
* @param {{expected?: any}} [options] - `expected`: the value read for this key (null or undefined when it
|
|
2307
|
+
* was absent). The write is then refused with a 409 when the stored value differs.
|
|
2136
2308
|
*/
|
|
2137
|
-
setConfig: (id, namespace, configKey, configValue, auditMessage) =>
|
|
2309
|
+
setConfig: (id, namespace, configKey, configValue, auditMessage, options) =>
|
|
2138
2310
|
this._request(
|
|
2139
2311
|
"PUT",
|
|
2140
2312
|
`/api/v1/projects/${id}/config/${namespace}/${configKey}`,
|
|
2141
|
-
|
|
2313
|
+
configRequest(auditMessage, configValue, options),
|
|
2142
2314
|
),
|
|
2143
2315
|
/**
|
|
2144
2316
|
* Remove a configuration value for a project.
|
|
2145
2317
|
* @param {string} id - The resource ID
|
|
2146
2318
|
* @param {string} namespace - The config namespace
|
|
2147
2319
|
* @param {string} configKey - The config key
|
|
2320
|
+
* @param {string} [auditMessage] - Audit message for this write
|
|
2321
|
+
* @param {{expected?: any}} [options] - `expected`, as on setConfig.
|
|
2148
2322
|
*/
|
|
2149
|
-
deleteConfig: (id, namespace, configKey, auditMessage) =>
|
|
2323
|
+
deleteConfig: (id, namespace, configKey, auditMessage, options) =>
|
|
2150
2324
|
this._request(
|
|
2151
2325
|
"DELETE",
|
|
2152
2326
|
`/api/v1/projects/${id}/config/${namespace}/${configKey}`,
|
|
2153
|
-
|
|
2327
|
+
configRequest(auditMessage, undefined, options, true),
|
|
2154
2328
|
),
|
|
2155
2329
|
/**
|
|
2156
2330
|
* Assign a user as a maintainer for this project.
|
|
@@ -2182,13 +2356,16 @@ class PlaidClient {
|
|
|
2182
2356
|
* types, spelled as in an entry's `op/type` (e.g.
|
|
2183
2357
|
* `['span-layer/create', 'span-layer/delete']`). An entry appears when
|
|
2184
2358
|
* one of its operations matches, carrying only the ones that did.
|
|
2359
|
+
* @param {string[]|string} [kinds] - Only the entries of operations of
|
|
2360
|
+
* these kinds (e.g. `['review', 'guess-adoption']`), each whole
|
|
2185
2361
|
*/
|
|
2186
|
-
audit: (projectId, startTime, endTime, opTypes) =>
|
|
2362
|
+
audit: (projectId, startTime, endTime, opTypes, kinds) =>
|
|
2187
2363
|
listAll(this, `/api/v1/projects/${projectId}/audit`, {
|
|
2188
2364
|
query: {
|
|
2189
2365
|
"start-time": startTime,
|
|
2190
2366
|
"end-time": endTime,
|
|
2191
2367
|
"op-types": opTypesParam(opTypes),
|
|
2368
|
+
kinds: kindsParam(kinds),
|
|
2192
2369
|
},
|
|
2193
2370
|
}),
|
|
2194
2371
|
/**
|
|
@@ -2204,7 +2381,7 @@ class PlaidClient {
|
|
|
2204
2381
|
*/
|
|
2205
2382
|
auditPage: (
|
|
2206
2383
|
projectId,
|
|
2207
|
-
{ startTime, endTime, opTypes, order, limit, cursor } = {},
|
|
2384
|
+
{ startTime, endTime, opTypes, kinds, order, limit, cursor } = {},
|
|
2208
2385
|
) =>
|
|
2209
2386
|
listPage(this, `/api/v1/projects/${projectId}/audit`, {
|
|
2210
2387
|
limit,
|
|
@@ -2213,6 +2390,7 @@ class PlaidClient {
|
|
|
2213
2390
|
"start-time": startTime,
|
|
2214
2391
|
"end-time": endTime,
|
|
2215
2392
|
"op-types": opTypesParam(opTypes),
|
|
2393
|
+
kinds: kindsParam(kinds),
|
|
2216
2394
|
order,
|
|
2217
2395
|
},
|
|
2218
2396
|
}),
|
|
@@ -2345,10 +2523,10 @@ class PlaidClient {
|
|
|
2345
2523
|
* Create a new project. Note: this also registers the user as a maintainer.
|
|
2346
2524
|
* @param {string} name - The name
|
|
2347
2525
|
*/
|
|
2348
|
-
create: (name, auditMessage) =>
|
|
2526
|
+
create: (name, auditMessage, { id } = {}) =>
|
|
2349
2527
|
this._request("POST", "/api/v1/projects", {
|
|
2350
2528
|
auditMessage,
|
|
2351
|
-
body: bodyOf({ name }),
|
|
2529
|
+
body: bodyOf({ id, name }),
|
|
2352
2530
|
}),
|
|
2353
2531
|
};
|
|
2354
2532
|
|
|
@@ -2359,30 +2537,29 @@ class PlaidClient {
|
|
|
2359
2537
|
* @param {string} namespace - The config namespace
|
|
2360
2538
|
* @param {string} configKey - The config key
|
|
2361
2539
|
* @param {any} configValue - Configuration value to set
|
|
2540
|
+
* @param {string} [auditMessage] - Audit message for this write
|
|
2541
|
+
* @param {{expected?: any}} [options] - `expected`: the value read for this key (null or undefined when it
|
|
2542
|
+
* was absent). The write is then refused with a 409 when the stored value differs.
|
|
2362
2543
|
*/
|
|
2363
|
-
setConfig: (
|
|
2364
|
-
textLayerId,
|
|
2365
|
-
namespace,
|
|
2366
|
-
configKey,
|
|
2367
|
-
configValue,
|
|
2368
|
-
auditMessage,
|
|
2369
|
-
) =>
|
|
2544
|
+
setConfig: (textLayerId, namespace, configKey, configValue, auditMessage, options) =>
|
|
2370
2545
|
this._request(
|
|
2371
2546
|
"PUT",
|
|
2372
2547
|
`/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`,
|
|
2373
|
-
|
|
2548
|
+
configRequest(auditMessage, configValue, options),
|
|
2374
2549
|
),
|
|
2375
2550
|
/**
|
|
2376
2551
|
* Remove a configuration value for a layer.
|
|
2377
2552
|
* @param {string} textLayerId - The text layer ID
|
|
2378
2553
|
* @param {string} namespace - The config namespace
|
|
2379
2554
|
* @param {string} configKey - The config key
|
|
2555
|
+
* @param {string} [auditMessage] - Audit message for this write
|
|
2556
|
+
* @param {{expected?: any}} [options] - `expected`, as on setConfig.
|
|
2380
2557
|
*/
|
|
2381
|
-
deleteConfig: (textLayerId, namespace, configKey, auditMessage) =>
|
|
2558
|
+
deleteConfig: (textLayerId, namespace, configKey, auditMessage, options) =>
|
|
2382
2559
|
this._request(
|
|
2383
2560
|
"DELETE",
|
|
2384
2561
|
`/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`,
|
|
2385
|
-
|
|
2562
|
+
configRequest(auditMessage, undefined, options, true),
|
|
2386
2563
|
),
|
|
2387
2564
|
/**
|
|
2388
2565
|
* Get a text layer by ID.
|
|
@@ -2423,10 +2600,10 @@ class PlaidClient {
|
|
|
2423
2600
|
* @param {string} projectId - The project ID
|
|
2424
2601
|
* @param {string} name - The name
|
|
2425
2602
|
*/
|
|
2426
|
-
create: (projectId, name, auditMessage) =>
|
|
2603
|
+
create: (projectId, name, auditMessage, { id } = {}) =>
|
|
2427
2604
|
this._request("POST", "/api/v1/text-layers", {
|
|
2428
2605
|
auditMessage,
|
|
2429
|
-
body: bodyOf({ "project-id": projectId, name }),
|
|
2606
|
+
body: bodyOf({ id, "project-id": projectId, name }),
|
|
2430
2607
|
}),
|
|
2431
2608
|
};
|
|
2432
2609
|
|
|
@@ -2465,10 +2642,10 @@ class PlaidClient {
|
|
|
2465
2642
|
* @param {string} form - The vocab item form
|
|
2466
2643
|
* @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
|
|
2467
2644
|
*/
|
|
2468
|
-
create: (vocabLayerId, form, metadata, auditMessage) =>
|
|
2645
|
+
create: (vocabLayerId, form, metadata, auditMessage, { id } = {}) =>
|
|
2469
2646
|
this._request("POST", "/api/v1/vocab-items", {
|
|
2470
2647
|
auditMessage,
|
|
2471
|
-
body: bodyOf({ "vocab-layer-id": vocabLayerId, form, metadata }),
|
|
2648
|
+
body: bodyOf({ id, "vocab-layer-id": vocabLayerId, form, metadata }),
|
|
2472
2649
|
}),
|
|
2473
2650
|
/**
|
|
2474
2651
|
* Create multiple vocab items in a single operation. Entries may target
|
|
@@ -2515,9 +2692,35 @@ class PlaidClient {
|
|
|
2515
2692
|
* of those links has its version bumped, and a strict-mode client picks
|
|
2516
2693
|
* up their new versions from the response.
|
|
2517
2694
|
* @param {string} id - The resource ID
|
|
2695
|
+
* @param {string} [auditMessage] - Audit message for this write
|
|
2696
|
+
* @param {{expectedLinkCount?: number}} [options] - `expectedLinkCount`: the
|
|
2697
|
+
* number of links the caller showed. The delete is refused with a 409 when
|
|
2698
|
+
* the entry has any other number of links by then.
|
|
2518
2699
|
*/
|
|
2519
|
-
delete: (id, auditMessage) =>
|
|
2520
|
-
this._request("DELETE", `/api/v1/vocab-items/${id}`, {
|
|
2700
|
+
delete: (id, auditMessage, { expectedLinkCount } = {}) =>
|
|
2701
|
+
this._request("DELETE", `/api/v1/vocab-items/${id}`, {
|
|
2702
|
+
auditMessage,
|
|
2703
|
+
queryParams: { "expected-link-count": expectedLinkCount },
|
|
2704
|
+
}),
|
|
2705
|
+
/**
|
|
2706
|
+
* Merge entries into this one, in one operation. Every link to a loser
|
|
2707
|
+
* moves to the survivor (keeping its id and metadata), except a link on
|
|
2708
|
+
* words the survivor is already linked to, which is deleted, and then the
|
|
2709
|
+
* losers are deleted. Links are read when the merge runs, so one made
|
|
2710
|
+
* after the caller looked moves too. Every loser must be in the
|
|
2711
|
+
* survivor's vocabulary, and a loser that is already gone is skipped, so
|
|
2712
|
+
* a repeated merge changes nothing. References to a loser in other
|
|
2713
|
+
* entries' metadata are the caller's to rewrite, in the same batch.
|
|
2714
|
+
* Needs maintainer rights on the vocabulary.
|
|
2715
|
+
* @param {string} survivorId - The entry that stays
|
|
2716
|
+
* @param {string[]} loserIds - The entries merged into it
|
|
2717
|
+
* @returns {Promise<{moved: number, duplicates: number, removed: string[]}>}
|
|
2718
|
+
*/
|
|
2719
|
+
merge: (survivorId, loserIds, auditMessage) =>
|
|
2720
|
+
this._request("POST", `/api/v1/vocab-items/${survivorId}/merge`, {
|
|
2721
|
+
auditMessage,
|
|
2722
|
+
body: { losers: loserIds },
|
|
2723
|
+
}),
|
|
2521
2724
|
/**
|
|
2522
2725
|
* Update a vocab item's form. A document read carries the entry's form on
|
|
2523
2726
|
* every link to it, so a rename restates those documents: each has its
|
|
@@ -2550,10 +2753,10 @@ class PlaidClient {
|
|
|
2550
2753
|
* @param {string} spanLayerId - The span layer ID
|
|
2551
2754
|
* @param {string} name - The name
|
|
2552
2755
|
*/
|
|
2553
|
-
create: (spanLayerId, name, auditMessage) =>
|
|
2756
|
+
create: (spanLayerId, name, auditMessage, { id } = {}) =>
|
|
2554
2757
|
this._request("POST", "/api/v1/relation-layers", {
|
|
2555
2758
|
auditMessage,
|
|
2556
|
-
body: bodyOf({ "span-layer-id": spanLayerId, name }),
|
|
2759
|
+
body: bodyOf({ id, "span-layer-id": spanLayerId, name }),
|
|
2557
2760
|
}),
|
|
2558
2761
|
/**
|
|
2559
2762
|
* Set a configuration value for a layer in an editor namespace.
|
|
@@ -2561,31 +2764,31 @@ class PlaidClient {
|
|
|
2561
2764
|
* @param {string} namespace - The config namespace
|
|
2562
2765
|
* @param {string} configKey - The config key
|
|
2563
2766
|
* @param {any} configValue - Configuration value to set
|
|
2767
|
+
* @param {string} [auditMessage] - Audit message for this write
|
|
2768
|
+
* @param {{expected?: any}} [options] - `expected`: the value read for this key (null or undefined when it
|
|
2769
|
+
* was absent). The write is then refused with a 409 when the stored value differs.
|
|
2564
2770
|
*/
|
|
2565
|
-
setConfig: (
|
|
2566
|
-
relationLayerId,
|
|
2567
|
-
namespace,
|
|
2568
|
-
configKey,
|
|
2569
|
-
configValue,
|
|
2570
|
-
auditMessage,
|
|
2571
|
-
) =>
|
|
2771
|
+
setConfig: (relationLayerId, namespace, configKey, configValue, auditMessage, options) =>
|
|
2572
2772
|
this._request(
|
|
2573
2773
|
"PUT",
|
|
2574
2774
|
`/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`,
|
|
2575
|
-
|
|
2775
|
+
configRequest(auditMessage, configValue, options),
|
|
2576
2776
|
),
|
|
2577
2777
|
/**
|
|
2578
2778
|
* Remove a configuration value for a layer.
|
|
2579
2779
|
* @param {string} relationLayerId - The relation layer ID
|
|
2580
2780
|
* @param {string} namespace - The config namespace
|
|
2581
2781
|
* @param {string} configKey - The config key
|
|
2782
|
+
* @param {string} [auditMessage] - Audit message for this write
|
|
2783
|
+
* @param {{expected?: any}} [options] - `expected`, as on setConfig.
|
|
2582
2784
|
*/
|
|
2583
|
-
deleteConfig: (relationLayerId, namespace, configKey, auditMessage) =>
|
|
2785
|
+
deleteConfig: (relationLayerId, namespace, configKey, auditMessage, options) =>
|
|
2584
2786
|
this._request(
|
|
2585
2787
|
"DELETE",
|
|
2586
2788
|
`/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`,
|
|
2587
|
-
|
|
2789
|
+
configRequest(auditMessage, undefined, options, true),
|
|
2588
2790
|
),
|
|
2791
|
+
...constraintMethods(this, "relation-layers"),
|
|
2589
2792
|
/**
|
|
2590
2793
|
* Get a relation layer by ID.
|
|
2591
2794
|
* @param {string} relationLayerId - The relation layer ID
|
|
@@ -2639,10 +2842,12 @@ class PlaidClient {
|
|
|
2639
2842
|
precedence,
|
|
2640
2843
|
metadata,
|
|
2641
2844
|
auditMessage,
|
|
2845
|
+
{ id } = {},
|
|
2642
2846
|
) =>
|
|
2643
2847
|
this._request("POST", "/api/v1/tokens", {
|
|
2644
2848
|
auditMessage,
|
|
2645
2849
|
body: bodyOf({
|
|
2850
|
+
id,
|
|
2646
2851
|
"token-layer-id": tokenLayerId,
|
|
2647
2852
|
text,
|
|
2648
2853
|
begin,
|
|
@@ -2707,11 +2912,21 @@ class PlaidClient {
|
|
|
2707
2912
|
* left half (keeps its ID, spans, vocab-links); the new right token's ID is returned.
|
|
2708
2913
|
* @param {string} tokenId - The token ID
|
|
2709
2914
|
* @param {number} position - Code-point offset to split at (strictly between begin and end)
|
|
2710
|
-
|
|
2711
|
-
|
|
2915
|
+
* @param {string} [auditMessage] - Audit message for this write
|
|
2916
|
+
* @param {{id?: string}} [options] - `id`: the right half's id, a UUIDv7 this
|
|
2917
|
+
* client minted. A relation layer whose relations must stay inside one token
|
|
2918
|
+
* of this layer declares a same-ancestor constraint, and the server deletes
|
|
2919
|
+
* the relations the split leaves crossing in the same transaction.
|
|
2920
|
+
*/
|
|
2921
|
+
split: (
|
|
2922
|
+
tokenId,
|
|
2923
|
+
position,
|
|
2924
|
+
auditMessage,
|
|
2925
|
+
{ id } = {},
|
|
2926
|
+
) =>
|
|
2712
2927
|
this._request("POST", `/api/v1/tokens/${tokenId}/split`, {
|
|
2713
2928
|
auditMessage,
|
|
2714
|
-
body: bodyOf({ position }),
|
|
2929
|
+
body: bodyOf({ id, position }),
|
|
2715
2930
|
}),
|
|
2716
2931
|
/**
|
|
2717
2932
|
* Merge two tokens. The left token (smaller begin) survives with the combined
|
|
@@ -3000,9 +3215,13 @@ class PlaidClient {
|
|
|
3000
3215
|
* @param {object} [opts]
|
|
3001
3216
|
* @param {string} [opts.anchorLabel] - What the comment is about, in words (at most 200 characters)
|
|
3002
3217
|
*/
|
|
3003
|
-
create: (entityType, entityId, body, { anchorLabel } = {}) =>
|
|
3218
|
+
create: (entityType, entityId, body, { anchorLabel, id } = {}) =>
|
|
3004
3219
|
this._request("POST", "/api/v1/comments", {
|
|
3220
|
+
// A comment is not audited, so it never joins an open operation,
|
|
3221
|
+
// and never takes a number from an edit's Idempotency-Key seed.
|
|
3222
|
+
noOperation: true,
|
|
3005
3223
|
body: bodyOf({
|
|
3224
|
+
id,
|
|
3006
3225
|
"entity-type": entityType,
|
|
3007
3226
|
"entity-id": entityId,
|
|
3008
3227
|
body,
|
|
@@ -3021,13 +3240,17 @@ class PlaidClient {
|
|
|
3021
3240
|
* @param {string} body - The replacement text
|
|
3022
3241
|
*/
|
|
3023
3242
|
update: (id, body) =>
|
|
3024
|
-
this._request("PATCH", `/api/v1/comments/${id}`, {
|
|
3243
|
+
this._request("PATCH", `/api/v1/comments/${id}`, {
|
|
3244
|
+
body: { body },
|
|
3245
|
+
noOperation: true,
|
|
3246
|
+
}),
|
|
3025
3247
|
/**
|
|
3026
3248
|
* Delete a comment. The author may delete their own; a project
|
|
3027
3249
|
* maintainer (or admin) may delete any.
|
|
3028
3250
|
* @param {string} id - The comment id
|
|
3029
3251
|
*/
|
|
3030
|
-
delete: (id) =>
|
|
3252
|
+
delete: (id) =>
|
|
3253
|
+
this._request("DELETE", `/api/v1/comments/${id}`, { noOperation: true }),
|
|
3031
3254
|
/**
|
|
3032
3255
|
* List comments in a project, oldest first. Transparently follows
|
|
3033
3256
|
* pagination cursors and returns the full flat array.
|
|
@@ -3279,10 +3502,10 @@ class PlaidClient {
|
|
|
3279
3502
|
* @param {string} [opts.body] - The Markdown text (up to 20000 characters; may be empty)
|
|
3280
3503
|
* @param {boolean} [opts.pinned] - Send this one to the assistant in full on every turn
|
|
3281
3504
|
*/
|
|
3282
|
-
create: (projectId, title, { body, pinned } = {}, auditMessage) =>
|
|
3505
|
+
create: (projectId, title, { body, pinned, id } = {}, auditMessage) =>
|
|
3283
3506
|
this._request("POST", `/api/v1/projects/${projectId}/guidelines`, {
|
|
3284
3507
|
auditMessage,
|
|
3285
|
-
body: bodyOf({ title, body, pinned }),
|
|
3508
|
+
body: bodyOf({ id, title, body, pinned }),
|
|
3286
3509
|
}),
|
|
3287
3510
|
/**
|
|
3288
3511
|
* Read one guideline, Markdown body included.
|
|
@@ -3414,20 +3637,47 @@ class PlaidClient {
|
|
|
3414
3637
|
*
|
|
3415
3638
|
* `kind` says what kind of operation this is, for a program reading the
|
|
3416
3639
|
* log: one of `assistant-plan`, `service-run`, `import`, `bulk-edit`,
|
|
3417
|
-
* `guess-adoption` or `
|
|
3418
|
-
* short string naming what the operation came from, in the
|
|
3419
|
-
* documents (the core manual, "Kinds of operation"). Both
|
|
3420
|
-
* the first write like the label, and a nested operation
|
|
3421
|
-
* one's.
|
|
3640
|
+
* `guess-adoption`, `repair` or `review` (the server refuses any other).
|
|
3641
|
+
* `ref` is a short string naming what the operation came from, in the
|
|
3642
|
+
* shape its kind documents (the core manual, "Kinds of operation"). Both
|
|
3643
|
+
* are recorded from the first write like the label, and a nested operation
|
|
3644
|
+
* keeps the outer one's.
|
|
3645
|
+
*
|
|
3646
|
+
* `keys` (from `keySeed()`) makes a run of the operation send the same
|
|
3647
|
+
* requests as an earlier run with the same keys: the nth keyed request that
|
|
3648
|
+
* joins it takes the Idempotency-Key `<seed>.<n>` and the document-version
|
|
3649
|
+
* its first run claimed, so a request that landed is answered from its
|
|
3650
|
+
* first send and writes nothing again. The count starts at 0 at each
|
|
3651
|
+
* outermost begin. A nested operation joins the outer one's keys, unless it
|
|
3652
|
+
* brings its own: then it numbers from 0 under its own seed until its
|
|
3653
|
+
* matching end, and the outer numbering resumes after it.
|
|
3422
3654
|
*
|
|
3423
3655
|
* @param {string} message - Human label for the operation.
|
|
3424
|
-
* @param {object} [opts] - Optional `{ id, kind, ref }`. `id` 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). `kind` and `
|
|
3656
|
+
* @param {object} [opts] - Optional `{ id, kind, ref, keys }`. `id` 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). `kind`, `ref` and `keys` are described above.
|
|
3425
3657
|
* @returns {string} The operation's group id.
|
|
3426
3658
|
*/
|
|
3427
|
-
beginOperation(message, { id, kind, ref } = {}) {
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3659
|
+
beginOperation(message, { id, kind, ref, keys, minted } = {}) {
|
|
3660
|
+
const frame = {
|
|
3661
|
+
keys: keys || null,
|
|
3662
|
+
count: 0,
|
|
3663
|
+
// Any iterable of ids (a Set, an array).
|
|
3664
|
+
minted: minted ? new Set(minted) : null,
|
|
3665
|
+
depth: 1,
|
|
3666
|
+
};
|
|
3667
|
+
const open = this.operationGroup;
|
|
3668
|
+
this._openedFrame = null;
|
|
3669
|
+
if (open) {
|
|
3670
|
+
// A nested operation joins the outer one. When it brings its own key
|
|
3671
|
+
// seed or ids (a queued edit run inside a longer operation), it numbers
|
|
3672
|
+
// the keys in a frame of its own until it ends, then the frame under it
|
|
3673
|
+
// numbers again.
|
|
3674
|
+
open.depth += 1;
|
|
3675
|
+
if (keys || minted) {
|
|
3676
|
+
frame.depth = open.depth;
|
|
3677
|
+
open.frames.push(frame);
|
|
3678
|
+
this._openedFrame = frame;
|
|
3679
|
+
}
|
|
3680
|
+
return open.id;
|
|
3431
3681
|
}
|
|
3432
3682
|
this.operationGroup = {
|
|
3433
3683
|
id: id || crypto.randomUUID(),
|
|
@@ -3437,10 +3687,22 @@ class PlaidClient {
|
|
|
3437
3687
|
depth: 1,
|
|
3438
3688
|
written: false,
|
|
3439
3689
|
refined: undefined,
|
|
3690
|
+
frames: [frame],
|
|
3440
3691
|
};
|
|
3692
|
+
this._openedFrame = frame;
|
|
3441
3693
|
return this.operationGroup.id;
|
|
3442
3694
|
}
|
|
3443
3695
|
|
|
3696
|
+
/**
|
|
3697
|
+
* A seed for the Idempotency-Keys of a logical operation that may be run
|
|
3698
|
+
* again from the top (see beginOperation's `keys`). Keep it with the work
|
|
3699
|
+
* it belongs to and pass it to every run.
|
|
3700
|
+
* @returns {{seed: string, stamps: Map<number, number|null>}}
|
|
3701
|
+
*/
|
|
3702
|
+
keySeed() {
|
|
3703
|
+
return { seed: uuidv7(), stamps: new Map() };
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3444
3706
|
/**
|
|
3445
3707
|
* End the current logical operation. With no argument this is purely local
|
|
3446
3708
|
* (no request). Pass a refined `message` to relabel the group now that the
|
|
@@ -3454,6 +3716,12 @@ class PlaidClient {
|
|
|
3454
3716
|
const group = this.operationGroup;
|
|
3455
3717
|
if (!group) return;
|
|
3456
3718
|
if (group.depth > 1) {
|
|
3719
|
+
// A frame begun at this depth by hand (beginOperation) ends here. One
|
|
3720
|
+
// begun by withOperation is ended by that call itself (_dropFrame).
|
|
3721
|
+
const top = group.frames.at(-1);
|
|
3722
|
+
if (group.frames.length > 1 && !top.owned && top.depth === group.depth) {
|
|
3723
|
+
group.frames.pop();
|
|
3724
|
+
}
|
|
3457
3725
|
group.depth -= 1;
|
|
3458
3726
|
return;
|
|
3459
3727
|
}
|
|
@@ -3486,22 +3754,40 @@ class PlaidClient {
|
|
|
3486
3754
|
*
|
|
3487
3755
|
* @param {string} message - Human label for the operation.
|
|
3488
3756
|
* @param {function} fn - The work to run; receives `setMessage(msg)` to refine the label once the outcome is known.
|
|
3489
|
-
* @param {object} [opts] - Optional `{ kind, ref }`, as for beginOperation.
|
|
3757
|
+
* @param {object} [opts] - Optional `{ kind, ref, id, keys }`, as for beginOperation.
|
|
3490
3758
|
* @returns {Promise<any>} Whatever `fn` resolves to.
|
|
3491
3759
|
*/
|
|
3492
|
-
async withOperation(message, fn, { kind, ref } = {}) {
|
|
3493
|
-
this.beginOperation(message, { kind, ref });
|
|
3760
|
+
async withOperation(message, fn, { kind, ref, id, keys, minted } = {}) {
|
|
3761
|
+
this.beginOperation(message, { kind, ref, id, keys, minted });
|
|
3494
3762
|
const group = this.operationGroup;
|
|
3763
|
+
// The key frame this call opened is ended by this call, whatever else
|
|
3764
|
+
// began or ended meanwhile, not by whichever end comes at its depth.
|
|
3765
|
+
const frame = this._openedFrame;
|
|
3766
|
+
if (frame) frame.owned = true;
|
|
3495
3767
|
const setMessage = (msg) => {
|
|
3496
3768
|
if (group.depth === 1) group.refined = msg;
|
|
3497
3769
|
};
|
|
3770
|
+
let result;
|
|
3498
3771
|
try {
|
|
3499
|
-
|
|
3500
|
-
}
|
|
3501
|
-
|
|
3772
|
+
result = await fn(setMessage);
|
|
3773
|
+
} catch (e) {
|
|
3774
|
+
// A relabel the server refuses (a token scoped to projects may not
|
|
3775
|
+
// relabel a group, which names none) must not take the place of the
|
|
3776
|
+
// error `fn` threw: the caller answers for that one.
|
|
3777
|
+
dropFrame(group, frame);
|
|
3778
|
+
try {
|
|
3779
|
+
await this.endOperation();
|
|
3780
|
+
} catch (relabel) {
|
|
3781
|
+
console.warn('The operation was not relabelled:', relabel);
|
|
3782
|
+
}
|
|
3783
|
+
throw e;
|
|
3502
3784
|
}
|
|
3785
|
+
dropFrame(group, frame);
|
|
3786
|
+
await this.endOperation();
|
|
3787
|
+
return result;
|
|
3503
3788
|
}
|
|
3504
3789
|
|
|
3790
|
+
|
|
3505
3791
|
/**
|
|
3506
3792
|
* Open a batch: a view of this client with the same bundles, on which every
|
|
3507
3793
|
* write of project data queues instead of going out. `submit()` sends the
|
|
@@ -3533,14 +3819,17 @@ class PlaidClient {
|
|
|
3533
3819
|
/**
|
|
3534
3820
|
* POST one batch request (at most MAX_BATCH_OPS operations). Resolves to
|
|
3535
3821
|
* one `{ status, headers, body }` per operation, with only the body
|
|
3536
|
-
* recased: the headers keep the server's spelling.
|
|
3822
|
+
* recased: the headers keep the server's spelling. `key` is the request's
|
|
3823
|
+
* Idempotency-Key: a batch whose answer is lost (no response, 502, 504) is
|
|
3824
|
+
* sent again under it, and answered from the first send if that landed.
|
|
3537
3825
|
*/
|
|
3538
|
-
async _postBatch(url, body) {
|
|
3826
|
+
async _postBatch(url, body, { key = uuidv7() } = {}) {
|
|
3539
3827
|
const fetchOptions = {
|
|
3540
3828
|
method: "POST",
|
|
3541
3829
|
headers: {
|
|
3542
3830
|
Authorization: `Bearer ${this.token}`,
|
|
3543
3831
|
"Content-Type": "application/json",
|
|
3832
|
+
[IDEMPOTENCY_HEADER]: key,
|
|
3544
3833
|
},
|
|
3545
3834
|
body: JSON.stringify(body),
|
|
3546
3835
|
};
|
|
@@ -3549,17 +3838,26 @@ class PlaidClient {
|
|
|
3549
3838
|
// Retry a 503: the batch is atomic, so a refused one wrote nothing and
|
|
3550
3839
|
// repeating it is safe. A fresh timeout signal per attempt — an
|
|
3551
3840
|
// AbortSignal.timeout stays aborted once it fires.
|
|
3552
|
-
const
|
|
3841
|
+
const attempt = async () => {
|
|
3553
3842
|
const signal = timeoutSignal(this.batchTimeout);
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3843
|
+
let res;
|
|
3844
|
+
try {
|
|
3845
|
+
res = await fetch(
|
|
3846
|
+
url,
|
|
3847
|
+
signal ? { ...fetchOptions, signal } : fetchOptions,
|
|
3848
|
+
);
|
|
3849
|
+
} catch (e) {
|
|
3850
|
+
throw makeNetworkError(e, url, "POST");
|
|
3851
|
+
}
|
|
3558
3852
|
if (!res.ok) {
|
|
3559
3853
|
throw makeHttpError(res, await parseErrorBody(res), url, "POST");
|
|
3560
3854
|
}
|
|
3561
3855
|
return res;
|
|
3856
|
+
};
|
|
3857
|
+
const response = await retryUnknown(() => retryWhileBusy(attempt), {
|
|
3858
|
+
delaysMs: this.retryDelaysMs,
|
|
3562
3859
|
}).catch((error) => {
|
|
3860
|
+
error.idempotencyKey = key;
|
|
3563
3861
|
// 401 means the token is missing, expired or invalid: fire the app's
|
|
3564
3862
|
// auth-error handler once, exactly as makeRequest does.
|
|
3565
3863
|
if (
|
|
@@ -3578,6 +3876,8 @@ class PlaidClient {
|
|
|
3578
3876
|
});
|
|
3579
3877
|
|
|
3580
3878
|
const results = await response.json();
|
|
3879
|
+
// A replayed batch carries the versions right after its first send.
|
|
3880
|
+
const replayed = response.headers?.get?.(REPLAYED_HEADER) === "true";
|
|
3581
3881
|
|
|
3582
3882
|
// Extract document versions from each batch response
|
|
3583
3883
|
for (const result of results) {
|
|
@@ -3588,10 +3888,11 @@ class PlaidClient {
|
|
|
3588
3888
|
);
|
|
3589
3889
|
if (typeof versionsMap === "object" && versionsMap !== null) {
|
|
3590
3890
|
// Clone once per response, then merge — not once per entry.
|
|
3591
|
-
this.documentVersions =
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3891
|
+
this.documentVersions = mergeVersions(
|
|
3892
|
+
this.documentVersions,
|
|
3893
|
+
versionsMap,
|
|
3894
|
+
replayed,
|
|
3895
|
+
);
|
|
3595
3896
|
}
|
|
3596
3897
|
} catch (e) {
|
|
3597
3898
|
console.warn(
|
|
@@ -3602,13 +3903,16 @@ class PlaidClient {
|
|
|
3602
3903
|
}
|
|
3603
3904
|
}
|
|
3604
3905
|
|
|
3605
|
-
|
|
3906
|
+
const out = results.map((result) => ({
|
|
3606
3907
|
...result,
|
|
3607
3908
|
body: transformResponse(result.body),
|
|
3608
3909
|
}));
|
|
3910
|
+
return replayed ? markReplayed(out) : out;
|
|
3609
3911
|
} catch (error) {
|
|
3610
|
-
if (error.status) throw error;
|
|
3611
|
-
throw makeNetworkError(error, url, "POST")
|
|
3912
|
+
if (error.status !== undefined) throw error;
|
|
3913
|
+
throw Object.assign(makeNetworkError(error, url, "POST"), {
|
|
3914
|
+
idempotencyKey: key,
|
|
3915
|
+
});
|
|
3612
3916
|
}
|
|
3613
3917
|
}
|
|
3614
3918
|
|
|
@@ -3777,8 +4081,102 @@ class PlaidClient {
|
|
|
3777
4081
|
}
|
|
3778
4082
|
}
|
|
3779
4083
|
|
|
4084
|
+
|
|
4085
|
+
/**
|
|
4086
|
+
* The request options of a config write. With `options.expected` present
|
|
4087
|
+
* (even as undefined, which means the key was absent) the write is a
|
|
4088
|
+
* compare-and-set: `?if-unchanged=true` and the body `{expected, value}`,
|
|
4089
|
+
* which the server refuses with a 409 when the stored value is no longer
|
|
4090
|
+
* `expected`. Without it the body is the value itself. Config is opaque, so
|
|
4091
|
+
* neither body is re-cased.
|
|
4092
|
+
*/
|
|
4093
|
+
function configRequest(auditMessage, value, options, isDelete = false) {
|
|
4094
|
+
if (!options || !Object.hasOwn(options, "expected")) {
|
|
4095
|
+
return isDelete
|
|
4096
|
+
? { auditMessage, skipResponseTransform: true }
|
|
4097
|
+
: { auditMessage, rawBody: value, skipResponseTransform: true };
|
|
4098
|
+
}
|
|
4099
|
+
const expected = options.expected === undefined ? null : options.expected;
|
|
4100
|
+
return {
|
|
4101
|
+
auditMessage,
|
|
4102
|
+
queryParams: { "if-unchanged": true },
|
|
4103
|
+
rawBody: isDelete ? { expected } : { expected, value },
|
|
4104
|
+
skipResponseTransform: true,
|
|
4105
|
+
};
|
|
4106
|
+
}
|
|
4107
|
+
|
|
4108
|
+
/**
|
|
4109
|
+
* The four layer-constraint methods of a layer bundle, on `/api/v1/<kind>`.
|
|
4110
|
+
* See constraints.js and the core manual, "Layer constraints".
|
|
4111
|
+
*/
|
|
4112
|
+
function constraintMethods(client, kind) {
|
|
4113
|
+
const base = (layerId) => `/api/v1/${kind}/${layerId}/constraints`;
|
|
4114
|
+
return {
|
|
4115
|
+
/**
|
|
4116
|
+
* Declare an app namespace's constraints on this layer, replacing that
|
|
4117
|
+
* namespace's list. Refused with 422 and the violations when the layer's
|
|
4118
|
+
* stored data breaks them (see violationsOf).
|
|
4119
|
+
* @param {string} layerId - The layer ID
|
|
4120
|
+
* @param {string} namespace - The declaring app's namespace, e.g. "igt"
|
|
4121
|
+
* @param {Array<object>} constraints - e.g. [{type: "single-span"}, {type: "value-set", values: ["N"]}]
|
|
4122
|
+
* @param {string} [auditMessage] - Audit message for this write
|
|
4123
|
+
* @param {{expected?: Array<object>|null}} [options] - `expected`: the list read for this
|
|
4124
|
+
* namespace (null when it was absent). The write is then refused with a 409 when the stored
|
|
4125
|
+
* list differs.
|
|
4126
|
+
* @returns {Promise<{constraints: object}>} the layer's whole constraint map
|
|
4127
|
+
*/
|
|
4128
|
+
setConstraints: (layerId, namespace, constraints, auditMessage, options) =>
|
|
4129
|
+
client._request("PUT", `${base(layerId)}/${namespace}`, {
|
|
4130
|
+
auditMessage,
|
|
4131
|
+
body: constraintsBody(constraints, options),
|
|
4132
|
+
}),
|
|
4133
|
+
/**
|
|
4134
|
+
* Remove an app namespace's constraints from this layer.
|
|
4135
|
+
* @param {string} layerId - The layer ID
|
|
4136
|
+
* @param {string} namespace - The declaring app's namespace
|
|
4137
|
+
* @param {string} [auditMessage] - Audit message for this write
|
|
4138
|
+
* @param {{expected?: Array<object>|null}} [options] - `expected`, as on setConstraints.
|
|
4139
|
+
*/
|
|
4140
|
+
deleteConstraints: (layerId, namespace, auditMessage, options) => {
|
|
4141
|
+
const body = constraintsBody(undefined, options);
|
|
4142
|
+
return client._request("DELETE", `${base(layerId)}/${namespace}`, {
|
|
4143
|
+
auditMessage,
|
|
4144
|
+
...(Object.keys(body).length ? { body } : {}),
|
|
4145
|
+
});
|
|
4146
|
+
},
|
|
4147
|
+
/**
|
|
4148
|
+
* The violations the given constraints would meet in this layer's stored
|
|
4149
|
+
* data. Writes nothing.
|
|
4150
|
+
* @param {string} layerId - The layer ID
|
|
4151
|
+
* @param {Array<object>} constraints - The list to check
|
|
4152
|
+
* @returns {Promise<{violations: Array<object>, violationCount: number}>}
|
|
4153
|
+
*/
|
|
4154
|
+
checkConstraints: (layerId, constraints) =>
|
|
4155
|
+
client._request("POST", `${base(layerId)}/check`, { body: { constraints } }),
|
|
4156
|
+
/**
|
|
4157
|
+
* Apply the remedies of the given constraints' remediable types
|
|
4158
|
+
* (coextensive, single-span, single-link, same-ancestor) to every
|
|
4159
|
+
* violation in this layer's stored data, one operation per document.
|
|
4160
|
+
* With `document`, only that document is repaired, and a writer may
|
|
4161
|
+
* ask. A document another user holds the lock on is left as it is and
|
|
4162
|
+
* listed under `locked`.
|
|
4163
|
+
* @param {string} layerId - The layer ID
|
|
4164
|
+
* @param {Array<object>} constraints - The list to repair for
|
|
4165
|
+
* @param {string} [auditMessage] - Audit message for this write
|
|
4166
|
+
* @param {{document?: string}} [options]
|
|
4167
|
+
* @returns {Promise<{repaired: Array<object>, locked: Array<{document: string, lockedBy: string}>, violations: Array<object>, violationCount: number}>}
|
|
4168
|
+
*/
|
|
4169
|
+
repairConstraints: (layerId, constraints, auditMessage, options) =>
|
|
4170
|
+
client._request("POST", `${base(layerId)}/repair`, {
|
|
4171
|
+
auditMessage,
|
|
4172
|
+
body: options?.document ? { constraints, document: options.document } : { constraints },
|
|
4173
|
+
}),
|
|
4174
|
+
};
|
|
4175
|
+
}
|
|
4176
|
+
|
|
3780
4177
|
export default PlaidClient;
|
|
3781
4178
|
export { PlaidClient };
|
|
4179
|
+
export { CONSTRAINT_TYPES, valueSetAllows, violationsOf } from "./constraints.js";
|
|
3782
4180
|
|
|
3783
4181
|
// Unicode code-point helpers for text offsets (token begin/end are code-point
|
|
3784
4182
|
// indices). See ./codepoint.js.
|
|
@@ -3790,10 +4188,13 @@ export {
|
|
|
3790
4188
|
cpToUtf16,
|
|
3791
4189
|
cpIndexOf,
|
|
3792
4190
|
} from "./codepoint.js";
|
|
4191
|
+
export { composeTextEdits, gapsToOps, applyTextOps } from "./textEdits.js";
|
|
3793
4192
|
export {
|
|
3794
4193
|
PLAID_NAMESPACE,
|
|
3795
4194
|
ROLE_KEY,
|
|
3796
4195
|
PRESERVE_ON_SPLIT_KEY,
|
|
4196
|
+
PLAIN_EDITS_KEY,
|
|
4197
|
+
SPLIT_ON_SPACE_KEY,
|
|
3797
4198
|
ROLES,
|
|
3798
4199
|
readRole,
|
|
3799
4200
|
findByRole,
|
|
@@ -3866,4 +4267,8 @@ export {
|
|
|
3866
4267
|
// The ids a create or bulk create answered with, off the call or its batch
|
|
3867
4268
|
// result. See ./created.js.
|
|
3868
4269
|
export { createdId, createdIds } from "./created.js";
|
|
4270
|
+
// UUIDv7 ids for what a client creates. See ./ids.js and the manual,
|
|
4271
|
+
// "Retrying a write".
|
|
4272
|
+
export { uuidv7 } from "./ids.js";
|
|
4273
|
+
export { wasReplayed } from "./replayed.js";
|
|
3869
4274
|
export { MAX_BATCH_OPS };
|