@larc-iu/plaid-client 0.2.0-alpha.2 → 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 +1638 -156
- package/package.json +1 -1
- package/src/batchRef.js +128 -0
- package/src/constraints.js +84 -0
- package/src/created.js +29 -0
- package/src/documentLock.js +342 -0
- package/src/events.js +266 -0
- package/src/http.js +719 -90
- package/src/ids.js +54 -0
- package/src/index.js +2781 -645
- package/src/metadataOps.js +117 -0
- package/src/pagination.js +19 -35
- package/src/provenance.js +268 -27
- package/src/replayed.js +27 -0
- package/src/requester.js +56 -0
- package/src/roles.js +26 -0
- package/src/serviceSchema.js +87 -33
- package/src/services.js +550 -54
- package/src/sse.js +60 -3
- package/src/textEdits.js +191 -0
- package/src/transforms.js +11 -1
package/src/index.js
CHANGED
|
@@ -2,26 +2,48 @@
|
|
|
2
2
|
* plaid-client - JavaScript client for the Plaid annotation API
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { transformRequest, transformResponse } from
|
|
5
|
+
import { transformRequest, transformResponse } from "./transforms.js";
|
|
6
6
|
import {
|
|
7
|
-
makeRequest,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
makeRequest,
|
|
8
|
+
queueRequest,
|
|
9
|
+
restampDocumentVersion,
|
|
10
|
+
extractDocumentVersions,
|
|
11
|
+
parseErrorBody,
|
|
12
|
+
makeHttpError,
|
|
13
|
+
makeNetworkError,
|
|
14
|
+
timeoutSignal,
|
|
15
|
+
retryWhileBusy,
|
|
16
|
+
retryUnknown,
|
|
17
|
+
nextIdempotencyKey,
|
|
18
|
+
mergeVersions,
|
|
19
|
+
IDEMPOTENCY_HEADER,
|
|
20
|
+
REPLAYED_HEADER,
|
|
21
|
+
DEFAULT_TIMEOUT_MS,
|
|
22
|
+
DEFAULT_BATCH_TIMEOUT_MS,
|
|
23
|
+
} from "./http.js";
|
|
24
|
+
import { uuidv7 } from "./ids.js";
|
|
25
|
+
import { constraintsBody } from "./constraints.js";
|
|
26
|
+
import { listAll, listPage, iterPages } from "./pagination.js";
|
|
27
|
+
import { recorderFor } from "./events.js";
|
|
28
|
+
import { withDocumentLock } from "./documentLock.js";
|
|
29
|
+
import { createSSEConnection } from "./sse.js";
|
|
30
|
+
import { makeBatchRef, rebaseRefs } from "./batchRef.js";
|
|
31
|
+
import { markReplayed, wasReplayed } from "./replayed.js";
|
|
12
32
|
import {
|
|
13
33
|
discoverServices,
|
|
14
34
|
discardService,
|
|
15
35
|
serve,
|
|
16
36
|
requestService,
|
|
17
|
-
|
|
37
|
+
attachServiceRequest,
|
|
38
|
+
cancelServiceRequest,
|
|
39
|
+
} from "./services.js";
|
|
18
40
|
|
|
19
41
|
// Helper: normalize a document-read `layers` filter to the wire's
|
|
20
42
|
// comma-separated form. Accepts an array of layer ids or a ready-made string;
|
|
21
43
|
// anything empty becomes undefined so no `?layers=` is sent at all.
|
|
22
44
|
function layersParam(layers) {
|
|
23
45
|
if (layers === undefined || layers === null) return undefined;
|
|
24
|
-
const joined = Array.isArray(layers) ? layers.join(
|
|
46
|
+
const joined = Array.isArray(layers) ? layers.join(",") : String(layers);
|
|
25
47
|
return joined.length > 0 ? joined : undefined;
|
|
26
48
|
}
|
|
27
49
|
|
|
@@ -30,10 +52,14 @@ function layersParam(layers) {
|
|
|
30
52
|
// becomes undefined so no `?op-types=` is sent at all.
|
|
31
53
|
function opTypesParam(opTypes) {
|
|
32
54
|
if (opTypes === undefined || opTypes === null) return undefined;
|
|
33
|
-
const joined = Array.isArray(opTypes) ? opTypes.join(
|
|
55
|
+
const joined = Array.isArray(opTypes) ? opTypes.join(",") : String(opTypes);
|
|
34
56
|
return joined.length > 0 ? joined : undefined;
|
|
35
57
|
}
|
|
36
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
|
+
|
|
37
63
|
// Helper: build body object, filtering out undefined values
|
|
38
64
|
function bodyOf(obj) {
|
|
39
65
|
const result = {};
|
|
@@ -47,26 +73,220 @@ function bodyOf(obj) {
|
|
|
47
73
|
// and redemption). Deliberately does not go through PlaidClient._request: these
|
|
48
74
|
// are called before any client exists, which is the whole point.
|
|
49
75
|
async function anonymousPost(baseUrl, path, body, options = {}) {
|
|
50
|
-
const base = baseUrl.replace(/\/$/,
|
|
76
|
+
const base = baseUrl.replace(/\/$/, "");
|
|
51
77
|
const url = `${base}${path}`;
|
|
52
78
|
try {
|
|
53
79
|
const fetchOptions = {
|
|
54
|
-
method:
|
|
55
|
-
headers: {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { "Content-Type": "application/json" },
|
|
56
82
|
body: JSON.stringify(body),
|
|
57
83
|
};
|
|
58
|
-
const signal = timeoutSignal(
|
|
84
|
+
const signal = timeoutSignal(
|
|
85
|
+
options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS,
|
|
86
|
+
);
|
|
87
|
+
if (signal) fetchOptions.signal = signal;
|
|
88
|
+
|
|
89
|
+
const response = await fetch(url, fetchOptions);
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw makeHttpError(
|
|
92
|
+
response,
|
|
93
|
+
await parseErrorBody(response),
|
|
94
|
+
url,
|
|
95
|
+
"POST",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return transformResponse(await response.json());
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error.status) throw error;
|
|
101
|
+
throw makeNetworkError(error, url, "POST");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* GET a path with no authentication, for the two endpoints a client may need
|
|
107
|
+
* before it has a session: /health and /api/v1/info.
|
|
108
|
+
*/
|
|
109
|
+
async function anonymousGet(baseUrl, path, options = {}) {
|
|
110
|
+
const base = baseUrl.replace(/\/$/, "");
|
|
111
|
+
const url = `${base}${path}`;
|
|
112
|
+
try {
|
|
113
|
+
const fetchOptions = { method: "GET" };
|
|
114
|
+
const signal = timeoutSignal(
|
|
115
|
+
options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS,
|
|
116
|
+
);
|
|
59
117
|
if (signal) fetchOptions.signal = signal;
|
|
60
118
|
|
|
61
119
|
const response = await fetch(url, fetchOptions);
|
|
62
120
|
if (!response.ok) {
|
|
63
|
-
throw makeHttpError(response, await parseErrorBody(response), url,
|
|
121
|
+
throw makeHttpError(response, await parseErrorBody(response), url, "GET");
|
|
64
122
|
}
|
|
65
123
|
return transformResponse(await response.json());
|
|
66
124
|
} catch (error) {
|
|
67
125
|
if (error.status) throw error;
|
|
68
|
-
throw makeNetworkError(error, url,
|
|
126
|
+
throw makeNetworkError(error, url, "GET");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The server's cap on operations per batch request (plaid.rest-api.v1.batch).
|
|
132
|
+
* A batch queued past it is submitted as consecutive requests.
|
|
133
|
+
*/
|
|
134
|
+
const MAX_BATCH_OPS = 1000;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A batch is the client with a different `_request` (see `queueRequest`) and
|
|
138
|
+
* its own copy of the bundles. Everything else resolves to the client through
|
|
139
|
+
* the prototype: the token and base URL, strict mode and the document versions
|
|
140
|
+
* it tracks, the open logical operation.
|
|
141
|
+
*/
|
|
142
|
+
function openBatch(client) {
|
|
143
|
+
const batch = Object.create(client);
|
|
144
|
+
batch.client = client;
|
|
145
|
+
batch.operations = [];
|
|
146
|
+
// The document each queued op's strict-mode stamp is for (null for none),
|
|
147
|
+
// and the logical operation it joined (null for none), index for index with
|
|
148
|
+
// `operations`.
|
|
149
|
+
batch.stampedDocuments = [];
|
|
150
|
+
batch.stampedGroups = [];
|
|
151
|
+
batch.open = true;
|
|
152
|
+
batch._request = (method, path, options = {}) =>
|
|
153
|
+
queueRequest(batch, method, path, options);
|
|
154
|
+
batch.batch = () => {
|
|
155
|
+
throw new Error("A batch is not nestable: queue on the batch you have");
|
|
156
|
+
};
|
|
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);
|
|
161
|
+
batch.submit = () => submitBatch(batch);
|
|
162
|
+
batch.abort = () => {
|
|
163
|
+
batch.operations = [];
|
|
164
|
+
batch.stampedDocuments = [];
|
|
165
|
+
batch.stampedGroups = [];
|
|
166
|
+
batch.open = false;
|
|
167
|
+
};
|
|
168
|
+
batch._installResources();
|
|
169
|
+
return batch;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function submitBatch(batch) {
|
|
173
|
+
if (!batch.open) {
|
|
174
|
+
throw new Error("This batch was already submitted or aborted");
|
|
175
|
+
}
|
|
176
|
+
batch.open = false;
|
|
177
|
+
const ops = batch.operations;
|
|
178
|
+
const stamps = batch.stampedDocuments;
|
|
179
|
+
const groups = batch.stampedGroups;
|
|
180
|
+
batch.operations = [];
|
|
181
|
+
batch.stampedDocuments = [];
|
|
182
|
+
batch.stampedGroups = [];
|
|
183
|
+
if (ops.length === 0) return [];
|
|
184
|
+
const client = batch.client;
|
|
185
|
+
const url = `${client.baseUrl}/api/v1/batch`;
|
|
186
|
+
// The server caps a batch at MAX_BATCH_OPS so one transaction cannot hold
|
|
187
|
+
// the write lock without bound. A larger batch goes as consecutive
|
|
188
|
+
// requests, results concatenated in queue order: it could not have been one
|
|
189
|
+
// transaction anyway, and a repair or bulk edit over a big document must
|
|
190
|
+
// not fail on its size alone. Past the first request, a strict-mode stamp
|
|
191
|
+
// claims the version the requests before it left (learned from their
|
|
192
|
+
// results), not the one the document had when the op was queued, which the
|
|
193
|
+
// first request has already moved on.
|
|
194
|
+
//
|
|
195
|
+
// Each request is atomic, the whole is not: a failure leaves the requests
|
|
196
|
+
// before it saved. The error says so, as `committed` (how many operations
|
|
197
|
+
// were saved) and `committedResults` (their results, in queue order), so a
|
|
198
|
+
// caller can tell a partial write from a batch that saved nothing. The
|
|
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`.
|
|
205
|
+
const results = [];
|
|
206
|
+
try {
|
|
207
|
+
await sendChunks(client, url, ops, stamps, results, groups);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (error && typeof error === "object") {
|
|
210
|
+
error.committed = results.length;
|
|
211
|
+
error.committedResults = results;
|
|
212
|
+
}
|
|
213
|
+
throw error;
|
|
69
214
|
}
|
|
215
|
+
return results;
|
|
216
|
+
}
|
|
217
|
+
|
|
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 = [];
|
|
228
|
+
for (let i = 0; i < ops.length; i += MAX_BATCH_OPS) {
|
|
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) {
|
|
251
|
+
chunk = chunk.map((op, j) => {
|
|
252
|
+
const docId = stamps[i + j];
|
|
253
|
+
if (!docId) return op;
|
|
254
|
+
const path = restampDocumentVersion(op.path, claim(docId));
|
|
255
|
+
return { ...op, path };
|
|
256
|
+
});
|
|
257
|
+
}
|
|
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
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
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
|
+
|
|
283
|
+
/** The query of an events read: `types` as the wire's comma-separated list. */
|
|
284
|
+
function eventQuery(types, startTime, endTime) {
|
|
285
|
+
return {
|
|
286
|
+
types: Array.isArray(types) ? types.join(",") || undefined : types,
|
|
287
|
+
"start-time": startTime,
|
|
288
|
+
"end-time": endTime,
|
|
289
|
+
};
|
|
70
290
|
}
|
|
71
291
|
|
|
72
292
|
class PlaidClient {
|
|
@@ -76,19 +296,41 @@ class PlaidClient {
|
|
|
76
296
|
* @param {string} token - The authentication token
|
|
77
297
|
* @param {object} [options] - Client options
|
|
78
298
|
* @param {number} [options.timeout=30000] - Per-request timeout in ms (0 or null disables it)
|
|
299
|
+
* @param {number} [options.batchTimeout=180000] - Timeout for batch submissions in ms (0 or null disables it)
|
|
79
300
|
*/
|
|
80
301
|
constructor(baseUrl, token, options = {}) {
|
|
81
|
-
this.baseUrl = baseUrl.replace(/\/$/,
|
|
302
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
82
303
|
this.token = token;
|
|
83
|
-
this.timeout =
|
|
84
|
-
|
|
85
|
-
|
|
304
|
+
this.timeout =
|
|
305
|
+
options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS;
|
|
306
|
+
// Batch submissions get a longer budget than a single request. Aborting a
|
|
307
|
+
// batch does NOT stop the server: it keeps running the transaction and
|
|
308
|
+
// holding the single SQLite write lock, so a client that gives up early
|
|
309
|
+
// just adds a retry on top of a write that is still in progress. Falls
|
|
310
|
+
// back to an explicit `timeout` when one was given.
|
|
311
|
+
this.batchTimeout =
|
|
312
|
+
options.batchTimeout !== undefined
|
|
313
|
+
? options.batchTimeout
|
|
314
|
+
: options.timeout !== undefined
|
|
315
|
+
? options.timeout
|
|
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;
|
|
86
320
|
this.documentVersions = {};
|
|
321
|
+
// The server's clock minus this machine's, in ms, from the last response
|
|
322
|
+
// with a Date header (null before one). See serverNow().
|
|
323
|
+
this.serverClockOffsetMs = null;
|
|
87
324
|
this.strictModeDocumentId = null;
|
|
325
|
+
// Set to a DocumentLockLost while a `documents.locked()` block's keep-alive
|
|
326
|
+
// has failed. Every write throws it until the block exits; see
|
|
327
|
+
// documentLock.js.
|
|
328
|
+
this.documentLockLost = null;
|
|
88
329
|
// The open logical operation (audit-log group), or null. While set, every
|
|
89
330
|
// write is stamped with `?group-id=` (+ `group-message`) so the audit log
|
|
90
331
|
// folds them into ONE expandable entry. See beginOperation / withOperation.
|
|
91
|
-
// 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.
|
|
92
334
|
this.operationGroup = null;
|
|
93
335
|
// Optional callback fired once (per client) when any request returns HTTP
|
|
94
336
|
// 401 — i.e. the token is missing/expired/invalid. Apps use it to discard
|
|
@@ -96,8 +338,27 @@ class PlaidClient {
|
|
|
96
338
|
this.onAuthError = options.onAuthError || null;
|
|
97
339
|
this._authErrorFired = false;
|
|
98
340
|
|
|
99
|
-
|
|
341
|
+
this._installResources();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* The server's time now, as its last response's Date header put it (to the
|
|
346
|
+
* second), else this machine's. Judge a time the server stamped, such as an
|
|
347
|
+
* audit entry's `ts`, against this rather than `Date.now()`: a browser
|
|
348
|
+
* clock can be minutes off.
|
|
349
|
+
* @returns {Date}
|
|
350
|
+
*/
|
|
351
|
+
serverNow() {
|
|
352
|
+
return new Date(Date.now() + (this.serverClockOffsetMs ?? 0));
|
|
353
|
+
}
|
|
100
354
|
|
|
355
|
+
/**
|
|
356
|
+
* The API bundles (`client.documents`, `client.tokens`, ...). Installed on
|
|
357
|
+
* the client, and again on every batch it opens (see `batch()`): a batch is
|
|
358
|
+
* a view of the client with its own `_request`, so the same bundles built
|
|
359
|
+
* on it queue instead of sending.
|
|
360
|
+
*/
|
|
361
|
+
_installResources() {
|
|
101
362
|
this.vocabLinks = {
|
|
102
363
|
/**
|
|
103
364
|
* Create a new vocab link between tokens and a vocab item.
|
|
@@ -105,9 +366,10 @@ class PlaidClient {
|
|
|
105
366
|
* @param {Array} tokens - The tokens to link
|
|
106
367
|
* @param {any} [metadata] - Metadata for the link. Omit to leave unset; pass null to send JSON null.
|
|
107
368
|
*/
|
|
108
|
-
create: (vocabItem, tokens, metadata, auditMessage) =>
|
|
109
|
-
this._request(
|
|
110
|
-
|
|
369
|
+
create: (vocabItem, tokens, metadata, auditMessage, { id } = {}) =>
|
|
370
|
+
this._request("POST", "/api/v1/vocab-links", {
|
|
371
|
+
auditMessage,
|
|
372
|
+
body: bodyOf({ id, "vocab-item": vocabItem, tokens, metadata }),
|
|
111
373
|
}),
|
|
112
374
|
/**
|
|
113
375
|
* Create multiple vocab links in a single operation. Entries may
|
|
@@ -117,80 +379,188 @@ class PlaidClient {
|
|
|
117
379
|
* @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
|
|
118
380
|
*/
|
|
119
381
|
bulkCreate: (body, auditMessage) =>
|
|
120
|
-
this._request(
|
|
382
|
+
this._request("POST", "/api/v1/vocab-links/bulk", {
|
|
383
|
+
auditMessage,
|
|
384
|
+
body,
|
|
385
|
+
}),
|
|
121
386
|
/**
|
|
122
387
|
* Delete multiple vocab links in a single operation. Provide an array of IDs.
|
|
123
388
|
* @param {string[]} body - The vocab link IDs to delete
|
|
124
389
|
*/
|
|
125
390
|
bulkDelete: (body, auditMessage) =>
|
|
126
|
-
this._request(
|
|
391
|
+
this._request("DELETE", "/api/v1/vocab-links/bulk", {
|
|
392
|
+
auditMessage,
|
|
393
|
+
body,
|
|
394
|
+
}),
|
|
127
395
|
/**
|
|
128
396
|
* Replace all metadata for a vocab link. The entire metadata map is replaced - existing metadata keys not included in the request will be removed.
|
|
129
397
|
* @param {string} id - The resource ID
|
|
130
398
|
* @param {any} body - The request body
|
|
131
399
|
*/
|
|
132
400
|
setMetadata: (id, body, auditMessage) =>
|
|
133
|
-
this._request(
|
|
134
|
-
|
|
401
|
+
this._request("PUT", `/api/v1/vocab-links/${id}/metadata`, {
|
|
402
|
+
auditMessage,
|
|
403
|
+
rawBody: body,
|
|
135
404
|
}),
|
|
136
405
|
/**
|
|
137
406
|
* Remove all metadata from a vocab link.
|
|
138
407
|
* @param {string} id - The resource ID
|
|
139
408
|
*/
|
|
140
409
|
deleteMetadata: (id, auditMessage) =>
|
|
141
|
-
this._request(
|
|
142
|
-
|
|
410
|
+
this._request("DELETE", `/api/v1/vocab-links/${id}/metadata`, {
|
|
411
|
+
auditMessage,
|
|
143
412
|
}),
|
|
144
413
|
/**
|
|
145
|
-
*
|
|
414
|
+
* Edit metadata for a vocab link with a list of ops applied in order, in one operation. `{op: 'set', path, value}` writes value at path, creating missing objects along it; `{op: 'delete', path}` removes the key at path (a no-op when absent). A path is a non-empty array of keys, the first a top-level key. A path through a non-object is refused (400). See metadataOps and applyMetadataOps.
|
|
146
415
|
* @param {string} id - The resource ID
|
|
147
|
-
* @param {any} body - The metadata
|
|
416
|
+
* @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
|
|
148
417
|
*/
|
|
149
418
|
patchMetadata: (id, body, auditMessage) =>
|
|
150
|
-
this._request(
|
|
151
|
-
|
|
419
|
+
this._request("PATCH", `/api/v1/vocab-links/${id}/metadata`, {
|
|
420
|
+
auditMessage,
|
|
421
|
+
rawBody: body,
|
|
152
422
|
}),
|
|
153
423
|
/**
|
|
154
424
|
* Get a vocab link by ID
|
|
155
425
|
* @param {string} id - The resource ID
|
|
156
|
-
* @param {string} [asOf] - Temporal query timestamp
|
|
157
426
|
*/
|
|
158
|
-
get: (id
|
|
159
|
-
this._request(
|
|
160
|
-
queryParams: { 'as-of': asOf },
|
|
161
|
-
}),
|
|
427
|
+
get: (id) =>
|
|
428
|
+
this._request("GET", `/api/v1/vocab-links/${id}`),
|
|
162
429
|
/**
|
|
163
430
|
* Delete a vocab link
|
|
164
431
|
* @param {string} id - The resource ID
|
|
165
432
|
*/
|
|
166
433
|
delete: (id, auditMessage) =>
|
|
167
|
-
this._request(
|
|
434
|
+
this._request("DELETE", `/api/v1/vocab-links/${id}`, { auditMessage }),
|
|
168
435
|
};
|
|
169
436
|
|
|
170
437
|
this.vocabLayers = {
|
|
171
438
|
/**
|
|
172
|
-
* Get a vocab layer by ID
|
|
439
|
+
* Get a vocab layer by ID. With `asOf`, the vocabulary as it was at that
|
|
440
|
+
* instant, read from its history: the same shape as the live read, its
|
|
441
|
+
* entries with `includeItems`. A vocabulary that did not exist then is a
|
|
442
|
+
* 404, a malformed or pruned time a 400.
|
|
173
443
|
* @param {string} id - The resource ID
|
|
174
444
|
* @param {boolean} [includeItems] - Include vocab items
|
|
175
|
-
* @param {string} [asOf] -
|
|
445
|
+
* @param {string} [asOf] - The moment to read at (ISO-8601 instant)
|
|
176
446
|
*/
|
|
177
447
|
get: (id, includeItems, asOf) =>
|
|
178
|
-
this._request(
|
|
179
|
-
queryParams: {
|
|
448
|
+
this._request("GET", `/api/v1/vocab-layers/${id}`, {
|
|
449
|
+
queryParams: { "include-items": includeItems, "as-of": asOf },
|
|
450
|
+
}),
|
|
451
|
+
/**
|
|
452
|
+
* One entry of the vocabulary as it was at `asOf`, also when it has
|
|
453
|
+
* been deleted since. The same shape as `vocabItems.get`. A 404 when
|
|
454
|
+
* the entry was not in this vocabulary at that time.
|
|
455
|
+
* @param {string} id - The vocabulary ID
|
|
456
|
+
* @param {string} itemId - The entry ID
|
|
457
|
+
* @param {string} asOf - The moment to read at (ISO-8601 instant)
|
|
458
|
+
*/
|
|
459
|
+
getItemAt: (id, itemId, asOf) =>
|
|
460
|
+
this._request("GET", `/api/v1/vocab-layers/${id}/items/${itemId}`, {
|
|
461
|
+
queryParams: { "as-of": asOf },
|
|
180
462
|
}),
|
|
463
|
+
/**
|
|
464
|
+
* Get the audit log of a vocabulary: every change to it or to its
|
|
465
|
+
* entries, folded into entries as the document log is. Links are not
|
|
466
|
+
* listed, they belong to the document they annotate. Transparently
|
|
467
|
+
* follows pagination cursors and returns the full flat array.
|
|
468
|
+
* @param {string} id - The vocabulary ID
|
|
469
|
+
* @param {string} [startTime] - Start of time range
|
|
470
|
+
* @param {string} [endTime] - End of time range
|
|
471
|
+
* @param {string[]|string} [opTypes] - Only return operations of these
|
|
472
|
+
* types, spelled as in an entry's `op/type` (e.g.
|
|
473
|
+
* `['vocab-item/delete', 'vocab-item/restore']`)
|
|
474
|
+
* @param {string} [itemId] - Only the changes that wrote this one entry,
|
|
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
|
|
478
|
+
*/
|
|
479
|
+
audit: (id, startTime, endTime, opTypes, itemId, kinds) =>
|
|
480
|
+
listAll(this, `/api/v1/vocab-layers/${id}/audit`, {
|
|
481
|
+
query: {
|
|
482
|
+
"start-time": startTime,
|
|
483
|
+
"end-time": endTime,
|
|
484
|
+
"op-types": opTypesParam(opTypes),
|
|
485
|
+
"item-id": itemId,
|
|
486
|
+
kinds: kindsParam(kinds),
|
|
487
|
+
},
|
|
488
|
+
}),
|
|
489
|
+
/**
|
|
490
|
+
* One page of the same log, newest-first with `order: "desc"`. Use this
|
|
491
|
+
* rather than audit() wherever the caller wants the recent end of a log
|
|
492
|
+
* that may be long: audit() walks every page before it resolves.
|
|
493
|
+
* @param {string} id - The vocabulary ID
|
|
494
|
+
* @param {object} [opts]
|
|
495
|
+
* @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
|
|
496
|
+
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
497
|
+
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
498
|
+
* @param {string} [opts.itemId] - Only the changes that wrote this one
|
|
499
|
+
* entry, each with only its operations that did
|
|
500
|
+
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
501
|
+
*/
|
|
502
|
+
auditPage: (
|
|
503
|
+
id,
|
|
504
|
+
{ startTime, endTime, opTypes, kinds, order, limit, cursor, itemId } = {},
|
|
505
|
+
) =>
|
|
506
|
+
listPage(this, `/api/v1/vocab-layers/${id}/audit`, {
|
|
507
|
+
limit,
|
|
508
|
+
cursor,
|
|
509
|
+
query: {
|
|
510
|
+
"start-time": startTime,
|
|
511
|
+
"end-time": endTime,
|
|
512
|
+
"op-types": opTypesParam(opTypes),
|
|
513
|
+
kinds: kindsParam(kinds),
|
|
514
|
+
order,
|
|
515
|
+
"item-id": itemId,
|
|
516
|
+
},
|
|
517
|
+
}),
|
|
518
|
+
/**
|
|
519
|
+
* Put one entry of the vocabulary back as it was at `asOf`, as one
|
|
520
|
+
* operation. A deleted entry comes back under its original id with its
|
|
521
|
+
* form and fields, and a living one has its form and fields set back.
|
|
522
|
+
* Links are not part of an entry: a deleted entry's links come back
|
|
523
|
+
* through each document's own `documents.restore`. Resolves to
|
|
524
|
+
* `{inserted, form, metadata, total}`, `total` 0 when nothing changes.
|
|
525
|
+
* A form set back bumps every linking document, and the new versions
|
|
526
|
+
* come back in X-Document-Versions (or, past fifty documents,
|
|
527
|
+
* X-Document-Versions-Omitted), which the client takes up as on any
|
|
528
|
+
* write. A time when the entry did not exist is a 400. Maintainers of
|
|
529
|
+
* the vocabulary only.
|
|
530
|
+
* @param {string} id - The vocabulary ID
|
|
531
|
+
* @param {string} itemId - The entry ID
|
|
532
|
+
* @param {string} asOf - The moment to go back to (ISO-8601 instant),
|
|
533
|
+
* typically a history entry's `endTime`
|
|
534
|
+
* @param {object} [options] - `{ dryRun }`: with `dryRun` true nothing
|
|
535
|
+
* is written and the summary says what would change
|
|
536
|
+
* @param {string} [auditMessage] - Custom audit message for this operation
|
|
537
|
+
*/
|
|
538
|
+
restoreItem: (id, itemId, asOf, { dryRun } = {}, auditMessage) =>
|
|
539
|
+
this._request(
|
|
540
|
+
"POST",
|
|
541
|
+
`/api/v1/vocab-layers/${id}/items/${itemId}/restore`,
|
|
542
|
+
{
|
|
543
|
+
queryParams: {
|
|
544
|
+
"as-of": asOf,
|
|
545
|
+
"dry-run": dryRun ? "true" : undefined,
|
|
546
|
+
},
|
|
547
|
+
auditMessage,
|
|
548
|
+
},
|
|
549
|
+
),
|
|
181
550
|
/**
|
|
182
551
|
* Delete a vocab layer.
|
|
183
552
|
* @param {string} id - The resource ID
|
|
184
553
|
*/
|
|
185
554
|
delete: (id, auditMessage) =>
|
|
186
|
-
this._request(
|
|
555
|
+
this._request("DELETE", `/api/v1/vocab-layers/${id}`, { auditMessage }),
|
|
187
556
|
/**
|
|
188
557
|
* Update a vocab layer's name.
|
|
189
558
|
* @param {string} id - The resource ID
|
|
190
559
|
* @param {string} name - The name
|
|
191
560
|
*/
|
|
192
561
|
update: (id, name, auditMessage) =>
|
|
193
|
-
this._request(
|
|
562
|
+
this._request("PATCH", `/api/v1/vocab-layers/${id}`, {
|
|
563
|
+
auditMessage,
|
|
194
564
|
body: bodyOf({ name }),
|
|
195
565
|
}),
|
|
196
566
|
/**
|
|
@@ -199,56 +569,65 @@ class PlaidClient {
|
|
|
199
569
|
* @param {string} namespace - The config namespace
|
|
200
570
|
* @param {string} configKey - The config key
|
|
201
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.
|
|
202
575
|
*/
|
|
203
|
-
setConfig: (id, namespace, configKey, configValue, auditMessage) =>
|
|
204
|
-
this._request(
|
|
205
|
-
|
|
206
|
-
|
|
576
|
+
setConfig: (id, namespace, configKey, configValue, auditMessage, options) =>
|
|
577
|
+
this._request(
|
|
578
|
+
"PUT",
|
|
579
|
+
`/api/v1/vocab-layers/${id}/config/${namespace}/${configKey}`,
|
|
580
|
+
configRequest(auditMessage, configValue, options),
|
|
581
|
+
),
|
|
207
582
|
/**
|
|
208
583
|
* Remove a configuration value for a layer.
|
|
209
584
|
* @param {string} id - The resource ID
|
|
210
585
|
* @param {string} namespace - The config namespace
|
|
211
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.
|
|
212
589
|
*/
|
|
213
|
-
deleteConfig: (id, namespace, configKey, auditMessage) =>
|
|
214
|
-
this._request(
|
|
215
|
-
|
|
216
|
-
|
|
590
|
+
deleteConfig: (id, namespace, configKey, auditMessage, options) =>
|
|
591
|
+
this._request(
|
|
592
|
+
"DELETE",
|
|
593
|
+
`/api/v1/vocab-layers/${id}/config/${namespace}/${configKey}`,
|
|
594
|
+
configRequest(auditMessage, undefined, options, true),
|
|
595
|
+
),
|
|
217
596
|
/**
|
|
218
597
|
* List all vocab layers accessible to user. Transparently follows
|
|
219
598
|
* pagination cursors and returns the full flat array.
|
|
220
|
-
* Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
|
|
221
|
-
* @param {string} [asOf] - Temporal query timestamp
|
|
222
599
|
*/
|
|
223
|
-
list: (
|
|
224
|
-
listAll(this, '/api/v1/vocab-layers', { query: { 'as-of': asOf } }),
|
|
600
|
+
list: () => listAll(this, "/api/v1/vocab-layers"),
|
|
225
601
|
/**
|
|
226
602
|
* Fetch a single page of vocab layers.
|
|
227
603
|
* @param {object} [opts]
|
|
228
604
|
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
229
605
|
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
230
|
-
* @param {string} [opts.asOf] - Temporal query timestamp
|
|
231
606
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
232
607
|
*/
|
|
233
|
-
listPage: ({ limit, cursor
|
|
234
|
-
listPage(this,
|
|
608
|
+
listPage: ({ limit, cursor } = {}) =>
|
|
609
|
+
listPage(this, "/api/v1/vocab-layers", {
|
|
610
|
+
limit,
|
|
611
|
+
cursor,
|
|
612
|
+
}),
|
|
235
613
|
/**
|
|
236
614
|
* Async-iterate vocab layers page by page; yields each page's entries array.
|
|
237
615
|
* @param {object} [opts]
|
|
238
616
|
* @param {number} [opts.pageSize] - Per-request page size
|
|
239
|
-
* @param {string} [opts.asOf] - Temporal query timestamp
|
|
240
|
-
* Cannot be used inside a batch (auto-paginates across requests); throws on first iteration if called while batching — use listPage() for a single page in a batch.
|
|
241
617
|
* @returns {AsyncGenerator<Array>}
|
|
242
618
|
*/
|
|
243
|
-
iterPages: ({ pageSize
|
|
244
|
-
iterPages(this,
|
|
619
|
+
iterPages: ({ pageSize } = {}) =>
|
|
620
|
+
iterPages(this, "/api/v1/vocab-layers", {
|
|
621
|
+
pageSize,
|
|
622
|
+
}),
|
|
245
623
|
/**
|
|
246
624
|
* Create a new vocab layer. Note: this also registers the user as a maintainer.
|
|
247
625
|
* @param {string} name - The name
|
|
248
626
|
*/
|
|
249
|
-
create: (name, auditMessage) =>
|
|
250
|
-
this._request(
|
|
251
|
-
|
|
627
|
+
create: (name, auditMessage, { id } = {}) =>
|
|
628
|
+
this._request("POST", "/api/v1/vocab-layers", {
|
|
629
|
+
auditMessage,
|
|
630
|
+
body: bodyOf({ id, name }),
|
|
252
631
|
}),
|
|
253
632
|
/**
|
|
254
633
|
* Assign a user as a maintainer for this vocab layer.
|
|
@@ -256,14 +635,22 @@ class PlaidClient {
|
|
|
256
635
|
* @param {string} userId - The user ID
|
|
257
636
|
*/
|
|
258
637
|
addMaintainer: (id, userId, auditMessage) =>
|
|
259
|
-
this._request(
|
|
638
|
+
this._request(
|
|
639
|
+
"POST",
|
|
640
|
+
`/api/v1/vocab-layers/${id}/maintainers/${userId}`,
|
|
641
|
+
{ auditMessage },
|
|
642
|
+
),
|
|
260
643
|
/**
|
|
261
644
|
* Remove a user's maintainer privileges for this vocab layer.
|
|
262
645
|
* @param {string} id - The resource ID
|
|
263
646
|
* @param {string} userId - The user ID
|
|
264
647
|
*/
|
|
265
648
|
removeMaintainer: (id, userId, auditMessage) =>
|
|
266
|
-
this._request(
|
|
649
|
+
this._request(
|
|
650
|
+
"DELETE",
|
|
651
|
+
`/api/v1/vocab-layers/${id}/maintainers/${userId}`,
|
|
652
|
+
{ auditMessage },
|
|
653
|
+
),
|
|
267
654
|
};
|
|
268
655
|
|
|
269
656
|
this.relations = {
|
|
@@ -273,25 +660,27 @@ class PlaidClient {
|
|
|
273
660
|
* @param {any} body - The request body
|
|
274
661
|
*/
|
|
275
662
|
setMetadata: (relationId, body, auditMessage) =>
|
|
276
|
-
this._request(
|
|
277
|
-
|
|
663
|
+
this._request("PUT", `/api/v1/relations/${relationId}/metadata`, {
|
|
664
|
+
auditMessage,
|
|
665
|
+
rawBody: body,
|
|
278
666
|
}),
|
|
279
667
|
/**
|
|
280
668
|
* Remove all metadata from a relation.
|
|
281
669
|
* @param {string} relationId - The relation ID
|
|
282
670
|
*/
|
|
283
671
|
deleteMetadata: (relationId, auditMessage) =>
|
|
284
|
-
this._request(
|
|
285
|
-
|
|
672
|
+
this._request("DELETE", `/api/v1/relations/${relationId}/metadata`, {
|
|
673
|
+
auditMessage,
|
|
286
674
|
}),
|
|
287
675
|
/**
|
|
288
|
-
*
|
|
676
|
+
* Edit metadata for a relation with a list of ops applied in order, in one operation. `{op: 'set', path, value}` writes value at path, creating missing objects along it; `{op: 'delete', path}` removes the key at path (a no-op when absent). A path is a non-empty array of keys, the first a top-level key. A path through a non-object is refused (400). See metadataOps and applyMetadataOps.
|
|
289
677
|
* @param {string} relationId - The relation ID
|
|
290
|
-
* @param {any} body - The metadata
|
|
678
|
+
* @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
|
|
291
679
|
*/
|
|
292
680
|
patchMetadata: (relationId, body, auditMessage) =>
|
|
293
|
-
this._request(
|
|
294
|
-
|
|
681
|
+
this._request("PATCH", `/api/v1/relations/${relationId}/metadata`, {
|
|
682
|
+
auditMessage,
|
|
683
|
+
rawBody: body,
|
|
295
684
|
}),
|
|
296
685
|
/**
|
|
297
686
|
* Update the target span of a relation.
|
|
@@ -299,31 +688,32 @@ class PlaidClient {
|
|
|
299
688
|
* @param {string} spanId - The span ID
|
|
300
689
|
*/
|
|
301
690
|
setTarget: (relationId, spanId, auditMessage) =>
|
|
302
|
-
this._request(
|
|
303
|
-
|
|
691
|
+
this._request("PUT", `/api/v1/relations/${relationId}/target`, {
|
|
692
|
+
auditMessage,
|
|
693
|
+
body: bodyOf({ "span-id": spanId }),
|
|
304
694
|
}),
|
|
305
695
|
/**
|
|
306
696
|
* Get a relation by ID.
|
|
307
697
|
* @param {string} relationId - The relation ID
|
|
308
|
-
* @param {string} [asOf] - Temporal query timestamp
|
|
309
698
|
*/
|
|
310
|
-
get: (relationId
|
|
311
|
-
this._request(
|
|
312
|
-
queryParams: { 'as-of': asOf },
|
|
313
|
-
}),
|
|
699
|
+
get: (relationId) =>
|
|
700
|
+
this._request("GET", `/api/v1/relations/${relationId}`),
|
|
314
701
|
/**
|
|
315
702
|
* Delete a relation.
|
|
316
703
|
* @param {string} relationId - The relation ID
|
|
317
704
|
*/
|
|
318
705
|
delete: (relationId, auditMessage) =>
|
|
319
|
-
this._request(
|
|
706
|
+
this._request("DELETE", `/api/v1/relations/${relationId}`, {
|
|
707
|
+
auditMessage,
|
|
708
|
+
}),
|
|
320
709
|
/**
|
|
321
710
|
* Update a relation's value.
|
|
322
711
|
* @param {string} relationId - The relation ID
|
|
323
712
|
* @param {any} value - The value
|
|
324
713
|
*/
|
|
325
714
|
update: (relationId, value, auditMessage) =>
|
|
326
|
-
this._request(
|
|
715
|
+
this._request("PATCH", `/api/v1/relations/${relationId}`, {
|
|
716
|
+
auditMessage,
|
|
327
717
|
body: bodyOf({ value }),
|
|
328
718
|
}),
|
|
329
719
|
/**
|
|
@@ -332,8 +722,9 @@ class PlaidClient {
|
|
|
332
722
|
* @param {string} spanId - The span ID
|
|
333
723
|
*/
|
|
334
724
|
setSource: (relationId, spanId, auditMessage) =>
|
|
335
|
-
this._request(
|
|
336
|
-
|
|
725
|
+
this._request("PUT", `/api/v1/relations/${relationId}/source`, {
|
|
726
|
+
auditMessage,
|
|
727
|
+
body: bodyOf({ "span-id": spanId }),
|
|
337
728
|
}),
|
|
338
729
|
/**
|
|
339
730
|
* Create a new relation. A relation is a directed edge between two spans
|
|
@@ -345,9 +736,25 @@ class PlaidClient {
|
|
|
345
736
|
* @param {any} value - The value
|
|
346
737
|
* @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
|
|
347
738
|
*/
|
|
348
|
-
create: (
|
|
349
|
-
|
|
350
|
-
|
|
739
|
+
create: (
|
|
740
|
+
layerId,
|
|
741
|
+
sourceId,
|
|
742
|
+
targetId,
|
|
743
|
+
value,
|
|
744
|
+
metadata,
|
|
745
|
+
auditMessage,
|
|
746
|
+
{ id } = {},
|
|
747
|
+
) =>
|
|
748
|
+
this._request("POST", "/api/v1/relations", {
|
|
749
|
+
auditMessage,
|
|
750
|
+
body: bodyOf({
|
|
751
|
+
id,
|
|
752
|
+
"layer-id": layerId,
|
|
753
|
+
"source-id": sourceId,
|
|
754
|
+
"target-id": targetId,
|
|
755
|
+
value,
|
|
756
|
+
metadata,
|
|
757
|
+
}),
|
|
351
758
|
}),
|
|
352
759
|
/**
|
|
353
760
|
* Create multiple relations in a single operation.
|
|
@@ -355,13 +762,26 @@ class PlaidClient {
|
|
|
355
762
|
* @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
|
|
356
763
|
*/
|
|
357
764
|
bulkCreate: (body, auditMessage) =>
|
|
358
|
-
this._request(
|
|
765
|
+
this._request("POST", "/api/v1/relations/bulk", { auditMessage, body }),
|
|
359
766
|
/**
|
|
360
767
|
* Delete multiple relations in a single operation. Provide an array of IDs.
|
|
361
768
|
* @param {Array} body - The request body
|
|
362
769
|
*/
|
|
363
770
|
bulkDelete: (body, auditMessage) =>
|
|
364
|
-
this._request(
|
|
771
|
+
this._request("DELETE", "/api/v1/relations/bulk", {
|
|
772
|
+
auditMessage,
|
|
773
|
+
body,
|
|
774
|
+
}),
|
|
775
|
+
/**
|
|
776
|
+
* Update many relations in a single operation: set values and/or patch metadata.
|
|
777
|
+
* @param {Array} body - Objects of the shape `{id, value?, metadata?}`. `value` is set only when the key is present (null sends JSON null); `metadata` is a list of metadata ops, as for patchMetadata. The relations may lie in several documents of one project; every document touched has its version bumped and every new version comes back in `X-Document-Versions` (past fifty documents, only their number, in `X-Document-Versions-Omitted`). A `document-version` precondition is accepted only when every entry lies in one document. An unknown id refuses the whole update.
|
|
778
|
+
* @returns {Promise<{count: number}>} How many relations were updated.
|
|
779
|
+
*/
|
|
780
|
+
bulkUpdate: (body, auditMessage) =>
|
|
781
|
+
this._request("PATCH", "/api/v1/relations/bulk", {
|
|
782
|
+
auditMessage,
|
|
783
|
+
body,
|
|
784
|
+
}),
|
|
365
785
|
};
|
|
366
786
|
|
|
367
787
|
this.spanLayers = {
|
|
@@ -371,43 +791,53 @@ class PlaidClient {
|
|
|
371
791
|
* @param {string} namespace - The config namespace
|
|
372
792
|
* @param {string} configKey - The config key
|
|
373
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.
|
|
374
797
|
*/
|
|
375
|
-
setConfig: (spanLayerId, namespace, configKey, configValue, auditMessage) =>
|
|
376
|
-
this._request(
|
|
377
|
-
|
|
378
|
-
|
|
798
|
+
setConfig: (spanLayerId, namespace, configKey, configValue, auditMessage, options) =>
|
|
799
|
+
this._request(
|
|
800
|
+
"PUT",
|
|
801
|
+
`/api/v1/span-layers/${spanLayerId}/config/${namespace}/${configKey}`,
|
|
802
|
+
configRequest(auditMessage, configValue, options),
|
|
803
|
+
),
|
|
379
804
|
/**
|
|
380
805
|
* Remove a configuration value for a layer.
|
|
381
806
|
* @param {string} spanLayerId - The span layer ID
|
|
382
807
|
* @param {string} namespace - The config namespace
|
|
383
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.
|
|
384
811
|
*/
|
|
385
|
-
deleteConfig: (spanLayerId, namespace, configKey, auditMessage) =>
|
|
386
|
-
this._request(
|
|
387
|
-
|
|
388
|
-
|
|
812
|
+
deleteConfig: (spanLayerId, namespace, configKey, auditMessage, options) =>
|
|
813
|
+
this._request(
|
|
814
|
+
"DELETE",
|
|
815
|
+
`/api/v1/span-layers/${spanLayerId}/config/${namespace}/${configKey}`,
|
|
816
|
+
configRequest(auditMessage, undefined, options, true),
|
|
817
|
+
),
|
|
818
|
+
...constraintMethods(this, "span-layers"),
|
|
389
819
|
/**
|
|
390
820
|
* Get a span layer by ID.
|
|
391
821
|
* @param {string} spanLayerId - The span layer ID
|
|
392
|
-
* @param {string} [asOf] - Temporal query timestamp
|
|
393
822
|
*/
|
|
394
|
-
get: (spanLayerId
|
|
395
|
-
this._request(
|
|
396
|
-
queryParams: { 'as-of': asOf },
|
|
397
|
-
}),
|
|
823
|
+
get: (spanLayerId) =>
|
|
824
|
+
this._request("GET", `/api/v1/span-layers/${spanLayerId}`),
|
|
398
825
|
/**
|
|
399
826
|
* Delete a span layer.
|
|
400
827
|
* @param {string} spanLayerId - The span layer ID
|
|
401
828
|
*/
|
|
402
829
|
delete: (spanLayerId, auditMessage) =>
|
|
403
|
-
this._request(
|
|
830
|
+
this._request("DELETE", `/api/v1/span-layers/${spanLayerId}`, {
|
|
831
|
+
auditMessage,
|
|
832
|
+
}),
|
|
404
833
|
/**
|
|
405
834
|
* Update a span layer's name.
|
|
406
835
|
* @param {string} spanLayerId - The span layer ID
|
|
407
836
|
* @param {string} name - The name
|
|
408
837
|
*/
|
|
409
838
|
update: (spanLayerId, name, auditMessage) =>
|
|
410
|
-
this._request(
|
|
839
|
+
this._request("PATCH", `/api/v1/span-layers/${spanLayerId}`, {
|
|
840
|
+
auditMessage,
|
|
411
841
|
body: bodyOf({ name }),
|
|
412
842
|
}),
|
|
413
843
|
/**
|
|
@@ -415,9 +845,10 @@ class PlaidClient {
|
|
|
415
845
|
* @param {string} tokenLayerId - The token layer ID
|
|
416
846
|
* @param {string} name - The name
|
|
417
847
|
*/
|
|
418
|
-
create: (tokenLayerId, name, auditMessage) =>
|
|
419
|
-
this._request(
|
|
420
|
-
|
|
848
|
+
create: (tokenLayerId, name, auditMessage, { id } = {}) =>
|
|
849
|
+
this._request("POST", "/api/v1/span-layers", {
|
|
850
|
+
auditMessage,
|
|
851
|
+
body: bodyOf({ id, "token-layer-id": tokenLayerId, name }),
|
|
421
852
|
}),
|
|
422
853
|
/**
|
|
423
854
|
* Shift a span layer's display order.
|
|
@@ -425,7 +856,8 @@ class PlaidClient {
|
|
|
425
856
|
* @param {string} direction - The direction ("up" or "down")
|
|
426
857
|
*/
|
|
427
858
|
shift: (spanLayerId, direction, auditMessage) =>
|
|
428
|
-
this._request(
|
|
859
|
+
this._request("POST", `/api/v1/span-layers/${spanLayerId}/shift`, {
|
|
860
|
+
auditMessage,
|
|
429
861
|
body: bodyOf({ direction }),
|
|
430
862
|
}),
|
|
431
863
|
};
|
|
@@ -437,7 +869,8 @@ class PlaidClient {
|
|
|
437
869
|
* @param {Array} tokens - The tokens
|
|
438
870
|
*/
|
|
439
871
|
setTokens: (spanId, tokens, auditMessage) =>
|
|
440
|
-
this._request(
|
|
872
|
+
this._request("PUT", `/api/v1/spans/${spanId}/tokens`, {
|
|
873
|
+
auditMessage,
|
|
441
874
|
body: bodyOf({ tokens }),
|
|
442
875
|
}),
|
|
443
876
|
/**
|
|
@@ -448,32 +881,37 @@ class PlaidClient {
|
|
|
448
881
|
* @param {any} value - The value
|
|
449
882
|
* @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
|
|
450
883
|
*/
|
|
451
|
-
create: (spanLayerId, tokens, value, metadata, auditMessage) =>
|
|
452
|
-
this._request(
|
|
453
|
-
|
|
884
|
+
create: (spanLayerId, tokens, value, metadata, auditMessage, { id } = {}) =>
|
|
885
|
+
this._request("POST", "/api/v1/spans", {
|
|
886
|
+
auditMessage,
|
|
887
|
+
body: bodyOf({
|
|
888
|
+
id,
|
|
889
|
+
"span-layer-id": spanLayerId,
|
|
890
|
+
tokens,
|
|
891
|
+
value,
|
|
892
|
+
metadata,
|
|
893
|
+
}),
|
|
454
894
|
}),
|
|
455
895
|
/**
|
|
456
896
|
* Get a span by ID.
|
|
457
897
|
* @param {string} spanId - The span ID
|
|
458
|
-
* @param {string} [asOf] - Temporal query timestamp
|
|
459
898
|
*/
|
|
460
|
-
get: (spanId
|
|
461
|
-
this._request(
|
|
462
|
-
queryParams: { 'as-of': asOf },
|
|
463
|
-
}),
|
|
899
|
+
get: (spanId) =>
|
|
900
|
+
this._request("GET", `/api/v1/spans/${spanId}`),
|
|
464
901
|
/**
|
|
465
902
|
* Delete a span.
|
|
466
903
|
* @param {string} spanId - The span ID
|
|
467
904
|
*/
|
|
468
905
|
delete: (spanId, auditMessage) =>
|
|
469
|
-
this._request(
|
|
906
|
+
this._request("DELETE", `/api/v1/spans/${spanId}`, { auditMessage }),
|
|
470
907
|
/**
|
|
471
908
|
* Update a span's value.
|
|
472
909
|
* @param {string} spanId - The span ID
|
|
473
910
|
* @param {any} value - The value
|
|
474
911
|
*/
|
|
475
912
|
update: (spanId, value, auditMessage) =>
|
|
476
|
-
this._request(
|
|
913
|
+
this._request("PATCH", `/api/v1/spans/${spanId}`, {
|
|
914
|
+
auditMessage,
|
|
477
915
|
body: bodyOf({ value }),
|
|
478
916
|
}),
|
|
479
917
|
/**
|
|
@@ -482,51 +920,101 @@ class PlaidClient {
|
|
|
482
920
|
* @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
|
|
483
921
|
*/
|
|
484
922
|
bulkCreate: (body, auditMessage) =>
|
|
485
|
-
this._request(
|
|
923
|
+
this._request("POST", "/api/v1/spans/bulk", { auditMessage, body }),
|
|
486
924
|
/**
|
|
487
925
|
* Delete multiple spans in a single operation. Provide an array of IDs.
|
|
488
926
|
* @param {Array} body - The request body
|
|
489
927
|
*/
|
|
490
928
|
bulkDelete: (body, auditMessage) =>
|
|
491
|
-
this._request(
|
|
929
|
+
this._request("DELETE", "/api/v1/spans/bulk", { auditMessage, body }),
|
|
930
|
+
/**
|
|
931
|
+
* Update many spans in a single operation: set values and/or patch metadata.
|
|
932
|
+
* @param {Array} body - Objects of the shape `{id, value?, metadata?}`. `value` is set only when the key is present (null sends JSON null); `metadata` is a list of metadata ops, as for patchMetadata. The spans may lie in several documents of one project; every document touched has its version bumped and every new version comes back in `X-Document-Versions` (past fifty documents, only their number, in `X-Document-Versions-Omitted`). A `document-version` precondition is accepted only when every entry lies in one document. An unknown id refuses the whole update.
|
|
933
|
+
* @returns {Promise<{count: number}>} How many spans were updated.
|
|
934
|
+
*/
|
|
935
|
+
bulkUpdate: (body, auditMessage) =>
|
|
936
|
+
this._request("PATCH", "/api/v1/spans/bulk", {
|
|
937
|
+
auditMessage,
|
|
938
|
+
body,
|
|
939
|
+
}),
|
|
492
940
|
/**
|
|
493
941
|
* Replace all metadata for a span.
|
|
494
942
|
* @param {string} spanId - The span ID
|
|
495
943
|
* @param {any} body - The request body
|
|
496
944
|
*/
|
|
497
945
|
setMetadata: (spanId, body, auditMessage) =>
|
|
498
|
-
this._request(
|
|
499
|
-
|
|
946
|
+
this._request("PUT", `/api/v1/spans/${spanId}/metadata`, {
|
|
947
|
+
auditMessage,
|
|
948
|
+
rawBody: body,
|
|
500
949
|
}),
|
|
501
950
|
/**
|
|
502
951
|
* Remove all metadata from a span.
|
|
503
952
|
* @param {string} spanId - The span ID
|
|
504
953
|
*/
|
|
505
954
|
deleteMetadata: (spanId, auditMessage) =>
|
|
506
|
-
this._request(
|
|
507
|
-
|
|
955
|
+
this._request("DELETE", `/api/v1/spans/${spanId}/metadata`, {
|
|
956
|
+
auditMessage,
|
|
508
957
|
}),
|
|
509
958
|
/**
|
|
510
|
-
*
|
|
959
|
+
* Edit metadata for a span with a list of ops applied in order, in one operation. `{op: 'set', path, value}` writes value at path, creating missing objects along it; `{op: 'delete', path}` removes the key at path (a no-op when absent). A path is a non-empty array of keys, the first a top-level key. A path through a non-object is refused (400). See metadataOps and applyMetadataOps.
|
|
511
960
|
* @param {string} spanId - The span ID
|
|
512
|
-
* @param {any} body - The metadata
|
|
961
|
+
* @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
|
|
513
962
|
*/
|
|
514
963
|
patchMetadata: (spanId, body, auditMessage) =>
|
|
515
|
-
this._request(
|
|
516
|
-
|
|
964
|
+
this._request("PATCH", `/api/v1/spans/${spanId}/metadata`, {
|
|
965
|
+
auditMessage,
|
|
966
|
+
rawBody: body,
|
|
517
967
|
}),
|
|
518
968
|
};
|
|
519
969
|
|
|
520
|
-
|
|
970
|
+
// Server-level facts. Fetched at most once: the limits cannot change while
|
|
971
|
+
// the server is up, and a caller asking "will this file be accepted" should
|
|
972
|
+
// not pay a round trip to find out.
|
|
973
|
+
let infoPromise = null;
|
|
974
|
+
this.server = {
|
|
975
|
+
/**
|
|
976
|
+
* This server's version and the limits it enforces, e.g.
|
|
977
|
+
* `{ limits: { mediaFileBytes, jsonBodyBytes, batchOperations, … } }`.
|
|
978
|
+
* Sizes are in bytes. Unauthenticated.
|
|
979
|
+
*/
|
|
980
|
+
info: () => {
|
|
981
|
+
if (!infoPromise) {
|
|
982
|
+
infoPromise = this._request("GET", "/api/v1/info").catch((err) => {
|
|
983
|
+
// A failure must not be cached: a client that starts before the
|
|
984
|
+
// server is up would never see the limits at all.
|
|
985
|
+
infoPromise = null;
|
|
986
|
+
throw err;
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
return infoPromise;
|
|
990
|
+
},
|
|
991
|
+
/** Just the limits, which is what a caller almost always wants. */
|
|
992
|
+
limits: async () => (await this.server.info()).limits,
|
|
521
993
|
/**
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
994
|
+
* Liveness, version and database size. Unauthenticated, and served
|
|
995
|
+
* outside the REST router at /health, so it answers even while the API
|
|
996
|
+
* is refusing requests. Never cached — the point is that it is current.
|
|
997
|
+
* @returns {Promise<{ok: boolean, version: string, uptimeMs: number, audit: object}>}
|
|
525
998
|
*/
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
999
|
+
health: () => PlaidClient.health(this.baseUrl, { timeout: this.timeout }),
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
// The signed-in user's sign-ins. Signing in is `PlaidClient.login`, which
|
|
1003
|
+
// makes the client.
|
|
1004
|
+
this.auth = {
|
|
1005
|
+
/**
|
|
1006
|
+
* End every sign-in of this user on every device: each browser tab,
|
|
1007
|
+
* script and service holding one of the user's sign-in tokens is
|
|
1008
|
+
* refused from now on, this client included. Named API tokens are not
|
|
1009
|
+
* affected (revoke one with `apiTokens.revoke`). To sign out of one tab
|
|
1010
|
+
* only, discard its token instead. A signal rather than project data:
|
|
1011
|
+
* made on a batch it still goes out at once, and it never joins an
|
|
1012
|
+
* operation.
|
|
1013
|
+
* @returns {Promise<void>}
|
|
1014
|
+
*/
|
|
1015
|
+
logoutEverywhere: async () => {
|
|
1016
|
+
await this._request("POST", "/api/v1/logout", { outOfBand: true });
|
|
1017
|
+
},
|
|
530
1018
|
};
|
|
531
1019
|
|
|
532
1020
|
this.texts = {
|
|
@@ -536,25 +1024,27 @@ class PlaidClient {
|
|
|
536
1024
|
* @param {any} body - The request body
|
|
537
1025
|
*/
|
|
538
1026
|
setMetadata: (textId, body, auditMessage) =>
|
|
539
|
-
this._request(
|
|
540
|
-
|
|
1027
|
+
this._request("PUT", `/api/v1/texts/${textId}/metadata`, {
|
|
1028
|
+
auditMessage,
|
|
1029
|
+
rawBody: body,
|
|
541
1030
|
}),
|
|
542
1031
|
/**
|
|
543
1032
|
* Remove all metadata from a text.
|
|
544
1033
|
* @param {string} textId - The text ID
|
|
545
1034
|
*/
|
|
546
1035
|
deleteMetadata: (textId, auditMessage) =>
|
|
547
|
-
this._request(
|
|
548
|
-
|
|
1036
|
+
this._request("DELETE", `/api/v1/texts/${textId}/metadata`, {
|
|
1037
|
+
auditMessage,
|
|
549
1038
|
}),
|
|
550
1039
|
/**
|
|
551
|
-
*
|
|
1040
|
+
* Edit metadata for a text with a list of ops applied in order, in one operation. `{op: 'set', path, value}` writes value at path, creating missing objects along it; `{op: 'delete', path}` removes the key at path (a no-op when absent). A path is a non-empty array of keys, the first a top-level key. A path through a non-object is refused (400). See metadataOps and applyMetadataOps.
|
|
552
1041
|
* @param {string} textId - The text ID
|
|
553
|
-
* @param {any} body - The metadata
|
|
1042
|
+
* @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
|
|
554
1043
|
*/
|
|
555
1044
|
patchMetadata: (textId, body, auditMessage) =>
|
|
556
|
-
this._request(
|
|
557
|
-
|
|
1045
|
+
this._request("PATCH", `/api/v1/texts/${textId}/metadata`, {
|
|
1046
|
+
auditMessage,
|
|
1047
|
+
rawBody: body,
|
|
558
1048
|
}),
|
|
559
1049
|
/**
|
|
560
1050
|
* Create a new text in a document's text layer. A text is a container for
|
|
@@ -564,35 +1054,84 @@ class PlaidClient {
|
|
|
564
1054
|
* @param {string} body - The request body
|
|
565
1055
|
* @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
|
|
566
1056
|
*/
|
|
567
|
-
create: (
|
|
568
|
-
|
|
569
|
-
|
|
1057
|
+
create: (
|
|
1058
|
+
textLayerId,
|
|
1059
|
+
documentId,
|
|
1060
|
+
body,
|
|
1061
|
+
metadata,
|
|
1062
|
+
auditMessage,
|
|
1063
|
+
{ id } = {},
|
|
1064
|
+
) =>
|
|
1065
|
+
this._request("POST", "/api/v1/texts", {
|
|
1066
|
+
auditMessage,
|
|
1067
|
+
body: bodyOf({
|
|
1068
|
+
id,
|
|
1069
|
+
"text-layer-id": textLayerId,
|
|
1070
|
+
"document-id": documentId,
|
|
1071
|
+
body,
|
|
1072
|
+
metadata,
|
|
1073
|
+
}),
|
|
570
1074
|
}),
|
|
571
1075
|
/**
|
|
572
1076
|
* Get a text.
|
|
573
1077
|
* @param {string} textId - The text ID
|
|
574
|
-
* @param {string} [asOf] - Temporal query timestamp
|
|
575
1078
|
*/
|
|
576
|
-
get: (textId
|
|
577
|
-
this._request(
|
|
578
|
-
queryParams: { 'as-of': asOf },
|
|
579
|
-
}),
|
|
1079
|
+
get: (textId) =>
|
|
1080
|
+
this._request("GET", `/api/v1/texts/${textId}`),
|
|
580
1081
|
/**
|
|
581
1082
|
* Delete a text and all dependent data.
|
|
582
1083
|
* @param {string} textId - The text ID
|
|
583
1084
|
*/
|
|
584
1085
|
delete: (textId, auditMessage) =>
|
|
585
|
-
this._request(
|
|
1086
|
+
this._request("DELETE", `/api/v1/texts/${textId}`, { auditMessage }),
|
|
586
1087
|
/**
|
|
587
1088
|
* Update a text's body. A diff is computed and token indices are updated
|
|
588
1089
|
* so that tokens remain intact. Alternatively, `body` can be a list of
|
|
589
|
-
* edit directives
|
|
1090
|
+
* edit directives (code-point indices, applied in order):
|
|
1091
|
+
* {type: 'delete', index, value: count}
|
|
1092
|
+
* {type: 'insert', index, value: string}
|
|
1093
|
+
* {type: 'replace', index, length, value: string} — unlike
|
|
1094
|
+
* delete+insert, a token covering the whole range is resized to keep
|
|
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).
|
|
590
1102
|
* @param {string} textId - The text ID
|
|
591
1103
|
* @param {any} body - The request body
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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]
|
|
1129
|
+
*/
|
|
1130
|
+
edit: (textId, edits, auditMessage, { base, versioned } = {}) =>
|
|
1131
|
+
this._request("PATCH", `/api/v1/texts/${textId}`, {
|
|
1132
|
+
auditMessage,
|
|
1133
|
+
body: bodyOf({ edits, base }),
|
|
1134
|
+
versioned: versioned ?? (base === undefined || base === null),
|
|
596
1135
|
}),
|
|
597
1136
|
};
|
|
598
1137
|
|
|
@@ -600,76 +1139,112 @@ class PlaidClient {
|
|
|
600
1139
|
/**
|
|
601
1140
|
* List (or search) users. Transparently follows pagination cursors and
|
|
602
1141
|
* returns the full flat array. Admin-or-maintainer only.
|
|
603
|
-
* Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
|
|
604
1142
|
* @param {object} [opts]
|
|
605
|
-
* @param {string} [opts.q] - Filter to
|
|
606
|
-
* @param {string} [opts.asOf] - Temporal query timestamp
|
|
1143
|
+
* @param {string} [opts.q] - Filter to users whose display name or email contains this text (case-insensitive)
|
|
607
1144
|
*/
|
|
608
|
-
list: ({ q
|
|
609
|
-
listAll(this,
|
|
1145
|
+
list: ({ q } = {}) =>
|
|
1146
|
+
listAll(this, "/api/v1/users", { query: { q } }),
|
|
610
1147
|
/**
|
|
611
1148
|
* Fetch a single page of users (optionally filtered by `q`).
|
|
612
1149
|
* @param {object} [opts]
|
|
613
|
-
* @param {string} [opts.q] - Filter to
|
|
1150
|
+
* @param {string} [opts.q] - Filter to users whose display name or email contains this text (case-insensitive)
|
|
614
1151
|
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
615
1152
|
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
616
|
-
* @param {string} [opts.asOf] - Temporal query timestamp
|
|
617
1153
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
618
1154
|
*/
|
|
619
|
-
listPage: ({ q, limit, cursor
|
|
620
|
-
listPage(this,
|
|
1155
|
+
listPage: ({ q, limit, cursor } = {}) =>
|
|
1156
|
+
listPage(this, "/api/v1/users", {
|
|
1157
|
+
limit,
|
|
1158
|
+
cursor,
|
|
1159
|
+
query: { q },
|
|
1160
|
+
}),
|
|
621
1161
|
/**
|
|
622
1162
|
* Async-iterate users page by page; yields each page's entries array.
|
|
623
1163
|
* @param {object} [opts]
|
|
624
|
-
* @param {string} [opts.q] - Filter to
|
|
1164
|
+
* @param {string} [opts.q] - Filter to users whose display name or email contains this text (case-insensitive)
|
|
625
1165
|
* @param {number} [opts.pageSize] - Per-request page size
|
|
626
|
-
* @param {string} [opts.asOf] - Temporal query timestamp
|
|
627
|
-
* Cannot be used inside a batch (auto-paginates across requests); throws on first iteration if called while batching — use listPage() for a single page in a batch.
|
|
628
1166
|
* @returns {AsyncGenerator<Array>}
|
|
629
1167
|
*/
|
|
630
|
-
iterPages: ({ q, pageSize
|
|
631
|
-
iterPages(this,
|
|
1168
|
+
iterPages: ({ q, pageSize } = {}) =>
|
|
1169
|
+
iterPages(this, "/api/v1/users", {
|
|
1170
|
+
pageSize,
|
|
1171
|
+
query: { q },
|
|
1172
|
+
}),
|
|
632
1173
|
/**
|
|
633
|
-
* Create a new user
|
|
634
|
-
* @param {string}
|
|
1174
|
+
* Create a new user.
|
|
1175
|
+
* @param {string} email - The account's email address. It becomes the
|
|
1176
|
+
* user's id and is what they log in with; it can never be changed.
|
|
635
1177
|
* @param {string} password - The password
|
|
636
1178
|
* @param {boolean} isAdmin - Whether the user is an admin
|
|
1179
|
+
* @param {string} [displayName] - How the user is shown in the UI.
|
|
1180
|
+
* Defaults to the local part of the email.
|
|
637
1181
|
*/
|
|
638
|
-
create: (
|
|
639
|
-
this._request(
|
|
640
|
-
|
|
1182
|
+
create: (email, password, isAdmin, displayName, auditMessage) =>
|
|
1183
|
+
this._request("POST", "/api/v1/users", {
|
|
1184
|
+
auditMessage,
|
|
1185
|
+
body: bodyOf({
|
|
1186
|
+
email,
|
|
1187
|
+
password,
|
|
1188
|
+
"is-admin": isAdmin,
|
|
1189
|
+
// Left out when not given (null too), so core gives the user the
|
|
1190
|
+
// local part of the email. Core refuses a null display name.
|
|
1191
|
+
"display-name": displayName ?? undefined,
|
|
1192
|
+
}),
|
|
641
1193
|
}),
|
|
642
1194
|
/**
|
|
643
1195
|
* Get audit log for a user's actions. Transparently follows pagination
|
|
644
1196
|
* cursors and returns the full flat array.
|
|
645
|
-
* Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
|
|
646
1197
|
* @param {string} userId - The user ID
|
|
647
1198
|
* @param {string} [startTime] - Start of time range
|
|
648
1199
|
* @param {string} [endTime] - End of time range
|
|
649
|
-
* @param {string} [asOf] - Temporal query timestamp
|
|
650
1200
|
* @param {string[]|string} [opTypes] - Only return operations of these
|
|
651
1201
|
* types, spelled as in an entry's `op/type` (e.g.
|
|
652
1202
|
* `['span-layer/create', 'span-layer/delete']`). An entry appears when
|
|
653
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
|
|
654
1206
|
*/
|
|
655
|
-
audit: (userId, startTime, endTime,
|
|
1207
|
+
audit: (userId, startTime, endTime, opTypes, kinds) =>
|
|
656
1208
|
listAll(this, `/api/v1/users/${userId}/audit`, {
|
|
657
1209
|
query: {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
1210
|
+
"start-time": startTime,
|
|
1211
|
+
"end-time": endTime,
|
|
1212
|
+
"op-types": opTypesParam(opTypes),
|
|
1213
|
+
kinds: kindsParam(kinds),
|
|
1214
|
+
},
|
|
1215
|
+
}),
|
|
1216
|
+
/**
|
|
1217
|
+
* One page of the same log, newest-first with `order: "desc"`. Use this
|
|
1218
|
+
* rather than audit() wherever the caller wants the recent end of a log
|
|
1219
|
+
* that may be long: audit() walks every page before it resolves.
|
|
1220
|
+
* @param {string} userId - The user ID
|
|
1221
|
+
* @param {object} [opts]
|
|
1222
|
+
* @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
|
|
1223
|
+
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
1224
|
+
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
1225
|
+
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
1226
|
+
*/
|
|
1227
|
+
auditPage: (
|
|
1228
|
+
userId,
|
|
1229
|
+
{ startTime, endTime, opTypes, kinds, order, limit, cursor } = {},
|
|
1230
|
+
) =>
|
|
1231
|
+
listPage(this, `/api/v1/users/${userId}/audit`, {
|
|
1232
|
+
limit,
|
|
1233
|
+
cursor,
|
|
1234
|
+
query: {
|
|
1235
|
+
"start-time": startTime,
|
|
1236
|
+
"end-time": endTime,
|
|
1237
|
+
"op-types": opTypesParam(opTypes),
|
|
1238
|
+
kinds: kindsParam(kinds),
|
|
1239
|
+
order,
|
|
662
1240
|
},
|
|
663
1241
|
}),
|
|
664
1242
|
/**
|
|
665
1243
|
* Get a user by ID
|
|
666
1244
|
* @param {string} id - The resource ID
|
|
667
|
-
* @param {string} [asOf] - Temporal query timestamp
|
|
668
1245
|
*/
|
|
669
|
-
get: (id
|
|
670
|
-
this._request(
|
|
671
|
-
queryParams: { 'as-of': asOf },
|
|
672
|
-
}),
|
|
1246
|
+
get: (id) =>
|
|
1247
|
+
this._request("GET", `/api/v1/users/${id}`),
|
|
673
1248
|
/**
|
|
674
1249
|
* Deactivate a user. Users are never hard-deleted: deactivation
|
|
675
1250
|
* rejects their logins and tokens, strips their project memberships
|
|
@@ -679,7 +1254,7 @@ class PlaidClient {
|
|
|
679
1254
|
* @param {string} id - The resource ID
|
|
680
1255
|
*/
|
|
681
1256
|
delete: (id, auditMessage) =>
|
|
682
|
-
this._request(
|
|
1257
|
+
this._request("DELETE", `/api/v1/users/${id}`, { auditMessage }),
|
|
683
1258
|
/**
|
|
684
1259
|
* Reactivate a deactivated user, restoring their ability to log in.
|
|
685
1260
|
* Project memberships, vocab maintainerships, and API tokens removed
|
|
@@ -687,19 +1262,27 @@ class PlaidClient {
|
|
|
687
1262
|
* @param {string} id - The resource ID
|
|
688
1263
|
*/
|
|
689
1264
|
activate: (id, auditMessage) =>
|
|
690
|
-
this._request(
|
|
1265
|
+
this._request("POST", `/api/v1/users/${id}/activate`, { auditMessage }),
|
|
691
1266
|
/**
|
|
692
|
-
* Modify a user. Admins may change the
|
|
693
|
-
* status of any user. All other users may only modify their own
|
|
694
|
-
* or password.
|
|
695
|
-
*
|
|
1267
|
+
* Modify a user. Admins may change the display name, password, and admin
|
|
1268
|
+
* status of any user. All other users may only modify their own display
|
|
1269
|
+
* name or password.
|
|
1270
|
+
*
|
|
1271
|
+
* A user's id is their email address and is fixed for the life of the
|
|
1272
|
+
* account — it is what they log in with, so nothing can change it.
|
|
1273
|
+
* @param {string} id - The resource ID (the user's email address)
|
|
696
1274
|
* @param {string} [password] - New password
|
|
697
|
-
* @param {string} [
|
|
1275
|
+
* @param {string} [displayName] - New display name
|
|
698
1276
|
* @param {boolean} [isAdmin] - New admin status
|
|
699
1277
|
*/
|
|
700
|
-
update: (id, password,
|
|
701
|
-
this._request(
|
|
702
|
-
|
|
1278
|
+
update: (id, password, displayName, isAdmin, auditMessage) =>
|
|
1279
|
+
this._request("PATCH", `/api/v1/users/${id}`, {
|
|
1280
|
+
auditMessage,
|
|
1281
|
+
body: bodyOf({
|
|
1282
|
+
password,
|
|
1283
|
+
"display-name": displayName,
|
|
1284
|
+
"is-admin": isAdmin,
|
|
1285
|
+
}),
|
|
703
1286
|
}),
|
|
704
1287
|
/**
|
|
705
1288
|
* Build a URL for a user's profile picture, suitable for use directly as
|
|
@@ -719,7 +1302,7 @@ class PlaidClient {
|
|
|
719
1302
|
avatarUrl: (id, avatarHash) => {
|
|
720
1303
|
if (avatarHash === null) return null;
|
|
721
1304
|
const params = new URLSearchParams({ token: this.token });
|
|
722
|
-
if (avatarHash) params.set(
|
|
1305
|
+
if (avatarHash) params.set("v", avatarHash);
|
|
723
1306
|
return `${this.baseUrl}/api/v1/users/${id}/avatar?${params}`;
|
|
724
1307
|
},
|
|
725
1308
|
/**
|
|
@@ -728,8 +1311,7 @@ class PlaidClient {
|
|
|
728
1311
|
* @param {string} id - The user ID
|
|
729
1312
|
*/
|
|
730
1313
|
getAvatar: (id) =>
|
|
731
|
-
this._request(
|
|
732
|
-
noBatch: true,
|
|
1314
|
+
this._request("GET", `/api/v1/users/${id}/avatar`, {
|
|
733
1315
|
binaryResponse: true,
|
|
734
1316
|
}),
|
|
735
1317
|
/**
|
|
@@ -742,19 +1324,137 @@ class PlaidClient {
|
|
|
742
1324
|
*/
|
|
743
1325
|
setAvatar: (id, file, auditMessage) => {
|
|
744
1326
|
const fd = new FormData();
|
|
745
|
-
fd.append(
|
|
746
|
-
return this._request(
|
|
747
|
-
|
|
1327
|
+
fd.append("file", file);
|
|
1328
|
+
return this._request("PUT", `/api/v1/users/${id}/avatar`, {
|
|
1329
|
+
auditMessage,
|
|
1330
|
+
body: fd,
|
|
1331
|
+
formData: true,
|
|
1332
|
+
noBatch: true,
|
|
748
1333
|
});
|
|
749
1334
|
},
|
|
750
1335
|
/**
|
|
751
1336
|
* Remove a profile picture. Your own, or anyone's if you are an admin.
|
|
752
1337
|
* @param {string} id - The user ID
|
|
753
1338
|
*/
|
|
1339
|
+
// No flag: the upload beside this one is multipart and cannot be
|
|
1340
|
+
// batched, but a DELETE carries no blob, so the batch transport takes
|
|
1341
|
+
// it. It is an ordinary audited write and queues like any other.
|
|
754
1342
|
deleteAvatar: (id, auditMessage) =>
|
|
755
|
-
this._request(
|
|
756
|
-
|
|
1343
|
+
this._request("DELETE", `/api/v1/users/${id}/avatar`, {
|
|
1344
|
+
auditMessage,
|
|
1345
|
+
}),
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
this.userData = {
|
|
1349
|
+
/**
|
|
1350
|
+
* List a user's private data entries ({key, updatedAt}, plus value when
|
|
1351
|
+
* includeValues), ordered by key. Owner or admin only. Transparently
|
|
1352
|
+
* follows pagination cursors and returns the full flat array.
|
|
1353
|
+
*
|
|
1354
|
+
* Narrow with `prefix` (the literal head of a key) and/or `pattern`, a
|
|
1355
|
+
* GLOB over the whole key (`*` any run, `?` one character) — the way to
|
|
1356
|
+
* ask for a key convention identified by a segment in the middle, e.g.
|
|
1357
|
+
* `igt:assistant:*:meta:*` for every conversation's sidebar entry across
|
|
1358
|
+
* every project without dragging down the transcripts beside them.
|
|
1359
|
+
*
|
|
1360
|
+
* `pageSize` is exposed here, and defaults lower than elsewhere, because
|
|
1361
|
+
* one value runs to 1 MB: a page of them with `includeValues` is the
|
|
1362
|
+
* largest response this API can be asked for. Raise it when the listing
|
|
1363
|
+
* is keys, or the values are known to be small.
|
|
1364
|
+
* @param {string} userId
|
|
1365
|
+
* @param {object} [opts]
|
|
1366
|
+
* @param {string} [opts.prefix] - Only keys starting with this prefix
|
|
1367
|
+
* @param {string} [opts.pattern] - Only keys matching this GLOB
|
|
1368
|
+
* @param {boolean} [opts.includeValues] - Also return each entry's value
|
|
1369
|
+
* @param {number} [opts.pageSize=100] - Entries per request (1..1000)
|
|
1370
|
+
*/
|
|
1371
|
+
list: (userId, { prefix, pattern, includeValues, pageSize = 100 } = {}) =>
|
|
1372
|
+
listAll(this, `/api/v1/users/${userId}/data`, {
|
|
1373
|
+
pageSize,
|
|
1374
|
+
query: { prefix, pattern, "include-values": includeValues },
|
|
1375
|
+
}),
|
|
1376
|
+
/**
|
|
1377
|
+
* One page of a user's private data entries, ordered by key.
|
|
1378
|
+
* @param {string} userId
|
|
1379
|
+
* @param {object} [opts]
|
|
1380
|
+
* @param {string} [opts.prefix] - Only keys starting with this prefix
|
|
1381
|
+
* @param {string} [opts.pattern] - Only keys matching this GLOB
|
|
1382
|
+
* @param {boolean} [opts.includeValues] - Also return each entry's value
|
|
1383
|
+
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
1384
|
+
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
1385
|
+
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
1386
|
+
*/
|
|
1387
|
+
listPage: (
|
|
1388
|
+
userId,
|
|
1389
|
+
{ prefix, pattern, includeValues, limit, cursor } = {},
|
|
1390
|
+
) =>
|
|
1391
|
+
listPage(this, `/api/v1/users/${userId}/data`, {
|
|
1392
|
+
limit,
|
|
1393
|
+
cursor,
|
|
1394
|
+
query: { prefix, pattern, "include-values": includeValues },
|
|
1395
|
+
}),
|
|
1396
|
+
/**
|
|
1397
|
+
* Async generator over a user's private data entries, one page at a time.
|
|
1398
|
+
* @param {string} userId
|
|
1399
|
+
* @param {object} [opts]
|
|
1400
|
+
* @param {string} [opts.prefix] - Only keys starting with this prefix
|
|
1401
|
+
* @param {string} [opts.pattern] - Only keys matching this GLOB
|
|
1402
|
+
* @param {boolean} [opts.includeValues] - Also return each entry's value
|
|
1403
|
+
* @param {number} [opts.pageSize=100] - Entries per request (1..1000)
|
|
1404
|
+
*/
|
|
1405
|
+
iterPages: (
|
|
1406
|
+
userId,
|
|
1407
|
+
{ prefix, pattern, includeValues, pageSize = 100 } = {},
|
|
1408
|
+
) =>
|
|
1409
|
+
iterPages(this, `/api/v1/users/${userId}/data`, {
|
|
1410
|
+
pageSize,
|
|
1411
|
+
query: { prefix, pattern, "include-values": includeValues },
|
|
757
1412
|
}),
|
|
1413
|
+
/**
|
|
1414
|
+
* Read one private data entry ({key, updatedAt, value}); 404 if absent.
|
|
1415
|
+
* @param {string} userId
|
|
1416
|
+
* @param {string} key
|
|
1417
|
+
*/
|
|
1418
|
+
get: (userId, key) =>
|
|
1419
|
+
this._request(
|
|
1420
|
+
"GET",
|
|
1421
|
+
`/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
|
|
1422
|
+
),
|
|
1423
|
+
/**
|
|
1424
|
+
* Create or replace one private data entry. `value` is any JSON (up to
|
|
1425
|
+
* 1 MB). Not audited, not batchable.
|
|
1426
|
+
*
|
|
1427
|
+
* The server stores it verbatim, but this client recases object keys on
|
|
1428
|
+
* the way out and back like any other body (`myKey` <-> `my-key`), so a
|
|
1429
|
+
* value whose keys are camelCase round-trips unchanged while one keyed
|
|
1430
|
+
* by arbitrary strings does not. Put such a map under a `metadata` key,
|
|
1431
|
+
* which both clients pass through untouched.
|
|
1432
|
+
* @param {string} userId
|
|
1433
|
+
* @param {string} key
|
|
1434
|
+
* @param {*} value
|
|
1435
|
+
*/
|
|
1436
|
+
put: (userId, key, value) =>
|
|
1437
|
+
this._request(
|
|
1438
|
+
"PUT",
|
|
1439
|
+
`/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
|
|
1440
|
+
{
|
|
1441
|
+
body: value,
|
|
1442
|
+
noBatch: true,
|
|
1443
|
+
},
|
|
1444
|
+
),
|
|
1445
|
+
/**
|
|
1446
|
+
* Delete one private data entry; 404 if absent.
|
|
1447
|
+
* @param {string} userId
|
|
1448
|
+
* @param {string} key
|
|
1449
|
+
*/
|
|
1450
|
+
delete: (userId, key) =>
|
|
1451
|
+
this._request(
|
|
1452
|
+
"DELETE",
|
|
1453
|
+
`/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
|
|
1454
|
+
{
|
|
1455
|
+
noBatch: true,
|
|
1456
|
+
},
|
|
1457
|
+
),
|
|
758
1458
|
};
|
|
759
1459
|
|
|
760
1460
|
this.apiTokens = {
|
|
@@ -762,11 +1462,9 @@ class PlaidClient {
|
|
|
762
1462
|
* List a user's named API tokens. Never includes the signed token
|
|
763
1463
|
* string itself — that is only returned once, by create().
|
|
764
1464
|
* Transparently follows pagination cursors and returns the full flat array.
|
|
765
|
-
* Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
|
|
766
1465
|
* @param {string} userId - The user ID who owns the tokens
|
|
767
1466
|
*/
|
|
768
|
-
list: (userId) =>
|
|
769
|
-
listAll(this, `/api/v1/users/${userId}/tokens`),
|
|
1467
|
+
list: (userId) => listAll(this, `/api/v1/users/${userId}/tokens`),
|
|
770
1468
|
/**
|
|
771
1469
|
* Fetch a single page of a user's named API tokens.
|
|
772
1470
|
* @param {string} userId - The user ID who owns the tokens
|
|
@@ -783,7 +1481,6 @@ class PlaidClient {
|
|
|
783
1481
|
* @param {string} userId - The user ID who owns the tokens
|
|
784
1482
|
* @param {object} [opts]
|
|
785
1483
|
* @param {number} [opts.pageSize] - Per-request page size
|
|
786
|
-
* Cannot be used inside a batch (auto-paginates across requests); throws on first iteration if called while batching — use listPage() for a single page in a batch.
|
|
787
1484
|
* @returns {AsyncGenerator<Array>}
|
|
788
1485
|
*/
|
|
789
1486
|
iterPages: (userId, { pageSize } = {}) =>
|
|
@@ -793,12 +1490,15 @@ class PlaidClient {
|
|
|
793
1490
|
* credential and is shown ONLY here — store it immediately. API tokens
|
|
794
1491
|
* do not expire and survive password changes / logout; revoke to kill.
|
|
795
1492
|
* @param {string} userId - The user ID who will own the token
|
|
796
|
-
* @param {string} name - A human label, e.g. "Stanza
|
|
1493
|
+
* @param {string} name - A human label, e.g. "Stanza parser"
|
|
797
1494
|
* @returns {Promise<{id: string, name: string, token: string}>}
|
|
798
1495
|
*/
|
|
799
1496
|
create: (userId, name, auditMessage) =>
|
|
800
|
-
this._request(
|
|
1497
|
+
this._request("POST", `/api/v1/users/${userId}/tokens`, {
|
|
1498
|
+
auditMessage,
|
|
801
1499
|
body: bodyOf({ name }),
|
|
1500
|
+
// The answer is the secret, which the server never keeps.
|
|
1501
|
+
noIdempotency: true,
|
|
802
1502
|
}),
|
|
803
1503
|
/**
|
|
804
1504
|
* Revoke a named API token (soft-revoke; idempotent).
|
|
@@ -806,7 +1506,9 @@ class PlaidClient {
|
|
|
806
1506
|
* @param {string} tokenId - The token ID to revoke
|
|
807
1507
|
*/
|
|
808
1508
|
revoke: (userId, tokenId, auditMessage) =>
|
|
809
|
-
this._request(
|
|
1509
|
+
this._request("DELETE", `/api/v1/users/${userId}/tokens/${tokenId}`, {
|
|
1510
|
+
auditMessage,
|
|
1511
|
+
}),
|
|
810
1512
|
};
|
|
811
1513
|
|
|
812
1514
|
this.invites = {
|
|
@@ -816,32 +1518,42 @@ class PlaidClient {
|
|
|
816
1518
|
* maintainer or admin on it. Never includes invite codes — the code is
|
|
817
1519
|
* returned once, by create(), and is not recoverable afterward.
|
|
818
1520
|
* Transparently follows pagination cursors and returns the full flat array.
|
|
819
|
-
* Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
|
|
820
1521
|
* @param {object} [opts]
|
|
821
1522
|
* @param {string} [opts.projectId] - List this project's invites instead of your own
|
|
1523
|
+
* @param {boolean} [opts.all] - List every invite on the server (admin only)
|
|
822
1524
|
*/
|
|
823
|
-
list: ({ projectId } = {}) =>
|
|
824
|
-
listAll(this,
|
|
1525
|
+
list: ({ projectId, all } = {}) =>
|
|
1526
|
+
listAll(this, "/api/v1/invites", {
|
|
1527
|
+
query: { "project-id": projectId, all },
|
|
1528
|
+
}),
|
|
825
1529
|
/**
|
|
826
1530
|
* Fetch a single page of invites.
|
|
827
1531
|
* @param {object} [opts]
|
|
828
1532
|
* @param {string} [opts.projectId] - List this project's invites instead of your own
|
|
1533
|
+
* @param {boolean} [opts.all] - List every invite on the server (admin only)
|
|
829
1534
|
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
830
1535
|
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
831
1536
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
832
1537
|
*/
|
|
833
|
-
listPage: ({ projectId, limit, cursor } = {}) =>
|
|
834
|
-
listPage(this,
|
|
1538
|
+
listPage: ({ projectId, all, limit, cursor } = {}) =>
|
|
1539
|
+
listPage(this, "/api/v1/invites", {
|
|
1540
|
+
limit,
|
|
1541
|
+
cursor,
|
|
1542
|
+
query: { "project-id": projectId, all },
|
|
1543
|
+
}),
|
|
835
1544
|
/**
|
|
836
1545
|
* Async-iterate invites page by page; yields each page's entries array.
|
|
837
1546
|
* @param {object} [opts]
|
|
838
1547
|
* @param {string} [opts.projectId] - List this project's invites instead of your own
|
|
1548
|
+
* @param {boolean} [opts.all] - List every invite on the server (admin only)
|
|
839
1549
|
* @param {number} [opts.pageSize] - Per-request page size
|
|
840
|
-
* Cannot be used inside a batch (auto-paginates across requests); throws on first iteration if called while batching — use listPage() for a single page in a batch.
|
|
841
1550
|
* @returns {AsyncGenerator<Array>}
|
|
842
1551
|
*/
|
|
843
|
-
iterPages: ({ projectId, pageSize } = {}) =>
|
|
844
|
-
iterPages(this,
|
|
1552
|
+
iterPages: ({ projectId, all, pageSize } = {}) =>
|
|
1553
|
+
iterPages(this, "/api/v1/invites", {
|
|
1554
|
+
pageSize,
|
|
1555
|
+
query: { "project-id": projectId, all },
|
|
1556
|
+
}),
|
|
845
1557
|
/**
|
|
846
1558
|
* Mint an invite. The returned `code` is shown ONLY here — it is never
|
|
847
1559
|
* stored and cannot be recovered, so build and hand off the link now.
|
|
@@ -849,23 +1561,48 @@ class PlaidClient {
|
|
|
849
1561
|
*
|
|
850
1562
|
* Admins may mint anything. A project maintainer may mint role grants on
|
|
851
1563
|
* projects they maintain, and nothing else: no admin grant, no grantless
|
|
852
|
-
* invite, no password resets
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
1564
|
+
* invite, no password resets — so for a non-admin, projectId/projectRole
|
|
1565
|
+
* are required in practice (403 without them).
|
|
1566
|
+
*
|
|
1567
|
+
* EVERY option is optional; `create()` with no arguments mints a
|
|
1568
|
+
* single-use signup link granting nothing but an account. Two pairing
|
|
1569
|
+
* rules the server enforces with a 400: projectId and projectRole must be
|
|
1570
|
+
* given TOGETHER, and targetUserId may not be combined with projectId,
|
|
1571
|
+
* grantAdmin, or a maxUses above 1.
|
|
1572
|
+
* @param {object} [opts]
|
|
1573
|
+
* @param {string} [opts.projectId] - Project the redeemer joins (requires projectRole)
|
|
1574
|
+
* @param {string} [opts.projectRole] - "reader" | "writer" | "maintainer" (requires projectId)
|
|
856
1575
|
* @param {boolean} [opts.grantAdmin] - Make the new account a global admin (admin only)
|
|
857
|
-
* @param {string} [opts.targetUserId] -
|
|
1576
|
+
* @param {string} [opts.targetUserId] - Password reset for that user instead of a signup; admin only, single-use, grants nothing
|
|
858
1577
|
* @param {number} [opts.maxUses] - How many accounts this link may create (default 1)
|
|
859
1578
|
* @param {number} [opts.ttlDays] - Days until it expires (default 14, max 365)
|
|
860
1579
|
* @param {string} [opts.note] - Human label shown in your invite list
|
|
861
1580
|
* @returns {Promise<{id: string, code: string, kind: string, status: string}>}
|
|
862
1581
|
*/
|
|
863
|
-
create: (
|
|
864
|
-
|
|
1582
|
+
create: (
|
|
1583
|
+
{
|
|
1584
|
+
projectId,
|
|
1585
|
+
projectRole,
|
|
1586
|
+
grantAdmin,
|
|
1587
|
+
targetUserId,
|
|
1588
|
+
maxUses,
|
|
1589
|
+
ttlDays,
|
|
1590
|
+
note,
|
|
1591
|
+
} = {},
|
|
1592
|
+
auditMessage,
|
|
1593
|
+
) =>
|
|
1594
|
+
this._request("POST", "/api/v1/invites", {
|
|
1595
|
+
auditMessage,
|
|
1596
|
+
// The answer is the code, which the server never keeps.
|
|
1597
|
+
noIdempotency: true,
|
|
865
1598
|
body: bodyOf({
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1599
|
+
"project-id": projectId,
|
|
1600
|
+
"project-role": projectRole,
|
|
1601
|
+
"grant-admin": grantAdmin,
|
|
1602
|
+
"target-user-id": targetUserId,
|
|
1603
|
+
"max-uses": maxUses,
|
|
1604
|
+
"ttl-days": ttlDays,
|
|
1605
|
+
note,
|
|
869
1606
|
}),
|
|
870
1607
|
}),
|
|
871
1608
|
/**
|
|
@@ -874,129 +1611,482 @@ class PlaidClient {
|
|
|
874
1611
|
* @param {string} id - The invite ID
|
|
875
1612
|
*/
|
|
876
1613
|
revoke: (id, auditMessage) =>
|
|
877
|
-
this._request(
|
|
1614
|
+
this._request("DELETE", `/api/v1/invites/${id}`, { auditMessage }),
|
|
878
1615
|
};
|
|
879
1616
|
|
|
880
|
-
this.
|
|
1617
|
+
this.admin = {
|
|
1618
|
+
/**
|
|
1619
|
+
* Everything about the server in one read: version and JVM uptime,
|
|
1620
|
+
* database size and per-table row counts, media directory usage, backup
|
|
1621
|
+
* configuration and the backups on disk, and the settings an operator
|
|
1622
|
+
* gets asked to confirm. Carries no secrets. Runs a count per table and
|
|
1623
|
+
* walks the media directory, so open it, do not poll it. Admin only.
|
|
1624
|
+
* @returns {Promise<{version: string, jvm: object, database: object, media: object, backup: object, settings: object}>}
|
|
1625
|
+
*/
|
|
1626
|
+
server: () => this._request("GET", "/api/v1/admin/server"),
|
|
1627
|
+
/**
|
|
1628
|
+
* Take a database backup right now, outside the nightly schedule.
|
|
1629
|
+
* Resolves to the backup block with `ok` reporting whether the snapshot
|
|
1630
|
+
* succeeded. Uses VACUUM INTO, which only reads, so it is safe while
|
|
1631
|
+
* people are working. Admin only.
|
|
1632
|
+
* @returns {Promise<{ok: boolean, directory: string, backups: Array}>}
|
|
1633
|
+
*
|
|
1634
|
+
* outOfBand, as every admin action on the server itself is: none of them
|
|
1635
|
+
* writes project data (see the note at the top of http.js).
|
|
1636
|
+
*/
|
|
1637
|
+
backup: () =>
|
|
1638
|
+
this._request("POST", "/api/v1/admin/backup", { outOfBand: true }),
|
|
881
1639
|
/**
|
|
882
|
-
*
|
|
883
|
-
*
|
|
884
|
-
* @
|
|
1640
|
+
* Documents currently held by an editing lock, with who holds each and
|
|
1641
|
+
* when it expires on its own. Admin only.
|
|
1642
|
+
* @returns {Promise<{entries: Array<{documentId: string, userId: string, expiresAt: number}>}>}
|
|
885
1643
|
*/
|
|
886
|
-
|
|
887
|
-
this._request('POST', `/api/v1/token-layers/${tokenLayerId}/shift`, { auditMessage,
|
|
888
|
-
body: bodyOf({ direction }),
|
|
889
|
-
}),
|
|
1644
|
+
locks: () => this._request("GET", "/api/v1/admin/locks"),
|
|
890
1645
|
/**
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
* @param {string}
|
|
894
|
-
* @param {string} [overlapMode] - Per-layer, immutable token invariant: "any" (default), "non-overlapping", or "partitioning". On partitioning layers, single token create/update/delete are rejected; use bulkCreate plus split/merge/shift.
|
|
895
|
-
* @param {string} [parentTokenLayerId] - Optional immutable parent token layer. Tokens in this layer must nest within a parent-layer token; the parent layer must be in the same text layer and be "non-overlapping" or "partitioning" (an "any" parent is rejected). A nested layer may be "any" or "non-overlapping" but not "partitioning" (partitioning is only for root layers), e.g. words (non-overlapping, parent=sentences) within sentences (partitioning).
|
|
1646
|
+
* Drop the lock on a document whoever holds it. Idempotent. For a client
|
|
1647
|
+
* that went away without releasing one. Admin only.
|
|
1648
|
+
* @param {string} documentId - The document ID
|
|
896
1649
|
*/
|
|
897
|
-
|
|
898
|
-
this._request(
|
|
899
|
-
|
|
1650
|
+
releaseLock: (documentId) =>
|
|
1651
|
+
this._request("DELETE", `/api/v1/admin/locks/${documentId}`, {
|
|
1652
|
+
outOfBand: true,
|
|
900
1653
|
}),
|
|
901
1654
|
/**
|
|
902
|
-
*
|
|
903
|
-
*
|
|
904
|
-
*
|
|
905
|
-
* @
|
|
906
|
-
* @param {any} configValue - Configuration value to set
|
|
1655
|
+
* Live login and invite rate-limit buckets: the address, the account
|
|
1656
|
+
* where there is one, failures inside the window, the limit, and whether
|
|
1657
|
+
* it is currently blocking. Admin only.
|
|
1658
|
+
* @returns {Promise<{windowMs: number, logins: Array, ips: Array, invites: Array}>}
|
|
907
1659
|
*/
|
|
908
|
-
|
|
909
|
-
this._request('PUT', `/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`, { auditMessage,
|
|
910
|
-
rawBody: configValue, skipResponseTransform: true,
|
|
911
|
-
}),
|
|
1660
|
+
rateLimits: () => this._request("GET", "/api/v1/admin/rate-limits"),
|
|
912
1661
|
/**
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
* @param {
|
|
1662
|
+
* Forget recorded rate-limit failures. With `ip`, clears that address,
|
|
1663
|
+
* narrowed to one account with `userId`. With neither, clears every
|
|
1664
|
+
* bucket. Only ever unblocks. Admin only.
|
|
1665
|
+
* @param {object} [opts]
|
|
1666
|
+
* @param {string} [opts.ip] - Clear only this address
|
|
1667
|
+
* @param {string} [opts.userId] - Narrow to one account on that address
|
|
917
1668
|
*/
|
|
918
|
-
|
|
919
|
-
this._request(
|
|
920
|
-
|
|
1669
|
+
clearRateLimits: ({ ip, userId } = {}) =>
|
|
1670
|
+
this._request("DELETE", "/api/v1/admin/rate-limits", {
|
|
1671
|
+
outOfBand: true,
|
|
1672
|
+
queryParams: { ip, "user-id": userId },
|
|
921
1673
|
}),
|
|
922
1674
|
/**
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
*
|
|
1675
|
+
* What the server has logged, structured and filtered, from an in-memory
|
|
1676
|
+
* buffer kept whether or not a log file is configured. `requests` is one
|
|
1677
|
+
* entry per HTTP request (method, path, status, duration, and who made
|
|
1678
|
+
* it), `events` is everything else, with a stack trace where there was
|
|
1679
|
+
* one. They are buffered separately so a burst of requests cannot evict
|
|
1680
|
+
* an error. Both come newest first, `matched` counts what passed the
|
|
1681
|
+
* filters, and request `stats` describe the filtered set. Covers Plaid's
|
|
1682
|
+
* own log stream since the last restart: for library messages and older
|
|
1683
|
+
* history, see `logFile`. Admin only.
|
|
1684
|
+
* @param {object} [opts]
|
|
1685
|
+
* @param {number} [opts.limit] - Max entries per kind (default 200, max 2000)
|
|
1686
|
+
* @param {string} [opts.q] - Substring of any field, case-insensitive
|
|
1687
|
+
* @param {string} [opts.level] - Minimum level for events, e.g. "warn"
|
|
1688
|
+
* @param {string} [opts.status] - "2xx".."5xx", an exact code, or "failures"
|
|
1689
|
+
* @param {string} [opts.user] - Only requests made by this account
|
|
1690
|
+
* @param {string} [opts.method] - Only requests with this HTTP method
|
|
1691
|
+
* @returns {Promise<{requests: object, events: object, file: (string|null)}>}
|
|
1692
|
+
*/
|
|
1693
|
+
logs: ({ limit, q, level, status, user, method } = {}) =>
|
|
1694
|
+
this._request("GET", "/api/v1/admin/logs", {
|
|
1695
|
+
queryParams: { limit, q, level, status, user, method },
|
|
1696
|
+
}),
|
|
1697
|
+
/**
|
|
1698
|
+
* The tail of the configured log file, as text lines. The only place
|
|
1699
|
+
* third-party library messages and anything from before the last restart
|
|
1700
|
+
* can be read. Resolves with an `error` string instead of lines when no
|
|
1701
|
+
* log file is configured or it does not exist yet. Admin only.
|
|
1702
|
+
* @param {object} [opts]
|
|
1703
|
+
* @param {number} [opts.lines] - How many lines (default 200, max 2000)
|
|
1704
|
+
* @returns {Promise<{file: (string|null), lines: string[], error?: string}>}
|
|
926
1705
|
*/
|
|
927
|
-
|
|
928
|
-
this._request(
|
|
929
|
-
queryParams: {
|
|
1706
|
+
logFile: ({ lines } = {}) =>
|
|
1707
|
+
this._request("GET", "/api/v1/admin/logs/file", {
|
|
1708
|
+
queryParams: { lines },
|
|
930
1709
|
}),
|
|
931
1710
|
/**
|
|
932
|
-
*
|
|
933
|
-
*
|
|
1711
|
+
* Private user-data entries across every account, each carrying its
|
|
1712
|
+
* `userId`. `/users/:userId/data` has always been owner-or-admin; this
|
|
1713
|
+
* is the same reach across accounts at once. Transparently follows
|
|
1714
|
+
* pagination cursors and returns the full flat array. Admin only.
|
|
1715
|
+
*
|
|
1716
|
+
* Narrow with `prefix` (the literal head of a key) and/or `pattern`, a
|
|
1717
|
+
* GLOB over the whole key (`*` any run, `?` one character) — the way to
|
|
1718
|
+
* ask for a key convention identified by a segment in the middle, e.g.
|
|
1719
|
+
* `igt:assistant:*:meta:*`. Values come only with `includeValues`, and
|
|
1720
|
+
* are recased like any other body (see `userData.put`).
|
|
1721
|
+
* @param {object} [opts]
|
|
1722
|
+
* @param {string} [opts.prefix] - Only keys starting with this
|
|
1723
|
+
* @param {string} [opts.pattern] - Only keys matching this GLOB
|
|
1724
|
+
* @param {boolean} [opts.includeValues] - Also return each entry's value
|
|
1725
|
+
* @returns {Promise<Array<{userId: string, key: string, updatedAt: string, value?: *}>>}
|
|
934
1726
|
*/
|
|
935
|
-
|
|
936
|
-
this
|
|
1727
|
+
userData: ({ prefix, pattern, includeValues } = {}) =>
|
|
1728
|
+
listAll(this, "/api/v1/admin/user-data", {
|
|
1729
|
+
query: {
|
|
1730
|
+
prefix,
|
|
1731
|
+
pattern,
|
|
1732
|
+
"include-values": includeValues,
|
|
1733
|
+
},
|
|
1734
|
+
}),
|
|
937
1735
|
/**
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
* @param {
|
|
1736
|
+
* One page of private user-data entries across accounts, ordered by
|
|
1737
|
+
* (user, key). Admin only.
|
|
1738
|
+
* @param {object} [opts]
|
|
1739
|
+
* @param {string} [opts.prefix] - Only keys starting with this
|
|
1740
|
+
* @param {string} [opts.pattern] - Only keys matching this GLOB
|
|
1741
|
+
* @param {boolean} [opts.includeValues] - Also return each entry's value
|
|
1742
|
+
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
1743
|
+
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
1744
|
+
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
941
1745
|
*/
|
|
942
|
-
|
|
943
|
-
this
|
|
944
|
-
|
|
1746
|
+
userDataPage: ({ prefix, pattern, includeValues, limit, cursor } = {}) =>
|
|
1747
|
+
listPage(this, "/api/v1/admin/user-data", {
|
|
1748
|
+
limit,
|
|
1749
|
+
cursor,
|
|
1750
|
+
query: {
|
|
1751
|
+
prefix,
|
|
1752
|
+
pattern,
|
|
1753
|
+
"include-values": includeValues,
|
|
1754
|
+
},
|
|
945
1755
|
}),
|
|
946
1756
|
};
|
|
947
1757
|
|
|
948
|
-
this.
|
|
1758
|
+
this.audit = {
|
|
949
1759
|
/**
|
|
950
|
-
*
|
|
951
|
-
*
|
|
952
|
-
*
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1760
|
+
* The audit log across every project, oldest first. Same fold, window
|
|
1761
|
+
* and op-type filter as the per-project read, with the entity scope
|
|
1762
|
+
* dropped. Admin only. Transparently follows pagination cursors and
|
|
1763
|
+
* returns the full flat array.
|
|
1764
|
+
* @param {object} [opts]
|
|
1765
|
+
* @param {string} [opts.startTime] - Only operations at or after this instant
|
|
1766
|
+
* @param {string} [opts.endTime] - Only operations at or before this instant
|
|
1767
|
+
* @param {string[]|string} [opts.opTypes] - Only these op types (e.g.
|
|
1768
|
+
* `['span-layer/create']`). An entry appears when one of its operations
|
|
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
|
|
1772
|
+
*/
|
|
1773
|
+
list: ({ startTime, endTime, opTypes, kinds } = {}) =>
|
|
1774
|
+
listAll(this, "/api/v1/audit", {
|
|
1775
|
+
query: {
|
|
1776
|
+
"start-time": startTime,
|
|
1777
|
+
"end-time": endTime,
|
|
1778
|
+
"op-types": opTypesParam(opTypes),
|
|
1779
|
+
kinds: kindsParam(kinds),
|
|
1780
|
+
},
|
|
957
1781
|
}),
|
|
958
1782
|
/**
|
|
959
|
-
*
|
|
960
|
-
* @param {
|
|
1783
|
+
* Fetch a single page of the instance-wide audit log. Admin only.
|
|
1784
|
+
* @param {object} [opts]
|
|
1785
|
+
* @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first, which
|
|
1786
|
+
* is what a feed wants. A cursor belongs to the direction that made it.
|
|
1787
|
+
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
1788
|
+
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
1789
|
+
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
961
1790
|
*/
|
|
962
|
-
|
|
963
|
-
this
|
|
1791
|
+
listPage: ({ startTime, endTime, opTypes, kinds, order, limit, cursor } = {}) =>
|
|
1792
|
+
listPage(this, "/api/v1/audit", {
|
|
1793
|
+
limit,
|
|
1794
|
+
cursor,
|
|
1795
|
+
query: {
|
|
1796
|
+
"start-time": startTime,
|
|
1797
|
+
"end-time": endTime,
|
|
1798
|
+
"op-types": opTypesParam(opTypes),
|
|
1799
|
+
kinds: kindsParam(kinds),
|
|
1800
|
+
order,
|
|
1801
|
+
},
|
|
1802
|
+
}),
|
|
964
1803
|
/**
|
|
965
|
-
*
|
|
966
|
-
* @
|
|
1804
|
+
* Async-iterate the instance-wide audit log page by page. Admin only.
|
|
1805
|
+
* @returns {AsyncGenerator<Array>}
|
|
1806
|
+
*/
|
|
1807
|
+
iterPages: ({ startTime, endTime, opTypes, kinds, pageSize } = {}) =>
|
|
1808
|
+
iterPages(this, "/api/v1/audit", {
|
|
1809
|
+
pageSize,
|
|
1810
|
+
query: {
|
|
1811
|
+
"start-time": startTime,
|
|
1812
|
+
"end-time": endTime,
|
|
1813
|
+
"op-types": opTypesParam(opTypes),
|
|
1814
|
+
kinds: kindsParam(kinds),
|
|
1815
|
+
},
|
|
1816
|
+
}),
|
|
1817
|
+
/**
|
|
1818
|
+
* Per-user activity counts. `changes` is the number of logical actions,
|
|
1819
|
+
* folded the way the audit feed folds them, so one "Confirm word
|
|
1820
|
+
* analysis" counts once however many rows it wrote; `operations` is the
|
|
1821
|
+
* unfolded row count. Only users who did something appear — subtract
|
|
1822
|
+
* from the roster you already hold to find the ones who did not.
|
|
1823
|
+
*
|
|
1824
|
+
* With `projectId`, scoped to that project and open to its maintainers.
|
|
1825
|
+
* Without one, instance-wide and admin only.
|
|
1826
|
+
* @param {object} [opts]
|
|
1827
|
+
* @param {string} [opts.projectId] - Scope to one project
|
|
1828
|
+
* @param {string} [opts.startTime] - Only count at or after this instant
|
|
1829
|
+
* @param {string} [opts.endTime] - Only count at or before this instant
|
|
1830
|
+
* @param {boolean} [opts.daily] - Also return `byDay`, a list of
|
|
1831
|
+
* `{date, changes}` per user, oldest first, at the cost of a second
|
|
1832
|
+
* grouped scan
|
|
1833
|
+
* @returns {Promise<Array<{user: object, operations: number, changes: number, documents: number, firstTs: string, lastTs: string, byDay?: object}>>}
|
|
1834
|
+
*/
|
|
1835
|
+
tally: async ({ projectId, startTime, endTime, daily } = {}) => {
|
|
1836
|
+
const path = projectId
|
|
1837
|
+
? `/api/v1/projects/${projectId}/audit/tally`
|
|
1838
|
+
: "/api/v1/audit/tally";
|
|
1839
|
+
const result = await this._request("GET", path, {
|
|
1840
|
+
queryParams: {
|
|
1841
|
+
"start-time": startTime,
|
|
1842
|
+
"end-time": endTime,
|
|
1843
|
+
daily,
|
|
1844
|
+
},
|
|
1845
|
+
});
|
|
1846
|
+
return result.entries;
|
|
1847
|
+
},
|
|
1848
|
+
};
|
|
1849
|
+
|
|
1850
|
+
this.tokenLayers = {
|
|
1851
|
+
/**
|
|
1852
|
+
* Shift a token layer's display order.
|
|
1853
|
+
* @param {string} tokenLayerId - The token layer ID
|
|
1854
|
+
* @param {string} direction - The direction ("up" or "down")
|
|
1855
|
+
*/
|
|
1856
|
+
shift: (tokenLayerId, direction, auditMessage) =>
|
|
1857
|
+
this._request("POST", `/api/v1/token-layers/${tokenLayerId}/shift`, {
|
|
1858
|
+
auditMessage,
|
|
1859
|
+
body: bodyOf({ direction }),
|
|
1860
|
+
}),
|
|
1861
|
+
/**
|
|
1862
|
+
* Create a new token layer.
|
|
1863
|
+
* @param {string} textLayerId - The text layer ID
|
|
1864
|
+
* @param {string} name - The name
|
|
1865
|
+
* @param {string} [overlapMode] - Per-layer, immutable token invariant: "any" (default), "non-overlapping", or "partitioning". On partitioning layers, single token create/update/delete are rejected; use bulkCreate plus split/merge/shift.
|
|
1866
|
+
* @param {string} [parentTokenLayerId] - Optional immutable parent token layer. Tokens in this layer must nest within a parent-layer token; the parent layer must be in the same text layer and be "non-overlapping" or "partitioning" (an "any" parent is rejected). A nested layer may be "any" or "non-overlapping" but not "partitioning" (partitioning is only for root layers), e.g. words (non-overlapping, parent=sentences) within sentences (partitioning).
|
|
1867
|
+
*/
|
|
1868
|
+
create: (
|
|
1869
|
+
textLayerId,
|
|
1870
|
+
name,
|
|
1871
|
+
overlapMode,
|
|
1872
|
+
parentTokenLayerId,
|
|
1873
|
+
auditMessage,
|
|
1874
|
+
{ id } = {},
|
|
1875
|
+
) =>
|
|
1876
|
+
this._request("POST", "/api/v1/token-layers", {
|
|
1877
|
+
auditMessage,
|
|
1878
|
+
body: bodyOf({
|
|
1879
|
+
id,
|
|
1880
|
+
"text-layer-id": textLayerId,
|
|
1881
|
+
name,
|
|
1882
|
+
"overlap-mode": overlapMode,
|
|
1883
|
+
"parent-token-layer-id": parentTokenLayerId,
|
|
1884
|
+
}),
|
|
1885
|
+
}),
|
|
1886
|
+
/**
|
|
1887
|
+
* Set a configuration value for a layer in an editor namespace.
|
|
1888
|
+
* @param {string} tokenLayerId - The token layer ID
|
|
1889
|
+
* @param {string} namespace - The config namespace
|
|
1890
|
+
* @param {string} configKey - The config key
|
|
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.
|
|
1895
|
+
*/
|
|
1896
|
+
setConfig: (tokenLayerId, namespace, configKey, configValue, auditMessage, options) =>
|
|
1897
|
+
this._request(
|
|
1898
|
+
"PUT",
|
|
1899
|
+
`/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`,
|
|
1900
|
+
configRequest(auditMessage, configValue, options),
|
|
1901
|
+
),
|
|
1902
|
+
/**
|
|
1903
|
+
* Remove a configuration value for a layer.
|
|
1904
|
+
* @param {string} tokenLayerId - The token layer ID
|
|
1905
|
+
* @param {string} namespace - The config namespace
|
|
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.
|
|
1909
|
+
*/
|
|
1910
|
+
deleteConfig: (tokenLayerId, namespace, configKey, auditMessage, options) =>
|
|
1911
|
+
this._request(
|
|
1912
|
+
"DELETE",
|
|
1913
|
+
`/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`,
|
|
1914
|
+
configRequest(auditMessage, undefined, options, true),
|
|
1915
|
+
),
|
|
1916
|
+
...constraintMethods(this, "token-layers"),
|
|
1917
|
+
/**
|
|
1918
|
+
* Get a token layer by ID.
|
|
1919
|
+
* @param {string} tokenLayerId - The token layer ID
|
|
1920
|
+
*/
|
|
1921
|
+
get: (tokenLayerId) =>
|
|
1922
|
+
this._request("GET", `/api/v1/token-layers/${tokenLayerId}`),
|
|
1923
|
+
/**
|
|
1924
|
+
* Delete a token layer.
|
|
1925
|
+
* @param {string} tokenLayerId - The token layer ID
|
|
1926
|
+
*/
|
|
1927
|
+
delete: (tokenLayerId, auditMessage) =>
|
|
1928
|
+
this._request("DELETE", `/api/v1/token-layers/${tokenLayerId}`, {
|
|
1929
|
+
auditMessage,
|
|
1930
|
+
}),
|
|
1931
|
+
/**
|
|
1932
|
+
* Update a token layer's name.
|
|
1933
|
+
* @param {string} tokenLayerId - The token layer ID
|
|
1934
|
+
* @param {string} name - The name
|
|
1935
|
+
*/
|
|
1936
|
+
update: (tokenLayerId, name, auditMessage) =>
|
|
1937
|
+
this._request("PATCH", `/api/v1/token-layers/${tokenLayerId}`, {
|
|
1938
|
+
auditMessage,
|
|
1939
|
+
body: bodyOf({ name }),
|
|
1940
|
+
}),
|
|
1941
|
+
};
|
|
1942
|
+
|
|
1943
|
+
this.documents = {
|
|
1944
|
+
/**
|
|
1945
|
+
* Check the lock status of a document.
|
|
1946
|
+
* @param {string} documentId - The document ID
|
|
967
1947
|
*/
|
|
968
|
-
|
|
969
|
-
this._request(
|
|
1948
|
+
checkLock: (documentId) =>
|
|
1949
|
+
this._request("GET", `/api/v1/documents/${documentId}/lock`),
|
|
970
1950
|
/**
|
|
971
|
-
*
|
|
1951
|
+
* Acquire a document lock as a new holder.
|
|
1952
|
+
*
|
|
1953
|
+
* The answer's `lockId` names the holder: `renewLock` and `releaseLock`
|
|
1954
|
+
* take it. While the lock is held, a second acquire is refused with
|
|
1955
|
+
* HTTP 423, whoever makes it, this user included.
|
|
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
|
+
*
|
|
1963
|
+
* outOfBand: the lock is a signal, not project data (see the note at
|
|
1964
|
+
* the top of http.js). Queued on a batch it would be taken only at
|
|
1965
|
+
* submit, after every write it was meant to guard, and until then answer
|
|
1966
|
+
* success to a caller that does not hold it and cannot see the 423
|
|
1967
|
+
* saying somebody else does.
|
|
972
1968
|
* @param {string} documentId - The document ID
|
|
973
|
-
* @param {string} [
|
|
1969
|
+
* @param {string} [auditMessage]
|
|
1970
|
+
* @param {string} [newLockId] - A holder id this client minted
|
|
1971
|
+
*/
|
|
1972
|
+
acquireLock: (documentId, auditMessage, newLockId) =>
|
|
1973
|
+
this._request("POST", `/api/v1/documents/${documentId}/lock`, {
|
|
1974
|
+
...(newLockId !== undefined && newLockId !== null
|
|
1975
|
+
? { queryParams: { "new-lock-id": newLockId } }
|
|
1976
|
+
: {}),
|
|
1977
|
+
auditMessage,
|
|
1978
|
+
outOfBand: true,
|
|
1979
|
+
}),
|
|
1980
|
+
/**
|
|
1981
|
+
* Renew the lock `lockId` holds while it is live. HTTP 423 if another
|
|
1982
|
+
* holder has it, and also once it has expired or been dropped, even when
|
|
1983
|
+
* nobody holds the document now: a renewal never takes a free document.
|
|
1984
|
+
* outOfBand, for the same reason as acquireLock.
|
|
1985
|
+
* @param {string} documentId - The document ID
|
|
1986
|
+
* @param {string} lockId - The `lockId` acquireLock answered with
|
|
974
1987
|
*/
|
|
975
|
-
|
|
976
|
-
this._request(
|
|
977
|
-
queryParams: {
|
|
978
|
-
|
|
1988
|
+
renewLock: (documentId, lockId, auditMessage) =>
|
|
1989
|
+
this._request("POST", `/api/v1/documents/${documentId}/lock`, {
|
|
1990
|
+
queryParams: { "lock-id": lockId },
|
|
1991
|
+
auditMessage,
|
|
1992
|
+
outOfBand: true,
|
|
1993
|
+
}),
|
|
1994
|
+
/**
|
|
1995
|
+
* Release the lock `lockId` holds. Idempotent: a lock that holder no
|
|
1996
|
+
* longer has is left alone. outOfBand, for the same reason as
|
|
1997
|
+
* acquireLock: queued, the lock would be held until the batch submits,
|
|
1998
|
+
* and not released at all if it aborts.
|
|
1999
|
+
* @param {string} documentId - The document ID
|
|
2000
|
+
* @param {string} lockId - The `lockId` acquireLock answered with
|
|
2001
|
+
*/
|
|
2002
|
+
releaseLock: (documentId, lockId, auditMessage) =>
|
|
2003
|
+
this._request("DELETE", `/api/v1/documents/${documentId}/lock`, {
|
|
2004
|
+
queryParams: { "lock-id": lockId },
|
|
2005
|
+
auditMessage,
|
|
2006
|
+
outOfBand: true,
|
|
2007
|
+
}),
|
|
2008
|
+
/**
|
|
2009
|
+
* Hold this document's server-enforced lock for the length of `fn`,
|
|
2010
|
+
* releasing it on the way out (including on error). Resolves to what
|
|
2011
|
+
* `fn` returned.
|
|
2012
|
+
*
|
|
2013
|
+
* await client.documents.locked(docId, async () => {
|
|
2014
|
+
* // delete + recreate tokens
|
|
2015
|
+
* });
|
|
2016
|
+
*
|
|
2017
|
+
* Wrap any multi-step, server-side mutation of a document that must not
|
|
2018
|
+
* interleave with a human editor or another service, e.g. a parser or
|
|
2019
|
+
* tokenizer that deletes and recreates a document's tokens, spans and
|
|
2020
|
+
* relations. A single atomic call does not need it. While the lock is
|
|
2021
|
+
* held, writes to the document by ANOTHER user are refused with HTTP
|
|
2022
|
+
* 423; the holder's own writes pass and renew it. If anyone already
|
|
2023
|
+
* holds it, another block of this same user included, this rejects with
|
|
2024
|
+
* a readable 423 and `fn` does not run.
|
|
2025
|
+
*
|
|
2026
|
+
* The lock is renewed for as long as `fn` runs, so work that computes
|
|
2027
|
+
* for minutes before it writes holds the lock the whole time rather than
|
|
2028
|
+
* only for its first minute. If a renewal fails the lock is gone: every
|
|
2029
|
+
* later write from this client throws `DocumentLockLost`, and a block
|
|
2030
|
+
* that got to the end anyway ends with that error rather than reporting
|
|
2031
|
+
* success. `fn` receives a lock handle and may read `lock.lost` (or call
|
|
2032
|
+
* `lock.raiseIfLost()`) to give up sooner.
|
|
2033
|
+
*
|
|
2034
|
+
* The lock is per HOLDER: each block is its own, named by
|
|
2035
|
+
* `lock.lockId`, and only that id renews or releases it. NOT
|
|
2036
|
+
* re-entrant: a nested `locked()` block on one document is a second
|
|
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
|
|
2041
|
+
* lost lock is recorded on the CLIENT, like strict mode, so it stops
|
|
2042
|
+
* every write the client makes (on any batch of it too) and not only the
|
|
2043
|
+
* ones this block makes.
|
|
2044
|
+
* @param {string} documentId - The document ID
|
|
2045
|
+
* @param {(lock: DocumentLock) => any} fn - The work to run while holding it
|
|
2046
|
+
* @param {object} [options] - `{ keepAlive }`: renew on a timer (default true)
|
|
2047
|
+
*/
|
|
2048
|
+
locked: (documentId, fn, options) =>
|
|
2049
|
+
withDocumentLock(this, documentId, fn, options),
|
|
2050
|
+
/**
|
|
2051
|
+
* Get the media file for a document. Media is not versioned, so there is
|
|
2052
|
+
* no as-of form: the route refuses the parameter. Prefer the document's
|
|
2053
|
+
* own `mediaUrl`, which carries the file's version for caching.
|
|
2054
|
+
* @param {string} documentId - The document ID
|
|
2055
|
+
*/
|
|
2056
|
+
getMedia: (documentId) =>
|
|
2057
|
+
this._request("GET", `/api/v1/documents/${documentId}/media`, {
|
|
979
2058
|
binaryResponse: true,
|
|
980
2059
|
}),
|
|
981
2060
|
/**
|
|
982
2061
|
* Upload a media file for a document. Uses Apache Tika for content validation.
|
|
983
2062
|
* @param {string} documentId - The document ID
|
|
984
2063
|
* @param {File} file - The file to upload
|
|
2064
|
+
* @param {string} [auditMessage] - Custom audit-log message for this write
|
|
2065
|
+
* @param {object} [options] - `{ onProgress }`: called with `{ loaded, total }` (bytes) as the file goes up. In a browser the upload then travels by XMLHttpRequest, and the request timeout only fires when the upload stalls, not on total time.
|
|
985
2066
|
*/
|
|
986
|
-
uploadMedia: (documentId, file, auditMessage) => {
|
|
2067
|
+
uploadMedia: (documentId, file, auditMessage, { onProgress } = {}) => {
|
|
987
2068
|
const fd = new FormData();
|
|
988
|
-
fd.append(
|
|
989
|
-
return this._request(
|
|
990
|
-
|
|
2069
|
+
fd.append("file", file);
|
|
2070
|
+
return this._request("PUT", `/api/v1/documents/${documentId}/media`, {
|
|
2071
|
+
auditMessage,
|
|
2072
|
+
body: fd,
|
|
2073
|
+
formData: true,
|
|
2074
|
+
noBatch: true,
|
|
2075
|
+
onUploadProgress: onProgress,
|
|
991
2076
|
});
|
|
992
2077
|
},
|
|
993
2078
|
/**
|
|
994
2079
|
* Delete media file for a document
|
|
995
2080
|
* @param {string} documentId - The document ID
|
|
996
2081
|
*/
|
|
2082
|
+
// The upload above is multipart and cannot be batched, but a DELETE
|
|
2083
|
+
// carries no blob, so the batch transport takes it. It is a write
|
|
2084
|
+
// of the document's own data and queues like any other. Note that the
|
|
2085
|
+
// file removal happens outside the server's transaction, so a batch that
|
|
2086
|
+
// aborts after this op does not bring the file back.
|
|
997
2087
|
deleteMedia: (documentId, auditMessage) =>
|
|
998
|
-
this._request(
|
|
999
|
-
|
|
2088
|
+
this._request("DELETE", `/api/v1/documents/${documentId}/media`, {
|
|
2089
|
+
auditMessage,
|
|
1000
2090
|
}),
|
|
1001
2091
|
/**
|
|
1002
2092
|
* Replace all metadata for a document.
|
|
@@ -1004,48 +2094,98 @@ class PlaidClient {
|
|
|
1004
2094
|
* @param {any} body - The request body
|
|
1005
2095
|
*/
|
|
1006
2096
|
setMetadata: (documentId, body, auditMessage) =>
|
|
1007
|
-
this._request(
|
|
1008
|
-
|
|
2097
|
+
this._request("PUT", `/api/v1/documents/${documentId}/metadata`, {
|
|
2098
|
+
auditMessage,
|
|
2099
|
+
rawBody: body,
|
|
1009
2100
|
}),
|
|
1010
2101
|
/**
|
|
1011
2102
|
* Remove all metadata from a document.
|
|
1012
2103
|
* @param {string} documentId - The document ID
|
|
1013
2104
|
*/
|
|
1014
2105
|
deleteMetadata: (documentId, auditMessage) =>
|
|
1015
|
-
this._request(
|
|
1016
|
-
|
|
2106
|
+
this._request("DELETE", `/api/v1/documents/${documentId}/metadata`, {
|
|
2107
|
+
auditMessage,
|
|
1017
2108
|
}),
|
|
1018
2109
|
/**
|
|
1019
|
-
*
|
|
2110
|
+
* Edit metadata for a document with a list of ops applied in order, in one operation. `{op: 'set', path, value}` writes value at path, creating missing objects along it; `{op: 'delete', path}` removes the key at path (a no-op when absent). A path is a non-empty array of keys, the first a top-level key. A path through a non-object is refused (400). See metadataOps and applyMetadataOps.
|
|
1020
2111
|
* @param {string} documentId - The document ID
|
|
1021
|
-
* @param {any} body - The metadata
|
|
2112
|
+
* @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
|
|
1022
2113
|
*/
|
|
1023
2114
|
patchMetadata: (documentId, body, auditMessage) =>
|
|
1024
|
-
this._request(
|
|
1025
|
-
|
|
2115
|
+
this._request("PATCH", `/api/v1/documents/${documentId}/metadata`, {
|
|
2116
|
+
auditMessage,
|
|
2117
|
+
rawBody: body,
|
|
1026
2118
|
}),
|
|
1027
2119
|
/**
|
|
1028
2120
|
* Get audit log for a document. Transparently follows pagination cursors
|
|
1029
2121
|
* and returns the full flat array.
|
|
1030
|
-
* Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
|
|
1031
2122
|
* @param {string} documentId - The document ID
|
|
1032
2123
|
* @param {string} [startTime] - Start of time range
|
|
1033
2124
|
* @param {string} [endTime] - End of time range
|
|
1034
|
-
* @param {string} [asOf] - Temporal query timestamp
|
|
1035
2125
|
* @param {string[]|string} [opTypes] - Only return operations of these
|
|
1036
2126
|
* types, spelled as in an entry's `op/type` (e.g.
|
|
1037
2127
|
* `['span-layer/create', 'span-layer/delete']`). An entry appears when
|
|
1038
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
|
|
1039
2131
|
*/
|
|
1040
|
-
audit: (documentId, startTime, endTime,
|
|
2132
|
+
audit: (documentId, startTime, endTime, opTypes, kinds) =>
|
|
1041
2133
|
listAll(this, `/api/v1/documents/${documentId}/audit`, {
|
|
1042
2134
|
query: {
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
2135
|
+
"start-time": startTime,
|
|
2136
|
+
"end-time": endTime,
|
|
2137
|
+
"op-types": opTypesParam(opTypes),
|
|
2138
|
+
kinds: kindsParam(kinds),
|
|
1047
2139
|
},
|
|
1048
2140
|
}),
|
|
2141
|
+
/**
|
|
2142
|
+
* One page of the same log, newest-first with `order: "desc"`. Use this
|
|
2143
|
+
* rather than audit() wherever the caller wants the recent end of a log
|
|
2144
|
+
* that may be long: audit() walks every page before it resolves.
|
|
2145
|
+
* @param {string} documentId - The document ID
|
|
2146
|
+
* @param {object} [opts]
|
|
2147
|
+
* @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
|
|
2148
|
+
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
2149
|
+
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
2150
|
+
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
2151
|
+
*/
|
|
2152
|
+
auditPage: (
|
|
2153
|
+
documentId,
|
|
2154
|
+
{ startTime, endTime, opTypes, kinds, order, limit, cursor } = {},
|
|
2155
|
+
) =>
|
|
2156
|
+
listPage(this, `/api/v1/documents/${documentId}/audit`, {
|
|
2157
|
+
limit,
|
|
2158
|
+
cursor,
|
|
2159
|
+
query: {
|
|
2160
|
+
"start-time": startTime,
|
|
2161
|
+
"end-time": endTime,
|
|
2162
|
+
"op-types": opTypesParam(opTypes),
|
|
2163
|
+
kinds: kindsParam(kinds),
|
|
2164
|
+
order,
|
|
2165
|
+
},
|
|
2166
|
+
}),
|
|
2167
|
+
/**
|
|
2168
|
+
* Restore a document to its state at an earlier time, as one
|
|
2169
|
+
* operation: what was deleted since then comes back under its original
|
|
2170
|
+
* id, what was added since is removed, and what changed is set back,
|
|
2171
|
+
* across every layer. A layer deleted since then, or a vocabulary entry
|
|
2172
|
+
* that no longer exists, is skipped and reported under `skipped`.
|
|
2173
|
+
* Resolves to a summary of the changes. Maintainers only.
|
|
2174
|
+
* @param {string} documentId - The document ID
|
|
2175
|
+
* @param {string} asOf - The moment to go back to (ISO-8601 instant),
|
|
2176
|
+
* typically a history entry's `endTime`
|
|
2177
|
+
* @param {object} [options] - `{ dryRun }`: with `dryRun` true nothing
|
|
2178
|
+
* is written and the summary says what would change
|
|
2179
|
+
* @param {string} [auditMessage] - Custom audit message for this operation
|
|
2180
|
+
*/
|
|
2181
|
+
restore: (documentId, asOf, { dryRun } = {}, auditMessage) =>
|
|
2182
|
+
this._request("POST", `/api/v1/documents/${documentId}/restore`, {
|
|
2183
|
+
queryParams: {
|
|
2184
|
+
"as-of": asOf,
|
|
2185
|
+
"dry-run": dryRun ? "true" : undefined,
|
|
2186
|
+
},
|
|
2187
|
+
auditMessage,
|
|
2188
|
+
}),
|
|
1049
2189
|
/**
|
|
1050
2190
|
* Get a document. Set `includeBody` to true to include all data.
|
|
1051
2191
|
*
|
|
@@ -1062,10 +2202,10 @@ class PlaidClient {
|
|
|
1062
2202
|
* @param {string[]|string} [layers] - Layer ids to restrict a body read to
|
|
1063
2203
|
*/
|
|
1064
2204
|
get: (documentId, includeBody, asOf, layers) =>
|
|
1065
|
-
this._request(
|
|
2205
|
+
this._request("GET", `/api/v1/documents/${documentId}`, {
|
|
1066
2206
|
queryParams: {
|
|
1067
|
-
|
|
1068
|
-
|
|
2207
|
+
"include-body": includeBody,
|
|
2208
|
+
"as-of": asOf,
|
|
1069
2209
|
layers: layersParam(layers),
|
|
1070
2210
|
},
|
|
1071
2211
|
}),
|
|
@@ -1074,14 +2214,17 @@ class PlaidClient {
|
|
|
1074
2214
|
* @param {string} documentId - The document ID
|
|
1075
2215
|
*/
|
|
1076
2216
|
delete: (documentId, auditMessage) =>
|
|
1077
|
-
this._request(
|
|
2217
|
+
this._request("DELETE", `/api/v1/documents/${documentId}`, {
|
|
2218
|
+
auditMessage,
|
|
2219
|
+
}),
|
|
1078
2220
|
/**
|
|
1079
2221
|
* Update a document's name.
|
|
1080
2222
|
* @param {string} documentId - The document ID
|
|
1081
2223
|
* @param {string} name - The name
|
|
1082
2224
|
*/
|
|
1083
2225
|
update: (documentId, name, auditMessage) =>
|
|
1084
|
-
this._request(
|
|
2226
|
+
this._request("PATCH", `/api/v1/documents/${documentId}`, {
|
|
2227
|
+
auditMessage,
|
|
1085
2228
|
body: bodyOf({ name }),
|
|
1086
2229
|
}),
|
|
1087
2230
|
/**
|
|
@@ -1090,9 +2233,29 @@ class PlaidClient {
|
|
|
1090
2233
|
* @param {string} name - The name
|
|
1091
2234
|
* @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
|
|
1092
2235
|
*/
|
|
1093
|
-
create: (projectId, name, metadata, auditMessage) =>
|
|
1094
|
-
this._request(
|
|
1095
|
-
|
|
2236
|
+
create: (projectId, name, metadata, auditMessage, { id } = {}) =>
|
|
2237
|
+
this._request("POST", "/api/v1/documents", {
|
|
2238
|
+
auditMessage,
|
|
2239
|
+
body: bodyOf({ id, "project-id": projectId, name, metadata }),
|
|
2240
|
+
}),
|
|
2241
|
+
/**
|
|
2242
|
+
* Copy a document and everything in it into a new document of the same
|
|
2243
|
+
* project, as one operation. The copy shares the source's layers and
|
|
2244
|
+
* the vocabulary entries its links name, and holds the source's texts,
|
|
2245
|
+
* tokens, spans, relations and vocab links under fresh ids, with their
|
|
2246
|
+
* metadata. Comments do not travel. The media file does, unless
|
|
2247
|
+
* `includeMedia` is false. Resolves to `{ id }`, plus `mediaError` when
|
|
2248
|
+
* the source had media the copy could not take with it.
|
|
2249
|
+
* @param {string} documentId - The document to copy
|
|
2250
|
+
* @param {string} name - The new document's name
|
|
2251
|
+
* @param {object} [options] - `{ includeMedia }`: false leaves the media
|
|
2252
|
+
* file behind
|
|
2253
|
+
* @param {string} [auditMessage] - Custom audit message for this operation
|
|
2254
|
+
*/
|
|
2255
|
+
copy: (documentId, name, { includeMedia, id } = {}, auditMessage) =>
|
|
2256
|
+
this._request("POST", `/api/v1/documents/${documentId}/copy`, {
|
|
2257
|
+
auditMessage,
|
|
2258
|
+
body: bodyOf({ id, name, "include-media": includeMedia }),
|
|
1096
2259
|
}),
|
|
1097
2260
|
};
|
|
1098
2261
|
|
|
@@ -1103,120 +2266,182 @@ class PlaidClient {
|
|
|
1103
2266
|
* @param {string} userId - The user ID
|
|
1104
2267
|
*/
|
|
1105
2268
|
addWriter: (id, userId, auditMessage) =>
|
|
1106
|
-
this._request(
|
|
2269
|
+
this._request("POST", `/api/v1/projects/${id}/writers/${userId}`, {
|
|
2270
|
+
auditMessage,
|
|
2271
|
+
}),
|
|
1107
2272
|
/**
|
|
1108
2273
|
* Remove a user's writer privileges for this project.
|
|
1109
2274
|
* @param {string} id - The resource ID
|
|
1110
2275
|
* @param {string} userId - The user ID
|
|
1111
2276
|
*/
|
|
1112
2277
|
removeWriter: (id, userId, auditMessage) =>
|
|
1113
|
-
this._request(
|
|
2278
|
+
this._request("DELETE", `/api/v1/projects/${id}/writers/${userId}`, {
|
|
2279
|
+
auditMessage,
|
|
2280
|
+
}),
|
|
1114
2281
|
/**
|
|
1115
2282
|
* Set a user's access level to read-only for this project.
|
|
1116
2283
|
* @param {string} id - The resource ID
|
|
1117
2284
|
* @param {string} userId - The user ID
|
|
1118
2285
|
*/
|
|
1119
2286
|
addReader: (id, userId, auditMessage) =>
|
|
1120
|
-
this._request(
|
|
2287
|
+
this._request("POST", `/api/v1/projects/${id}/readers/${userId}`, {
|
|
2288
|
+
auditMessage,
|
|
2289
|
+
}),
|
|
1121
2290
|
/**
|
|
1122
2291
|
* Remove a user's reader privileges for this project.
|
|
1123
2292
|
* @param {string} id - The resource ID
|
|
1124
2293
|
* @param {string} userId - The user ID
|
|
1125
2294
|
*/
|
|
1126
2295
|
removeReader: (id, userId, auditMessage) =>
|
|
1127
|
-
this._request(
|
|
2296
|
+
this._request("DELETE", `/api/v1/projects/${id}/readers/${userId}`, {
|
|
2297
|
+
auditMessage,
|
|
2298
|
+
}),
|
|
1128
2299
|
/**
|
|
1129
2300
|
* Set a configuration value for a project in an editor namespace.
|
|
1130
2301
|
* @param {string} id - The resource ID
|
|
1131
2302
|
* @param {string} namespace - The config namespace
|
|
1132
2303
|
* @param {string} configKey - The config key
|
|
1133
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.
|
|
1134
2308
|
*/
|
|
1135
|
-
setConfig: (id, namespace, configKey, configValue, auditMessage) =>
|
|
1136
|
-
this._request(
|
|
1137
|
-
|
|
1138
|
-
|
|
2309
|
+
setConfig: (id, namespace, configKey, configValue, auditMessage, options) =>
|
|
2310
|
+
this._request(
|
|
2311
|
+
"PUT",
|
|
2312
|
+
`/api/v1/projects/${id}/config/${namespace}/${configKey}`,
|
|
2313
|
+
configRequest(auditMessage, configValue, options),
|
|
2314
|
+
),
|
|
1139
2315
|
/**
|
|
1140
2316
|
* Remove a configuration value for a project.
|
|
1141
2317
|
* @param {string} id - The resource ID
|
|
1142
2318
|
* @param {string} namespace - The config namespace
|
|
1143
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.
|
|
1144
2322
|
*/
|
|
1145
|
-
deleteConfig: (id, namespace, configKey, auditMessage) =>
|
|
1146
|
-
this._request(
|
|
1147
|
-
|
|
1148
|
-
|
|
2323
|
+
deleteConfig: (id, namespace, configKey, auditMessage, options) =>
|
|
2324
|
+
this._request(
|
|
2325
|
+
"DELETE",
|
|
2326
|
+
`/api/v1/projects/${id}/config/${namespace}/${configKey}`,
|
|
2327
|
+
configRequest(auditMessage, undefined, options, true),
|
|
2328
|
+
),
|
|
1149
2329
|
/**
|
|
1150
2330
|
* Assign a user as a maintainer for this project.
|
|
1151
2331
|
* @param {string} id - The resource ID
|
|
1152
2332
|
* @param {string} userId - The user ID
|
|
1153
2333
|
*/
|
|
1154
2334
|
addMaintainer: (id, userId, auditMessage) =>
|
|
1155
|
-
this._request(
|
|
2335
|
+
this._request("POST", `/api/v1/projects/${id}/maintainers/${userId}`, {
|
|
2336
|
+
auditMessage,
|
|
2337
|
+
}),
|
|
1156
2338
|
/**
|
|
1157
2339
|
* Remove a user's maintainer privileges for this project.
|
|
1158
2340
|
* @param {string} id - The resource ID
|
|
1159
2341
|
* @param {string} userId - The user ID
|
|
1160
2342
|
*/
|
|
1161
2343
|
removeMaintainer: (id, userId, auditMessage) =>
|
|
1162
|
-
this._request(
|
|
2344
|
+
this._request(
|
|
2345
|
+
"DELETE",
|
|
2346
|
+
`/api/v1/projects/${id}/maintainers/${userId}`,
|
|
2347
|
+
{ auditMessage },
|
|
2348
|
+
),
|
|
1163
2349
|
/**
|
|
1164
2350
|
* Get audit log for a project. Transparently follows pagination cursors
|
|
1165
2351
|
* and returns the full flat array.
|
|
1166
|
-
* Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
|
|
1167
2352
|
* @param {string} projectId - The project ID
|
|
1168
2353
|
* @param {string} [startTime] - Start of time range
|
|
1169
2354
|
* @param {string} [endTime] - End of time range
|
|
1170
|
-
* @param {string} [asOf] - Temporal query timestamp
|
|
1171
2355
|
* @param {string[]|string} [opTypes] - Only return operations of these
|
|
1172
2356
|
* types, spelled as in an entry's `op/type` (e.g.
|
|
1173
2357
|
* `['span-layer/create', 'span-layer/delete']`). An entry appears when
|
|
1174
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
|
|
1175
2361
|
*/
|
|
1176
|
-
audit: (projectId, startTime, endTime,
|
|
2362
|
+
audit: (projectId, startTime, endTime, opTypes, kinds) =>
|
|
1177
2363
|
listAll(this, `/api/v1/projects/${projectId}/audit`, {
|
|
1178
2364
|
query: {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
2365
|
+
"start-time": startTime,
|
|
2366
|
+
"end-time": endTime,
|
|
2367
|
+
"op-types": opTypesParam(opTypes),
|
|
2368
|
+
kinds: kindsParam(kinds),
|
|
2369
|
+
},
|
|
2370
|
+
}),
|
|
2371
|
+
/**
|
|
2372
|
+
* One page of the same log, newest-first with `order: "desc"`. Use this
|
|
2373
|
+
* rather than audit() wherever the caller wants the recent end of a log
|
|
2374
|
+
* that may be long: audit() walks every page before it resolves.
|
|
2375
|
+
* @param {string} projectId - The project ID
|
|
2376
|
+
* @param {object} [opts]
|
|
2377
|
+
* @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
|
|
2378
|
+
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
2379
|
+
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
2380
|
+
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
2381
|
+
*/
|
|
2382
|
+
auditPage: (
|
|
2383
|
+
projectId,
|
|
2384
|
+
{ startTime, endTime, opTypes, kinds, order, limit, cursor } = {},
|
|
2385
|
+
) =>
|
|
2386
|
+
listPage(this, `/api/v1/projects/${projectId}/audit`, {
|
|
2387
|
+
limit,
|
|
2388
|
+
cursor,
|
|
2389
|
+
query: {
|
|
2390
|
+
"start-time": startTime,
|
|
2391
|
+
"end-time": endTime,
|
|
2392
|
+
"op-types": opTypesParam(opTypes),
|
|
2393
|
+
kinds: kindsParam(kinds),
|
|
2394
|
+
order,
|
|
1183
2395
|
},
|
|
1184
2396
|
}),
|
|
2397
|
+
/**
|
|
2398
|
+
* When the calling user last wrote to each document in a project, as a
|
|
2399
|
+
* `{documentId: timestamp}` map. Documents they have never written to
|
|
2400
|
+
* are absent. One request covers a whole document list.
|
|
2401
|
+
*
|
|
2402
|
+
* The response is NOT key-transformed: its keys are document ids, and
|
|
2403
|
+
* camelCasing would mangle the hyphens in a UUID.
|
|
2404
|
+
* @param {string} projectId - The project ID
|
|
2405
|
+
* @returns {Promise<Object<string, string>>}
|
|
2406
|
+
*/
|
|
2407
|
+
myLastEdits: (projectId) =>
|
|
2408
|
+
this._request("GET", `/api/v1/projects/${projectId}/audit/last-edits`, {
|
|
2409
|
+
skipResponseTransform: true,
|
|
2410
|
+
}),
|
|
1185
2411
|
/**
|
|
1186
2412
|
* Link a vocabulary to a project.
|
|
1187
2413
|
* @param {string} id - The resource ID
|
|
1188
2414
|
* @param {string} vocabId - The vocab layer ID
|
|
1189
2415
|
*/
|
|
1190
2416
|
linkVocab: (id, vocabId, auditMessage) =>
|
|
1191
|
-
this._request(
|
|
2417
|
+
this._request("POST", `/api/v1/projects/${id}/vocabs/${vocabId}`, {
|
|
2418
|
+
auditMessage,
|
|
2419
|
+
}),
|
|
1192
2420
|
/**
|
|
1193
2421
|
* Unlink a vocabulary from a project.
|
|
1194
2422
|
* @param {string} id - The resource ID
|
|
1195
2423
|
* @param {string} vocabId - The vocab layer ID
|
|
1196
2424
|
*/
|
|
1197
2425
|
unlinkVocab: (id, vocabId, auditMessage) =>
|
|
1198
|
-
this._request(
|
|
2426
|
+
this._request("DELETE", `/api/v1/projects/${id}/vocabs/${vocabId}`, {
|
|
2427
|
+
auditMessage,
|
|
2428
|
+
}),
|
|
1199
2429
|
/**
|
|
1200
2430
|
* Get a project by ID. To fetch the project's documents, use
|
|
1201
2431
|
* listDocuments(id) — the include-documents flag has been removed.
|
|
1202
2432
|
* @param {string} id - The resource ID
|
|
1203
|
-
* @param {string} [asOf] - Temporal query timestamp
|
|
1204
2433
|
*/
|
|
1205
|
-
get: (id
|
|
1206
|
-
this._request(
|
|
1207
|
-
queryParams: { 'as-of': asOf },
|
|
1208
|
-
}),
|
|
2434
|
+
get: (id) =>
|
|
2435
|
+
this._request("GET", `/api/v1/projects/${id}`),
|
|
1209
2436
|
/**
|
|
1210
2437
|
* List all documents in a project. Transparently follows pagination
|
|
1211
2438
|
* cursors and returns the full flat array.
|
|
1212
|
-
* Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
|
|
1213
2439
|
*
|
|
1214
2440
|
* Note: this endpoint does not support temporal (`as-of`) queries; the
|
|
1215
2441
|
* server rejects `?as-of=` on the documents-list route with a 400.
|
|
1216
2442
|
* @param {string} id - The project ID
|
|
1217
2443
|
*/
|
|
1218
|
-
listDocuments: (id) =>
|
|
1219
|
-
listAll(this, `/api/v1/projects/${id}/documents`),
|
|
2444
|
+
listDocuments: (id) => listAll(this, `/api/v1/projects/${id}/documents`),
|
|
1220
2445
|
/**
|
|
1221
2446
|
* Fetch a single page of a project's documents.
|
|
1222
2447
|
*
|
|
@@ -1239,68 +2464,69 @@ class PlaidClient {
|
|
|
1239
2464
|
* @param {string} id - The project ID
|
|
1240
2465
|
* @param {object} [opts]
|
|
1241
2466
|
* @param {number} [opts.pageSize] - Per-request page size
|
|
1242
|
-
* Cannot be used inside a batch (auto-paginates across requests); throws on first iteration if called while batching — use listPage() for a single page in a batch.
|
|
1243
2467
|
* @returns {AsyncGenerator<Array>}
|
|
1244
2468
|
*/
|
|
1245
2469
|
iterDocuments: (id, { pageSize } = {}) =>
|
|
1246
2470
|
iterPages(this, `/api/v1/projects/${id}/documents`, { pageSize }),
|
|
1247
2471
|
/**
|
|
1248
|
-
* Delete a project and everything in it. This is irrecoverable.
|
|
1249
|
-
*
|
|
1250
|
-
*
|
|
1251
|
-
* per-request timeout is DISABLED by default. Pass `{ timeout }` to
|
|
1252
|
-
* impose a finite bound.
|
|
2472
|
+
* Delete a project and everything in it. This is irrecoverable. The
|
|
2473
|
+
* project is gone when this returns, and what it holds is removed on
|
|
2474
|
+
* the server afterwards.
|
|
1253
2475
|
* @param {string} id - The resource ID
|
|
1254
2476
|
* @param {string} [auditMessage] - Custom audit-log message
|
|
1255
2477
|
* @param {object} [options]
|
|
1256
|
-
* @param {number} [options.timeout
|
|
2478
|
+
* @param {number} [options.timeout] - Per-request timeout in ms, the client's own by default (0/null disables)
|
|
1257
2479
|
*/
|
|
1258
|
-
delete: (id, auditMessage, { timeout
|
|
1259
|
-
this._request(
|
|
2480
|
+
delete: (id, auditMessage, { timeout } = {}) =>
|
|
2481
|
+
this._request("DELETE", `/api/v1/projects/${id}`, {
|
|
2482
|
+
auditMessage,
|
|
2483
|
+
timeout,
|
|
2484
|
+
}),
|
|
1260
2485
|
/**
|
|
1261
2486
|
* Update a project's name.
|
|
1262
2487
|
* @param {string} id - The resource ID
|
|
1263
2488
|
* @param {string} name - The name
|
|
1264
2489
|
*/
|
|
1265
2490
|
update: (id, name, auditMessage) =>
|
|
1266
|
-
this._request(
|
|
2491
|
+
this._request("PATCH", `/api/v1/projects/${id}`, {
|
|
2492
|
+
auditMessage,
|
|
1267
2493
|
body: bodyOf({ name }),
|
|
1268
2494
|
}),
|
|
1269
2495
|
/**
|
|
1270
2496
|
* List all projects accessible to user. Transparently follows pagination
|
|
1271
2497
|
* cursors and returns the full flat array.
|
|
1272
|
-
* Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
|
|
1273
|
-
* @param {string} [asOf] - Temporal query timestamp
|
|
1274
2498
|
*/
|
|
1275
|
-
list: (
|
|
1276
|
-
listAll(this, '/api/v1/projects', { query: { 'as-of': asOf } }),
|
|
2499
|
+
list: () => listAll(this, "/api/v1/projects"),
|
|
1277
2500
|
/**
|
|
1278
2501
|
* Fetch a single page of projects.
|
|
1279
2502
|
* @param {object} [opts]
|
|
1280
2503
|
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
1281
2504
|
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
1282
|
-
* @param {string} [opts.asOf] - Temporal query timestamp
|
|
1283
2505
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
1284
2506
|
*/
|
|
1285
|
-
listPage: ({ limit, cursor
|
|
1286
|
-
listPage(this,
|
|
2507
|
+
listPage: ({ limit, cursor } = {}) =>
|
|
2508
|
+
listPage(this, "/api/v1/projects", {
|
|
2509
|
+
limit,
|
|
2510
|
+
cursor,
|
|
2511
|
+
}),
|
|
1287
2512
|
/**
|
|
1288
2513
|
* Async-iterate projects page by page; yields each page's entries array.
|
|
1289
2514
|
* @param {object} [opts]
|
|
1290
2515
|
* @param {number} [opts.pageSize] - Per-request page size
|
|
1291
|
-
* @param {string} [opts.asOf] - Temporal query timestamp
|
|
1292
|
-
* Cannot be used inside a batch (auto-paginates across requests); throws on first iteration if called while batching — use listPage() for a single page in a batch.
|
|
1293
2516
|
* @returns {AsyncGenerator<Array>}
|
|
1294
2517
|
*/
|
|
1295
|
-
iterPages: ({ pageSize
|
|
1296
|
-
iterPages(this,
|
|
2518
|
+
iterPages: ({ pageSize } = {}) =>
|
|
2519
|
+
iterPages(this, "/api/v1/projects", {
|
|
2520
|
+
pageSize,
|
|
2521
|
+
}),
|
|
1297
2522
|
/**
|
|
1298
2523
|
* Create a new project. Note: this also registers the user as a maintainer.
|
|
1299
2524
|
* @param {string} name - The name
|
|
1300
2525
|
*/
|
|
1301
|
-
create: (name, auditMessage) =>
|
|
1302
|
-
this._request(
|
|
1303
|
-
|
|
2526
|
+
create: (name, auditMessage, { id } = {}) =>
|
|
2527
|
+
this._request("POST", "/api/v1/projects", {
|
|
2528
|
+
auditMessage,
|
|
2529
|
+
body: bodyOf({ id, name }),
|
|
1304
2530
|
}),
|
|
1305
2531
|
};
|
|
1306
2532
|
|
|
@@ -1311,43 +2537,52 @@ class PlaidClient {
|
|
|
1311
2537
|
* @param {string} namespace - The config namespace
|
|
1312
2538
|
* @param {string} configKey - The config key
|
|
1313
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.
|
|
1314
2543
|
*/
|
|
1315
|
-
setConfig: (textLayerId, namespace, configKey, configValue, auditMessage) =>
|
|
1316
|
-
this._request(
|
|
1317
|
-
|
|
1318
|
-
|
|
2544
|
+
setConfig: (textLayerId, namespace, configKey, configValue, auditMessage, options) =>
|
|
2545
|
+
this._request(
|
|
2546
|
+
"PUT",
|
|
2547
|
+
`/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`,
|
|
2548
|
+
configRequest(auditMessage, configValue, options),
|
|
2549
|
+
),
|
|
1319
2550
|
/**
|
|
1320
2551
|
* Remove a configuration value for a layer.
|
|
1321
2552
|
* @param {string} textLayerId - The text layer ID
|
|
1322
2553
|
* @param {string} namespace - The config namespace
|
|
1323
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.
|
|
1324
2557
|
*/
|
|
1325
|
-
deleteConfig: (textLayerId, namespace, configKey, auditMessage) =>
|
|
1326
|
-
this._request(
|
|
1327
|
-
|
|
1328
|
-
|
|
2558
|
+
deleteConfig: (textLayerId, namespace, configKey, auditMessage, options) =>
|
|
2559
|
+
this._request(
|
|
2560
|
+
"DELETE",
|
|
2561
|
+
`/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`,
|
|
2562
|
+
configRequest(auditMessage, undefined, options, true),
|
|
2563
|
+
),
|
|
1329
2564
|
/**
|
|
1330
2565
|
* Get a text layer by ID.
|
|
1331
2566
|
* @param {string} textLayerId - The text layer ID
|
|
1332
|
-
* @param {string} [asOf] - Temporal query timestamp
|
|
1333
2567
|
*/
|
|
1334
|
-
get: (textLayerId
|
|
1335
|
-
this._request(
|
|
1336
|
-
queryParams: { 'as-of': asOf },
|
|
1337
|
-
}),
|
|
2568
|
+
get: (textLayerId) =>
|
|
2569
|
+
this._request("GET", `/api/v1/text-layers/${textLayerId}`),
|
|
1338
2570
|
/**
|
|
1339
2571
|
* Delete a text layer.
|
|
1340
2572
|
* @param {string} textLayerId - The text layer ID
|
|
1341
2573
|
*/
|
|
1342
2574
|
delete: (textLayerId, auditMessage) =>
|
|
1343
|
-
this._request(
|
|
2575
|
+
this._request("DELETE", `/api/v1/text-layers/${textLayerId}`, {
|
|
2576
|
+
auditMessage,
|
|
2577
|
+
}),
|
|
1344
2578
|
/**
|
|
1345
2579
|
* Update a text layer's name.
|
|
1346
2580
|
* @param {string} textLayerId - The text layer ID
|
|
1347
2581
|
* @param {string} name - The name
|
|
1348
2582
|
*/
|
|
1349
2583
|
update: (textLayerId, name, auditMessage) =>
|
|
1350
|
-
this._request(
|
|
2584
|
+
this._request("PATCH", `/api/v1/text-layers/${textLayerId}`, {
|
|
2585
|
+
auditMessage,
|
|
1351
2586
|
body: bodyOf({ name }),
|
|
1352
2587
|
}),
|
|
1353
2588
|
/**
|
|
@@ -1356,7 +2591,8 @@ class PlaidClient {
|
|
|
1356
2591
|
* @param {string} direction - The direction ("up" or "down")
|
|
1357
2592
|
*/
|
|
1358
2593
|
shift: (textLayerId, direction, auditMessage) =>
|
|
1359
|
-
this._request(
|
|
2594
|
+
this._request("POST", `/api/v1/text-layers/${textLayerId}/shift`, {
|
|
2595
|
+
auditMessage,
|
|
1360
2596
|
body: bodyOf({ direction }),
|
|
1361
2597
|
}),
|
|
1362
2598
|
/**
|
|
@@ -1364,9 +2600,10 @@ class PlaidClient {
|
|
|
1364
2600
|
* @param {string} projectId - The project ID
|
|
1365
2601
|
* @param {string} name - The name
|
|
1366
2602
|
*/
|
|
1367
|
-
create: (projectId, name, auditMessage) =>
|
|
1368
|
-
this._request(
|
|
1369
|
-
|
|
2603
|
+
create: (projectId, name, auditMessage, { id } = {}) =>
|
|
2604
|
+
this._request("POST", "/api/v1/text-layers", {
|
|
2605
|
+
auditMessage,
|
|
2606
|
+
body: bodyOf({ id, "project-id": projectId, name }),
|
|
1370
2607
|
}),
|
|
1371
2608
|
};
|
|
1372
2609
|
|
|
@@ -1377,25 +2614,27 @@ class PlaidClient {
|
|
|
1377
2614
|
* @param {any} body - The request body
|
|
1378
2615
|
*/
|
|
1379
2616
|
setMetadata: (id, body, auditMessage) =>
|
|
1380
|
-
this._request(
|
|
1381
|
-
|
|
2617
|
+
this._request("PUT", `/api/v1/vocab-items/${id}/metadata`, {
|
|
2618
|
+
auditMessage,
|
|
2619
|
+
rawBody: body,
|
|
1382
2620
|
}),
|
|
1383
2621
|
/**
|
|
1384
2622
|
* Remove all metadata from a vocab item.
|
|
1385
2623
|
* @param {string} id - The resource ID
|
|
1386
2624
|
*/
|
|
1387
2625
|
deleteMetadata: (id, auditMessage) =>
|
|
1388
|
-
this._request(
|
|
1389
|
-
|
|
2626
|
+
this._request("DELETE", `/api/v1/vocab-items/${id}/metadata`, {
|
|
2627
|
+
auditMessage,
|
|
1390
2628
|
}),
|
|
1391
2629
|
/**
|
|
1392
|
-
*
|
|
2630
|
+
* Edit metadata for a vocab item with a list of ops applied in order, in one operation. `{op: 'set', path, value}` writes value at path, creating missing objects along it; `{op: 'delete', path}` removes the key at path (a no-op when absent). A path is a non-empty array of keys, the first a top-level key. A path through a non-object is refused (400). See metadataOps and applyMetadataOps.
|
|
1393
2631
|
* @param {string} id - The resource ID
|
|
1394
|
-
* @param {any} body - The metadata
|
|
2632
|
+
* @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
|
|
1395
2633
|
*/
|
|
1396
2634
|
patchMetadata: (id, body, auditMessage) =>
|
|
1397
|
-
this._request(
|
|
1398
|
-
|
|
2635
|
+
this._request("PATCH", `/api/v1/vocab-items/${id}/metadata`, {
|
|
2636
|
+
auditMessage,
|
|
2637
|
+
rawBody: body,
|
|
1399
2638
|
}),
|
|
1400
2639
|
/**
|
|
1401
2640
|
* Create a new vocab item
|
|
@@ -1403,9 +2642,10 @@ class PlaidClient {
|
|
|
1403
2642
|
* @param {string} form - The vocab item form
|
|
1404
2643
|
* @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
|
|
1405
2644
|
*/
|
|
1406
|
-
create: (vocabLayerId, form, metadata, auditMessage) =>
|
|
1407
|
-
this._request(
|
|
1408
|
-
|
|
2645
|
+
create: (vocabLayerId, form, metadata, auditMessage, { id } = {}) =>
|
|
2646
|
+
this._request("POST", "/api/v1/vocab-items", {
|
|
2647
|
+
auditMessage,
|
|
2648
|
+
body: bodyOf({ id, "vocab-layer-id": vocabLayerId, form, metadata }),
|
|
1409
2649
|
}),
|
|
1410
2650
|
/**
|
|
1411
2651
|
* Create multiple vocab items in a single operation. Entries may target
|
|
@@ -1413,36 +2653,85 @@ class PlaidClient {
|
|
|
1413
2653
|
* @param {Array<{vocabLayerId: string, form: string, metadata?: any}>} body - The vocab items to create
|
|
1414
2654
|
*/
|
|
1415
2655
|
bulkCreate: (body, auditMessage) =>
|
|
1416
|
-
this._request(
|
|
2656
|
+
this._request("POST", "/api/v1/vocab-items/bulk", {
|
|
2657
|
+
auditMessage,
|
|
2658
|
+
body,
|
|
2659
|
+
}),
|
|
2660
|
+
/**
|
|
2661
|
+
* Update many vocab items in a single operation: set forms and/or patch
|
|
2662
|
+
* metadata.
|
|
2663
|
+
* @param {Array<{id: string, form?: string, metadata?: Array<object>}>} body - `form` is set only when the key is present; `metadata` is a list of metadata ops, as for patchMetadata. The entries may lie in several vocab layers; the user must have write access to each. An unknown id refuses the whole update, and an id may appear only once. Only an entry whose form really changes restates the documents linking it, and a strict-mode client picks up their new versions from the response.
|
|
2664
|
+
* @returns {Promise<{count: number}>} How many vocab items were updated.
|
|
2665
|
+
*/
|
|
2666
|
+
bulkUpdate: (body, auditMessage) =>
|
|
2667
|
+
this._request("PATCH", "/api/v1/vocab-items/bulk", {
|
|
2668
|
+
auditMessage,
|
|
2669
|
+
body,
|
|
2670
|
+
}),
|
|
1417
2671
|
/**
|
|
1418
2672
|
* Delete multiple vocab items in a single operation. Each item's
|
|
1419
2673
|
* descendant vocab links are deleted too. Provide an array of IDs.
|
|
2674
|
+
*
|
|
2675
|
+
* Every document holding one of those links has its version bumped, and
|
|
2676
|
+
* a strict-mode client picks up their new versions from the response.
|
|
1420
2677
|
* @param {string[]} body - The vocab item IDs to delete
|
|
1421
2678
|
*/
|
|
1422
2679
|
bulkDelete: (body, auditMessage) =>
|
|
1423
|
-
this._request(
|
|
2680
|
+
this._request("DELETE", "/api/v1/vocab-items/bulk", {
|
|
2681
|
+
auditMessage,
|
|
2682
|
+
body,
|
|
2683
|
+
}),
|
|
1424
2684
|
/**
|
|
1425
2685
|
* Get a vocab item by ID
|
|
1426
2686
|
* @param {string} id - The resource ID
|
|
1427
|
-
* @param {string} [asOf] - Temporal query timestamp
|
|
1428
2687
|
*/
|
|
1429
|
-
get: (id
|
|
1430
|
-
this._request(
|
|
1431
|
-
queryParams: { 'as-of': asOf },
|
|
1432
|
-
}),
|
|
2688
|
+
get: (id) =>
|
|
2689
|
+
this._request("GET", `/api/v1/vocab-items/${id}`),
|
|
1433
2690
|
/**
|
|
1434
|
-
* Delete a vocab item
|
|
2691
|
+
* Delete a vocab item, and every link to it. Every document holding one
|
|
2692
|
+
* of those links has its version bumped, and a strict-mode client picks
|
|
2693
|
+
* up their new versions from the response.
|
|
1435
2694
|
* @param {string} id - The resource ID
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
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.
|
|
2699
|
+
*/
|
|
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
|
+
}),
|
|
1439
2724
|
/**
|
|
1440
|
-
* Update a vocab item's form
|
|
2725
|
+
* Update a vocab item's form. A document read carries the entry's form on
|
|
2726
|
+
* every link to it, so a rename restates those documents: each has its
|
|
2727
|
+
* version bumped, and a strict-mode client picks up their new versions
|
|
2728
|
+
* from the response.
|
|
1441
2729
|
* @param {string} id - The resource ID
|
|
1442
2730
|
* @param {string} form - The vocab item form
|
|
1443
2731
|
*/
|
|
1444
2732
|
update: (id, form, auditMessage) =>
|
|
1445
|
-
this._request(
|
|
2733
|
+
this._request("PATCH", `/api/v1/vocab-items/${id}`, {
|
|
2734
|
+
auditMessage,
|
|
1446
2735
|
body: bodyOf({ form }),
|
|
1447
2736
|
}),
|
|
1448
2737
|
};
|
|
@@ -1454,17 +2743,20 @@ class PlaidClient {
|
|
|
1454
2743
|
* @param {string} direction - The direction ("up" or "down")
|
|
1455
2744
|
*/
|
|
1456
2745
|
shift: (relationLayerId, direction, auditMessage) =>
|
|
1457
|
-
this._request(
|
|
1458
|
-
|
|
1459
|
-
|
|
2746
|
+
this._request(
|
|
2747
|
+
"POST",
|
|
2748
|
+
`/api/v1/relation-layers/${relationLayerId}/shift`,
|
|
2749
|
+
{ auditMessage, body: bodyOf({ direction }) },
|
|
2750
|
+
),
|
|
1460
2751
|
/**
|
|
1461
2752
|
* Create a new relation layer.
|
|
1462
2753
|
* @param {string} spanLayerId - The span layer ID
|
|
1463
2754
|
* @param {string} name - The name
|
|
1464
2755
|
*/
|
|
1465
|
-
create: (spanLayerId, name, auditMessage) =>
|
|
1466
|
-
this._request(
|
|
1467
|
-
|
|
2756
|
+
create: (spanLayerId, name, auditMessage, { id } = {}) =>
|
|
2757
|
+
this._request("POST", "/api/v1/relation-layers", {
|
|
2758
|
+
auditMessage,
|
|
2759
|
+
body: bodyOf({ id, "span-layer-id": spanLayerId, name }),
|
|
1468
2760
|
}),
|
|
1469
2761
|
/**
|
|
1470
2762
|
* Set a configuration value for a layer in an editor namespace.
|
|
@@ -1472,43 +2764,53 @@ class PlaidClient {
|
|
|
1472
2764
|
* @param {string} namespace - The config namespace
|
|
1473
2765
|
* @param {string} configKey - The config key
|
|
1474
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.
|
|
1475
2770
|
*/
|
|
1476
|
-
setConfig: (relationLayerId, namespace, configKey, configValue, auditMessage) =>
|
|
1477
|
-
this._request(
|
|
1478
|
-
|
|
1479
|
-
|
|
2771
|
+
setConfig: (relationLayerId, namespace, configKey, configValue, auditMessage, options) =>
|
|
2772
|
+
this._request(
|
|
2773
|
+
"PUT",
|
|
2774
|
+
`/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`,
|
|
2775
|
+
configRequest(auditMessage, configValue, options),
|
|
2776
|
+
),
|
|
1480
2777
|
/**
|
|
1481
2778
|
* Remove a configuration value for a layer.
|
|
1482
2779
|
* @param {string} relationLayerId - The relation layer ID
|
|
1483
2780
|
* @param {string} namespace - The config namespace
|
|
1484
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.
|
|
1485
2784
|
*/
|
|
1486
|
-
deleteConfig: (relationLayerId, namespace, configKey, auditMessage) =>
|
|
1487
|
-
this._request(
|
|
1488
|
-
|
|
1489
|
-
|
|
2785
|
+
deleteConfig: (relationLayerId, namespace, configKey, auditMessage, options) =>
|
|
2786
|
+
this._request(
|
|
2787
|
+
"DELETE",
|
|
2788
|
+
`/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`,
|
|
2789
|
+
configRequest(auditMessage, undefined, options, true),
|
|
2790
|
+
),
|
|
2791
|
+
...constraintMethods(this, "relation-layers"),
|
|
1490
2792
|
/**
|
|
1491
2793
|
* Get a relation layer by ID.
|
|
1492
2794
|
* @param {string} relationLayerId - The relation layer ID
|
|
1493
|
-
* @param {string} [asOf] - Temporal query timestamp
|
|
1494
2795
|
*/
|
|
1495
|
-
get: (relationLayerId
|
|
1496
|
-
this._request(
|
|
1497
|
-
queryParams: { 'as-of': asOf },
|
|
1498
|
-
}),
|
|
2796
|
+
get: (relationLayerId) =>
|
|
2797
|
+
this._request("GET", `/api/v1/relation-layers/${relationLayerId}`),
|
|
1499
2798
|
/**
|
|
1500
2799
|
* Delete a relation layer.
|
|
1501
2800
|
* @param {string} relationLayerId - The relation layer ID
|
|
1502
2801
|
*/
|
|
1503
2802
|
delete: (relationLayerId, auditMessage) =>
|
|
1504
|
-
this._request(
|
|
2803
|
+
this._request("DELETE", `/api/v1/relation-layers/${relationLayerId}`, {
|
|
2804
|
+
auditMessage,
|
|
2805
|
+
}),
|
|
1505
2806
|
/**
|
|
1506
2807
|
* Update a relation layer's name.
|
|
1507
2808
|
* @param {string} relationLayerId - The relation layer ID
|
|
1508
2809
|
* @param {string} name - The name
|
|
1509
2810
|
*/
|
|
1510
2811
|
update: (relationLayerId, name, auditMessage) =>
|
|
1511
|
-
this._request(
|
|
2812
|
+
this._request("PATCH", `/api/v1/relation-layers/${relationLayerId}`, {
|
|
2813
|
+
auditMessage,
|
|
1512
2814
|
body: bodyOf({ name }),
|
|
1513
2815
|
}),
|
|
1514
2816
|
};
|
|
@@ -1532,26 +2834,41 @@ class PlaidClient {
|
|
|
1532
2834
|
* @param {number} [precedence] - Ordering precedence
|
|
1533
2835
|
* @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
|
|
1534
2836
|
*/
|
|
1535
|
-
create: (
|
|
1536
|
-
|
|
1537
|
-
|
|
2837
|
+
create: (
|
|
2838
|
+
tokenLayerId,
|
|
2839
|
+
text,
|
|
2840
|
+
begin,
|
|
2841
|
+
end,
|
|
2842
|
+
precedence,
|
|
2843
|
+
metadata,
|
|
2844
|
+
auditMessage,
|
|
2845
|
+
{ id } = {},
|
|
2846
|
+
) =>
|
|
2847
|
+
this._request("POST", "/api/v1/tokens", {
|
|
2848
|
+
auditMessage,
|
|
2849
|
+
body: bodyOf({
|
|
2850
|
+
id,
|
|
2851
|
+
"token-layer-id": tokenLayerId,
|
|
2852
|
+
text,
|
|
2853
|
+
begin,
|
|
2854
|
+
end,
|
|
2855
|
+
precedence,
|
|
2856
|
+
metadata,
|
|
2857
|
+
}),
|
|
1538
2858
|
}),
|
|
1539
2859
|
/**
|
|
1540
2860
|
* Get a token.
|
|
1541
2861
|
* @param {string} tokenId - The token ID
|
|
1542
|
-
* @param {string} [asOf] - Temporal query timestamp
|
|
1543
2862
|
*/
|
|
1544
|
-
get: (tokenId
|
|
1545
|
-
this._request(
|
|
1546
|
-
queryParams: { 'as-of': asOf },
|
|
1547
|
-
}),
|
|
2863
|
+
get: (tokenId) =>
|
|
2864
|
+
this._request("GET", `/api/v1/tokens/${tokenId}`),
|
|
1548
2865
|
/**
|
|
1549
2866
|
* Delete a token and remove it from any spans. If this causes a span to
|
|
1550
2867
|
* have no remaining tokens, the span will also be deleted.
|
|
1551
2868
|
* @param {string} tokenId - The token ID
|
|
1552
2869
|
*/
|
|
1553
2870
|
delete: (tokenId, auditMessage) =>
|
|
1554
|
-
this._request(
|
|
2871
|
+
this._request("DELETE", `/api/v1/tokens/${tokenId}`, { auditMessage }),
|
|
1555
2872
|
/**
|
|
1556
2873
|
* Update a token.
|
|
1557
2874
|
* @param {string} tokenId - The token ID
|
|
@@ -1563,7 +2880,8 @@ class PlaidClient {
|
|
|
1563
2880
|
* drops undefined, so the three cases map correctly to the server.
|
|
1564
2881
|
*/
|
|
1565
2882
|
update: (tokenId, begin, end, precedence, auditMessage) =>
|
|
1566
|
-
this._request(
|
|
2883
|
+
this._request("PATCH", `/api/v1/tokens/${tokenId}`, {
|
|
2884
|
+
auditMessage,
|
|
1567
2885
|
body: bodyOf({ begin, end, precedence }),
|
|
1568
2886
|
}),
|
|
1569
2887
|
/**
|
|
@@ -1572,22 +2890,43 @@ class PlaidClient {
|
|
|
1572
2890
|
* @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
|
|
1573
2891
|
*/
|
|
1574
2892
|
bulkCreate: (body, auditMessage) =>
|
|
1575
|
-
this._request(
|
|
2893
|
+
this._request("POST", "/api/v1/tokens/bulk", { auditMessage, body }),
|
|
1576
2894
|
/**
|
|
1577
2895
|
* Delete multiple tokens in a single operation. Provide an array of IDs.
|
|
1578
2896
|
* @param {Array} body - The request body
|
|
1579
2897
|
*/
|
|
1580
2898
|
bulkDelete: (body, auditMessage) =>
|
|
1581
|
-
this._request(
|
|
2899
|
+
this._request("DELETE", "/api/v1/tokens/bulk", { auditMessage, body }),
|
|
2900
|
+
/**
|
|
2901
|
+
* Update many tokens in a single operation: patch metadata.
|
|
2902
|
+
* @param {Array} body - Objects of the shape `{id, metadata}`. `metadata` is a list of metadata ops, as for patchMetadata. The tokens may lie in several documents of one project; every document touched has its version bumped and every new version comes back in `X-Document-Versions` (past fifty documents, only their number, in `X-Document-Versions-Omitted`). A `document-version` precondition is accepted only when every entry lies in one document. An unknown id refuses the whole update.
|
|
2903
|
+
* @returns {Promise<{count: number}>} How many tokens were updated.
|
|
2904
|
+
*/
|
|
2905
|
+
bulkUpdate: (body, auditMessage) =>
|
|
2906
|
+
this._request("PATCH", "/api/v1/tokens/bulk", {
|
|
2907
|
+
auditMessage,
|
|
2908
|
+
body,
|
|
2909
|
+
}),
|
|
1582
2910
|
/**
|
|
1583
2911
|
* Split a token at a Unicode code-point offset. The original token becomes the
|
|
1584
2912
|
* left half (keeps its ID, spans, vocab-links); the new right token's ID is returned.
|
|
1585
2913
|
* @param {string} tokenId - The token ID
|
|
1586
2914
|
* @param {number} position - Code-point offset to split at (strictly between begin and end)
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
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
|
+
) =>
|
|
2927
|
+
this._request("POST", `/api/v1/tokens/${tokenId}/split`, {
|
|
2928
|
+
auditMessage,
|
|
2929
|
+
body: bodyOf({ id, position }),
|
|
1591
2930
|
}),
|
|
1592
2931
|
/**
|
|
1593
2932
|
* Merge two tokens. The left token (smaller begin) survives with the combined
|
|
@@ -1598,8 +2937,9 @@ class PlaidClient {
|
|
|
1598
2937
|
* @param {string} otherTokenId - The other token to merge in
|
|
1599
2938
|
*/
|
|
1600
2939
|
merge: (tokenId, otherTokenId, auditMessage) =>
|
|
1601
|
-
this._request(
|
|
1602
|
-
|
|
2940
|
+
this._request("POST", `/api/v1/tokens/${tokenId}/merge`, {
|
|
2941
|
+
auditMessage,
|
|
2942
|
+
body: bodyOf({ "other-token-id": otherTokenId }),
|
|
1603
2943
|
}),
|
|
1604
2944
|
/**
|
|
1605
2945
|
* Shift a token's boundary. On partitioning layers the adjacent token is
|
|
@@ -1610,7 +2950,8 @@ class PlaidClient {
|
|
|
1610
2950
|
* @param {number} [end] - New end offset, exclusive (Unicode code points)
|
|
1611
2951
|
*/
|
|
1612
2952
|
shift: (tokenId, begin, end, auditMessage) =>
|
|
1613
|
-
this._request(
|
|
2953
|
+
this._request("POST", `/api/v1/tokens/${tokenId}/shift`, {
|
|
2954
|
+
auditMessage,
|
|
1614
2955
|
body: bodyOf({ begin, end }),
|
|
1615
2956
|
}),
|
|
1616
2957
|
/**
|
|
@@ -1619,25 +2960,27 @@ class PlaidClient {
|
|
|
1619
2960
|
* @param {any} body - The request body
|
|
1620
2961
|
*/
|
|
1621
2962
|
setMetadata: (tokenId, body, auditMessage) =>
|
|
1622
|
-
this._request(
|
|
1623
|
-
|
|
2963
|
+
this._request("PUT", `/api/v1/tokens/${tokenId}/metadata`, {
|
|
2964
|
+
auditMessage,
|
|
2965
|
+
rawBody: body,
|
|
1624
2966
|
}),
|
|
1625
2967
|
/**
|
|
1626
2968
|
* Remove all metadata from a token.
|
|
1627
2969
|
* @param {string} tokenId - The token ID
|
|
1628
2970
|
*/
|
|
1629
2971
|
deleteMetadata: (tokenId, auditMessage) =>
|
|
1630
|
-
this._request(
|
|
1631
|
-
|
|
2972
|
+
this._request("DELETE", `/api/v1/tokens/${tokenId}/metadata`, {
|
|
2973
|
+
auditMessage,
|
|
1632
2974
|
}),
|
|
1633
2975
|
/**
|
|
1634
|
-
*
|
|
2976
|
+
* Edit metadata for a token with a list of ops applied in order, in one operation. `{op: 'set', path, value}` writes value at path, creating missing objects along it; `{op: 'delete', path}` removes the key at path (a no-op when absent). A path is a non-empty array of keys, the first a top-level key. A path through a non-object is refused (400). See metadataOps and applyMetadataOps.
|
|
1635
2977
|
* @param {string} tokenId - The token ID
|
|
1636
|
-
* @param {any} body - The metadata
|
|
2978
|
+
* @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
|
|
1637
2979
|
*/
|
|
1638
2980
|
patchMetadata: (tokenId, body, auditMessage) =>
|
|
1639
|
-
this._request(
|
|
1640
|
-
|
|
2981
|
+
this._request("PATCH", `/api/v1/tokens/${tokenId}/metadata`, {
|
|
2982
|
+
auditMessage,
|
|
2983
|
+
rawBody: body,
|
|
1641
2984
|
}),
|
|
1642
2985
|
};
|
|
1643
2986
|
|
|
@@ -1661,12 +3004,17 @@ class PlaidClient {
|
|
|
1661
3004
|
* this a key such as `case-marker` would reach listeners as
|
|
1662
3005
|
* `caseMarker` in JS and `case_marker` in Python. `listen` restores it
|
|
1663
3006
|
* verbatim on the way in.
|
|
3007
|
+
*
|
|
3008
|
+
* A message is not saved and writes nothing to History, so it never
|
|
3009
|
+
* joins an open operation (`withOperation`). Made on a batch it still
|
|
3010
|
+
* queues, so a message queued after the writes goes out after them.
|
|
1664
3011
|
* @param {string} projectId - The UUID of the project to send to
|
|
1665
3012
|
* @param {any} data - The message data to send
|
|
1666
3013
|
* @returns {Promise<any>} Response from the send operation
|
|
1667
3014
|
*/
|
|
1668
|
-
sendMessage: (projectId, data
|
|
1669
|
-
this._request(
|
|
3015
|
+
sendMessage: (projectId, data) =>
|
|
3016
|
+
this._request("POST", `/api/v1/projects/${projectId}/message`, {
|
|
3017
|
+
noOperation: true,
|
|
1670
3018
|
rawBody: { body: data },
|
|
1671
3019
|
}),
|
|
1672
3020
|
|
|
@@ -1677,8 +3025,7 @@ class PlaidClient {
|
|
|
1677
3025
|
* @param {string} projectId - The UUID of the project to query
|
|
1678
3026
|
* @returns {Promise<Array>} Array of discovered service information
|
|
1679
3027
|
*/
|
|
1680
|
-
discoverServices: (projectId) =>
|
|
1681
|
-
discoverServices(this, projectId),
|
|
3028
|
+
discoverServices: (projectId) => discoverServices(this, projectId),
|
|
1682
3029
|
|
|
1683
3030
|
/**
|
|
1684
3031
|
* Forget a previously-seen (offline) service. Maintainer-only; 409 if
|
|
@@ -1692,26 +3039,102 @@ class PlaidClient {
|
|
|
1692
3039
|
|
|
1693
3040
|
/**
|
|
1694
3041
|
* Register as a service and handle incoming work requests.
|
|
3042
|
+
*
|
|
3043
|
+
* The registration reopens its channel whenever it drops, so a server
|
|
3044
|
+
* restart needs no service restart.
|
|
1695
3045
|
* @param {string} projectId - The UUID of the project to serve
|
|
1696
3046
|
* @param {Object} serviceInfo - Service information {serviceId, serviceName, description}
|
|
1697
3047
|
* @param {function} onServiceRequest - Callback (data, responseHelper)
|
|
1698
3048
|
* @param {Object} [extras] - Optional additional service metadata
|
|
1699
|
-
* @
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
3049
|
+
* @param {function} [onStatus] - Optional callback (event, projectId, detail) for
|
|
3050
|
+
* connection transitions: 'registered', 'reconnected', 'disconnected',
|
|
3051
|
+
* and 'stopped' when the server refuses the channel for good, which
|
|
3052
|
+
* ends the registration
|
|
3053
|
+
* @returns {Object} Service registration with .stop(), .isRunning(), .isConnected()
|
|
3054
|
+
*/
|
|
3055
|
+
serve: (projectId, serviceInfo, onServiceRequest, extras, onStatus) =>
|
|
3056
|
+
serve(this, projectId, serviceInfo, onServiceRequest, extras, onStatus),
|
|
1703
3057
|
|
|
1704
3058
|
/**
|
|
1705
3059
|
* Request a service to perform work and await its result.
|
|
3060
|
+
*
|
|
3061
|
+
* The request outlives this call: after a timeout, an abort, or a
|
|
3062
|
+
* dropped connection the service goes on, and `attachServiceRequest`
|
|
3063
|
+
* collects the result given the request id, which `opts.onAccepted`
|
|
3064
|
+
* receives as soon as the server has taken the request. Pass
|
|
3065
|
+
* `opts.requestId` (a UUID you mint) to know the id before submitting;
|
|
3066
|
+
* submitting an id that names a request you already made rejoins it
|
|
3067
|
+
* instead of starting another.
|
|
1706
3068
|
* @param {string} projectId - The UUID of the project
|
|
1707
3069
|
* @param {string} serviceId - The ID of the service to request
|
|
1708
3070
|
* @param {any} data - The request data
|
|
1709
|
-
* @param {number} [timeout] - Timeout in
|
|
3071
|
+
* @param {number} [timeout=10000] - Timeout in ms
|
|
1710
3072
|
* @param {function} [onProgress] - Called with each progress payload {percent, message}
|
|
1711
|
-
* @
|
|
3073
|
+
* @param {AbortSignal} [signal] - Abort to stop waiting; rejects with an AbortError
|
|
3074
|
+
* @param {Object} [opts] - {requestId, onAccepted, projectIds, noOperation}
|
|
3075
|
+
* @returns {Promise<any>} The service's result
|
|
3076
|
+
*/
|
|
3077
|
+
requestService: (
|
|
3078
|
+
projectId,
|
|
3079
|
+
serviceId,
|
|
3080
|
+
data,
|
|
3081
|
+
timeout,
|
|
3082
|
+
onProgress,
|
|
3083
|
+
signal,
|
|
3084
|
+
opts,
|
|
3085
|
+
) =>
|
|
3086
|
+
requestService(
|
|
3087
|
+
this,
|
|
3088
|
+
projectId,
|
|
3089
|
+
serviceId,
|
|
3090
|
+
data,
|
|
3091
|
+
timeout,
|
|
3092
|
+
onProgress,
|
|
3093
|
+
signal,
|
|
3094
|
+
opts,
|
|
3095
|
+
),
|
|
3096
|
+
|
|
3097
|
+
/**
|
|
3098
|
+
* Rejoin a service request made earlier and await its result: the
|
|
3099
|
+
* latest progress is replayed, then the result comes, or at once if the
|
|
3100
|
+
* request already finished. Only the user who submitted it (or an
|
|
3101
|
+
* admin). Rejects with an error whose `status` is 404 when the request
|
|
3102
|
+
* is unknown or expired (a finished request's result is kept for a
|
|
3103
|
+
* while, not forever).
|
|
3104
|
+
* @param {string} projectId - The UUID of the project
|
|
3105
|
+
* @param {string} requestId - The request id
|
|
3106
|
+
* @param {number} [timeout=10000] - Timeout in ms
|
|
3107
|
+
* @param {function} [onProgress] - Called with each progress payload
|
|
3108
|
+
* @param {AbortSignal} [signal] - Abort to stop waiting
|
|
3109
|
+
* @returns {Promise<any>} The service's result
|
|
3110
|
+
*/
|
|
3111
|
+
attachServiceRequest: (
|
|
3112
|
+
projectId,
|
|
3113
|
+
requestId,
|
|
3114
|
+
timeout,
|
|
3115
|
+
onProgress,
|
|
3116
|
+
signal,
|
|
3117
|
+
) =>
|
|
3118
|
+
attachServiceRequest(
|
|
3119
|
+
this,
|
|
3120
|
+
projectId,
|
|
3121
|
+
requestId,
|
|
3122
|
+
timeout,
|
|
3123
|
+
onProgress,
|
|
3124
|
+
signal,
|
|
3125
|
+
),
|
|
3126
|
+
|
|
3127
|
+
/**
|
|
3128
|
+
* Ask the service to stop a request made earlier. The request still
|
|
3129
|
+
* ends with whatever the service then reports, on the stream of whoever
|
|
3130
|
+
* is awaiting it. Rejects with 404 if unknown or expired, 409 once
|
|
3131
|
+
* finished.
|
|
3132
|
+
* @param {string} projectId - The UUID of the project
|
|
3133
|
+
* @param {string} requestId - The request id
|
|
3134
|
+
* @returns {Promise<void>}
|
|
1712
3135
|
*/
|
|
1713
|
-
|
|
1714
|
-
|
|
3136
|
+
cancelServiceRequest: (projectId, requestId) =>
|
|
3137
|
+
cancelServiceRequest(this, projectId, requestId),
|
|
1715
3138
|
};
|
|
1716
3139
|
|
|
1717
3140
|
/**
|
|
@@ -1746,11 +3169,15 @@ class PlaidClient {
|
|
|
1746
3169
|
* @param {Object} body - The query AST ({find, where, scope?, limit?, orderBy?,
|
|
1747
3170
|
* return?, bindings?}).
|
|
1748
3171
|
* @returns {Promise<Object>} For 'ids'/'entities': {columns, results, count, truncated}.
|
|
1749
|
-
* For 'count': {return: 'count', count}
|
|
1750
|
-
*
|
|
3172
|
+
* For 'count': {return: 'count', count, truncated}, where `truncated` says
|
|
3173
|
+
* the count stopped at the server's cap. Entity cells are full entity
|
|
3174
|
+
* objects (same shape as the GET endpoints).
|
|
1751
3175
|
*/
|
|
1752
|
-
|
|
1753
|
-
|
|
3176
|
+
// outOfBand: a query is a read that travels as a POST (see the note at
|
|
3177
|
+
// 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.
|
|
3179
|
+
this.query = (body) =>
|
|
3180
|
+
this._request("POST", "/api/v1/query", { body, outOfBand: true });
|
|
1754
3181
|
|
|
1755
3182
|
// Logical-operation groups (audit-log grouping). There is no create: a
|
|
1756
3183
|
// group row is made lazily by the first write carrying `?group-id=`
|
|
@@ -1760,15 +3187,406 @@ class PlaidClient {
|
|
|
1760
3187
|
* Get a logical-operation group (its label + creator).
|
|
1761
3188
|
* @param {string} id - The group id
|
|
1762
3189
|
*/
|
|
1763
|
-
get: (id) =>
|
|
1764
|
-
this._request('GET', `/api/v1/operation-groups/${id}`),
|
|
3190
|
+
get: (id) => this._request("GET", `/api/v1/operation-groups/${id}`),
|
|
1765
3191
|
/**
|
|
1766
3192
|
* Relabel a logical-operation group after the fact. Owner or admin only.
|
|
1767
3193
|
* @param {string} id - The group id
|
|
1768
3194
|
* @param {string|null} message - The new label
|
|
1769
3195
|
*/
|
|
1770
3196
|
update: (id, message) =>
|
|
1771
|
-
this._request(
|
|
3197
|
+
this._request("PATCH", `/api/v1/operation-groups/${id}`, {
|
|
3198
|
+
body: { message },
|
|
3199
|
+
}),
|
|
3200
|
+
};
|
|
3201
|
+
|
|
3202
|
+
this.comments = {
|
|
3203
|
+
/**
|
|
3204
|
+
* Post a comment on an entity. Requires write access to the entity's
|
|
3205
|
+
* project, or to the vocabulary for a vocab-item; the author is the
|
|
3206
|
+
* authenticated caller. Comments are not audited and do not bump the
|
|
3207
|
+
* document version.
|
|
3208
|
+
*
|
|
3209
|
+
* A comment outlives its anchor: deleting the entity does not delete
|
|
3210
|
+
* the comment. `anchorLabel` is the caption shown once that happens
|
|
3211
|
+
* ("Gloss of ktab, sentence 4"), so pass what the comment is about.
|
|
3212
|
+
* @param {'document'|'text'|'token'|'span'|'relation'|'vocab-item'} entityType - What kind of thing is being commented on
|
|
3213
|
+
* @param {string} entityId - The commented entity's id
|
|
3214
|
+
* @param {string} body - The comment text (1..10000 characters)
|
|
3215
|
+
* @param {object} [opts]
|
|
3216
|
+
* @param {string} [opts.anchorLabel] - What the comment is about, in words (at most 200 characters)
|
|
3217
|
+
*/
|
|
3218
|
+
create: (entityType, entityId, body, { anchorLabel, id } = {}) =>
|
|
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,
|
|
3223
|
+
body: bodyOf({
|
|
3224
|
+
id,
|
|
3225
|
+
"entity-type": entityType,
|
|
3226
|
+
"entity-id": entityId,
|
|
3227
|
+
body,
|
|
3228
|
+
"anchor-label": anchorLabel,
|
|
3229
|
+
}),
|
|
3230
|
+
}),
|
|
3231
|
+
/**
|
|
3232
|
+
* Read one comment.
|
|
3233
|
+
* @param {string} id - The comment id
|
|
3234
|
+
*/
|
|
3235
|
+
get: (id) => this._request("GET", `/api/v1/comments/${id}`),
|
|
3236
|
+
/**
|
|
3237
|
+
* Edit a comment's body. Only the comment's AUTHOR may do this - not
|
|
3238
|
+
* maintainers, not admins. Sets `edited` on the comment.
|
|
3239
|
+
* @param {string} id - The comment id
|
|
3240
|
+
* @param {string} body - The replacement text
|
|
3241
|
+
*/
|
|
3242
|
+
update: (id, body) =>
|
|
3243
|
+
this._request("PATCH", `/api/v1/comments/${id}`, {
|
|
3244
|
+
body: { body },
|
|
3245
|
+
noOperation: true,
|
|
3246
|
+
}),
|
|
3247
|
+
/**
|
|
3248
|
+
* Delete a comment. The author may delete their own; a project
|
|
3249
|
+
* maintainer (or admin) may delete any.
|
|
3250
|
+
* @param {string} id - The comment id
|
|
3251
|
+
*/
|
|
3252
|
+
delete: (id) =>
|
|
3253
|
+
this._request("DELETE", `/api/v1/comments/${id}`, { noOperation: true }),
|
|
3254
|
+
/**
|
|
3255
|
+
* List comments in a project, oldest first. Transparently follows
|
|
3256
|
+
* pagination cursors and returns the full flat array.
|
|
3257
|
+
* @param {string} projectId - The project to read
|
|
3258
|
+
* @param {object} [filters] - Narrow the scope
|
|
3259
|
+
* @param {string} [filters.documentId] - Every comment anywhere in one document
|
|
3260
|
+
* @param {string} [filters.entityType] - With entityId, one entity's thread
|
|
3261
|
+
* @param {string} [filters.entityId] - With entityType, one entity's thread
|
|
3262
|
+
*/
|
|
3263
|
+
list: (projectId, { documentId, entityType, entityId } = {}) =>
|
|
3264
|
+
listAll(this, `/api/v1/projects/${projectId}/comments`, {
|
|
3265
|
+
query: {
|
|
3266
|
+
"document-id": documentId,
|
|
3267
|
+
"entity-type": entityType,
|
|
3268
|
+
"entity-id": entityId,
|
|
3269
|
+
},
|
|
3270
|
+
}),
|
|
3271
|
+
/**
|
|
3272
|
+
* Fetch a single page of a project's comments.
|
|
3273
|
+
* @param {string} projectId - The project to read
|
|
3274
|
+
* @param {object} [opts]
|
|
3275
|
+
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
3276
|
+
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
3277
|
+
* @param {string} [opts.documentId] - Every comment anywhere in one document
|
|
3278
|
+
* @param {string} [opts.entityType] - With entityId, one entity's thread
|
|
3279
|
+
* @param {string} [opts.entityId] - With entityType, one entity's thread
|
|
3280
|
+
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
3281
|
+
*/
|
|
3282
|
+
listPage: (
|
|
3283
|
+
projectId,
|
|
3284
|
+
{ limit, cursor, documentId, entityType, entityId } = {},
|
|
3285
|
+
) =>
|
|
3286
|
+
listPage(this, `/api/v1/projects/${projectId}/comments`, {
|
|
3287
|
+
limit,
|
|
3288
|
+
cursor,
|
|
3289
|
+
query: {
|
|
3290
|
+
"document-id": documentId,
|
|
3291
|
+
"entity-type": entityType,
|
|
3292
|
+
"entity-id": entityId,
|
|
3293
|
+
},
|
|
3294
|
+
}),
|
|
3295
|
+
/**
|
|
3296
|
+
* Async-iterate a project's comments page by page; yields each page's entries array.
|
|
3297
|
+
* @param {string} projectId - The project to read
|
|
3298
|
+
* @param {object} [opts]
|
|
3299
|
+
* @param {number} [opts.pageSize] - Per-request page size
|
|
3300
|
+
* @param {string} [opts.documentId] - Every comment anywhere in one document
|
|
3301
|
+
* @param {string} [opts.entityType] - With entityId, one entity's thread
|
|
3302
|
+
* @param {string} [opts.entityId] - With entityType, one entity's thread
|
|
3303
|
+
* @returns {AsyncGenerator<Array>}
|
|
3304
|
+
*/
|
|
3305
|
+
iterPages: (
|
|
3306
|
+
projectId,
|
|
3307
|
+
{ pageSize, documentId, entityType, entityId } = {},
|
|
3308
|
+
) =>
|
|
3309
|
+
iterPages(this, `/api/v1/projects/${projectId}/comments`, {
|
|
3310
|
+
pageSize,
|
|
3311
|
+
query: {
|
|
3312
|
+
"document-id": documentId,
|
|
3313
|
+
"entity-type": entityType,
|
|
3314
|
+
"entity-id": entityId,
|
|
3315
|
+
},
|
|
3316
|
+
}),
|
|
3317
|
+
/**
|
|
3318
|
+
* Comment counts per entity, as an `{entityId: n}` map, over the same
|
|
3319
|
+
* scope and filters as `list`. One cheap request paints a comment
|
|
3320
|
+
* indicator on every annotated item in a document without paging
|
|
3321
|
+
* through the bodies.
|
|
3322
|
+
*
|
|
3323
|
+
* The response is NOT key-transformed: its keys are entity ids, and
|
|
3324
|
+
* camelCasing would mangle the hyphens in a UUID.
|
|
3325
|
+
* @param {string} projectId - The project to read
|
|
3326
|
+
* @param {object} [filters] - Same filters as list()
|
|
3327
|
+
* @returns {Promise<Object<string, number>>}
|
|
3328
|
+
*/
|
|
3329
|
+
counts: (projectId, { documentId, entityType, entityId } = {}) =>
|
|
3330
|
+
this._request("GET", `/api/v1/projects/${projectId}/comments/counts`, {
|
|
3331
|
+
queryParams: {
|
|
3332
|
+
"document-id": documentId,
|
|
3333
|
+
"entity-type": entityType,
|
|
3334
|
+
"entity-id": entityId,
|
|
3335
|
+
},
|
|
3336
|
+
skipResponseTransform: true,
|
|
3337
|
+
}),
|
|
3338
|
+
/**
|
|
3339
|
+
* List the comments on a vocabulary's entries, oldest first. Requires
|
|
3340
|
+
* read access to the vocabulary. Transparently follows pagination
|
|
3341
|
+
* cursors and returns the full flat array.
|
|
3342
|
+
* @param {string} vocabId - The vocab layer to read
|
|
3343
|
+
* @param {object} [filters]
|
|
3344
|
+
* @param {string} [filters.entityId] - One entry's thread
|
|
3345
|
+
*/
|
|
3346
|
+
listInVocab: (vocabId, { entityId } = {}) =>
|
|
3347
|
+
listAll(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
|
|
3348
|
+
query: { "entity-id": entityId },
|
|
3349
|
+
}),
|
|
3350
|
+
/**
|
|
3351
|
+
* Fetch a single page of a vocabulary's comments.
|
|
3352
|
+
* @param {string} vocabId - The vocab layer to read
|
|
3353
|
+
* @param {object} [opts]
|
|
3354
|
+
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
3355
|
+
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
3356
|
+
* @param {string} [opts.entityId] - One entry's thread
|
|
3357
|
+
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
3358
|
+
*/
|
|
3359
|
+
listInVocabPage: (vocabId, { limit, cursor, entityId } = {}) =>
|
|
3360
|
+
listPage(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
|
|
3361
|
+
limit,
|
|
3362
|
+
cursor,
|
|
3363
|
+
query: { "entity-id": entityId },
|
|
3364
|
+
}),
|
|
3365
|
+
/**
|
|
3366
|
+
* Async-iterate a vocabulary's comments page by page, oldest first;
|
|
3367
|
+
* yields each page's entries array.
|
|
3368
|
+
* @param {string} vocabId - The vocab layer to read
|
|
3369
|
+
* @param {object} [opts]
|
|
3370
|
+
* @param {string} [opts.entityId] - One entry's thread
|
|
3371
|
+
* @param {number} [opts.pageSize] - Per-request page size
|
|
3372
|
+
* @returns {AsyncGenerator<Array>}
|
|
3373
|
+
*/
|
|
3374
|
+
iterInVocabPages: (vocabId, { entityId, pageSize } = {}) =>
|
|
3375
|
+
iterPages(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
|
|
3376
|
+
pageSize,
|
|
3377
|
+
query: { "entity-id": entityId },
|
|
3378
|
+
}),
|
|
3379
|
+
/**
|
|
3380
|
+
* Comment counts per entry of a vocabulary, as an `{entityId: n}` map.
|
|
3381
|
+
* The response is NOT key-transformed: its keys are entry ids.
|
|
3382
|
+
* @param {string} vocabId - The vocab layer to read
|
|
3383
|
+
* @param {object} [filters]
|
|
3384
|
+
* @param {string} [filters.entityId] - One entry only
|
|
3385
|
+
* @returns {Promise<Object<string, number>>}
|
|
3386
|
+
*/
|
|
3387
|
+
countsInVocab: (vocabId, { entityId } = {}) =>
|
|
3388
|
+
this._request(
|
|
3389
|
+
"GET",
|
|
3390
|
+
`/api/v1/vocab-layers/${vocabId}/comments/counts`,
|
|
3391
|
+
{
|
|
3392
|
+
queryParams: { "entity-id": entityId },
|
|
3393
|
+
skipResponseTransform: true,
|
|
3394
|
+
},
|
|
3395
|
+
),
|
|
3396
|
+
};
|
|
3397
|
+
|
|
3398
|
+
this.events = {
|
|
3399
|
+
/**
|
|
3400
|
+
* Record one research-telemetry event (core manual, "Research
|
|
3401
|
+
* telemetry"). Fire and forget: the event is buffered and sent with
|
|
3402
|
+
* others every ten seconds, at fifty events, and when the page is
|
|
3403
|
+
* hidden. A batch that fails is dropped, nothing is retried, and
|
|
3404
|
+
* nothing is ever thrown. Sends nothing for a project whose switch
|
|
3405
|
+
* (`config.plaid.research.telemetry`) is off, which it reads from the
|
|
3406
|
+
* project itself. `suggestion.shown` is recorded once per target,
|
|
3407
|
+
* field and value in a page session.
|
|
3408
|
+
* @param {'suggestion.shown'|'suggestion.adopted'|'suggestion.dismissed'|'plan.opened'} type - The event type
|
|
3409
|
+
* @param {object} fields
|
|
3410
|
+
* @param {string} fields.projectId - The project it happened in
|
|
3411
|
+
* @param {string} [fields.documentId] - The document it happened in
|
|
3412
|
+
* @param {string} [fields.targetId] - What it is about: a token, a plan
|
|
3413
|
+
* @param {object} [fields.data] - Its details, keyed by single lowercase words (`value`, `source`, `field`, `written`, `conversation`)
|
|
3414
|
+
* @returns {boolean} Whether the event was buffered
|
|
3415
|
+
*/
|
|
3416
|
+
record: (type, fields) => recorderFor(this).record(this, type, fields),
|
|
3417
|
+
/**
|
|
3418
|
+
* Send what the recorder holds now, rather than at its next interval.
|
|
3419
|
+
*/
|
|
3420
|
+
flush: () => recorderFor(this).flush(),
|
|
3421
|
+
/**
|
|
3422
|
+
* Tell the recorder a project's switch at once, e.g. right after a
|
|
3423
|
+
* maintainer changed it. Turning it off drops what is buffered.
|
|
3424
|
+
* @param {string} projectId - The project
|
|
3425
|
+
* @param {boolean} on - Whether telemetry is on
|
|
3426
|
+
*/
|
|
3427
|
+
setEnabled: (projectId, on) => recorderFor(this).setEnabled(projectId, on),
|
|
3428
|
+
/**
|
|
3429
|
+
* Record events directly, with no buffering. Requires write access, and
|
|
3430
|
+
* the server refuses with a 403 while the project's switch is off. All
|
|
3431
|
+
* or nothing: one bad event refuses the request with a 400 naming it.
|
|
3432
|
+
* @param {string} projectId - The project the events happened in
|
|
3433
|
+
* @param {Array<{type: string, documentId?: string, targetId?: string, data?: object, clientTs?: string}>} events - At most 500 events
|
|
3434
|
+
* @returns {Promise<{count: number}>}
|
|
3435
|
+
*/
|
|
3436
|
+
create: (projectId, events) =>
|
|
3437
|
+
this._request("POST", `/api/v1/projects/${projectId}/events`, {
|
|
3438
|
+
body: events,
|
|
3439
|
+
outOfBand: true,
|
|
3440
|
+
}),
|
|
3441
|
+
/**
|
|
3442
|
+
* A project's events in arrival order. Maintainer or admin only.
|
|
3443
|
+
* Transparently follows pagination cursors and returns the full flat
|
|
3444
|
+
* array.
|
|
3445
|
+
* @param {string} projectId - The project to read
|
|
3446
|
+
* @param {object} [filters]
|
|
3447
|
+
* @param {string|string[]} [filters.types] - Only these types
|
|
3448
|
+
* @param {string} [filters.startTime] - Only events the server stamped at or after this instant
|
|
3449
|
+
* @param {string} [filters.endTime] - Only events the server stamped at or before this instant
|
|
3450
|
+
*/
|
|
3451
|
+
list: (projectId, { types, startTime, endTime } = {}) =>
|
|
3452
|
+
listAll(this, `/api/v1/projects/${projectId}/events`, {
|
|
3453
|
+
query: eventQuery(types, startTime, endTime),
|
|
3454
|
+
}),
|
|
3455
|
+
/**
|
|
3456
|
+
* One page of a project's events. Maintainer or admin only.
|
|
3457
|
+
* @param {string} projectId - The project to read
|
|
3458
|
+
* @param {object} [opts]
|
|
3459
|
+
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
3460
|
+
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
3461
|
+
* @param {string|string[]} [opts.types] - Only these types
|
|
3462
|
+
* @param {string} [opts.startTime] - Only events at or after this instant
|
|
3463
|
+
* @param {string} [opts.endTime] - Only events at or before this instant
|
|
3464
|
+
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
3465
|
+
*/
|
|
3466
|
+
listPage: (
|
|
3467
|
+
projectId,
|
|
3468
|
+
{ limit, cursor, types, startTime, endTime } = {},
|
|
3469
|
+
) =>
|
|
3470
|
+
listPage(this, `/api/v1/projects/${projectId}/events`, {
|
|
3471
|
+
limit,
|
|
3472
|
+
cursor,
|
|
3473
|
+
query: eventQuery(types, startTime, endTime),
|
|
3474
|
+
}),
|
|
3475
|
+
/**
|
|
3476
|
+
* Async-iterate a project's events page by page; yields each page's entries array.
|
|
3477
|
+
* @param {string} projectId - The project to read
|
|
3478
|
+
* @param {object} [opts]
|
|
3479
|
+
* @param {number} [opts.pageSize] - Per-request page size
|
|
3480
|
+
* @param {string|string[]} [opts.types] - Only these types
|
|
3481
|
+
* @param {string} [opts.startTime] - Only events at or after this instant
|
|
3482
|
+
* @param {string} [opts.endTime] - Only events at or before this instant
|
|
3483
|
+
* @returns {AsyncGenerator<Array>}
|
|
3484
|
+
*/
|
|
3485
|
+
iterPages: (projectId, { pageSize, types, startTime, endTime } = {}) =>
|
|
3486
|
+
iterPages(this, `/api/v1/projects/${projectId}/events`, {
|
|
3487
|
+
pageSize,
|
|
3488
|
+
query: eventQuery(types, startTime, endTime),
|
|
3489
|
+
}),
|
|
3490
|
+
};
|
|
3491
|
+
|
|
3492
|
+
this.guidelines = {
|
|
3493
|
+
/**
|
|
3494
|
+
* Create a guideline in a project. `title` is the handle an assistant
|
|
3495
|
+
* asks for one by, and the only thing a person has to keep current. It is
|
|
3496
|
+
* NOT required to be unique, so a title already in use is written like
|
|
3497
|
+
* any other: warn about it, do not refuse it. A `pinned` guideline is one
|
|
3498
|
+
* the assistant is given in full on every turn.
|
|
3499
|
+
* @param {string} projectId - The project the guideline belongs to
|
|
3500
|
+
* @param {string} title - The handle an assistant asks for one by (1..100 characters)
|
|
3501
|
+
* @param {object} [opts]
|
|
3502
|
+
* @param {string} [opts.body] - The Markdown text (up to 20000 characters; may be empty)
|
|
3503
|
+
* @param {boolean} [opts.pinned] - Send this one to the assistant in full on every turn
|
|
3504
|
+
*/
|
|
3505
|
+
create: (projectId, title, { body, pinned, id } = {}, auditMessage) =>
|
|
3506
|
+
this._request("POST", `/api/v1/projects/${projectId}/guidelines`, {
|
|
3507
|
+
auditMessage,
|
|
3508
|
+
body: bodyOf({ id, title, body, pinned }),
|
|
3509
|
+
}),
|
|
3510
|
+
/**
|
|
3511
|
+
* Read one guideline, Markdown body included.
|
|
3512
|
+
* @param {string} id - The guideline id
|
|
3513
|
+
*/
|
|
3514
|
+
get: (id) => this._request("GET", `/api/v1/guidelines/${id}`),
|
|
3515
|
+
/**
|
|
3516
|
+
* Update a guideline. Every field is optional and an omitted one is
|
|
3517
|
+
* left alone, so an edit to the body need not restate the title.
|
|
3518
|
+
*
|
|
3519
|
+
* Pass `expectedUpdatedAt` (the `updatedAt` you last read) when a person
|
|
3520
|
+
* has been editing prose: the write then fails with 409 rather than
|
|
3521
|
+
* overwriting somebody who saved in between. Leave it off for a pin
|
|
3522
|
+
* toggle or a script, which have nothing of anyone's to lose.
|
|
3523
|
+
* @param {string} id - The guideline id
|
|
3524
|
+
* @param {object} [changes]
|
|
3525
|
+
* @param {string} [changes.title] - The new handle
|
|
3526
|
+
* @param {string} [changes.body] - The new Markdown text
|
|
3527
|
+
* @param {boolean} [changes.pinned] - Whether the assistant always gets it in full
|
|
3528
|
+
* @param {string} [changes.expectedUpdatedAt] - Write only if this is still the stored updatedAt
|
|
3529
|
+
*/
|
|
3530
|
+
update: (
|
|
3531
|
+
id,
|
|
3532
|
+
{ title, body, pinned, expectedUpdatedAt } = {},
|
|
3533
|
+
auditMessage,
|
|
3534
|
+
) =>
|
|
3535
|
+
this._request("PATCH", `/api/v1/guidelines/${id}`, {
|
|
3536
|
+
auditMessage,
|
|
3537
|
+
queryParams: { "updated-at": expectedUpdatedAt },
|
|
3538
|
+
body: bodyOf({ title, body, pinned }),
|
|
3539
|
+
}),
|
|
3540
|
+
/**
|
|
3541
|
+
* Delete a guideline.
|
|
3542
|
+
* @param {string} id - The guideline id
|
|
3543
|
+
*/
|
|
3544
|
+
delete: (id, auditMessage) =>
|
|
3545
|
+
this._request("DELETE", `/api/v1/guidelines/${id}`, { auditMessage }),
|
|
3546
|
+
/**
|
|
3547
|
+
* List a project's guidelines, by title. Transparently follows
|
|
3548
|
+
* pagination cursors and returns the full flat array.
|
|
3549
|
+
*
|
|
3550
|
+
* Without `includeBodies` each entry carries `bodyChars`, the length of
|
|
3551
|
+
* its body, so a caller can budget before fetching any. Pinned
|
|
3552
|
+
* guidelines are NOT sorted first: `pinned` is on every entry and
|
|
3553
|
+
* grouping is the caller's.
|
|
3554
|
+
* @param {string} projectId - The project to read
|
|
3555
|
+
* @param {object} [opts]
|
|
3556
|
+
* @param {boolean} [opts.includeBodies] - Return each body instead of its length
|
|
3557
|
+
*/
|
|
3558
|
+
list: (projectId, { includeBodies } = {}) =>
|
|
3559
|
+
listAll(this, `/api/v1/projects/${projectId}/guidelines`, {
|
|
3560
|
+
query: { "include-bodies": includeBodies },
|
|
3561
|
+
}),
|
|
3562
|
+
/**
|
|
3563
|
+
* Fetch a single page of a project's guidelines.
|
|
3564
|
+
* @param {string} projectId - The project to read
|
|
3565
|
+
* @param {object} [opts]
|
|
3566
|
+
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
3567
|
+
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
3568
|
+
* @param {boolean} [opts.includeBodies] - Return each body instead of its length
|
|
3569
|
+
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
3570
|
+
*/
|
|
3571
|
+
listPage: (projectId, { limit, cursor, includeBodies } = {}) =>
|
|
3572
|
+
listPage(this, `/api/v1/projects/${projectId}/guidelines`, {
|
|
3573
|
+
limit,
|
|
3574
|
+
cursor,
|
|
3575
|
+
query: { "include-bodies": includeBodies },
|
|
3576
|
+
}),
|
|
3577
|
+
/**
|
|
3578
|
+
* Async-iterate a project's guidelines page by page; yields each page's entries array.
|
|
3579
|
+
* @param {string} projectId - The project to read
|
|
3580
|
+
* @param {object} [opts]
|
|
3581
|
+
* @param {number} [opts.pageSize] - Per-request page size
|
|
3582
|
+
* @param {boolean} [opts.includeBodies] - Return each body instead of its length
|
|
3583
|
+
* @returns {AsyncGenerator<Array>}
|
|
3584
|
+
*/
|
|
3585
|
+
iterPages: (projectId, { pageSize, includeBodies } = {}) =>
|
|
3586
|
+
iterPages(this, `/api/v1/projects/${projectId}/guidelines`, {
|
|
3587
|
+
pageSize,
|
|
3588
|
+
query: { "include-bodies": includeBodies },
|
|
3589
|
+
}),
|
|
1772
3590
|
};
|
|
1773
3591
|
}
|
|
1774
3592
|
|
|
@@ -1811,25 +3629,80 @@ class PlaidClient {
|
|
|
1811
3629
|
* write, so an operation that is never ended (crash, closed tab) is still
|
|
1812
3630
|
* labeled in the log.
|
|
1813
3631
|
*
|
|
3632
|
+
* "Every write" is the writes of project data. Reads never join, and neither
|
|
3633
|
+
* do the out-of-band signals shaped like a write (a document lock taken or
|
|
3634
|
+
* renewed, a stopped service request, a service reporting itself, an admin
|
|
3635
|
+
* control), and neither does a broadcast message (`messages.sendMessage`):
|
|
3636
|
+
* none of them is audited, so there would be nothing under the label.
|
|
3637
|
+
*
|
|
3638
|
+
* `kind` says what kind of operation this is, for a program reading the
|
|
3639
|
+
* log: one of `assistant-plan`, `service-run`, `import`, `bulk-edit`,
|
|
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.
|
|
3654
|
+
*
|
|
1814
3655
|
* @param {string} message - Human label for the operation.
|
|
1815
|
-
* @param {object} [opts] - Optional `{ id }
|
|
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.
|
|
1816
3657
|
* @returns {string} The operation's group id.
|
|
1817
3658
|
*/
|
|
1818
|
-
beginOperation(message, { id } = {}) {
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
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;
|
|
1822
3681
|
}
|
|
1823
3682
|
this.operationGroup = {
|
|
1824
3683
|
id: id || crypto.randomUUID(),
|
|
1825
3684
|
message: message == null ? null : String(message),
|
|
3685
|
+
kind: kind == null ? null : String(kind),
|
|
3686
|
+
ref: ref == null ? null : String(ref),
|
|
1826
3687
|
depth: 1,
|
|
1827
3688
|
written: false,
|
|
1828
3689
|
refined: undefined,
|
|
3690
|
+
frames: [frame],
|
|
1829
3691
|
};
|
|
3692
|
+
this._openedFrame = frame;
|
|
1830
3693
|
return this.operationGroup.id;
|
|
1831
3694
|
}
|
|
1832
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
|
+
|
|
1833
3706
|
/**
|
|
1834
3707
|
* End the current logical operation. With no argument this is purely local
|
|
1835
3708
|
* (no request). Pass a refined `message` to relabel the group now that the
|
|
@@ -1843,6 +3716,12 @@ class PlaidClient {
|
|
|
1843
3716
|
const group = this.operationGroup;
|
|
1844
3717
|
if (!group) return;
|
|
1845
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
|
+
}
|
|
1846
3725
|
group.depth -= 1;
|
|
1847
3726
|
return;
|
|
1848
3727
|
}
|
|
@@ -1865,157 +3744,227 @@ class PlaidClient {
|
|
|
1865
3744
|
* callback to refine the label once the outcome is known.
|
|
1866
3745
|
*
|
|
1867
3746
|
* await client.withOperation('Merge morphemes', async (setMessage) => {
|
|
1868
|
-
* await client.batched(() => { ... });
|
|
3747
|
+
* await client.batched(async (b) => { ... });
|
|
1869
3748
|
* setMessage(`Merged ${n} morphemes`);
|
|
1870
3749
|
* });
|
|
1871
3750
|
*
|
|
3751
|
+
* The kind and reference (see beginOperation) come after `fn`:
|
|
3752
|
+
*
|
|
3753
|
+
* await client.withOperation('Import ELAN corpus', run, { kind: 'import', ref: 'format:elan' });
|
|
3754
|
+
*
|
|
1872
3755
|
* @param {string} message - Human label for the operation.
|
|
1873
3756
|
* @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, id, keys }`, as for beginOperation.
|
|
1874
3758
|
* @returns {Promise<any>} Whatever `fn` resolves to.
|
|
1875
3759
|
*/
|
|
1876
|
-
async withOperation(message, fn) {
|
|
1877
|
-
this.beginOperation(message);
|
|
3760
|
+
async withOperation(message, fn, { kind, ref, id, keys, minted } = {}) {
|
|
3761
|
+
this.beginOperation(message, { kind, ref, id, keys, minted });
|
|
1878
3762
|
const group = this.operationGroup;
|
|
1879
|
-
|
|
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;
|
|
3767
|
+
const setMessage = (msg) => {
|
|
3768
|
+
if (group.depth === 1) group.refined = msg;
|
|
3769
|
+
};
|
|
3770
|
+
let result;
|
|
1880
3771
|
try {
|
|
1881
|
-
|
|
1882
|
-
}
|
|
1883
|
-
|
|
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;
|
|
1884
3784
|
}
|
|
3785
|
+
dropFrame(group, frame);
|
|
3786
|
+
await this.endOperation();
|
|
3787
|
+
return result;
|
|
1885
3788
|
}
|
|
1886
3789
|
|
|
1887
|
-
/** Begin a batch of operations. Subsequent API calls will be queued. */
|
|
1888
|
-
beginBatch() {
|
|
1889
|
-
this.isBatching = true;
|
|
1890
|
-
this.batchOperations = [];
|
|
1891
|
-
// Strict mode stamps the expected document-version on the FIRST write of
|
|
1892
|
-
// the batch only (see _request) — reset the marker per batch.
|
|
1893
|
-
this.batchVersionStamped = false;
|
|
1894
|
-
}
|
|
1895
3790
|
|
|
1896
3791
|
/**
|
|
1897
|
-
*
|
|
1898
|
-
*
|
|
1899
|
-
*
|
|
3792
|
+
* Open a batch: a view of this client with the same bundles, on which every
|
|
3793
|
+
* write of project data queues instead of going out. `submit()` sends the
|
|
3794
|
+
* queued operations as ONE atomic request (larger than the server's cap,
|
|
3795
|
+
* as consecutive requests with the results concatenated in queue order,
|
|
3796
|
+
* each atomic on its own, so a failure in a later one leaves the earlier
|
|
3797
|
+
* ones committed, and the error it throws carries `committed`, the count
|
|
3798
|
+
* saved, and `committedResults`, their results) and
|
|
3799
|
+
* resolves to one result per operation; `abort()` drops them. A call made
|
|
3800
|
+
* on the client itself is never touched by an open batch, and a read or an
|
|
3801
|
+
* out-of-band signal made on the batch goes over the wire now (see the note
|
|
3802
|
+
* at the top of `http.js`).
|
|
3803
|
+
*
|
|
3804
|
+
* const b = client.batch();
|
|
3805
|
+
* b.tokens.bulkCreate(sentenceOps);
|
|
3806
|
+
* b.tokens.bulkCreate(wordOps);
|
|
3807
|
+
* const [sentRes, wordRes] = await b.submit();
|
|
3808
|
+
*
|
|
3809
|
+
* Server-side a batch runs sequentially in one transaction: a child op sees
|
|
3810
|
+
* parents created earlier in the same batch, and any op's failure rolls the
|
|
3811
|
+
* whole batch back. A batch is not nestable. Prefer `batched()`, which
|
|
3812
|
+
* submits or aborts for you.
|
|
3813
|
+
* @returns {PlaidClient} the batch view
|
|
1900
3814
|
*/
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
}
|
|
3815
|
+
batch() {
|
|
3816
|
+
return openBatch(this);
|
|
3817
|
+
}
|
|
1905
3818
|
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
3819
|
+
/**
|
|
3820
|
+
* POST one batch request (at most MAX_BATCH_OPS operations). Resolves to
|
|
3821
|
+
* one `{ status, headers, body }` per operation, with only the body
|
|
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.
|
|
3825
|
+
*/
|
|
3826
|
+
async _postBatch(url, body, { key = uuidv7() } = {}) {
|
|
3827
|
+
const fetchOptions = {
|
|
3828
|
+
method: "POST",
|
|
3829
|
+
headers: {
|
|
3830
|
+
Authorization: `Bearer ${this.token}`,
|
|
3831
|
+
"Content-Type": "application/json",
|
|
3832
|
+
[IDEMPOTENCY_HEADER]: key,
|
|
3833
|
+
},
|
|
3834
|
+
body: JSON.stringify(body),
|
|
3835
|
+
};
|
|
1910
3836
|
|
|
1911
3837
|
try {
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
3838
|
+
// Retry a 503: the batch is atomic, so a refused one wrote nothing and
|
|
3839
|
+
// repeating it is safe. A fresh timeout signal per attempt — an
|
|
3840
|
+
// AbortSignal.timeout stays aborted once it fires.
|
|
3841
|
+
const attempt = async () => {
|
|
3842
|
+
const signal = timeoutSignal(this.batchTimeout);
|
|
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
|
+
}
|
|
3852
|
+
if (!res.ok) {
|
|
3853
|
+
throw makeHttpError(res, await parseErrorBody(res), url, "POST");
|
|
3854
|
+
}
|
|
3855
|
+
return res;
|
|
1926
3856
|
};
|
|
1927
|
-
const
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
3857
|
+
const response = await retryUnknown(() => retryWhileBusy(attempt), {
|
|
3858
|
+
delaysMs: this.retryDelaysMs,
|
|
3859
|
+
}).catch((error) => {
|
|
3860
|
+
error.idempotencyKey = key;
|
|
3861
|
+
// 401 means the token is missing, expired or invalid: fire the app's
|
|
3862
|
+
// auth-error handler once, exactly as makeRequest does.
|
|
3863
|
+
if (
|
|
3864
|
+
error?.status === 401 &&
|
|
3865
|
+
typeof this.onAuthError === "function" &&
|
|
3866
|
+
!this._authErrorFired
|
|
3867
|
+
) {
|
|
3868
|
+
this._authErrorFired = true;
|
|
3869
|
+
try {
|
|
3870
|
+
this.onAuthError(error);
|
|
3871
|
+
} catch (_) {
|
|
3872
|
+
/* handler must not mask the original error */
|
|
3873
|
+
}
|
|
1934
3874
|
}
|
|
3875
|
+
throw error;
|
|
3876
|
+
});
|
|
3877
|
+
|
|
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";
|
|
1935
3881
|
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
3882
|
+
// Extract document versions from each batch response
|
|
3883
|
+
for (const result of results) {
|
|
3884
|
+
if (result.headers && result.headers["X-Document-Versions"]) {
|
|
3885
|
+
try {
|
|
3886
|
+
const versionsMap = JSON.parse(
|
|
3887
|
+
result.headers["X-Document-Versions"],
|
|
3888
|
+
);
|
|
3889
|
+
if (typeof versionsMap === "object" && versionsMap !== null) {
|
|
3890
|
+
// Clone once per response, then merge — not once per entry.
|
|
3891
|
+
this.documentVersions = mergeVersions(
|
|
3892
|
+
this.documentVersions,
|
|
3893
|
+
versionsMap,
|
|
3894
|
+
replayed,
|
|
3895
|
+
);
|
|
1949
3896
|
}
|
|
3897
|
+
} catch (e) {
|
|
3898
|
+
console.warn(
|
|
3899
|
+
"Failed to parse document versions header from batch response:",
|
|
3900
|
+
e,
|
|
3901
|
+
);
|
|
1950
3902
|
}
|
|
1951
3903
|
}
|
|
1952
|
-
|
|
1953
|
-
return results.map(result => transformResponse(result));
|
|
1954
|
-
} catch (error) {
|
|
1955
|
-
if (error.status) throw error;
|
|
1956
|
-
throw makeNetworkError(error, url, 'POST');
|
|
1957
3904
|
}
|
|
1958
|
-
} finally {
|
|
1959
|
-
this.isBatching = false;
|
|
1960
|
-
this.batchOperations = [];
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
/** Abort the current batch without executing any operations. */
|
|
1965
|
-
abortBatch() {
|
|
1966
|
-
this.isBatching = false;
|
|
1967
|
-
this.batchOperations = [];
|
|
1968
|
-
}
|
|
1969
3905
|
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
3906
|
+
const out = results.map((result) => ({
|
|
3907
|
+
...result,
|
|
3908
|
+
body: transformResponse(result.body),
|
|
3909
|
+
}));
|
|
3910
|
+
return replayed ? markReplayed(out) : out;
|
|
3911
|
+
} catch (error) {
|
|
3912
|
+
if (error.status !== undefined) throw error;
|
|
3913
|
+
throw Object.assign(makeNetworkError(error, url, "POST"), {
|
|
3914
|
+
idempotencyKey: key,
|
|
3915
|
+
});
|
|
3916
|
+
}
|
|
1976
3917
|
}
|
|
1977
3918
|
|
|
1978
3919
|
/**
|
|
1979
|
-
* Run `fn` with a batch
|
|
1980
|
-
* request
|
|
1981
|
-
*
|
|
1982
|
-
*
|
|
1983
|
-
* array (`[]` if `fn` queued nothing).
|
|
3920
|
+
* Run `fn` with a batch, then submit all queued ops as `submit()` does (ONE
|
|
3921
|
+
* atomic request up to MAX_BATCH_OPS operations, consecutive requests past
|
|
3922
|
+
* it), or abort the batch if `fn` throws. `fn` receives the batch and makes its
|
|
3923
|
+
* writes on it; it must NOT call `submit()` itself. Resolves to the batch
|
|
3924
|
+
* results array (`[]` if `fn` queued nothing).
|
|
1984
3925
|
*
|
|
1985
|
-
* const [sentRes, wordRes] = await client.batched(async () => {
|
|
1986
|
-
*
|
|
1987
|
-
*
|
|
3926
|
+
* const [sentRes, wordRes] = await client.batched(async (b) => {
|
|
3927
|
+
* b.tokens.bulkCreate(sentenceOps);
|
|
3928
|
+
* b.tokens.bulkCreate(wordOps);
|
|
1988
3929
|
* });
|
|
1989
3930
|
*
|
|
1990
|
-
*
|
|
1991
|
-
*
|
|
1992
|
-
* batch
|
|
1993
|
-
* low-level batch resource.
|
|
1994
|
-
* @param {() => (void | Promise<void>)} fn
|
|
3931
|
+
* A write made on `client` inside `fn` is not part of the batch: it goes
|
|
3932
|
+
* over the wire at once, as it would anywhere else. See `batch()`.
|
|
3933
|
+
* @param {(batch: PlaidClient) => (void | Promise<void>)} fn
|
|
1995
3934
|
* @returns {Promise<Array>}
|
|
1996
3935
|
*/
|
|
1997
3936
|
async batched(fn) {
|
|
1998
|
-
this.
|
|
3937
|
+
const batch = this.batch();
|
|
1999
3938
|
try {
|
|
2000
|
-
await fn();
|
|
3939
|
+
await fn(batch);
|
|
2001
3940
|
} catch (e) {
|
|
2002
|
-
|
|
2003
|
-
// later plain calls don't queue into a never-submitted batch.
|
|
2004
|
-
if (this.isBatchMode()) this.abortBatch();
|
|
3941
|
+
batch.abort();
|
|
2005
3942
|
throw e;
|
|
2006
3943
|
}
|
|
2007
|
-
return
|
|
3944
|
+
return batch.submit();
|
|
2008
3945
|
}
|
|
2009
3946
|
|
|
2010
3947
|
/**
|
|
2011
|
-
*
|
|
2012
|
-
*
|
|
2013
|
-
*
|
|
2014
|
-
* @param {string}
|
|
2015
|
-
* @param {
|
|
2016
|
-
|
|
2017
|
-
|
|
3948
|
+
* Liveness, version and database size, with NO authentication and no client
|
|
3949
|
+
* instance — for a launcher or status page checking whether a server is up
|
|
3950
|
+
* before anyone logs in.
|
|
3951
|
+
* @param {string} baseUrl - The API base URL
|
|
3952
|
+
* @param {object} [options]
|
|
3953
|
+
*/
|
|
3954
|
+
static async health(baseUrl, options = {}) {
|
|
3955
|
+
return anonymousGet(baseUrl, "/health", options);
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
/**
|
|
3959
|
+
* The limits this server enforces, with NO authentication and no client
|
|
3960
|
+
* instance. See the instance method for the shape.
|
|
3961
|
+
* @param {string} baseUrl - The API base URL
|
|
3962
|
+
* @param {object} [options]
|
|
2018
3963
|
*/
|
|
3964
|
+
static async info(baseUrl, options = {}) {
|
|
3965
|
+
return anonymousGet(baseUrl, "/api/v1/info", options);
|
|
3966
|
+
}
|
|
3967
|
+
|
|
2019
3968
|
/**
|
|
2020
3969
|
* Build the link to hand someone for an invite code. The server never sees
|
|
2021
3970
|
* an app URL, so the app that minted the invite is the one that names it.
|
|
@@ -2026,7 +3975,7 @@ class PlaidClient {
|
|
|
2026
3975
|
* @returns {string}
|
|
2027
3976
|
*/
|
|
2028
3977
|
static inviteUrl(appUrl, code) {
|
|
2029
|
-
return `${appUrl.replace(/#.*$/,
|
|
3978
|
+
return `${appUrl.replace(/#.*$/, "").replace(/\/$/, "")}/#/invite/${encodeURIComponent(code)}`;
|
|
2030
3979
|
}
|
|
2031
3980
|
|
|
2032
3981
|
/**
|
|
@@ -2040,74 +3989,216 @@ class PlaidClient {
|
|
|
2040
3989
|
* @param {string} baseUrl - The API base URL
|
|
2041
3990
|
* @param {string} code - The invite code
|
|
2042
3991
|
* @param {object} [options]
|
|
2043
|
-
* @returns {Promise<{kind: string, status: string, expiresAt: string, projectName?: string, projectRole?: string,
|
|
3992
|
+
* @returns {Promise<{kind: string, status: string, expiresAt: string, projectName?: string, projectRole?: string, email?: string, grantAdmin: boolean}>}
|
|
2044
3993
|
*/
|
|
2045
3994
|
static async lookupInvite(baseUrl, code, options = {}) {
|
|
2046
|
-
return anonymousPost(baseUrl,
|
|
3995
|
+
return anonymousPost(baseUrl, "/api/v1/invites/lookup", { code }, options);
|
|
2047
3996
|
}
|
|
2048
3997
|
|
|
2049
3998
|
/**
|
|
2050
3999
|
* Redeem an invite code, with NO authentication.
|
|
2051
4000
|
*
|
|
2052
|
-
* For a signup invite, pass `
|
|
2053
|
-
* the invite's grants are applied in the
|
|
2054
|
-
* reset link, pass `password` only.
|
|
2055
|
-
* like login() — the redeemer just
|
|
2056
|
-
* reason to send them to a login
|
|
4001
|
+
* For a signup invite, pass `email` and `password` to create the account
|
|
4002
|
+
* (and optionally `displayName`); the invite's grants are applied in the
|
|
4003
|
+
* same transaction. For a password reset link, pass `password` only.
|
|
4004
|
+
* Resolves to a logged-in client, exactly like login() — the redeemer just
|
|
4005
|
+
* chose these credentials, so there is no reason to send them to a login
|
|
4006
|
+
* form to retype them.
|
|
2057
4007
|
* @param {string} baseUrl - The API base URL
|
|
2058
4008
|
* @param {string} code - The invite code
|
|
2059
4009
|
* @param {object} credentials
|
|
2060
|
-
* @param {string} [credentials.
|
|
4010
|
+
* @param {string} [credentials.email] - The new account's email address,
|
|
4011
|
+
* which becomes its id and login (signup only)
|
|
4012
|
+
* @param {string} [credentials.displayName] - How the new user is shown in
|
|
4013
|
+
* the UI; defaults to the local part of the email (signup only)
|
|
2061
4014
|
* @param {string} credentials.password - Desired password (min 8 characters)
|
|
2062
4015
|
* @param {object} [options]
|
|
2063
4016
|
* @returns {Promise<{client: PlaidClient, userId: string, kind: string}>}
|
|
2064
4017
|
*/
|
|
2065
|
-
static async redeemInvite(
|
|
4018
|
+
static async redeemInvite(
|
|
4019
|
+
baseUrl,
|
|
4020
|
+
code,
|
|
4021
|
+
{ email, password, displayName } = {},
|
|
4022
|
+
options = {},
|
|
4023
|
+
) {
|
|
2066
4024
|
const data = await anonymousPost(
|
|
2067
|
-
baseUrl,
|
|
2068
|
-
|
|
4025
|
+
baseUrl,
|
|
4026
|
+
"/api/v1/invites/redeem",
|
|
4027
|
+
bodyOf({ code, email, password, "display-name": displayName }),
|
|
4028
|
+
options,
|
|
2069
4029
|
);
|
|
2070
4030
|
return {
|
|
2071
|
-
client: new PlaidClient(
|
|
4031
|
+
client: new PlaidClient(
|
|
4032
|
+
baseUrl.replace(/\/$/, ""),
|
|
4033
|
+
data.token || "",
|
|
4034
|
+
options,
|
|
4035
|
+
),
|
|
2072
4036
|
userId: data.userId,
|
|
2073
4037
|
kind: data.kind,
|
|
2074
4038
|
};
|
|
2075
4039
|
}
|
|
2076
4040
|
|
|
4041
|
+
/**
|
|
4042
|
+
* Authenticate and return a new client instance with token. This is the
|
|
4043
|
+
* single auth entry point — there is no `client.login` resource.
|
|
4044
|
+
* @param {string} baseUrl - The base URL for the API
|
|
4045
|
+
* @param {string} userId - User ID for authentication
|
|
4046
|
+
* @param {string} password - Password for authentication
|
|
4047
|
+
* @param {object} [options] - Client options forwarded to the constructor (e.g. { timeout })
|
|
4048
|
+
* @returns {Promise<PlaidClient>} - Authenticated client instance
|
|
4049
|
+
*/
|
|
2077
4050
|
static async login(baseUrl, userId, password, options = {}) {
|
|
2078
|
-
baseUrl = baseUrl.replace(/\/$/,
|
|
4051
|
+
baseUrl = baseUrl.replace(/\/$/, "");
|
|
2079
4052
|
const url = `${baseUrl}/api/v1/login`;
|
|
2080
4053
|
try {
|
|
2081
4054
|
const fetchOptions = {
|
|
2082
|
-
method:
|
|
2083
|
-
headers: {
|
|
2084
|
-
body: JSON.stringify({
|
|
4055
|
+
method: "POST",
|
|
4056
|
+
headers: { "Content-Type": "application/json" },
|
|
4057
|
+
body: JSON.stringify({ "user-id": userId, password }),
|
|
2085
4058
|
};
|
|
2086
|
-
const signal = timeoutSignal(
|
|
4059
|
+
const signal = timeoutSignal(
|
|
4060
|
+
options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS,
|
|
4061
|
+
);
|
|
2087
4062
|
if (signal) fetchOptions.signal = signal;
|
|
2088
4063
|
|
|
2089
4064
|
const response = await fetch(url, fetchOptions);
|
|
2090
4065
|
if (!response.ok) {
|
|
2091
|
-
throw makeHttpError(
|
|
4066
|
+
throw makeHttpError(
|
|
4067
|
+
response,
|
|
4068
|
+
await parseErrorBody(response),
|
|
4069
|
+
url,
|
|
4070
|
+
"POST",
|
|
4071
|
+
);
|
|
2092
4072
|
}
|
|
2093
4073
|
|
|
2094
4074
|
const data = await response.json();
|
|
2095
|
-
const token = data.token ||
|
|
4075
|
+
const token = data.token || "";
|
|
2096
4076
|
return new PlaidClient(baseUrl, token, options);
|
|
2097
4077
|
} catch (error) {
|
|
2098
4078
|
if (error.status) throw error;
|
|
2099
|
-
throw makeNetworkError(error, url,
|
|
4079
|
+
throw makeNetworkError(error, url, "POST");
|
|
2100
4080
|
}
|
|
2101
4081
|
}
|
|
2102
4082
|
}
|
|
2103
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
|
+
|
|
2104
4177
|
export default PlaidClient;
|
|
2105
4178
|
export { PlaidClient };
|
|
4179
|
+
export { CONSTRAINT_TYPES, valueSetAllows, violationsOf } from "./constraints.js";
|
|
2106
4180
|
|
|
2107
4181
|
// Unicode code-point helpers for text offsets (token begin/end are code-point
|
|
2108
4182
|
// indices). See ./codepoint.js.
|
|
2109
|
-
export {
|
|
2110
|
-
|
|
4183
|
+
export {
|
|
4184
|
+
cpLength,
|
|
4185
|
+
cpSlice,
|
|
4186
|
+
cpSlicer,
|
|
4187
|
+
utf16ToCp,
|
|
4188
|
+
cpToUtf16,
|
|
4189
|
+
cpIndexOf,
|
|
4190
|
+
} from "./codepoint.js";
|
|
4191
|
+
export { composeTextEdits, gapsToOps, applyTextOps } from "./textEdits.js";
|
|
4192
|
+
export {
|
|
4193
|
+
PLAID_NAMESPACE,
|
|
4194
|
+
ROLE_KEY,
|
|
4195
|
+
PRESERVE_ON_SPLIT_KEY,
|
|
4196
|
+
PLAIN_EDITS_KEY,
|
|
4197
|
+
SPLIT_ON_SPACE_KEY,
|
|
4198
|
+
ROLES,
|
|
4199
|
+
readRole,
|
|
4200
|
+
findByRole,
|
|
4201
|
+
} from "./roles.js";
|
|
2111
4202
|
// Service self-description helpers: filter discovered services by task, read a
|
|
2112
4203
|
// service's parameter schema/summary, and build/coerce form values. See
|
|
2113
4204
|
// ./serviceSchema.js.
|
|
@@ -2119,20 +4210,65 @@ export {
|
|
|
2119
4210
|
getServiceSummary,
|
|
2120
4211
|
buildDefaultValues,
|
|
2121
4212
|
coerceParamValues,
|
|
2122
|
-
} from
|
|
2123
|
-
//
|
|
2124
|
-
//
|
|
2125
|
-
//
|
|
2126
|
-
// "
|
|
4213
|
+
} from "./serviceSchema.js";
|
|
4214
|
+
// Cooperative cancellation for a service AUTHORED in JS: `progress()` is a
|
|
4215
|
+
// checkpoint that throws ServiceCancelled once the requester stops the
|
|
4216
|
+
// request, and `critical()` holds that off around writes. See ./services.js
|
|
4217
|
+
// and the manual, "Stopping a request".
|
|
4218
|
+
export { ServiceCancelled, createCancelScope } from "./services.js";
|
|
4219
|
+
// Who asked for a service run, named in its History label and in what it
|
|
4220
|
+
// stores. See ./requester.js.
|
|
4221
|
+
export { REQUESTED_BY, makeRequester, requesterOf } from "./requester.js";
|
|
4222
|
+
// Document locks: `client.documents.locked()` holds one for a block of work and
|
|
4223
|
+
// renews it while the block runs. DocumentLockLost is what a write throws once
|
|
4224
|
+
// a renewal has failed. See ./documentLock.js.
|
|
4225
|
+
export {
|
|
4226
|
+
DOCUMENT_LOCK_TTL_MS,
|
|
4227
|
+
DocumentLock,
|
|
4228
|
+
DocumentLockLost,
|
|
4229
|
+
} from "./documentLock.js";
|
|
4230
|
+
// Provenance: the cross-app convention for who made an annotation (flat
|
|
4231
|
+
// prov/provSource/provConfirmed metadata; absence = a verifier; 'inferred' =
|
|
4232
|
+
// a machine; 'contributed' = a person whose work is reviewed), plus the
|
|
4233
|
+
// machine-writer contract. See ./provenance.js and the manual, "Provenance".
|
|
2127
4234
|
export {
|
|
2128
4235
|
PROV,
|
|
2129
4236
|
PROV_STATES,
|
|
2130
4237
|
PROV_CONFIRMED,
|
|
4238
|
+
PROVENANCE_KEYS,
|
|
2131
4239
|
stampInferred,
|
|
2132
4240
|
confirmedInferred,
|
|
4241
|
+
stampContributed,
|
|
2133
4242
|
provState,
|
|
4243
|
+
provOrigin,
|
|
2134
4244
|
isMachine,
|
|
2135
4245
|
isProtected,
|
|
4246
|
+
needsReview,
|
|
2136
4247
|
verifyOnEdit,
|
|
4248
|
+
contributeOnEdit,
|
|
2137
4249
|
serviceSource,
|
|
2138
|
-
|
|
4250
|
+
userSource,
|
|
4251
|
+
REVIEW_KEY,
|
|
4252
|
+
PROJECT_ROLES,
|
|
4253
|
+
readReview,
|
|
4254
|
+
projectRole,
|
|
4255
|
+
isReviewed,
|
|
4256
|
+
withReviewedUser,
|
|
4257
|
+
writerPolicy,
|
|
4258
|
+
} from "./provenance.js";
|
|
4259
|
+
// Metadata ops: the body of a metadata PATCH and of a bulk update entry's
|
|
4260
|
+
// `metadata`. See ./metadataOps.js.
|
|
4261
|
+
export {
|
|
4262
|
+
metadataOps,
|
|
4263
|
+
applyMetadataOps,
|
|
4264
|
+
mergeMetadata,
|
|
4265
|
+
isReservedMetadataKey,
|
|
4266
|
+
} from "./metadataOps.js";
|
|
4267
|
+
// The ids a create or bulk create answered with, off the call or its batch
|
|
4268
|
+
// result. See ./created.js.
|
|
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";
|
|
4274
|
+
export { MAX_BATCH_OPS };
|