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

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 CHANGED
@@ -15,7 +15,7 @@ interface ServiceParam {
15
15
  /** Key the value is sent under in the request payload. */
16
16
  key: string;
17
17
  label: string;
18
- type: "string" | "number" | "boolean" | "enum" | "multiselect";
18
+ type: "string" | "number" | "boolean" | "enum" | "multiselect" | "field";
19
19
  description?: string;
20
20
  default?: any;
21
21
  required?: boolean;
@@ -30,6 +30,11 @@ interface ServiceParam {
30
30
  /** `string` only. */
31
31
  placeholder?: string;
32
32
  multiline?: boolean;
33
+ /**
34
+ * `field` only: the scope of the project's annotation fields the value names
35
+ * one of. A `field` value is a string everywhere else.
36
+ */
37
+ scope?: "Sentence" | "Word" | "Morpheme";
33
38
  }
34
39
 
35
40
  /** A service's standardized self-description (lives in `extras`). */
@@ -257,10 +262,6 @@ interface SpansBundle {
257
262
  patchMetadata(spanId: string, body: any, auditMessage?: string): Promise<any>;
258
263
  }
259
264
 
260
- interface BatchBundle {
261
- submit(body: any[], auditMessage?: string): Promise<any>;
262
- }
263
-
264
265
  interface TextsBundle {
265
266
  setMetadata(textId: string, body: any, auditMessage?: string): Promise<any>;
266
267
  deleteMetadata(textId: string, auditMessage?: string): Promise<any>;
@@ -361,6 +362,60 @@ interface CommentFilters {
361
362
  type CommentableType =
362
363
  "document" | "text" | "token" | "span" | "relation" | "vocab-item";
363
364
 
365
+ /**
366
+ * One entry in a project's annotation manual: a short Markdown document
367
+ * stating a convention the project follows. Read by the people on the
368
+ * project, and by the assistant before it proposes anything.
369
+ */
370
+ interface Guideline {
371
+ id: string;
372
+ projectId: string;
373
+ /** The handle the assistant asks for a guideline by. Not required to be unique. */
374
+ title: string;
375
+ /** The Markdown text. Present on a single read and on a list made with `includeBodies`. */
376
+ body?: string;
377
+ /** The body's length in characters. Present on a list made WITHOUT `includeBodies`. */
378
+ bodyChars?: number;
379
+ /** Whether the assistant is given this one in full on every turn. */
380
+ pinned: boolean;
381
+ createdAt: string;
382
+ updatedAt: string;
383
+ }
384
+
385
+ interface GuidelinesBundle {
386
+ create(
387
+ projectId: string,
388
+ title: string,
389
+ opts?: { body?: string; pinned?: boolean },
390
+ auditMessage?: string,
391
+ ): Promise<{ id: string }>;
392
+ get(id: string): Promise<Guideline>;
393
+ update(
394
+ id: string,
395
+ changes?: {
396
+ title?: string;
397
+ body?: string;
398
+ pinned?: boolean;
399
+ /** Write only if this is still the stored updatedAt; 409 otherwise. */
400
+ expectedUpdatedAt?: string;
401
+ },
402
+ auditMessage?: string,
403
+ ): Promise<Guideline>;
404
+ delete(id: string, auditMessage?: string): Promise<any>;
405
+ list(
406
+ projectId: string,
407
+ opts?: { includeBodies?: boolean },
408
+ ): Promise<Guideline[]>;
409
+ listPage(
410
+ projectId: string,
411
+ opts?: { includeBodies?: boolean; limit?: number; cursor?: string },
412
+ ): Promise<Page<Guideline>>;
413
+ iterPages(
414
+ projectId: string,
415
+ opts?: { includeBodies?: boolean; pageSize?: number },
416
+ ): AsyncGenerator<Guideline[]>;
417
+ }
418
+
364
419
  interface Comment {
365
420
  id: string;
366
421
  /** The owning project, or null for a comment on a vocabulary entry. */
@@ -1012,6 +1067,7 @@ interface VocabItemsBundle {
1012
1067
  ): Promise<any>;
1013
1068
  bulkCreate(body: any[], auditMessage?: string): Promise<{ ids: string[] }>;
1014
1069
  bulkDelete(body: any[], auditMessage?: string): Promise<void>;
1070
+ bulkUpdate(body: any[], auditMessage?: string): Promise<{ count: number }>;
1015
1071
  get(id: string, asOf?: string): Promise<any>;
1016
1072
  delete(id: string, auditMessage?: string): Promise<any>;
1017
1073
  update(id: string, form: string, auditMessage?: string): Promise<any>;
@@ -1116,6 +1172,21 @@ export interface OperationGroupsBundle {
1116
1172
  update(id: string, message: string | null): Promise<any>;
1117
1173
  }
1118
1174
 
1175
+ /**
1176
+ * A batch: the client's bundles, on which every write of project data queues
1177
+ * until `submit()`. See `PlaidClient.batch()`.
1178
+ */
1179
+ export interface PlaidBatch extends PlaidClient {
1180
+ /** The client this batch was opened on. */
1181
+ readonly client: PlaidClient;
1182
+ /** The queued operations, in order. */
1183
+ readonly operations: Array<{ path: string; method: string; body?: any }>;
1184
+ /** Send the queued operations as one atomic request; one result per operation. */
1185
+ submit(): Promise<any[]>;
1186
+ /** Drop the queued operations without sending them. */
1187
+ abort(): void;
1188
+ }
1189
+
1119
1190
  export declare class PlaidClient {
1120
1191
  constructor(baseUrl: string, token: string, options?: PlaidClientOptions);
1121
1192
  static login(
@@ -1144,23 +1215,20 @@ export declare class PlaidClient {
1144
1215
  /** Fired once on HTTP 401 (see PlaidClientOptions.onAuthError). */
1145
1216
  onAuthError: ((error: Error) => void) | null;
1146
1217
 
1147
- // Batch control methods.
1218
+ // Batches.
1148
1219
  //
1149
- // Batch mode is one flag on the whole client, so it catches every write made
1150
- // while it is open, including writes made by code that knows nothing about
1151
- // the batch. Reads are never caught: every read method goes over the wire
1152
- // while a batch is open and is answered from the state the batch has not
1153
- // committed yet, so app chrome sharing a client with an importer keeps
1154
- // working. Neither are the calls that signal something out of band rather
1155
- // than write project data: stopping a service request, a service reporting
1156
- // its progress or result, taking and dropping a document lock, and the admin
1157
- // actions on the server itself.
1158
- beginBatch(): void;
1159
- submitBatch(): Promise<any[]>;
1160
- abortBatch(): void;
1161
- isBatchMode(): boolean;
1162
- /** Run `fn` with a batch open, then submit atomically (or abort if `fn` throws). Resolves to the results array. Writes inside `fn` queue; reads go over the wire and see the pre-batch state. */
1163
- batched(fn: () => void | Promise<void>): Promise<any[]>;
1220
+ // A batch is a view of the client with the same bundles. A write of project
1221
+ // data made on the batch queues; a call made on the client itself always
1222
+ // goes over the wire, whatever batches are open, so a write by code that
1223
+ // knows nothing about a batch can never land inside it. A read, or a signal
1224
+ // that carries no project data (stopping a service request, a service
1225
+ // reporting its progress, taking and dropping a document lock, the admin
1226
+ // actions on the server itself), goes over the wire even when made on the
1227
+ // batch.
1228
+ /** Open a batch. Queue writes on it, then `submit()` them as one atomic request, or `abort()`. Not nestable. */
1229
+ batch(): PlaidBatch;
1230
+ /** Run `fn` with a batch, then submit atomically (or abort if `fn` throws). Resolves to the results array, one entry per queued write. */
1231
+ batched(fn: (batch: PlaidBatch) => void | Promise<void>): Promise<any[]>;
1164
1232
 
1165
1233
  // Strict mode methods
1166
1234
  enterStrictMode(documentId: string): void;
@@ -1186,7 +1254,6 @@ export declare class PlaidClient {
1186
1254
  relations: RelationsBundle;
1187
1255
  spanLayers: SpanLayersBundle;
1188
1256
  spans: SpansBundle;
1189
- batch: BatchBundle;
1190
1257
  texts: TextsBundle;
1191
1258
  users: UsersBundle;
1192
1259
  apiTokens: ApiTokensBundle;
@@ -1196,6 +1263,7 @@ export declare class PlaidClient {
1196
1263
  admin: AdminBundle;
1197
1264
  audit: AuditBundle;
1198
1265
  comments: CommentsBundle;
1266
+ guidelines: GuidelinesBundle;
1199
1267
  tokenLayers: TokenLayersBundle;
1200
1268
  documents: DocumentsBundle;
1201
1269
  messages: MessagesBundle;
@@ -1265,6 +1333,7 @@ export const TASKS: {
1265
1333
  readonly TRANSLATE: "translate";
1266
1334
  readonly ASSIST: "assist";
1267
1335
  readonly DETECT_SPEECH: "detect-speech";
1336
+ readonly COMPARE: "compare";
1268
1337
  };
1269
1338
  /** Whether a service serves a task (declared `extras.tasks`, legacy id-prefix fallback). */
1270
1339
  export function servesTask(service: DiscoveredService, task: string): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larc-iu/plaid-client",
3
- "version": "0.2.0-alpha.14",
3
+ "version": "0.2.0-alpha.16",
4
4
  "description": "JavaScript client for the Plaid annotation API",
5
5
  "type": "module",
6
6
  "repository": {
package/src/http.js CHANGED
@@ -1,41 +1,41 @@
1
1
  import { transformRequest, transformResponse } from "./transforms.js";
2
2
 
3
3
  // ---------------------------------------------------------------------------
4
- // Batch mode: which calls join a batch, and which go over the wire anyway.
4
+ // Batches: which calls a batch carries, and which go over the wire anyway.
5
5
  //
6
- // `client.batched()` sets ONE flag on the whole client, and every call made
7
- // while it is set is queued, whatever code made it. A browser client is shared
8
- // by an editor, an importer and the app's chrome at once, so the call that
9
- // lands inside someone else's batch is usually not the one that opened it.
10
- // Every call reaching this layer is one of three things, and the third column
11
- // is what a new endpoint has to pick.
6
+ // A batch is an OBJECT (`client.batch()`, or the argument `client.batched(fn)`
7
+ // hands `fn`), a view of the client with the same bundles. A write made on the
8
+ // batch queues; a call made on the client itself always goes over the wire,
9
+ // whatever batches happen to be open. So the editor, the importer and the
10
+ // app's chrome can share one client, and a write by code that knows nothing
11
+ // about a batch can never land inside it. Every call reaching this layer is
12
+ // one of three things, and the third column is what a new endpoint has to
13
+ // pick.
12
14
  //
13
15
  // 1. A WRITE of project data: an entity, its metadata, a layer, a membership,
14
- // a comment. This is what a batch is for, so it QUEUES. It is the default
15
- // and needs no flag.
16
+ // a comment. This is what a batch is for, so on a batch it QUEUES. It is
17
+ // the default and needs no flag.
16
18
  //
17
- // 2. A READ. Pass `bypassBatch`. A queued read is broken three ways: the
18
- // caller gets `{ batched: true }` instead of data, the queued GET takes a
19
- // slot in the batch's results array and shifts every positional read after
20
- // it, and server-side the sub-request runs against the batch's transaction
21
- // connection rather than the pool, where `/query` throws and 500s the whole
22
- // batch, rolling back every write in it. Bypassed, the read is answered
23
- // from the pool and sees exactly the state it would have seen had the batch
24
- // not been open.
19
+ // 2. A READ. Every GET is answered from the wire even when made on a batch:
20
+ // the caller gets data rather than `{ batched: true }`, it takes no slot in
21
+ // the batch's results, and server-side it runs against the pool rather than
22
+ // the batch's transaction connection. A read that travels as a POST
23
+ // (`query`) says so with `outOfBand`.
25
24
  //
26
25
  // 3. An OUT-OF-BAND SIGNAL: stopping a service request, reporting a service's
27
- // progress or result, taking or dropping a document lock, an admin action
28
- // on the server itself. It is shaped like a write but carries no project
29
- // data, and its whole value is that it happens NOW. Queued, it happens at
30
- // submit, or never if the batch aborts, while its caller reads success.
31
- // Pass `bypassBatch` for these too.
26
+ // progress or result, taking or dropping a document lock, forgetting a
27
+ // service, an admin action on the server itself. It is shaped like a write
28
+ // but carries no project data, and its whole value is that it happens NOW.
29
+ // Pass `outOfBand`: made on a batch it still goes over the wire, and it
30
+ // never joins an open logical operation. The audit group is a label for
31
+ // the writes, and a signal that is never audited would mark the group
32
+ // written and leave the relabel PATCH 404ing on a group nothing ever
33
+ // created.
32
34
  //
33
- // A bypassed call neither joins the batch nor spends its stamp. Strict mode
34
- // marks the batch's one expected document-version onto the first QUEUED write,
35
- // and a call that went over the wire on its own is not that write. It does not
36
- // join an open logical operation either: the audit group is a label for the
37
- // writes, and a signal that is never audited would mark the group written and
38
- // leave the relabel PATCH 404ing on a group nothing ever created.
35
+ // Strict mode stamps the batch's one expected document-version onto the first
36
+ // write queued on it (whole-batch OCC: stamping every op would 409 the second
37
+ // against the bump the first caused). A call that went over the wire on its
38
+ // own carries its own stamp and spends nothing of any batch's.
39
39
  //
40
40
  // `noBatch` is not part of that judgment. It marks the five calls the batch
41
41
  // transport cannot carry at all (a batch inside a batch, the multipart media
@@ -300,12 +300,13 @@ export function xhrSend(
300
300
  * rawBody - Body value passed directly (no transform). Mutually exclusive with body.
301
301
  * formData - If true, body is FormData; skip Content-Type header
302
302
  * queryParams - Object of query param key/values to append
303
- * noBatch - If true, throw when in batch mode. Only for calls the
303
+ * noBatch - If true, throw when made on a batch. Only for calls the
304
304
  * batch transport cannot carry at all (see the note at the
305
305
  * top of this file); never for a read.
306
- * bypassBatch - If true, go over the wire even while a batch is open.
307
- * Every read carries it, and so does every out-of-band
308
- * signal (see the note at the top of this file).
306
+ * outOfBand - If true, the call is a signal rather than a write of
307
+ * project data: made on a batch it still goes over the
308
+ * wire, and it never joins an open logical operation (see
309
+ * the note at the top of this file).
309
310
  * skipResponseTransform - Return raw parsed JSON (no transformResponse)
310
311
  * noAuth - Skip Authorization header
311
312
  * binaryResponse - Return arrayBuffer instead of JSON/text
@@ -318,21 +319,24 @@ export function xhrSend(
318
319
  * where the timeout only fires when the upload stalls;
319
320
  * elsewhere the callback is ignored and fetch is used.
320
321
  */
321
- export async function makeRequest(client, method, path, options = {}) {
322
- const {
323
- body,
324
- rawBody,
325
- formData,
326
- queryParams,
327
- noBatch,
328
- bypassBatch,
329
- skipResponseTransform,
330
- noAuth,
331
- binaryResponse,
332
- auditMessage,
333
- timeout,
334
- onUploadProgress,
335
- } = options;
322
+ /**
323
+ * Everything a request is before it goes anywhere: the URL with its query
324
+ * params and the stamps strict mode, a per-call audit message and an open
325
+ * logical operation add, plus the transformed body. Shared by the wire path
326
+ * (`makeRequest`) and the batch path (`queueRequest`), so a queued op is
327
+ * exactly the request that would have gone out. `batch` is the batch the call
328
+ * is being queued on, if any: strict mode stamps its document-version onto the
329
+ * first write queued there and no other.
330
+ */
331
+ export function prepareRequest(
332
+ client,
333
+ method,
334
+ path,
335
+ options = {},
336
+ batch = null,
337
+ ) {
338
+ const { body, rawBody, formData, queryParams, outOfBand, auditMessage } =
339
+ options;
336
340
 
337
341
  // A write must not go out on a lock that lapsed. `documents.locked()`
338
342
  // records the loss here when its keep-alive cannot renew, and from that
@@ -340,11 +344,7 @@ export async function makeRequest(client, method, path, options = {}) {
340
344
  // have landed between the read the work was planned from and the write about
341
345
  // to go out. Reads pass, and so do the lock routes themselves, which is how
342
346
  // the block still releases on its way out.
343
- if (
344
- client.documentLockLost &&
345
- method !== "GET" &&
346
- !path.endsWith("/lock")
347
- ) {
347
+ if (client.documentLockLost && method !== "GET" && !path.endsWith("/lock")) {
348
348
  throw client.documentLockLost;
349
349
  }
350
350
 
@@ -376,27 +376,19 @@ export async function makeRequest(client, method, path, options = {}) {
376
376
  requestBody = transformRequest(body);
377
377
  }
378
378
 
379
- // Strict mode: append document-version for non-GET requests.
380
- // Inside a batch, stamp ONLY the first queued write: batches run atomically
381
- // server-side, so a version check on the first op gives whole-batch OCC
382
- // semantics, while stamping every op would 409 the second op against the
383
- // version bump the first op itself caused (every queued op captures the
384
- // same pre-batch version).
385
- if (
386
- client.strictModeDocumentId &&
387
- method !== "GET" &&
388
- !(client.isBatching && client.batchVersionStamped)
389
- ) {
379
+ // Strict mode: append document-version for non-GET requests, on EVERY
380
+ // queued write of a batch. The server validates the first write it can
381
+ // (one whose route resolves a document) and skips the rest of that
382
+ // document's, so the version bump a sub-op causes does not 409 the next
383
+ // one. Stamping only the first write was silently no check at all
384
+ // whenever that write was one the route ignores, such as a vocabulary
385
+ // entry's metadata, which is exactly what the igt editor queues first.
386
+ if (client.strictModeDocumentId && method !== "GET") {
390
387
  const docId = client.strictModeDocumentId;
391
388
  if (client.documentVersions[docId]) {
392
389
  const docVersion = client.documentVersions[docId];
393
390
  const separator = url.includes("?") ? "&" : "?";
394
391
  url += `${separator}document-version=${encodeURIComponent(docVersion)}`;
395
- // A bypassing call queues nothing, so it must not spend the batch's one
396
- // stamp. `query` is a POST and lands here from app chrome while someone
397
- // else's batch is open: marking the batch stamped there left the first
398
- // real write unversioned and the batch unguarded.
399
- if (client.isBatching && !bypassBatch) client.batchVersionStamped = true;
400
392
  }
401
393
  }
402
394
 
@@ -414,16 +406,11 @@ export async function makeRequest(client, method, path, options = {}) {
414
406
  // with the group id; the message rides along too so the server can label
415
407
  // the group lazily on whichever tagged write lands first.
416
408
  //
417
- // A bypassing call is not one of those writes. The out-of-band signals are
418
- // shaped like a write and carry no project data (a lock taken or renewed, a
419
- // cancelled service request, a service reporting its progress, an admin
420
- // control), and `query` is a read that travels as a POST. None of them lands
421
- // in the audit log, so a stamp does nothing server-side while `written`
422
- // promises a group that will never exist: the relabel PATCH then 404s. Same
423
- // rule as the batch and the OCC stamp above, for the same reason: a
424
- // bypassing call belongs to whoever made it, not to whatever operation
425
- // happens to be open on this shared client.
426
- if (client.operationGroup && method !== "GET" && !bypassBatch) {
409
+ // An out-of-band signal is not one of those writes (see the note at the top
410
+ // of this file): it never lands in the audit log, so a stamp does nothing
411
+ // server-side while `written` promises a group that will never exist, and
412
+ // the relabel PATCH then 404s.
413
+ if (client.operationGroup && method !== "GET" && !outOfBand) {
427
414
  const group = client.operationGroup;
428
415
  const separator = url.includes("?") ? "&" : "?";
429
416
  url += `${separator}group-id=${encodeURIComponent(group.id)}`;
@@ -432,24 +419,54 @@ export async function makeRequest(client, method, path, options = {}) {
432
419
  group.written = true;
433
420
  }
434
421
 
435
- // Batch mode. A `bypassBatch` call is deliberately NOT queued: it belongs to
436
- // whoever made it, not to whatever batch happens to be open on this shared
437
- // client (a document reconcile, an import). See the three classes at the top
438
- // of this file.
439
- if (client.isBatching && !bypassBatch) {
440
- if (noBatch) {
441
- throw new Error(`This endpoint cannot be used in batch mode: ${path}`);
442
- }
443
- const operation = {
444
- path: url.replace(client.baseUrl, ""),
445
- method: method.toUpperCase(),
446
- };
447
- if (requestBody !== undefined) {
448
- operation.body = requestBody;
449
- }
450
- client.batchOperations.push(operation);
451
- return { batched: true };
422
+ return { url, requestBody };
423
+ }
424
+
425
+ /**
426
+ * A call made on a batch (see `PlaidClient#batch`). A write of project data is
427
+ * queued as one operation of the batch and answers `{ batched: true }`; its
428
+ * result is the matching entry of what `submit()` resolves to. A read, and a
429
+ * signal marked `outOfBand`, is the client's to make and goes over the wire
430
+ * now, exactly as if it had been made on the client.
431
+ */
432
+ export async function queueRequest(batch, method, path, options = {}) {
433
+ if (method === "GET" || options.outOfBand) {
434
+ return batch.client._request(method, path, options);
435
+ }
436
+ if (!batch.open) {
437
+ throw new Error(`This batch was already submitted or aborted: ${path}`);
452
438
  }
439
+ if (options.noBatch) {
440
+ throw new Error(`This endpoint cannot be used in a batch: ${path}`);
441
+ }
442
+ const { url, requestBody } = prepareRequest(
443
+ batch.client,
444
+ method,
445
+ path,
446
+ options,
447
+ batch,
448
+ );
449
+ const operation = {
450
+ path: url.replace(batch.client.baseUrl, ""),
451
+ method: method.toUpperCase(),
452
+ };
453
+ if (requestBody !== undefined) {
454
+ operation.body = requestBody;
455
+ }
456
+ batch.operations.push(operation);
457
+ return { batched: true };
458
+ }
459
+
460
+ export async function makeRequest(client, method, path, options = {}) {
461
+ const {
462
+ formData,
463
+ skipResponseTransform,
464
+ noAuth,
465
+ binaryResponse,
466
+ timeout,
467
+ onUploadProgress,
468
+ } = options;
469
+ const { url, requestBody } = prepareRequest(client, method, path, options);
453
470
 
454
471
  // Build fetch options
455
472
  const headers = {};