@larc-iu/plaid-client 0.2.0-alpha.16 → 0.2.0-alpha.17

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/src/index.js CHANGED
@@ -6,6 +6,7 @@ import { transformRequest, transformResponse } from "./transforms.js";
6
6
  import {
7
7
  makeRequest,
8
8
  queueRequest,
9
+ restampDocumentVersion,
9
10
  extractDocumentVersions,
10
11
  parseErrorBody,
11
12
  makeHttpError,
@@ -128,6 +129,9 @@ function openBatch(client) {
128
129
  const batch = Object.create(client);
129
130
  batch.client = client;
130
131
  batch.operations = [];
132
+ // The document each queued op's strict-mode stamp is for (null for none),
133
+ // index for index with `operations`.
134
+ batch.stampedDocuments = [];
131
135
  batch.open = true;
132
136
  batch._request = (method, path, options = {}) =>
133
137
  queueRequest(batch, method, path, options);
@@ -138,6 +142,7 @@ function openBatch(client) {
138
142
  batch.submit = () => submitBatch(batch);
139
143
  batch.abort = () => {
140
144
  batch.operations = [];
145
+ batch.stampedDocuments = [];
141
146
  batch.open = false;
142
147
  };
143
148
  batch._installResources();
@@ -150,23 +155,57 @@ async function submitBatch(batch) {
150
155
  }
151
156
  batch.open = false;
152
157
  const ops = batch.operations;
158
+ const stamps = batch.stampedDocuments;
153
159
  batch.operations = [];
160
+ batch.stampedDocuments = [];
154
161
  if (ops.length === 0) return [];
155
- const url = `${batch.client.baseUrl}/api/v1/batch`;
162
+ const client = batch.client;
163
+ const url = `${client.baseUrl}/api/v1/batch`;
156
164
  // The server caps a batch at MAX_BATCH_OPS so one transaction cannot hold
157
165
  // the write lock without bound. A larger batch goes as consecutive
158
166
  // requests, results concatenated in queue order: it could not have been one
159
167
  // transaction anyway, and a repair or bulk edit over a big document must
160
- // not fail on its size alone.
168
+ // not fail on its size alone. Past the first request, a strict-mode stamp
169
+ // claims the version the requests before it left (learned from their
170
+ // results), not the one the document had when the op was queued, which the
171
+ // first request has already moved on.
172
+ //
173
+ // Each request is atomic, the whole is not: a failure leaves the requests
174
+ // before it saved. The error says so, as `committed` (how many operations
175
+ // were saved) and `committedResults` (their results, in queue order), so a
176
+ // caller can tell a partial write from a batch that saved nothing. The
177
+ // failed request itself counts as unsaved, even when its answer was lost.
161
178
  const results = [];
162
- for (let i = 0; i < ops.length; i += MAX_BATCH_OPS) {
163
- results.push(
164
- ...(await batch.client._postBatch(url, ops.slice(i, i + MAX_BATCH_OPS))),
165
- );
179
+ try {
180
+ await sendChunks(client, url, ops, stamps, results);
181
+ } catch (error) {
182
+ if (error && typeof error === "object") {
183
+ error.committed = results.length;
184
+ error.committedResults = results;
185
+ }
186
+ throw error;
166
187
  }
167
188
  return results;
168
189
  }
169
190
 
191
+ async function sendChunks(client, url, ops, stamps, results) {
192
+ for (let i = 0; i < ops.length; i += MAX_BATCH_OPS) {
193
+ let chunk = ops.slice(i, i + MAX_BATCH_OPS);
194
+ if (i > 0) {
195
+ chunk = chunk.map((op, j) => {
196
+ const docId = stamps[i + j];
197
+ if (!docId) return op;
198
+ const path = restampDocumentVersion(
199
+ op.path,
200
+ client.documentVersions[docId],
201
+ );
202
+ return { ...op, path };
203
+ });
204
+ }
205
+ results.push(...(await client._postBatch(url, chunk)));
206
+ }
207
+ }
208
+
170
209
  class PlaidClient {
171
210
  /**
172
211
  * Create a new PlaidClient instance
@@ -261,7 +300,6 @@ class PlaidClient {
261
300
  this._request("PUT", `/api/v1/vocab-links/${id}/metadata`, {
262
301
  auditMessage,
263
302
  rawBody: body,
264
- skipResponseTransform: true,
265
303
  }),
266
304
  /**
267
305
  * Remove all metadata from a vocab link.
@@ -270,28 +308,23 @@ class PlaidClient {
270
308
  deleteMetadata: (id, auditMessage) =>
271
309
  this._request("DELETE", `/api/v1/vocab-links/${id}/metadata`, {
272
310
  auditMessage,
273
- skipResponseTransform: true,
274
311
  }),
275
312
  /**
276
- * Patch (shallow-merge) metadata for a vocab link. Keys present in the body are set or overwritten; keys not present are left untouched; a key whose value is null is deleted. Merging is top-level only (nested objects are replaced wholesale, not deep-merged), so a literal null cannot be stored as a value. An empty body changes no metadata.
313
+ * 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.
277
314
  * @param {string} id - The resource ID
278
- * @param {any} body - The metadata patch
315
+ * @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
279
316
  */
280
317
  patchMetadata: (id, body, auditMessage) =>
281
318
  this._request("PATCH", `/api/v1/vocab-links/${id}/metadata`, {
282
319
  auditMessage,
283
320
  rawBody: body,
284
- skipResponseTransform: true,
285
321
  }),
286
322
  /**
287
323
  * Get a vocab link by ID
288
324
  * @param {string} id - The resource ID
289
- * @param {string} [asOf] - Temporal query timestamp
290
325
  */
291
- get: (id, asOf) =>
292
- this._request("GET", `/api/v1/vocab-links/${id}`, {
293
- queryParams: { "as-of": asOf },
294
- }),
326
+ get: (id) =>
327
+ this._request("GET", `/api/v1/vocab-links/${id}`),
295
328
  /**
296
329
  * Delete a vocab link
297
330
  * @param {string} id - The resource ID
@@ -302,15 +335,113 @@ class PlaidClient {
302
335
 
303
336
  this.vocabLayers = {
304
337
  /**
305
- * Get a vocab layer by ID
338
+ * Get a vocab layer by ID. With `asOf`, the vocabulary as it was at that
339
+ * instant, read from its history: the same shape as the live read, its
340
+ * entries with `includeItems`. A vocabulary that did not exist then is a
341
+ * 404, a malformed or pruned time a 400.
306
342
  * @param {string} id - The resource ID
307
343
  * @param {boolean} [includeItems] - Include vocab items
308
- * @param {string} [asOf] - Temporal query timestamp
344
+ * @param {string} [asOf] - The moment to read at (ISO-8601 instant)
309
345
  */
310
346
  get: (id, includeItems, asOf) =>
311
347
  this._request("GET", `/api/v1/vocab-layers/${id}`, {
312
348
  queryParams: { "include-items": includeItems, "as-of": asOf },
313
349
  }),
350
+ /**
351
+ * One entry of the vocabulary as it was at `asOf`, also when it has
352
+ * been deleted since. The same shape as `vocabItems.get`. A 404 when
353
+ * the entry was not in this vocabulary at that time.
354
+ * @param {string} id - The vocabulary ID
355
+ * @param {string} itemId - The entry ID
356
+ * @param {string} asOf - The moment to read at (ISO-8601 instant)
357
+ */
358
+ getItemAt: (id, itemId, asOf) =>
359
+ this._request("GET", `/api/v1/vocab-layers/${id}/items/${itemId}`, {
360
+ queryParams: { "as-of": asOf },
361
+ }),
362
+ /**
363
+ * Get the audit log of a vocabulary: every change to it or to its
364
+ * entries, folded into entries as the document log is. Links are not
365
+ * listed, they belong to the document they annotate. Transparently
366
+ * follows pagination cursors and returns the full flat array.
367
+ * @param {string} id - The vocabulary ID
368
+ * @param {string} [startTime] - Start of time range
369
+ * @param {string} [endTime] - End of time range
370
+ * @param {string[]|string} [opTypes] - Only return operations of these
371
+ * types, spelled as in an entry's `op/type` (e.g.
372
+ * `['vocab-item/delete', 'vocab-item/restore']`)
373
+ * @param {string} [itemId] - Only the changes that wrote this one entry,
374
+ * each with only its operations that did
375
+ */
376
+ audit: (id, startTime, endTime, opTypes, itemId) =>
377
+ listAll(this, `/api/v1/vocab-layers/${id}/audit`, {
378
+ query: {
379
+ "start-time": startTime,
380
+ "end-time": endTime,
381
+ "op-types": opTypesParam(opTypes),
382
+ "item-id": itemId,
383
+ },
384
+ }),
385
+ /**
386
+ * One page of the same log, newest-first with `order: "desc"`. Use this
387
+ * rather than audit() wherever the caller wants the recent end of a log
388
+ * that may be long: audit() walks every page before it resolves.
389
+ * @param {string} id - The vocabulary ID
390
+ * @param {object} [opts]
391
+ * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
392
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
393
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
394
+ * @param {string} [opts.itemId] - Only the changes that wrote this one
395
+ * entry, each with only its operations that did
396
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
397
+ */
398
+ auditPage: (
399
+ id,
400
+ { startTime, endTime, opTypes, order, limit, cursor, itemId } = {},
401
+ ) =>
402
+ listPage(this, `/api/v1/vocab-layers/${id}/audit`, {
403
+ limit,
404
+ cursor,
405
+ query: {
406
+ "start-time": startTime,
407
+ "end-time": endTime,
408
+ "op-types": opTypesParam(opTypes),
409
+ order,
410
+ "item-id": itemId,
411
+ },
412
+ }),
413
+ /**
414
+ * Put one entry of the vocabulary back as it was at `asOf`, as one
415
+ * operation. A deleted entry comes back under its original id with its
416
+ * form and fields, and a living one has its form and fields set back.
417
+ * Links are not part of an entry: a deleted entry's links come back
418
+ * through each document's own `documents.restore`. Resolves to
419
+ * `{inserted, form, metadata, total}`, `total` 0 when nothing changes.
420
+ * A form set back bumps every linking document, and the new versions
421
+ * come back in X-Document-Versions (or, past fifty documents,
422
+ * X-Document-Versions-Omitted), which the client takes up as on any
423
+ * write. A time when the entry did not exist is a 400. Maintainers of
424
+ * the vocabulary only.
425
+ * @param {string} id - The vocabulary ID
426
+ * @param {string} itemId - The entry ID
427
+ * @param {string} asOf - The moment to go back to (ISO-8601 instant),
428
+ * typically a history entry's `endTime`
429
+ * @param {object} [options] - `{ dryRun }`: with `dryRun` true nothing
430
+ * is written and the summary says what would change
431
+ * @param {string} [auditMessage] - Custom audit message for this operation
432
+ */
433
+ restoreItem: (id, itemId, asOf, { dryRun } = {}, auditMessage) =>
434
+ this._request(
435
+ "POST",
436
+ `/api/v1/vocab-layers/${id}/items/${itemId}/restore`,
437
+ {
438
+ queryParams: {
439
+ "as-of": asOf,
440
+ "dry-run": dryRun ? "true" : undefined,
441
+ },
442
+ auditMessage,
443
+ },
444
+ ),
314
445
  /**
315
446
  * Delete a vocab layer.
316
447
  * @param {string} id - The resource ID
@@ -355,35 +486,29 @@ class PlaidClient {
355
486
  /**
356
487
  * List all vocab layers accessible to user. Transparently follows
357
488
  * pagination cursors and returns the full flat array.
358
- * @param {string} [asOf] - Temporal query timestamp
359
489
  */
360
- list: (asOf) =>
361
- listAll(this, "/api/v1/vocab-layers", { query: { "as-of": asOf } }),
490
+ list: () => listAll(this, "/api/v1/vocab-layers"),
362
491
  /**
363
492
  * Fetch a single page of vocab layers.
364
493
  * @param {object} [opts]
365
494
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
366
495
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
367
- * @param {string} [opts.asOf] - Temporal query timestamp
368
496
  * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
369
497
  */
370
- listPage: ({ limit, cursor, asOf } = {}) =>
498
+ listPage: ({ limit, cursor } = {}) =>
371
499
  listPage(this, "/api/v1/vocab-layers", {
372
500
  limit,
373
501
  cursor,
374
- query: { "as-of": asOf },
375
502
  }),
376
503
  /**
377
504
  * Async-iterate vocab layers page by page; yields each page's entries array.
378
505
  * @param {object} [opts]
379
506
  * @param {number} [opts.pageSize] - Per-request page size
380
- * @param {string} [opts.asOf] - Temporal query timestamp
381
507
  * @returns {AsyncGenerator<Array>}
382
508
  */
383
- iterPages: ({ pageSize, asOf } = {}) =>
509
+ iterPages: ({ pageSize } = {}) =>
384
510
  iterPages(this, "/api/v1/vocab-layers", {
385
511
  pageSize,
386
- query: { "as-of": asOf },
387
512
  }),
388
513
  /**
389
514
  * Create a new vocab layer. Note: this also registers the user as a maintainer.
@@ -428,7 +553,6 @@ class PlaidClient {
428
553
  this._request("PUT", `/api/v1/relations/${relationId}/metadata`, {
429
554
  auditMessage,
430
555
  rawBody: body,
431
- skipResponseTransform: true,
432
556
  }),
433
557
  /**
434
558
  * Remove all metadata from a relation.
@@ -437,18 +561,16 @@ class PlaidClient {
437
561
  deleteMetadata: (relationId, auditMessage) =>
438
562
  this._request("DELETE", `/api/v1/relations/${relationId}/metadata`, {
439
563
  auditMessage,
440
- skipResponseTransform: true,
441
564
  }),
442
565
  /**
443
- * Patch (shallow-merge) metadata for a relation. Keys present in the body are set or overwritten; keys not present are left untouched; a key whose value is null is deleted. Merging is top-level only (nested objects are replaced wholesale, not deep-merged), so a literal null cannot be stored as a value. An empty body changes no metadata.
566
+ * 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.
444
567
  * @param {string} relationId - The relation ID
445
- * @param {any} body - The metadata patch
568
+ * @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
446
569
  */
447
570
  patchMetadata: (relationId, body, auditMessage) =>
448
571
  this._request("PATCH", `/api/v1/relations/${relationId}/metadata`, {
449
572
  auditMessage,
450
573
  rawBody: body,
451
- skipResponseTransform: true,
452
574
  }),
453
575
  /**
454
576
  * Update the target span of a relation.
@@ -463,12 +585,9 @@ class PlaidClient {
463
585
  /**
464
586
  * Get a relation by ID.
465
587
  * @param {string} relationId - The relation ID
466
- * @param {string} [asOf] - Temporal query timestamp
467
588
  */
468
- get: (relationId, asOf) =>
469
- this._request("GET", `/api/v1/relations/${relationId}`, {
470
- queryParams: { "as-of": asOf },
471
- }),
589
+ get: (relationId) =>
590
+ this._request("GET", `/api/v1/relations/${relationId}`),
472
591
  /**
473
592
  * Delete a relation.
474
593
  * @param {string} relationId - The relation ID
@@ -536,7 +655,7 @@ class PlaidClient {
536
655
  }),
537
656
  /**
538
657
  * Update many relations in a single operation: set values and/or patch metadata.
539
- * @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 patch, a null value deleting that key. 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`. A `document-version` precondition is accepted only when every entry lies in one document. An unknown id refuses the whole update.
658
+ * @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.
540
659
  * @returns {Promise<{count: number}>} How many relations were updated.
541
660
  */
542
661
  bulkUpdate: (body, auditMessage) =>
@@ -581,12 +700,9 @@ class PlaidClient {
581
700
  /**
582
701
  * Get a span layer by ID.
583
702
  * @param {string} spanLayerId - The span layer ID
584
- * @param {string} [asOf] - Temporal query timestamp
585
703
  */
586
- get: (spanLayerId, asOf) =>
587
- this._request("GET", `/api/v1/span-layers/${spanLayerId}`, {
588
- queryParams: { "as-of": asOf },
589
- }),
704
+ get: (spanLayerId) =>
705
+ this._request("GET", `/api/v1/span-layers/${spanLayerId}`),
590
706
  /**
591
707
  * Delete a span layer.
592
708
  * @param {string} spanLayerId - The span layer ID
@@ -659,12 +775,9 @@ class PlaidClient {
659
775
  /**
660
776
  * Get a span by ID.
661
777
  * @param {string} spanId - The span ID
662
- * @param {string} [asOf] - Temporal query timestamp
663
778
  */
664
- get: (spanId, asOf) =>
665
- this._request("GET", `/api/v1/spans/${spanId}`, {
666
- queryParams: { "as-of": asOf },
667
- }),
779
+ get: (spanId) =>
780
+ this._request("GET", `/api/v1/spans/${spanId}`),
668
781
  /**
669
782
  * Delete a span.
670
783
  * @param {string} spanId - The span ID
@@ -696,7 +809,7 @@ class PlaidClient {
696
809
  this._request("DELETE", "/api/v1/spans/bulk", { auditMessage, body }),
697
810
  /**
698
811
  * Update many spans in a single operation: set values and/or patch metadata.
699
- * @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 patch, a null value deleting that key. 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`. A `document-version` precondition is accepted only when every entry lies in one document. An unknown id refuses the whole update.
812
+ * @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.
700
813
  * @returns {Promise<{count: number}>} How many spans were updated.
701
814
  */
702
815
  bulkUpdate: (body, auditMessage) =>
@@ -713,7 +826,6 @@ class PlaidClient {
713
826
  this._request("PUT", `/api/v1/spans/${spanId}/metadata`, {
714
827
  auditMessage,
715
828
  rawBody: body,
716
- skipResponseTransform: true,
717
829
  }),
718
830
  /**
719
831
  * Remove all metadata from a span.
@@ -722,18 +834,16 @@ class PlaidClient {
722
834
  deleteMetadata: (spanId, auditMessage) =>
723
835
  this._request("DELETE", `/api/v1/spans/${spanId}/metadata`, {
724
836
  auditMessage,
725
- skipResponseTransform: true,
726
837
  }),
727
838
  /**
728
- * Patch (shallow-merge) metadata for a span. Keys present in the body are set or overwritten; keys not present are left untouched; a key whose value is null is deleted. Merging is top-level only (nested objects are replaced wholesale, not deep-merged), so a literal null cannot be stored as a value. An empty body changes no metadata.
839
+ * 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.
729
840
  * @param {string} spanId - The span ID
730
- * @param {any} body - The metadata patch
841
+ * @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
731
842
  */
732
843
  patchMetadata: (spanId, body, auditMessage) =>
733
844
  this._request("PATCH", `/api/v1/spans/${spanId}/metadata`, {
734
845
  auditMessage,
735
846
  rawBody: body,
736
- skipResponseTransform: true,
737
847
  }),
738
848
  };
739
849
 
@@ -769,6 +879,24 @@ class PlaidClient {
769
879
  health: () => PlaidClient.health(this.baseUrl, { timeout: this.timeout }),
770
880
  };
771
881
 
882
+ // The signed-in user's sign-ins. Signing in is `PlaidClient.login`, which
883
+ // makes the client.
884
+ this.auth = {
885
+ /**
886
+ * End every sign-in of this user on every device: each browser tab,
887
+ * script and service holding one of the user's sign-in tokens is
888
+ * refused from now on, this client included. Named API tokens are not
889
+ * affected (revoke one with `apiTokens.revoke`). To sign out of one tab
890
+ * only, discard its token instead. A signal rather than project data:
891
+ * made on a batch it still goes out at once, and it never joins an
892
+ * operation.
893
+ * @returns {Promise<void>}
894
+ */
895
+ logoutEverywhere: async () => {
896
+ await this._request("POST", "/api/v1/logout", { outOfBand: true });
897
+ },
898
+ };
899
+
772
900
  this.texts = {
773
901
  /**
774
902
  * Replace all metadata for a text.
@@ -779,7 +907,6 @@ class PlaidClient {
779
907
  this._request("PUT", `/api/v1/texts/${textId}/metadata`, {
780
908
  auditMessage,
781
909
  rawBody: body,
782
- skipResponseTransform: true,
783
910
  }),
784
911
  /**
785
912
  * Remove all metadata from a text.
@@ -788,18 +915,16 @@ class PlaidClient {
788
915
  deleteMetadata: (textId, auditMessage) =>
789
916
  this._request("DELETE", `/api/v1/texts/${textId}/metadata`, {
790
917
  auditMessage,
791
- skipResponseTransform: true,
792
918
  }),
793
919
  /**
794
- * Patch (shallow-merge) metadata for a text. Keys present in the body are set or overwritten; keys not present are left untouched; a key whose value is null is deleted. Merging is top-level only (nested objects are replaced wholesale, not deep-merged), so a literal null cannot be stored as a value. An empty body changes no metadata.
920
+ * 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.
795
921
  * @param {string} textId - The text ID
796
- * @param {any} body - The metadata patch
922
+ * @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
797
923
  */
798
924
  patchMetadata: (textId, body, auditMessage) =>
799
925
  this._request("PATCH", `/api/v1/texts/${textId}/metadata`, {
800
926
  auditMessage,
801
927
  rawBody: body,
802
- skipResponseTransform: true,
803
928
  }),
804
929
  /**
805
930
  * Create a new text in a document's text layer. A text is a container for
@@ -822,12 +947,9 @@ class PlaidClient {
822
947
  /**
823
948
  * Get a text.
824
949
  * @param {string} textId - The text ID
825
- * @param {string} [asOf] - Temporal query timestamp
826
950
  */
827
- get: (textId, asOf) =>
828
- this._request("GET", `/api/v1/texts/${textId}`, {
829
- queryParams: { "as-of": asOf },
830
- }),
951
+ get: (textId) =>
952
+ this._request("GET", `/api/v1/texts/${textId}`),
831
953
  /**
832
954
  * Delete a text and all dependent data.
833
955
  * @param {string} textId - The text ID
@@ -859,37 +981,34 @@ class PlaidClient {
859
981
  * returns the full flat array. Admin-or-maintainer only.
860
982
  * @param {object} [opts]
861
983
  * @param {string} [opts.q] - Filter to users whose display name or email contains this text (case-insensitive)
862
- * @param {string} [opts.asOf] - Temporal query timestamp
863
984
  */
864
- list: ({ q, asOf } = {}) =>
865
- listAll(this, "/api/v1/users", { query: { q, "as-of": asOf } }),
985
+ list: ({ q } = {}) =>
986
+ listAll(this, "/api/v1/users", { query: { q } }),
866
987
  /**
867
988
  * Fetch a single page of users (optionally filtered by `q`).
868
989
  * @param {object} [opts]
869
990
  * @param {string} [opts.q] - Filter to users whose display name or email contains this text (case-insensitive)
870
991
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
871
992
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
872
- * @param {string} [opts.asOf] - Temporal query timestamp
873
993
  * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
874
994
  */
875
- listPage: ({ q, limit, cursor, asOf } = {}) =>
995
+ listPage: ({ q, limit, cursor } = {}) =>
876
996
  listPage(this, "/api/v1/users", {
877
997
  limit,
878
998
  cursor,
879
- query: { q, "as-of": asOf },
999
+ query: { q },
880
1000
  }),
881
1001
  /**
882
1002
  * Async-iterate users page by page; yields each page's entries array.
883
1003
  * @param {object} [opts]
884
1004
  * @param {string} [opts.q] - Filter to users whose display name or email contains this text (case-insensitive)
885
1005
  * @param {number} [opts.pageSize] - Per-request page size
886
- * @param {string} [opts.asOf] - Temporal query timestamp
887
1006
  * @returns {AsyncGenerator<Array>}
888
1007
  */
889
- iterPages: ({ q, pageSize, asOf } = {}) =>
1008
+ iterPages: ({ q, pageSize } = {}) =>
890
1009
  iterPages(this, "/api/v1/users", {
891
1010
  pageSize,
892
- query: { q, "as-of": asOf },
1011
+ query: { q },
893
1012
  }),
894
1013
  /**
895
1014
  * Create a new user.
@@ -907,7 +1026,9 @@ class PlaidClient {
907
1026
  email,
908
1027
  password,
909
1028
  "is-admin": isAdmin,
910
- "display-name": displayName,
1029
+ // Left out when not given (null too), so core gives the user the
1030
+ // local part of the email. Core refuses a null display name.
1031
+ "display-name": displayName ?? undefined,
911
1032
  }),
912
1033
  }),
913
1034
  /**
@@ -916,18 +1037,16 @@ class PlaidClient {
916
1037
  * @param {string} userId - The user ID
917
1038
  * @param {string} [startTime] - Start of time range
918
1039
  * @param {string} [endTime] - End of time range
919
- * @param {string} [asOf] - Temporal query timestamp
920
1040
  * @param {string[]|string} [opTypes] - Only return operations of these
921
1041
  * types, spelled as in an entry's `op/type` (e.g.
922
1042
  * `['span-layer/create', 'span-layer/delete']`). An entry appears when
923
1043
  * one of its operations matches, carrying only the ones that did.
924
1044
  */
925
- audit: (userId, startTime, endTime, asOf, opTypes) =>
1045
+ audit: (userId, startTime, endTime, opTypes) =>
926
1046
  listAll(this, `/api/v1/users/${userId}/audit`, {
927
1047
  query: {
928
1048
  "start-time": startTime,
929
1049
  "end-time": endTime,
930
- "as-of": asOf,
931
1050
  "op-types": opTypesParam(opTypes),
932
1051
  },
933
1052
  }),
@@ -935,6 +1054,7 @@ class PlaidClient {
935
1054
  * One page of the same log, newest-first with `order: "desc"`. Use this
936
1055
  * rather than audit() wherever the caller wants the recent end of a log
937
1056
  * that may be long: audit() walks every page before it resolves.
1057
+ * @param {string} userId - The user ID
938
1058
  * @param {object} [opts]
939
1059
  * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
940
1060
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
@@ -943,7 +1063,7 @@ class PlaidClient {
943
1063
  */
944
1064
  auditPage: (
945
1065
  userId,
946
- { startTime, endTime, asOf, opTypes, order, limit, cursor } = {},
1066
+ { startTime, endTime, opTypes, order, limit, cursor } = {},
947
1067
  ) =>
948
1068
  listPage(this, `/api/v1/users/${userId}/audit`, {
949
1069
  limit,
@@ -951,7 +1071,6 @@ class PlaidClient {
951
1071
  query: {
952
1072
  "start-time": startTime,
953
1073
  "end-time": endTime,
954
- "as-of": asOf,
955
1074
  "op-types": opTypesParam(opTypes),
956
1075
  order,
957
1076
  },
@@ -959,12 +1078,9 @@ class PlaidClient {
959
1078
  /**
960
1079
  * Get a user by ID
961
1080
  * @param {string} id - The resource ID
962
- * @param {string} [asOf] - Temporal query timestamp
963
1081
  */
964
- get: (id, asOf) =>
965
- this._request("GET", `/api/v1/users/${id}`, {
966
- queryParams: { "as-of": asOf },
967
- }),
1082
+ get: (id) =>
1083
+ this._request("GET", `/api/v1/users/${id}`),
968
1084
  /**
969
1085
  * Deactivate a user. Users are never hard-deleted: deactivation
970
1086
  * rejects their logins and tokens, strips their project memberships
@@ -1142,7 +1258,7 @@ class PlaidClient {
1142
1258
  ),
1143
1259
  /**
1144
1260
  * Create or replace one private data entry. `value` is any JSON (up to
1145
- * 1 MB), stored as sent. Not audited, not batchable.
1261
+ * 1 MB). Not audited, not batchable.
1146
1262
  *
1147
1263
  * The server stores it verbatim, but this client recases object keys on
1148
1264
  * the way out and back like any other body (`myKey` <-> `my-key`), so a
@@ -1210,7 +1326,7 @@ class PlaidClient {
1210
1326
  * credential and is shown ONLY here — store it immediately. API tokens
1211
1327
  * do not expire and survive password changes / logout; revoke to kill.
1212
1328
  * @param {string} userId - The user ID who will own the token
1213
- * @param {string} name - A human label, e.g. "Stanza Parser"
1329
+ * @param {string} name - A human label, e.g. "Stanza parser"
1214
1330
  * @returns {Promise<{id: string, name: string, token: string}>}
1215
1331
  */
1216
1332
  create: (userId, name, auditMessage) =>
@@ -1626,12 +1742,9 @@ class PlaidClient {
1626
1742
  /**
1627
1743
  * Get a token layer by ID.
1628
1744
  * @param {string} tokenLayerId - The token layer ID
1629
- * @param {string} [asOf] - Temporal query timestamp
1630
1745
  */
1631
- get: (tokenLayerId, asOf) =>
1632
- this._request("GET", `/api/v1/token-layers/${tokenLayerId}`, {
1633
- queryParams: { "as-of": asOf },
1634
- }),
1746
+ get: (tokenLayerId) =>
1747
+ this._request("GET", `/api/v1/token-layers/${tokenLayerId}`),
1635
1748
  /**
1636
1749
  * Delete a token layer.
1637
1750
  * @param {string} tokenLayerId - The token layer ID
@@ -1656,12 +1769,9 @@ class PlaidClient {
1656
1769
  /**
1657
1770
  * Check the lock status of a document.
1658
1771
  * @param {string} documentId - The document ID
1659
- * @param {string} [asOf] - Temporal query timestamp
1660
1772
  */
1661
- checkLock: (documentId, asOf) =>
1662
- this._request("GET", `/api/v1/documents/${documentId}/lock`, {
1663
- queryParams: { "as-of": asOf },
1664
- }),
1773
+ checkLock: (documentId) =>
1774
+ this._request("GET", `/api/v1/documents/${documentId}/lock`),
1665
1775
  /**
1666
1776
  * Acquire or refresh a document lock.
1667
1777
  *
@@ -1717,8 +1827,8 @@ class PlaidClient {
1717
1827
  * NOT re-entrant: nesting two `locked()` blocks on one document would
1718
1828
  * release on the inner exit and leave the outer unprotected. Lock at
1719
1829
  * exactly one level per call path. A lost lock is recorded on the
1720
- * CLIENT, like batch and strict mode, so it stops every write the client
1721
- * makes and not only the ones this block makes.
1830
+ * CLIENT, like strict mode, so it stops every write the client makes
1831
+ * (on any batch of it too) and not only the ones this block makes.
1722
1832
  * @param {string} documentId - The document ID
1723
1833
  * @param {(lock: DocumentLock) => any} fn - The work to run while holding it
1724
1834
  * @param {object} [options] - `{ keepAlive }`: renew on a timer (default true)
@@ -1775,7 +1885,6 @@ class PlaidClient {
1775
1885
  this._request("PUT", `/api/v1/documents/${documentId}/metadata`, {
1776
1886
  auditMessage,
1777
1887
  rawBody: body,
1778
- skipResponseTransform: true,
1779
1888
  }),
1780
1889
  /**
1781
1890
  * Remove all metadata from a document.
@@ -1784,18 +1893,16 @@ class PlaidClient {
1784
1893
  deleteMetadata: (documentId, auditMessage) =>
1785
1894
  this._request("DELETE", `/api/v1/documents/${documentId}/metadata`, {
1786
1895
  auditMessage,
1787
- skipResponseTransform: true,
1788
1896
  }),
1789
1897
  /**
1790
- * Patch (shallow-merge) metadata for a document. Keys present in the body are set or overwritten; keys not present are left untouched; a key whose value is null is deleted. Merging is top-level only (nested objects are replaced wholesale, not deep-merged), so a literal null cannot be stored as a value. An empty body changes no metadata.
1898
+ * 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.
1791
1899
  * @param {string} documentId - The document ID
1792
- * @param {any} body - The metadata patch
1900
+ * @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
1793
1901
  */
1794
1902
  patchMetadata: (documentId, body, auditMessage) =>
1795
1903
  this._request("PATCH", `/api/v1/documents/${documentId}/metadata`, {
1796
1904
  auditMessage,
1797
1905
  rawBody: body,
1798
- skipResponseTransform: true,
1799
1906
  }),
1800
1907
  /**
1801
1908
  * Get audit log for a document. Transparently follows pagination cursors
@@ -1803,18 +1910,16 @@ class PlaidClient {
1803
1910
  * @param {string} documentId - The document ID
1804
1911
  * @param {string} [startTime] - Start of time range
1805
1912
  * @param {string} [endTime] - End of time range
1806
- * @param {string} [asOf] - Temporal query timestamp
1807
1913
  * @param {string[]|string} [opTypes] - Only return operations of these
1808
1914
  * types, spelled as in an entry's `op/type` (e.g.
1809
1915
  * `['span-layer/create', 'span-layer/delete']`). An entry appears when
1810
1916
  * one of its operations matches, carrying only the ones that did.
1811
1917
  */
1812
- audit: (documentId, startTime, endTime, asOf, opTypes) =>
1918
+ audit: (documentId, startTime, endTime, opTypes) =>
1813
1919
  listAll(this, `/api/v1/documents/${documentId}/audit`, {
1814
1920
  query: {
1815
1921
  "start-time": startTime,
1816
1922
  "end-time": endTime,
1817
- "as-of": asOf,
1818
1923
  "op-types": opTypesParam(opTypes),
1819
1924
  },
1820
1925
  }),
@@ -1822,6 +1927,7 @@ class PlaidClient {
1822
1927
  * One page of the same log, newest-first with `order: "desc"`. Use this
1823
1928
  * rather than audit() wherever the caller wants the recent end of a log
1824
1929
  * that may be long: audit() walks every page before it resolves.
1930
+ * @param {string} documentId - The document ID
1825
1931
  * @param {object} [opts]
1826
1932
  * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
1827
1933
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
@@ -1830,7 +1936,7 @@ class PlaidClient {
1830
1936
  */
1831
1937
  auditPage: (
1832
1938
  documentId,
1833
- { startTime, endTime, asOf, opTypes, order, limit, cursor } = {},
1939
+ { startTime, endTime, opTypes, order, limit, cursor } = {},
1834
1940
  ) =>
1835
1941
  listPage(this, `/api/v1/documents/${documentId}/audit`, {
1836
1942
  limit,
@@ -1838,7 +1944,6 @@ class PlaidClient {
1838
1944
  query: {
1839
1945
  "start-time": startTime,
1840
1946
  "end-time": endTime,
1841
- "as-of": asOf,
1842
1947
  "op-types": opTypesParam(opTypes),
1843
1948
  order,
1844
1949
  },
@@ -2026,18 +2131,16 @@ class PlaidClient {
2026
2131
  * @param {string} projectId - The project ID
2027
2132
  * @param {string} [startTime] - Start of time range
2028
2133
  * @param {string} [endTime] - End of time range
2029
- * @param {string} [asOf] - Temporal query timestamp
2030
2134
  * @param {string[]|string} [opTypes] - Only return operations of these
2031
2135
  * types, spelled as in an entry's `op/type` (e.g.
2032
2136
  * `['span-layer/create', 'span-layer/delete']`). An entry appears when
2033
2137
  * one of its operations matches, carrying only the ones that did.
2034
2138
  */
2035
- audit: (projectId, startTime, endTime, asOf, opTypes) =>
2139
+ audit: (projectId, startTime, endTime, opTypes) =>
2036
2140
  listAll(this, `/api/v1/projects/${projectId}/audit`, {
2037
2141
  query: {
2038
2142
  "start-time": startTime,
2039
2143
  "end-time": endTime,
2040
- "as-of": asOf,
2041
2144
  "op-types": opTypesParam(opTypes),
2042
2145
  },
2043
2146
  }),
@@ -2045,6 +2148,7 @@ class PlaidClient {
2045
2148
  * One page of the same log, newest-first with `order: "desc"`. Use this
2046
2149
  * rather than audit() wherever the caller wants the recent end of a log
2047
2150
  * that may be long: audit() walks every page before it resolves.
2151
+ * @param {string} projectId - The project ID
2048
2152
  * @param {object} [opts]
2049
2153
  * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
2050
2154
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
@@ -2053,7 +2157,7 @@ class PlaidClient {
2053
2157
  */
2054
2158
  auditPage: (
2055
2159
  projectId,
2056
- { startTime, endTime, asOf, opTypes, order, limit, cursor } = {},
2160
+ { startTime, endTime, opTypes, order, limit, cursor } = {},
2057
2161
  ) =>
2058
2162
  listPage(this, `/api/v1/projects/${projectId}/audit`, {
2059
2163
  limit,
@@ -2061,7 +2165,6 @@ class PlaidClient {
2061
2165
  query: {
2062
2166
  "start-time": startTime,
2063
2167
  "end-time": endTime,
2064
- "as-of": asOf,
2065
2168
  "op-types": opTypesParam(opTypes),
2066
2169
  order,
2067
2170
  },
@@ -2102,12 +2205,9 @@ class PlaidClient {
2102
2205
  * Get a project by ID. To fetch the project's documents, use
2103
2206
  * listDocuments(id) — the include-documents flag has been removed.
2104
2207
  * @param {string} id - The resource ID
2105
- * @param {string} [asOf] - Temporal query timestamp
2106
2208
  */
2107
- get: (id, asOf) =>
2108
- this._request("GET", `/api/v1/projects/${id}`, {
2109
- queryParams: { "as-of": asOf },
2110
- }),
2209
+ get: (id) =>
2210
+ this._request("GET", `/api/v1/projects/${id}`),
2111
2211
  /**
2112
2212
  * List all documents in a project. Transparently follows pagination
2113
2213
  * cursors and returns the full flat array.
@@ -2144,17 +2244,15 @@ class PlaidClient {
2144
2244
  iterDocuments: (id, { pageSize } = {}) =>
2145
2245
  iterPages(this, `/api/v1/projects/${id}/documents`, { pageSize }),
2146
2246
  /**
2147
- * Delete a project and everything in it. This is irrecoverable.
2148
- *
2149
- * For a large project this can take a long time, so the client's
2150
- * per-request timeout is DISABLED by default. Pass `{ timeout }` to
2151
- * impose a finite bound.
2247
+ * Delete a project and everything in it. This is irrecoverable. The
2248
+ * project is gone when this returns, and what it holds is removed on
2249
+ * the server afterwards.
2152
2250
  * @param {string} id - The resource ID
2153
2251
  * @param {string} [auditMessage] - Custom audit-log message
2154
2252
  * @param {object} [options]
2155
- * @param {number} [options.timeout=0] - Per-request timeout in ms (0/null disables)
2253
+ * @param {number} [options.timeout] - Per-request timeout in ms, the client's own by default (0/null disables)
2156
2254
  */
2157
- delete: (id, auditMessage, { timeout = 0 } = {}) =>
2255
+ delete: (id, auditMessage, { timeout } = {}) =>
2158
2256
  this._request("DELETE", `/api/v1/projects/${id}`, {
2159
2257
  auditMessage,
2160
2258
  timeout,
@@ -2172,35 +2270,29 @@ class PlaidClient {
2172
2270
  /**
2173
2271
  * List all projects accessible to user. Transparently follows pagination
2174
2272
  * cursors and returns the full flat array.
2175
- * @param {string} [asOf] - Temporal query timestamp
2176
2273
  */
2177
- list: (asOf) =>
2178
- listAll(this, "/api/v1/projects", { query: { "as-of": asOf } }),
2274
+ list: () => listAll(this, "/api/v1/projects"),
2179
2275
  /**
2180
2276
  * Fetch a single page of projects.
2181
2277
  * @param {object} [opts]
2182
2278
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
2183
2279
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
2184
- * @param {string} [opts.asOf] - Temporal query timestamp
2185
2280
  * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
2186
2281
  */
2187
- listPage: ({ limit, cursor, asOf } = {}) =>
2282
+ listPage: ({ limit, cursor } = {}) =>
2188
2283
  listPage(this, "/api/v1/projects", {
2189
2284
  limit,
2190
2285
  cursor,
2191
- query: { "as-of": asOf },
2192
2286
  }),
2193
2287
  /**
2194
2288
  * Async-iterate projects page by page; yields each page's entries array.
2195
2289
  * @param {object} [opts]
2196
2290
  * @param {number} [opts.pageSize] - Per-request page size
2197
- * @param {string} [opts.asOf] - Temporal query timestamp
2198
2291
  * @returns {AsyncGenerator<Array>}
2199
2292
  */
2200
- iterPages: ({ pageSize, asOf } = {}) =>
2293
+ iterPages: ({ pageSize } = {}) =>
2201
2294
  iterPages(this, "/api/v1/projects", {
2202
2295
  pageSize,
2203
- query: { "as-of": asOf },
2204
2296
  }),
2205
2297
  /**
2206
2298
  * Create a new project. Note: this also registers the user as a maintainer.
@@ -2248,12 +2340,9 @@ class PlaidClient {
2248
2340
  /**
2249
2341
  * Get a text layer by ID.
2250
2342
  * @param {string} textLayerId - The text layer ID
2251
- * @param {string} [asOf] - Temporal query timestamp
2252
2343
  */
2253
- get: (textLayerId, asOf) =>
2254
- this._request("GET", `/api/v1/text-layers/${textLayerId}`, {
2255
- queryParams: { "as-of": asOf },
2256
- }),
2344
+ get: (textLayerId) =>
2345
+ this._request("GET", `/api/v1/text-layers/${textLayerId}`),
2257
2346
  /**
2258
2347
  * Delete a text layer.
2259
2348
  * @param {string} textLayerId - The text layer ID
@@ -2304,7 +2393,6 @@ class PlaidClient {
2304
2393
  this._request("PUT", `/api/v1/vocab-items/${id}/metadata`, {
2305
2394
  auditMessage,
2306
2395
  rawBody: body,
2307
- skipResponseTransform: true,
2308
2396
  }),
2309
2397
  /**
2310
2398
  * Remove all metadata from a vocab item.
@@ -2313,18 +2401,16 @@ class PlaidClient {
2313
2401
  deleteMetadata: (id, auditMessage) =>
2314
2402
  this._request("DELETE", `/api/v1/vocab-items/${id}/metadata`, {
2315
2403
  auditMessage,
2316
- skipResponseTransform: true,
2317
2404
  }),
2318
2405
  /**
2319
- * Patch (shallow-merge) metadata for a vocab item. Keys present in the body are set or overwritten; keys not present are left untouched; a key whose value is null is deleted. Merging is top-level only (nested objects are replaced wholesale, not deep-merged), so a literal null cannot be stored as a value. An empty body changes no metadata.
2406
+ * 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.
2320
2407
  * @param {string} id - The resource ID
2321
- * @param {any} body - The metadata patch
2408
+ * @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
2322
2409
  */
2323
2410
  patchMetadata: (id, body, auditMessage) =>
2324
2411
  this._request("PATCH", `/api/v1/vocab-items/${id}/metadata`, {
2325
2412
  auditMessage,
2326
2413
  rawBody: body,
2327
- skipResponseTransform: true,
2328
2414
  }),
2329
2415
  /**
2330
2416
  * Create a new vocab item
@@ -2350,7 +2436,7 @@ class PlaidClient {
2350
2436
  /**
2351
2437
  * Update many vocab items in a single operation: set forms and/or patch
2352
2438
  * metadata.
2353
- * @param {Array<{id: string, form?: string, metadata?: any}>} body - `form` is set only when the key is present; `metadata` is a patch, a null value deleting that key. The entries may lie in several vocab layers; the user must have write access to each. An unknown id refuses the whole update, and an id may appear only once. Only an entry whose form really changes restates the documents linking it, and a strict-mode client picks up their new versions from the response.
2439
+ * @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.
2354
2440
  * @returns {Promise<{count: number}>} How many vocab items were updated.
2355
2441
  */
2356
2442
  bulkUpdate: (body, auditMessage) =>
@@ -2374,12 +2460,9 @@ class PlaidClient {
2374
2460
  /**
2375
2461
  * Get a vocab item by ID
2376
2462
  * @param {string} id - The resource ID
2377
- * @param {string} [asOf] - Temporal query timestamp
2378
2463
  */
2379
- get: (id, asOf) =>
2380
- this._request("GET", `/api/v1/vocab-items/${id}`, {
2381
- queryParams: { "as-of": asOf },
2382
- }),
2464
+ get: (id) =>
2465
+ this._request("GET", `/api/v1/vocab-items/${id}`),
2383
2466
  /**
2384
2467
  * Delete a vocab item, and every link to it. Every document holding one
2385
2468
  * of those links has its version bumped, and a strict-mode client picks
@@ -2459,12 +2542,9 @@ class PlaidClient {
2459
2542
  /**
2460
2543
  * Get a relation layer by ID.
2461
2544
  * @param {string} relationLayerId - The relation layer ID
2462
- * @param {string} [asOf] - Temporal query timestamp
2463
2545
  */
2464
- get: (relationLayerId, asOf) =>
2465
- this._request("GET", `/api/v1/relation-layers/${relationLayerId}`, {
2466
- queryParams: { "as-of": asOf },
2467
- }),
2546
+ get: (relationLayerId) =>
2547
+ this._request("GET", `/api/v1/relation-layers/${relationLayerId}`),
2468
2548
  /**
2469
2549
  * Delete a relation layer.
2470
2550
  * @param {string} relationLayerId - The relation layer ID
@@ -2527,12 +2607,9 @@ class PlaidClient {
2527
2607
  /**
2528
2608
  * Get a token.
2529
2609
  * @param {string} tokenId - The token ID
2530
- * @param {string} [asOf] - Temporal query timestamp
2531
2610
  */
2532
- get: (tokenId, asOf) =>
2533
- this._request("GET", `/api/v1/tokens/${tokenId}`, {
2534
- queryParams: { "as-of": asOf },
2535
- }),
2611
+ get: (tokenId) =>
2612
+ this._request("GET", `/api/v1/tokens/${tokenId}`),
2536
2613
  /**
2537
2614
  * Delete a token and remove it from any spans. If this causes a span to
2538
2615
  * have no remaining tokens, the span will also be deleted.
@@ -2570,7 +2647,7 @@ class PlaidClient {
2570
2647
  this._request("DELETE", "/api/v1/tokens/bulk", { auditMessage, body }),
2571
2648
  /**
2572
2649
  * Update many tokens in a single operation: patch metadata.
2573
- * @param {Array} body - Objects of the shape `{id, metadata}`. `metadata` is a patch, a null value deleting that key. 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`. A `document-version` precondition is accepted only when every entry lies in one document. An unknown id refuses the whole update.
2650
+ * @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.
2574
2651
  * @returns {Promise<{count: number}>} How many tokens were updated.
2575
2652
  */
2576
2653
  bulkUpdate: (body, auditMessage) =>
@@ -2624,7 +2701,6 @@ class PlaidClient {
2624
2701
  this._request("PUT", `/api/v1/tokens/${tokenId}/metadata`, {
2625
2702
  auditMessage,
2626
2703
  rawBody: body,
2627
- skipResponseTransform: true,
2628
2704
  }),
2629
2705
  /**
2630
2706
  * Remove all metadata from a token.
@@ -2633,18 +2709,16 @@ class PlaidClient {
2633
2709
  deleteMetadata: (tokenId, auditMessage) =>
2634
2710
  this._request("DELETE", `/api/v1/tokens/${tokenId}/metadata`, {
2635
2711
  auditMessage,
2636
- skipResponseTransform: true,
2637
2712
  }),
2638
2713
  /**
2639
- * Patch (shallow-merge) metadata for a token. Keys present in the body are set or overwritten; keys not present are left untouched; a key whose value is null is deleted. Merging is top-level only (nested objects are replaced wholesale, not deep-merged), so a literal null cannot be stored as a value. An empty body changes no metadata.
2714
+ * 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.
2640
2715
  * @param {string} tokenId - The token ID
2641
- * @param {any} body - The metadata patch
2716
+ * @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
2642
2717
  */
2643
2718
  patchMetadata: (tokenId, body, auditMessage) =>
2644
2719
  this._request("PATCH", `/api/v1/tokens/${tokenId}/metadata`, {
2645
2720
  auditMessage,
2646
2721
  rawBody: body,
2647
- skipResponseTransform: true,
2648
2722
  }),
2649
2723
  };
2650
2724
 
@@ -2668,13 +2742,17 @@ class PlaidClient {
2668
2742
  * this a key such as `case-marker` would reach listeners as
2669
2743
  * `caseMarker` in JS and `case_marker` in Python. `listen` restores it
2670
2744
  * verbatim on the way in.
2745
+ *
2746
+ * A message is not saved and writes nothing to History, so it never
2747
+ * joins an open operation (`withOperation`). Made on a batch it still
2748
+ * queues, so a message queued after the writes goes out after them.
2671
2749
  * @param {string} projectId - The UUID of the project to send to
2672
2750
  * @param {any} data - The message data to send
2673
2751
  * @returns {Promise<any>} Response from the send operation
2674
2752
  */
2675
- sendMessage: (projectId, data, auditMessage) =>
2753
+ sendMessage: (projectId, data) =>
2676
2754
  this._request("POST", `/api/v1/projects/${projectId}/message`, {
2677
- auditMessage,
2755
+ noOperation: true,
2678
2756
  rawBody: { body: data },
2679
2757
  }),
2680
2758
 
@@ -2707,7 +2785,9 @@ class PlaidClient {
2707
2785
  * @param {function} onServiceRequest - Callback (data, responseHelper)
2708
2786
  * @param {Object} [extras] - Optional additional service metadata
2709
2787
  * @param {function} [onStatus] - Optional callback (event, projectId, detail) for
2710
- * connection transitions: 'registered', 'reconnected', 'disconnected'
2788
+ * connection transitions: 'registered', 'reconnected', 'disconnected',
2789
+ * and 'stopped' when the server refuses the channel for good, which
2790
+ * ends the registration
2711
2791
  * @returns {Object} Service registration with .stop(), .isRunning(), .isConnected()
2712
2792
  */
2713
2793
  serve: (projectId, serviceInfo, onServiceRequest, extras, onStatus) =>
@@ -2729,7 +2809,7 @@ class PlaidClient {
2729
2809
  * @param {number} [timeout=10000] - Timeout in ms
2730
2810
  * @param {function} [onProgress] - Called with each progress payload {percent, message}
2731
2811
  * @param {AbortSignal} [signal] - Abort to stop waiting; rejects with an AbortError
2732
- * @param {Object} [opts] - {requestId, onAccepted}
2812
+ * @param {Object} [opts] - {requestId, onAccepted, projectIds}
2733
2813
  * @returns {Promise<any>} The service's result
2734
2814
  */
2735
2815
  requestService: (
@@ -2827,18 +2907,15 @@ class PlaidClient {
2827
2907
  * @param {Object} body - The query AST ({find, where, scope?, limit?, orderBy?,
2828
2908
  * return?, bindings?}).
2829
2909
  * @returns {Promise<Object>} For 'ids'/'entities': {columns, results, count, truncated}.
2830
- * For 'count': {return: 'count', count}. Entity cells are full entity objects
2831
- * (same shape as the GET endpoints).
2910
+ * For 'count': {return: 'count', count, truncated}, where `truncated` says
2911
+ * the count stopped at the server's cap. Entity cells are full entity
2912
+ * objects (same shape as the GET endpoints).
2832
2913
  */
2833
2914
  // outOfBand: a query is a read that travels as a POST (see the note at
2834
2915
  // the top of http.js). Made on a batch it goes over the wire like any
2835
2916
  // read, and it never joins a logical operation.
2836
- this.query = (body, auditMessage) =>
2837
- this._request("POST", "/api/v1/query", {
2838
- auditMessage,
2839
- body,
2840
- outOfBand: true,
2841
- });
2917
+ this.query = (body) =>
2918
+ this._request("POST", "/api/v1/query", { body, outOfBand: true });
2842
2919
 
2843
2920
  // Logical-operation groups (audit-log grouping). There is no create: a
2844
2921
  // group row is made lazily by the first write carrying `?group-id=`
@@ -3015,6 +3092,20 @@ class PlaidClient {
3015
3092
  cursor,
3016
3093
  query: { "entity-id": entityId },
3017
3094
  }),
3095
+ /**
3096
+ * Async-iterate a vocabulary's comments page by page, oldest first;
3097
+ * yields each page's entries array.
3098
+ * @param {string} vocabId - The vocab layer to read
3099
+ * @param {object} [opts]
3100
+ * @param {string} [opts.entityId] - One entry's thread
3101
+ * @param {number} [opts.pageSize] - Per-request page size
3102
+ * @returns {AsyncGenerator<Array>}
3103
+ */
3104
+ iterInVocabPages: (vocabId, { entityId, pageSize } = {}) =>
3105
+ iterPages(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
3106
+ pageSize,
3107
+ query: { "entity-id": entityId },
3108
+ }),
3018
3109
  /**
3019
3110
  * Comment counts per entry of a vocabulary, as an `{entityId: n}` map.
3020
3111
  * The response is NOT key-transformed: its keys are entry ids.
@@ -3177,8 +3268,8 @@ class PlaidClient {
3177
3268
  * "Every write" is the writes of project data. Reads never join, and neither
3178
3269
  * do the out-of-band signals shaped like a write (a document lock taken or
3179
3270
  * renewed, a stopped service request, a service reporting itself, an admin
3180
- * control): none of them is audited, so there would be nothing under the
3181
- * label.
3271
+ * control), and neither does a broadcast message (`messages.sendMessage`):
3272
+ * none of them is audited, so there would be nothing under the label.
3182
3273
  *
3183
3274
  * @param {string} message - Human label for the operation.
3184
3275
  * @param {object} [opts] - Optional `{ id }`: adopt an existing group id instead of minting one (a service joining the requester's operation; `requestService` propagates an open operation to the service automatically).
@@ -3259,7 +3350,10 @@ class PlaidClient {
3259
3350
  * Open a batch: a view of this client with the same bundles, on which every
3260
3351
  * write of project data queues instead of going out. `submit()` sends the
3261
3352
  * queued operations as ONE atomic request (larger than the server's cap,
3262
- * as consecutive requests with the results concatenated in queue order) and
3353
+ * as consecutive requests with the results concatenated in queue order,
3354
+ * each atomic on its own, so a failure in a later one leaves the earlier
3355
+ * ones committed, and the error it throws carries `committed`, the count
3356
+ * saved, and `committedResults`, their results) and
3263
3357
  * resolves to one result per operation; `abort()` drops them. A call made
3264
3358
  * on the client itself is never touched by an open batch, and a read or an
3265
3359
  * out-of-band signal made on the batch goes over the wire now (see the note
@@ -3280,7 +3374,11 @@ class PlaidClient {
3280
3374
  return openBatch(this);
3281
3375
  }
3282
3376
 
3283
- /** POST one batch request (at most MAX_BATCH_OPS operations); resolves to its transformed results. */
3377
+ /**
3378
+ * POST one batch request (at most MAX_BATCH_OPS operations). Resolves to
3379
+ * one `{ status, headers, body }` per operation, with only the body
3380
+ * recased: the headers keep the server's spelling.
3381
+ */
3284
3382
  async _postBatch(url, body) {
3285
3383
  const fetchOptions = {
3286
3384
  method: "POST",
@@ -3305,6 +3403,22 @@ class PlaidClient {
3305
3403
  throw makeHttpError(res, await parseErrorBody(res), url, "POST");
3306
3404
  }
3307
3405
  return res;
3406
+ }).catch((error) => {
3407
+ // 401 means the token is missing, expired or invalid: fire the app's
3408
+ // auth-error handler once, exactly as makeRequest does.
3409
+ if (
3410
+ error?.status === 401 &&
3411
+ typeof this.onAuthError === "function" &&
3412
+ !this._authErrorFired
3413
+ ) {
3414
+ this._authErrorFired = true;
3415
+ try {
3416
+ this.onAuthError(error);
3417
+ } catch (_) {
3418
+ /* handler must not mask the original error */
3419
+ }
3420
+ }
3421
+ throw error;
3308
3422
  });
3309
3423
 
3310
3424
  const results = await response.json();
@@ -3332,7 +3446,10 @@ class PlaidClient {
3332
3446
  }
3333
3447
  }
3334
3448
 
3335
- return results.map((result) => transformResponse(result));
3449
+ return results.map((result) => ({
3450
+ ...result,
3451
+ body: transformResponse(result.body),
3452
+ }));
3336
3453
  } catch (error) {
3337
3454
  if (error.status) throw error;
3338
3455
  throw makeNetworkError(error, url, "POST");
@@ -3340,8 +3457,9 @@ class PlaidClient {
3340
3457
  }
3341
3458
 
3342
3459
  /**
3343
- * Run `fn` with a batch, then submit all queued ops as ONE atomic request,
3344
- * or abort the batch if `fn` throws. `fn` receives the batch and makes its
3460
+ * Run `fn` with a batch, then submit all queued ops as `submit()` does (ONE
3461
+ * atomic request up to MAX_BATCH_OPS operations, consecutive requests past
3462
+ * it), or abort the batch if `fn` throws. `fn` receives the batch and makes its
3345
3463
  * writes on it; it must NOT call `submit()` itself. Resolves to the batch
3346
3464
  * results array (`[]` if `fn` queued nothing).
3347
3465
  *
@@ -3366,15 +3484,6 @@ class PlaidClient {
3366
3484
  return batch.submit();
3367
3485
  }
3368
3486
 
3369
- /**
3370
- * Authenticate and return a new client instance with token. This is the
3371
- * single auth entry point — there is no `client.login` resource.
3372
- * @param {string} baseUrl - The base URL for the API
3373
- * @param {string} userId - User ID for authentication
3374
- * @param {string} password - Password for authentication
3375
- * @param {object} [options] - Client options forwarded to the constructor (e.g. { timeout })
3376
- * @returns {Promise<PlaidClient>} - Authenticated client instance
3377
- */
3378
3487
  /**
3379
3488
  * Liveness, version and database size, with NO authentication and no client
3380
3489
  * instance — for a launcher or status page checking whether a server is up
@@ -3469,6 +3578,15 @@ class PlaidClient {
3469
3578
  };
3470
3579
  }
3471
3580
 
3581
+ /**
3582
+ * Authenticate and return a new client instance with token. This is the
3583
+ * single auth entry point — there is no `client.login` resource.
3584
+ * @param {string} baseUrl - The base URL for the API
3585
+ * @param {string} userId - User ID for authentication
3586
+ * @param {string} password - Password for authentication
3587
+ * @param {object} [options] - Client options forwarded to the constructor (e.g. { timeout })
3588
+ * @returns {Promise<PlaidClient>} - Authenticated client instance
3589
+ */
3472
3590
  static async login(baseUrl, userId, password, options = {}) {
3473
3591
  baseUrl = baseUrl.replace(/\/$/, "");
3474
3592
  const url = `${baseUrl}/api/v1/login`;
@@ -3568,7 +3686,6 @@ export {
3568
3686
  needsReview,
3569
3687
  verifyOnEdit,
3570
3688
  contributeOnEdit,
3571
- mergeMetadata,
3572
3689
  serviceSource,
3573
3690
  userSource,
3574
3691
  REVIEW_KEY,
@@ -3579,4 +3696,15 @@ export {
3579
3696
  withReviewedUser,
3580
3697
  writerPolicy,
3581
3698
  } from "./provenance.js";
3699
+ // Metadata ops: the body of a metadata PATCH and of a bulk update entry's
3700
+ // `metadata`. See ./metadataOps.js.
3701
+ export {
3702
+ metadataOps,
3703
+ applyMetadataOps,
3704
+ mergeMetadata,
3705
+ isReservedMetadataKey,
3706
+ } from "./metadataOps.js";
3707
+ // The ids a create or bulk create answered with, off the call or its batch
3708
+ // result. See ./created.js.
3709
+ export { createdId, createdIds } from "./created.js";
3582
3710
  export { MAX_BATCH_OPS };