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