@larc-iu/plaid-client 0.2.0-alpha.7 → 0.2.0-alpha.9

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
@@ -21,6 +21,8 @@ import {
21
21
  discardService,
22
22
  serve,
23
23
  requestService,
24
+ attachServiceRequest,
25
+ cancelServiceRequest,
24
26
  } from "./services.js";
25
27
 
26
28
  // Helper: normalize a document-read `layers` filter to the wire's
@@ -83,6 +85,12 @@ async function anonymousPost(baseUrl, path, body, options = {}) {
83
85
  }
84
86
  }
85
87
 
88
+ /**
89
+ * The server's cap on operations per batch request (plaid.rest-api.v1.batch).
90
+ * A batch queued past it is submitted as consecutive requests.
91
+ */
92
+ const MAX_BATCH_OPS = 1000;
93
+
86
94
  class PlaidClient {
87
95
  /**
88
96
  * Create a new PlaidClient instance
@@ -1247,13 +1255,13 @@ class PlaidClient {
1247
1255
  auditMessage,
1248
1256
  }),
1249
1257
  /**
1250
- * Get media file for a document
1258
+ * Get the media file for a document. Media is not versioned, so there is
1259
+ * no as-of form: the route refuses the parameter. Prefer the document's
1260
+ * own `mediaUrl`, which carries the file's version for caching.
1251
1261
  * @param {string} documentId - The document ID
1252
- * @param {string} [asOf] - Temporal query timestamp
1253
1262
  */
1254
- getMedia: (documentId, asOf) =>
1263
+ getMedia: (documentId) =>
1255
1264
  this._request("GET", `/api/v1/documents/${documentId}/media`, {
1256
- queryParams: { "as-of": asOf },
1257
1265
  noBatch: true,
1258
1266
  binaryResponse: true,
1259
1267
  }),
@@ -1261,8 +1269,10 @@ class PlaidClient {
1261
1269
  * Upload a media file for a document. Uses Apache Tika for content validation.
1262
1270
  * @param {string} documentId - The document ID
1263
1271
  * @param {File} file - The file to upload
1272
+ * @param {string} [auditMessage] - Custom audit-log message for this write
1273
+ * @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.
1264
1274
  */
1265
- uploadMedia: (documentId, file, auditMessage) => {
1275
+ uploadMedia: (documentId, file, auditMessage, { onProgress } = {}) => {
1266
1276
  const fd = new FormData();
1267
1277
  fd.append("file", file);
1268
1278
  return this._request("PUT", `/api/v1/documents/${documentId}/media`, {
@@ -1270,6 +1280,7 @@ class PlaidClient {
1270
1280
  body: fd,
1271
1281
  formData: true,
1272
1282
  noBatch: true,
1283
+ onUploadProgress: onProgress,
1273
1284
  });
1274
1285
  },
1275
1286
  /**
@@ -1334,6 +1345,25 @@ class PlaidClient {
1334
1345
  "op-types": opTypesParam(opTypes),
1335
1346
  },
1336
1347
  }),
1348
+ /**
1349
+ * Restore a document to its state at an earlier time, as one
1350
+ * operation: what was deleted since then comes back under its original
1351
+ * id, what was added since is removed, and what changed is set back,
1352
+ * across every layer. A layer deleted since then, or a vocabulary entry
1353
+ * that no longer exists, is skipped and reported under `skipped`.
1354
+ * Resolves to a summary of the changes. Maintainers only.
1355
+ * @param {string} documentId - The document ID
1356
+ * @param {string} asOf - The moment to go back to (ISO-8601 instant),
1357
+ * typically a history entry's `endTime`
1358
+ * @param {object} [options] - `{ dryRun }`: with `dryRun` true nothing
1359
+ * is written and the summary says what would change
1360
+ * @param {string} [auditMessage] - Custom audit message for this operation
1361
+ */
1362
+ restore: (documentId, asOf, { dryRun } = {}, auditMessage) =>
1363
+ this._request("POST", `/api/v1/documents/${documentId}/restore`, {
1364
+ queryParams: { "as-of": asOf, "dry-run": dryRun ? "true" : undefined },
1365
+ auditMessage,
1366
+ }),
1337
1367
  /**
1338
1368
  * Get a document. Set `includeBody` to true to include all data.
1339
1369
  *
@@ -2102,16 +2132,54 @@ class PlaidClient {
2102
2132
 
2103
2133
  /**
2104
2134
  * Request a service to perform work and await its result.
2135
+ *
2136
+ * The request outlives this call: after a timeout, an abort, or a
2137
+ * dropped connection the service goes on, and `attachServiceRequest`
2138
+ * collects the result given the request id, which `opts.onAccepted`
2139
+ * receives as soon as the server has taken the request. Pass
2140
+ * `opts.requestId` (a UUID you mint) to know the id before submitting;
2141
+ * submitting an id that names a request you already made rejoins it
2142
+ * instead of starting another.
2105
2143
  * @param {string} projectId - The UUID of the project
2106
2144
  * @param {string} serviceId - The ID of the service to request
2107
2145
  * @param {any} data - The request data
2108
- * @param {number} [timeout] - Timeout in milliseconds (default: 10000)
2146
+ * @param {number} [timeout=10000] - Timeout in ms
2109
2147
  * @param {function} [onProgress] - Called with each progress payload {percent, message}
2110
2148
  * @param {AbortSignal} [signal] - Abort to stop waiting; rejects with an AbortError
2111
- * @returns {Promise<any>} Service response
2149
+ * @param {Object} [opts] - {requestId, onAccepted}
2150
+ * @returns {Promise<any>} The service's result
2151
+ */
2152
+ requestService: (projectId, serviceId, data, timeout, onProgress, signal, opts) =>
2153
+ requestService(this, projectId, serviceId, data, timeout, onProgress, signal, opts),
2154
+
2155
+ /**
2156
+ * Rejoin a service request made earlier and await its result: the
2157
+ * latest progress is replayed, then the result comes, or at once if the
2158
+ * request already finished. Only the user who submitted it (or an
2159
+ * admin). Rejects with an error whose `status` is 404 when the request
2160
+ * is unknown or expired (a finished request's result is kept for a
2161
+ * while, not forever).
2162
+ * @param {string} projectId - The UUID of the project
2163
+ * @param {string} requestId - The request id
2164
+ * @param {number} [timeout=10000] - Timeout in ms
2165
+ * @param {function} [onProgress] - Called with each progress payload
2166
+ * @param {AbortSignal} [signal] - Abort to stop waiting
2167
+ * @returns {Promise<any>} The service's result
2168
+ */
2169
+ attachServiceRequest: (projectId, requestId, timeout, onProgress, signal) =>
2170
+ attachServiceRequest(this, projectId, requestId, timeout, onProgress, signal),
2171
+
2172
+ /**
2173
+ * Ask the service to stop a request made earlier. The request still
2174
+ * ends with whatever the service then reports, on the stream of whoever
2175
+ * is awaiting it. Rejects with 404 if unknown or expired, 409 once
2176
+ * finished.
2177
+ * @param {string} projectId - The UUID of the project
2178
+ * @param {string} requestId - The request id
2179
+ * @returns {Promise<void>}
2112
2180
  */
2113
- requestService: (projectId, serviceId, data, timeout, onProgress, signal) =>
2114
- requestService(this, projectId, serviceId, data, timeout, onProgress, signal),
2181
+ cancelServiceRequest: (projectId, requestId) =>
2182
+ cancelServiceRequest(this, projectId, requestId),
2115
2183
  };
2116
2184
 
2117
2185
  /**
@@ -2181,15 +2249,27 @@ class PlaidClient {
2181
2249
  this.comments = {
2182
2250
  /**
2183
2251
  * Post a comment on an entity. Requires write access to the entity's
2184
- * project; the author is the authenticated caller. Comments are not
2185
- * audited and do not bump the document version.
2186
- * @param {'document'|'text'|'token'|'span'|'relation'} entityType - What kind of thing is being commented on
2252
+ * project, or to the vocabulary for a vocab-item; the author is the
2253
+ * authenticated caller. Comments are not audited and do not bump the
2254
+ * document version.
2255
+ *
2256
+ * A comment outlives its anchor: deleting the entity does not delete
2257
+ * the comment. `anchorLabel` is the caption shown once that happens
2258
+ * ("Gloss of ktab, sentence 4"), so pass what the comment is about.
2259
+ * @param {'document'|'text'|'token'|'span'|'relation'|'vocab-item'} entityType - What kind of thing is being commented on
2187
2260
  * @param {string} entityId - The commented entity's id
2188
2261
  * @param {string} body - The comment text (1..10000 characters)
2262
+ * @param {object} [opts]
2263
+ * @param {string} [opts.anchorLabel] - What the comment is about, in words (at most 200 characters)
2189
2264
  */
2190
- create: (entityType, entityId, body) =>
2265
+ create: (entityType, entityId, body, { anchorLabel } = {}) =>
2191
2266
  this._request("POST", "/api/v1/comments", {
2192
- body: bodyOf({ "entity-type": entityType, "entity-id": entityId, body }),
2267
+ body: bodyOf({
2268
+ "entity-type": entityType,
2269
+ "entity-id": entityId,
2270
+ body,
2271
+ "anchor-label": anchorLabel,
2272
+ }),
2193
2273
  }),
2194
2274
  /**
2195
2275
  * Read one comment.
@@ -2290,6 +2370,47 @@ class PlaidClient {
2290
2370
  },
2291
2371
  skipResponseTransform: true,
2292
2372
  }),
2373
+ /**
2374
+ * List the comments on a vocabulary's entries, oldest first. Requires
2375
+ * read access to the vocabulary. Transparently follows pagination
2376
+ * cursors and returns the full flat array.
2377
+ * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching - use listInVocabPage() for a single page in a batch.
2378
+ * @param {string} vocabId - The vocab layer to read
2379
+ * @param {object} [filters]
2380
+ * @param {string} [filters.entityId] - One entry's thread
2381
+ */
2382
+ listInVocab: (vocabId, { entityId } = {}) =>
2383
+ listAll(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
2384
+ query: { "entity-id": entityId },
2385
+ }),
2386
+ /**
2387
+ * Fetch a single page of a vocabulary's comments.
2388
+ * @param {string} vocabId - The vocab layer to read
2389
+ * @param {object} [opts]
2390
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
2391
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
2392
+ * @param {string} [opts.entityId] - One entry's thread
2393
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
2394
+ */
2395
+ listInVocabPage: (vocabId, { limit, cursor, entityId } = {}) =>
2396
+ listPage(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
2397
+ limit,
2398
+ cursor,
2399
+ query: { "entity-id": entityId },
2400
+ }),
2401
+ /**
2402
+ * Comment counts per entry of a vocabulary, as an `{entityId: n}` map.
2403
+ * The response is NOT key-transformed: its keys are entry ids.
2404
+ * @param {string} vocabId - The vocab layer to read
2405
+ * @param {object} [filters]
2406
+ * @param {string} [filters.entityId] - One entry only
2407
+ * @returns {Promise<Object<string, number>>}
2408
+ */
2409
+ countsInVocab: (vocabId, { entityId } = {}) =>
2410
+ this._request("GET", `/api/v1/vocab-layers/${vocabId}/comments/counts`, {
2411
+ queryParams: { "entity-id": entityId },
2412
+ skipResponseTransform: true,
2413
+ }),
2293
2414
  };
2294
2415
  }
2295
2416
 
@@ -2432,71 +2553,84 @@ class PlaidClient {
2432
2553
  }
2433
2554
 
2434
2555
  try {
2435
- let url = `${this.baseUrl}/api/v1/batch`;
2436
- const body = this.batchOperations.map((op) => ({
2556
+ const url = `${this.baseUrl}/api/v1/batch`;
2557
+ const ops = this.batchOperations.map((op) => ({
2437
2558
  path: op.path,
2438
2559
  method: op.method.toUpperCase(),
2439
2560
  ...(op.body && { body: op.body }),
2440
2561
  }));
2562
+ // The server caps a batch at MAX_BATCH_OPS so one transaction cannot
2563
+ // hold the write lock without bound. A larger batch goes as consecutive
2564
+ // requests, results concatenated in queue order: it could not have been
2565
+ // one transaction anyway, and a repair or bulk edit over a big document
2566
+ // must not fail on its size alone.
2567
+ const results = [];
2568
+ for (let i = 0; i < ops.length; i += MAX_BATCH_OPS) {
2569
+ results.push(...(await this._postBatch(url, ops.slice(i, i + MAX_BATCH_OPS))));
2570
+ }
2571
+ return results;
2572
+ } finally {
2573
+ this.isBatching = false;
2574
+ this.batchOperations = [];
2575
+ }
2576
+ }
2441
2577
 
2442
- const fetchOptions = {
2443
- method: "POST",
2444
- headers: {
2445
- Authorization: `Bearer ${this.token}`,
2446
- "Content-Type": "application/json",
2447
- },
2448
- body: JSON.stringify(body),
2449
- };
2578
+ /** POST one batch request (at most MAX_BATCH_OPS operations); resolves to its transformed results. */
2579
+ async _postBatch(url, body) {
2580
+ const fetchOptions = {
2581
+ method: "POST",
2582
+ headers: {
2583
+ Authorization: `Bearer ${this.token}`,
2584
+ "Content-Type": "application/json",
2585
+ },
2586
+ body: JSON.stringify(body),
2587
+ };
2450
2588
 
2451
- try {
2452
- // Retry a 503: the batch is atomic, so a refused one wrote nothing and
2453
- // repeating it is safe. A fresh timeout signal per attempt — an
2454
- // AbortSignal.timeout stays aborted once it fires.
2455
- const response = await retryWhileBusy(async () => {
2456
- const signal = timeoutSignal(this.batchTimeout);
2457
- const res = await fetch(
2458
- url,
2459
- signal ? { ...fetchOptions, signal } : fetchOptions,
2460
- );
2461
- if (!res.ok) {
2462
- throw makeHttpError(res, await parseErrorBody(res), url, "POST");
2463
- }
2464
- return res;
2465
- });
2589
+ try {
2590
+ // Retry a 503: the batch is atomic, so a refused one wrote nothing and
2591
+ // repeating it is safe. A fresh timeout signal per attempt — an
2592
+ // AbortSignal.timeout stays aborted once it fires.
2593
+ const response = await retryWhileBusy(async () => {
2594
+ const signal = timeoutSignal(this.batchTimeout);
2595
+ const res = await fetch(
2596
+ url,
2597
+ signal ? { ...fetchOptions, signal } : fetchOptions,
2598
+ );
2599
+ if (!res.ok) {
2600
+ throw makeHttpError(res, await parseErrorBody(res), url, "POST");
2601
+ }
2602
+ return res;
2603
+ });
2466
2604
 
2467
- const results = await response.json();
2468
-
2469
- // Extract document versions from each batch response
2470
- for (const result of results) {
2471
- if (result.headers && result.headers["X-Document-Versions"]) {
2472
- try {
2473
- const versionsMap = JSON.parse(
2474
- result.headers["X-Document-Versions"],
2475
- );
2476
- if (typeof versionsMap === "object" && versionsMap !== null) {
2477
- // Clone once per response, then merge — not once per entry.
2478
- this.documentVersions = {
2479
- ...this.documentVersions,
2480
- ...versionsMap,
2481
- };
2482
- }
2483
- } catch (e) {
2484
- console.warn(
2485
- "Failed to parse document versions header from batch response:",
2486
- e,
2487
- );
2605
+ const results = await response.json();
2606
+
2607
+ // Extract document versions from each batch response
2608
+ for (const result of results) {
2609
+ if (result.headers && result.headers["X-Document-Versions"]) {
2610
+ try {
2611
+ const versionsMap = JSON.parse(
2612
+ result.headers["X-Document-Versions"],
2613
+ );
2614
+ if (typeof versionsMap === "object" && versionsMap !== null) {
2615
+ // Clone once per response, then merge — not once per entry.
2616
+ this.documentVersions = {
2617
+ ...this.documentVersions,
2618
+ ...versionsMap,
2619
+ };
2488
2620
  }
2621
+ } catch (e) {
2622
+ console.warn(
2623
+ "Failed to parse document versions header from batch response:",
2624
+ e,
2625
+ );
2489
2626
  }
2490
2627
  }
2491
-
2492
- return results.map((result) => transformResponse(result));
2493
- } catch (error) {
2494
- if (error.status) throw error;
2495
- throw makeNetworkError(error, url, "POST");
2496
2628
  }
2497
- } finally {
2498
- this.isBatching = false;
2499
- this.batchOperations = [];
2629
+
2630
+ return results.map((result) => transformResponse(result));
2631
+ } catch (error) {
2632
+ if (error.status) throw error;
2633
+ throw makeNetworkError(error, url, "POST");
2500
2634
  }
2501
2635
  }
2502
2636
 
@@ -2699,19 +2833,33 @@ export {
2699
2833
  buildDefaultValues,
2700
2834
  coerceParamValues,
2701
2835
  } from "./serviceSchema.js";
2702
- // Provenance: the cross-app convention for machine-provided vs human-labeled
2703
- // information (flat prov/provSource/provConfirmed metadata; absence = human),
2704
- // plus the machine-writer contract. See ./provenance.js and the manual,
2705
- // "Provenance".
2836
+ // Provenance: the cross-app convention for who made an annotation (flat
2837
+ // prov/provSource/provConfirmed metadata; absence = a verifier; 'inferred' =
2838
+ // a machine; 'contributed' = a person whose work is reviewed), plus the
2839
+ // machine-writer contract. See ./provenance.js and the manual, "Provenance".
2706
2840
  export {
2707
2841
  PROV,
2708
2842
  PROV_STATES,
2709
2843
  PROV_CONFIRMED,
2710
2844
  stampInferred,
2711
2845
  confirmedInferred,
2846
+ stampContributed,
2712
2847
  provState,
2848
+ provOrigin,
2713
2849
  isMachine,
2714
2850
  isProtected,
2851
+ needsReview,
2715
2852
  verifyOnEdit,
2853
+ contributeOnEdit,
2854
+ mergeMetadata,
2716
2855
  serviceSource,
2856
+ userSource,
2857
+ REVIEW_KEY,
2858
+ PROJECT_ROLES,
2859
+ readReview,
2860
+ projectRole,
2861
+ isReviewed,
2862
+ withReviewedUser,
2863
+ writerPolicy,
2717
2864
  } from "./provenance.js";
2865
+ export { MAX_BATCH_OPS };