@larc-iu/plaid-client 0.0.0

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 ADDED
@@ -0,0 +1,1532 @@
1
+ /**
2
+ * plaid-client - JavaScript client for the Plaid annotation API
3
+ */
4
+
5
+ import { transformRequest, transformResponse } from './transforms.js';
6
+ import {
7
+ makeRequest, extractDocumentVersions, parseErrorBody, makeHttpError,
8
+ makeNetworkError, timeoutSignal, DEFAULT_TIMEOUT_MS,
9
+ } from './http.js';
10
+ import { listAll, listPage, iterPages } from './pagination.js';
11
+ import { createSSEConnection } from './sse.js';
12
+ import {
13
+ discoverServices,
14
+ serve,
15
+ requestService,
16
+ } from './services.js';
17
+
18
+ // Helper: build body object, filtering out undefined values
19
+ function bodyOf(obj) {
20
+ const result = {};
21
+ for (const [key, value] of Object.entries(obj)) {
22
+ if (value !== undefined) result[key] = value;
23
+ }
24
+ return result;
25
+ }
26
+
27
+ class PlaidClient {
28
+ /**
29
+ * Create a new PlaidClient instance
30
+ * @param {string} baseUrl - The base URL for the API
31
+ * @param {string} token - The authentication token
32
+ * @param {object} [options] - Client options
33
+ * @param {number} [options.timeout=30000] - Per-request timeout in ms (0 or null disables it)
34
+ */
35
+ constructor(baseUrl, token, options = {}) {
36
+ this.baseUrl = baseUrl.replace(/\/$/, '');
37
+ this.token = token;
38
+ this.timeout = options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS;
39
+ this.isBatching = false;
40
+ this.batchOperations = [];
41
+ this.documentVersions = {};
42
+ this.strictModeDocumentId = null;
43
+
44
+ // --- API Bundles ---
45
+
46
+ this.vocabLinks = {
47
+ /**
48
+ * Create a new vocab link between tokens and a vocab item.
49
+ * @param {string} vocabItem - The vocab item to link
50
+ * @param {Array} tokens - The tokens to link
51
+ * @param {any} [metadata] - Metadata for the link. Omit to leave unset; pass null to send JSON null.
52
+ */
53
+ create: (vocabItem, tokens, metadata) =>
54
+ this._request('POST', '/api/v1/vocab-links', {
55
+ body: bodyOf({ 'vocab-item': vocabItem, tokens, metadata }),
56
+ }),
57
+ /**
58
+ * Replace all metadata for a vocab link. The entire metadata map is replaced - existing metadata keys not included in the request will be removed.
59
+ * @param {string} id - The resource ID
60
+ * @param {any} body - The request body
61
+ */
62
+ setMetadata: (id, body) =>
63
+ this._request('PUT', `/api/v1/vocab-links/${id}/metadata`, {
64
+ rawBody: body, skipResponseTransform: true,
65
+ }),
66
+ /**
67
+ * Remove all metadata from a vocab link.
68
+ * @param {string} id - The resource ID
69
+ */
70
+ deleteMetadata: (id) =>
71
+ this._request('DELETE', `/api/v1/vocab-links/${id}/metadata`, {
72
+ skipResponseTransform: true,
73
+ }),
74
+ /**
75
+ * Get a vocab link by ID
76
+ * @param {string} id - The resource ID
77
+ * @param {string} [asOf] - Temporal query timestamp
78
+ */
79
+ get: (id, asOf) =>
80
+ this._request('GET', `/api/v1/vocab-links/${id}`, {
81
+ queryParams: { 'as-of': asOf },
82
+ }),
83
+ /**
84
+ * Delete a vocab link
85
+ * @param {string} id - The resource ID
86
+ */
87
+ delete: (id) =>
88
+ this._request('DELETE', `/api/v1/vocab-links/${id}`),
89
+ };
90
+
91
+ this.vocabLayers = {
92
+ /**
93
+ * Get a vocab layer by ID
94
+ * @param {string} id - The resource ID
95
+ * @param {boolean} [includeItems] - Include vocab items
96
+ * @param {string} [asOf] - Temporal query timestamp
97
+ */
98
+ get: (id, includeItems, asOf) =>
99
+ this._request('GET', `/api/v1/vocab-layers/${id}`, {
100
+ queryParams: { 'include-items': includeItems, 'as-of': asOf },
101
+ }),
102
+ /**
103
+ * Delete a vocab layer.
104
+ * @param {string} id - The resource ID
105
+ */
106
+ delete: (id) =>
107
+ this._request('DELETE', `/api/v1/vocab-layers/${id}`),
108
+ /**
109
+ * Update a vocab layer's name.
110
+ * @param {string} id - The resource ID
111
+ * @param {string} name - The name
112
+ */
113
+ update: (id, name) =>
114
+ this._request('PATCH', `/api/v1/vocab-layers/${id}`, {
115
+ body: bodyOf({ name }),
116
+ }),
117
+ /**
118
+ * Set a configuration value for a layer in an editor namespace.
119
+ * @param {string} id - The resource ID
120
+ * @param {string} namespace - The config namespace
121
+ * @param {string} configKey - The config key
122
+ * @param {any} configValue - Configuration value to set
123
+ */
124
+ setConfig: (id, namespace, configKey, configValue) =>
125
+ this._request('PUT', `/api/v1/vocab-layers/${id}/config/${namespace}/${configKey}`, {
126
+ rawBody: configValue, skipResponseTransform: true,
127
+ }),
128
+ /**
129
+ * Remove a configuration value for a layer.
130
+ * @param {string} id - The resource ID
131
+ * @param {string} namespace - The config namespace
132
+ * @param {string} configKey - The config key
133
+ */
134
+ deleteConfig: (id, namespace, configKey) =>
135
+ this._request('DELETE', `/api/v1/vocab-layers/${id}/config/${namespace}/${configKey}`, {
136
+ skipResponseTransform: true,
137
+ }),
138
+ /**
139
+ * List all vocab layers accessible to user. Transparently follows
140
+ * pagination cursors and returns the full flat array.
141
+ * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
142
+ * @param {string} [asOf] - Temporal query timestamp
143
+ */
144
+ list: (asOf) =>
145
+ listAll(this, '/api/v1/vocab-layers', { query: { 'as-of': asOf } }),
146
+ /**
147
+ * Fetch a single page of vocab layers.
148
+ * @param {object} [opts]
149
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
150
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
151
+ * @param {string} [opts.asOf] - Temporal query timestamp
152
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
153
+ */
154
+ listPage: ({ limit, cursor, asOf } = {}) =>
155
+ listPage(this, '/api/v1/vocab-layers', { limit, cursor, query: { 'as-of': asOf } }),
156
+ /**
157
+ * Async-iterate vocab layers page by page; yields each page's entries array.
158
+ * @param {object} [opts]
159
+ * @param {number} [opts.pageSize] - Per-request page size
160
+ * @param {string} [opts.asOf] - Temporal query timestamp
161
+ * Cannot be used inside a batch (auto-paginates across requests); throws on first iteration if called while batching — use listPage() for a single page in a batch.
162
+ * @returns {AsyncGenerator<Array>}
163
+ */
164
+ iterPages: ({ pageSize, asOf } = {}) =>
165
+ iterPages(this, '/api/v1/vocab-layers', { pageSize, query: { 'as-of': asOf } }),
166
+ /**
167
+ * Create a new vocab layer. Note: this also registers the user as a maintainer.
168
+ * @param {string} name - The name
169
+ */
170
+ create: (name) =>
171
+ this._request('POST', '/api/v1/vocab-layers', {
172
+ body: bodyOf({ name }),
173
+ }),
174
+ /**
175
+ * Assign a user as a maintainer for this vocab layer.
176
+ * @param {string} id - The resource ID
177
+ * @param {string} userId - The user ID
178
+ */
179
+ addMaintainer: (id, userId) =>
180
+ this._request('POST', `/api/v1/vocab-layers/${id}/maintainers/${userId}`),
181
+ /**
182
+ * Remove a user's maintainer privileges for this vocab layer.
183
+ * @param {string} id - The resource ID
184
+ * @param {string} userId - The user ID
185
+ */
186
+ removeMaintainer: (id, userId) =>
187
+ this._request('DELETE', `/api/v1/vocab-layers/${id}/maintainers/${userId}`),
188
+ };
189
+
190
+ this.relations = {
191
+ /**
192
+ * Replace all metadata for a relation.
193
+ * @param {string} relationId - The relation ID
194
+ * @param {any} body - The request body
195
+ */
196
+ setMetadata: (relationId, body) =>
197
+ this._request('PUT', `/api/v1/relations/${relationId}/metadata`, {
198
+ rawBody: body, skipResponseTransform: true,
199
+ }),
200
+ /**
201
+ * Remove all metadata from a relation.
202
+ * @param {string} relationId - The relation ID
203
+ */
204
+ deleteMetadata: (relationId) =>
205
+ this._request('DELETE', `/api/v1/relations/${relationId}/metadata`, {
206
+ skipResponseTransform: true,
207
+ }),
208
+ /**
209
+ * Update the target span of a relation.
210
+ * @param {string} relationId - The relation ID
211
+ * @param {string} spanId - The span ID
212
+ */
213
+ setTarget: (relationId, spanId) =>
214
+ this._request('PUT', `/api/v1/relations/${relationId}/target`, {
215
+ body: bodyOf({ 'span-id': spanId }),
216
+ }),
217
+ /**
218
+ * Get a relation by ID.
219
+ * @param {string} relationId - The relation ID
220
+ * @param {string} [asOf] - Temporal query timestamp
221
+ */
222
+ get: (relationId, asOf) =>
223
+ this._request('GET', `/api/v1/relations/${relationId}`, {
224
+ queryParams: { 'as-of': asOf },
225
+ }),
226
+ /**
227
+ * Delete a relation.
228
+ * @param {string} relationId - The relation ID
229
+ */
230
+ delete: (relationId) =>
231
+ this._request('DELETE', `/api/v1/relations/${relationId}`),
232
+ /**
233
+ * Update a relation's value.
234
+ * @param {string} relationId - The relation ID
235
+ * @param {any} value - The value
236
+ */
237
+ update: (relationId, value) =>
238
+ this._request('PATCH', `/api/v1/relations/${relationId}`, {
239
+ body: bodyOf({ value }),
240
+ }),
241
+ /**
242
+ * Update the source span of a relation.
243
+ * @param {string} relationId - The relation ID
244
+ * @param {string} spanId - The span ID
245
+ */
246
+ setSource: (relationId, spanId) =>
247
+ this._request('PUT', `/api/v1/relations/${relationId}/source`, {
248
+ body: bodyOf({ 'span-id': spanId }),
249
+ }),
250
+ /**
251
+ * Create a new relation. A relation is a directed edge between two spans
252
+ * with a value, useful for expressing phenomena such as syntactic or
253
+ * semantic relations.
254
+ * @param {string} layerId - The relation layer ID
255
+ * @param {string} sourceId - The source span ID
256
+ * @param {string} targetId - The target span ID
257
+ * @param {any} value - The value
258
+ * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
259
+ */
260
+ create: (layerId, sourceId, targetId, value, metadata) =>
261
+ this._request('POST', '/api/v1/relations', {
262
+ body: bodyOf({ 'layer-id': layerId, 'source-id': sourceId, 'target-id': targetId, value, metadata }),
263
+ }),
264
+ /**
265
+ * Create multiple relations in a single operation.
266
+ * @param {Array} body - The request body
267
+ */
268
+ bulkCreate: (body) =>
269
+ this._request('POST', '/api/v1/relations/bulk', { body }),
270
+ /**
271
+ * Delete multiple relations in a single operation. Provide an array of IDs.
272
+ * @param {Array} body - The request body
273
+ */
274
+ bulkDelete: (body) =>
275
+ this._request('DELETE', '/api/v1/relations/bulk', { body }),
276
+ };
277
+
278
+ this.spanLayers = {
279
+ /**
280
+ * Set a configuration value for a layer in an editor namespace.
281
+ * @param {string} spanLayerId - The span layer ID
282
+ * @param {string} namespace - The config namespace
283
+ * @param {string} configKey - The config key
284
+ * @param {any} configValue - Configuration value to set
285
+ */
286
+ setConfig: (spanLayerId, namespace, configKey, configValue) =>
287
+ this._request('PUT', `/api/v1/span-layers/${spanLayerId}/config/${namespace}/${configKey}`, {
288
+ rawBody: configValue, skipResponseTransform: true,
289
+ }),
290
+ /**
291
+ * Remove a configuration value for a layer.
292
+ * @param {string} spanLayerId - The span layer ID
293
+ * @param {string} namespace - The config namespace
294
+ * @param {string} configKey - The config key
295
+ */
296
+ deleteConfig: (spanLayerId, namespace, configKey) =>
297
+ this._request('DELETE', `/api/v1/span-layers/${spanLayerId}/config/${namespace}/${configKey}`, {
298
+ skipResponseTransform: true,
299
+ }),
300
+ /**
301
+ * Get a span layer by ID.
302
+ * @param {string} spanLayerId - The span layer ID
303
+ * @param {string} [asOf] - Temporal query timestamp
304
+ */
305
+ get: (spanLayerId, asOf) =>
306
+ this._request('GET', `/api/v1/span-layers/${spanLayerId}`, {
307
+ queryParams: { 'as-of': asOf },
308
+ }),
309
+ /**
310
+ * Delete a span layer.
311
+ * @param {string} spanLayerId - The span layer ID
312
+ */
313
+ delete: (spanLayerId) =>
314
+ this._request('DELETE', `/api/v1/span-layers/${spanLayerId}`),
315
+ /**
316
+ * Update a span layer's name.
317
+ * @param {string} spanLayerId - The span layer ID
318
+ * @param {string} name - The name
319
+ */
320
+ update: (spanLayerId, name) =>
321
+ this._request('PATCH', `/api/v1/span-layers/${spanLayerId}`, {
322
+ body: bodyOf({ name }),
323
+ }),
324
+ /**
325
+ * Create a new span layer.
326
+ * @param {string} tokenLayerId - The token layer ID
327
+ * @param {string} name - The name
328
+ */
329
+ create: (tokenLayerId, name) =>
330
+ this._request('POST', '/api/v1/span-layers', {
331
+ body: bodyOf({ 'token-layer-id': tokenLayerId, name }),
332
+ }),
333
+ /**
334
+ * Shift a span layer's display order.
335
+ * @param {string} spanLayerId - The span layer ID
336
+ * @param {string} direction - The direction ("up" or "down")
337
+ */
338
+ shift: (spanLayerId, direction) =>
339
+ this._request('POST', `/api/v1/span-layers/${spanLayerId}/shift`, {
340
+ body: bodyOf({ direction }),
341
+ }),
342
+ };
343
+
344
+ this.spans = {
345
+ /**
346
+ * Replace tokens for a span.
347
+ * @param {string} spanId - The span ID
348
+ * @param {Array} tokens - The tokens
349
+ */
350
+ setTokens: (spanId, tokens) =>
351
+ this._request('PUT', `/api/v1/spans/${spanId}/tokens`, {
352
+ body: bodyOf({ tokens }),
353
+ }),
354
+ /**
355
+ * Create a new span. A span holds a primary atomic value and optional
356
+ * metadata, and must at all times be associated with one or more tokens.
357
+ * @param {string} spanLayerId - The span layer ID
358
+ * @param {Array} tokens - The tokens
359
+ * @param {any} value - The value
360
+ * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
361
+ */
362
+ create: (spanLayerId, tokens, value, metadata) =>
363
+ this._request('POST', '/api/v1/spans', {
364
+ body: bodyOf({ 'span-layer-id': spanLayerId, tokens, value, metadata }),
365
+ }),
366
+ /**
367
+ * Get a span by ID.
368
+ * @param {string} spanId - The span ID
369
+ * @param {string} [asOf] - Temporal query timestamp
370
+ */
371
+ get: (spanId, asOf) =>
372
+ this._request('GET', `/api/v1/spans/${spanId}`, {
373
+ queryParams: { 'as-of': asOf },
374
+ }),
375
+ /**
376
+ * Delete a span.
377
+ * @param {string} spanId - The span ID
378
+ */
379
+ delete: (spanId) =>
380
+ this._request('DELETE', `/api/v1/spans/${spanId}`),
381
+ /**
382
+ * Update a span's value.
383
+ * @param {string} spanId - The span ID
384
+ * @param {any} value - The value
385
+ */
386
+ update: (spanId, value) =>
387
+ this._request('PATCH', `/api/v1/spans/${spanId}`, {
388
+ body: bodyOf({ value }),
389
+ }),
390
+ /**
391
+ * Create multiple spans in a single operation.
392
+ * @param {Array} body - The request body
393
+ */
394
+ bulkCreate: (body) =>
395
+ this._request('POST', '/api/v1/spans/bulk', { body }),
396
+ /**
397
+ * Delete multiple spans in a single operation. Provide an array of IDs.
398
+ * @param {Array} body - The request body
399
+ */
400
+ bulkDelete: (body) =>
401
+ this._request('DELETE', '/api/v1/spans/bulk', { body }),
402
+ /**
403
+ * Replace all metadata for a span.
404
+ * @param {string} spanId - The span ID
405
+ * @param {any} body - The request body
406
+ */
407
+ setMetadata: (spanId, body) =>
408
+ this._request('PUT', `/api/v1/spans/${spanId}/metadata`, {
409
+ rawBody: body, skipResponseTransform: true,
410
+ }),
411
+ /**
412
+ * Remove all metadata from a span.
413
+ * @param {string} spanId - The span ID
414
+ */
415
+ deleteMetadata: (spanId) =>
416
+ this._request('DELETE', `/api/v1/spans/${spanId}/metadata`, {
417
+ skipResponseTransform: true,
418
+ }),
419
+ };
420
+
421
+ this.batch = {
422
+ /**
423
+ * Execute multiple API operations atomically. If any operation fails, all
424
+ * changes are rolled back.
425
+ * @param {Array} body - The request body
426
+ */
427
+ submit: (body) =>
428
+ this._request('POST', '/api/v1/batch', {
429
+ body, noBatch: true,
430
+ }),
431
+ };
432
+
433
+ this.texts = {
434
+ /**
435
+ * Replace all metadata for a text.
436
+ * @param {string} textId - The text ID
437
+ * @param {any} body - The request body
438
+ */
439
+ setMetadata: (textId, body) =>
440
+ this._request('PUT', `/api/v1/texts/${textId}/metadata`, {
441
+ rawBody: body, skipResponseTransform: true,
442
+ }),
443
+ /**
444
+ * Remove all metadata from a text.
445
+ * @param {string} textId - The text ID
446
+ */
447
+ deleteMetadata: (textId) =>
448
+ this._request('DELETE', `/api/v1/texts/${textId}/metadata`, {
449
+ skipResponseTransform: true,
450
+ }),
451
+ /**
452
+ * Create a new text in a document's text layer. A text is a container for
453
+ * one long string in `body` for a given layer.
454
+ * @param {string} textLayerId - The text layer ID
455
+ * @param {string} documentId - The document ID
456
+ * @param {string} body - The request body
457
+ * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
458
+ */
459
+ create: (textLayerId, documentId, body, metadata) =>
460
+ this._request('POST', '/api/v1/texts', {
461
+ body: bodyOf({ 'text-layer-id': textLayerId, 'document-id': documentId, body, metadata }),
462
+ }),
463
+ /**
464
+ * Get a text.
465
+ * @param {string} textId - The text ID
466
+ * @param {string} [asOf] - Temporal query timestamp
467
+ */
468
+ get: (textId, asOf) =>
469
+ this._request('GET', `/api/v1/texts/${textId}`, {
470
+ queryParams: { 'as-of': asOf },
471
+ }),
472
+ /**
473
+ * Delete a text and all dependent data.
474
+ * @param {string} textId - The text ID
475
+ */
476
+ delete: (textId) =>
477
+ this._request('DELETE', `/api/v1/texts/${textId}`),
478
+ /**
479
+ * Update a text's body. A diff is computed and token indices are updated
480
+ * so that tokens remain intact. Alternatively, `body` can be a list of
481
+ * edit directives.
482
+ * @param {string} textId - The text ID
483
+ * @param {any} body - The request body
484
+ */
485
+ update: (textId, body) =>
486
+ this._request('PATCH', `/api/v1/texts/${textId}`, {
487
+ body: bodyOf({ body }),
488
+ }),
489
+ };
490
+
491
+ this.users = {
492
+ /**
493
+ * List all users. Transparently follows pagination cursors and returns
494
+ * the full flat array.
495
+ * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
496
+ * @param {string} [asOf] - Temporal query timestamp
497
+ */
498
+ list: (asOf) =>
499
+ listAll(this, '/api/v1/users', { query: { 'as-of': asOf } }),
500
+ /**
501
+ * Fetch a single page of users.
502
+ * @param {object} [opts]
503
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
504
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
505
+ * @param {string} [opts.asOf] - Temporal query timestamp
506
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
507
+ */
508
+ listPage: ({ limit, cursor, asOf } = {}) =>
509
+ listPage(this, '/api/v1/users', { limit, cursor, query: { 'as-of': asOf } }),
510
+ /**
511
+ * Async-iterate users page by page; yields each page's entries array.
512
+ * @param {object} [opts]
513
+ * @param {number} [opts.pageSize] - Per-request page size
514
+ * @param {string} [opts.asOf] - Temporal query timestamp
515
+ * Cannot be used inside a batch (auto-paginates across requests); throws on first iteration if called while batching — use listPage() for a single page in a batch.
516
+ * @returns {AsyncGenerator<Array>}
517
+ */
518
+ iterPages: ({ pageSize, asOf } = {}) =>
519
+ iterPages(this, '/api/v1/users', { pageSize, query: { 'as-of': asOf } }),
520
+ /**
521
+ * Create a new user
522
+ * @param {string} username - The username
523
+ * @param {string} password - The password
524
+ * @param {boolean} isAdmin - Whether the user is an admin
525
+ */
526
+ create: (username, password, isAdmin) =>
527
+ this._request('POST', '/api/v1/users', {
528
+ body: bodyOf({ username, password, 'is-admin': isAdmin }),
529
+ }),
530
+ /**
531
+ * Get audit log for a user's actions. Transparently follows pagination
532
+ * cursors and returns the full flat array.
533
+ * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
534
+ * @param {string} userId - The user ID
535
+ * @param {string} [startTime] - Start of time range
536
+ * @param {string} [endTime] - End of time range
537
+ * @param {string} [asOf] - Temporal query timestamp
538
+ */
539
+ audit: (userId, startTime, endTime, asOf) =>
540
+ listAll(this, `/api/v1/users/${userId}/audit`, {
541
+ query: { 'start-time': startTime, 'end-time': endTime, 'as-of': asOf },
542
+ }),
543
+ /**
544
+ * Get a user by ID
545
+ * @param {string} id - The resource ID
546
+ * @param {string} [asOf] - Temporal query timestamp
547
+ */
548
+ get: (id, asOf) =>
549
+ this._request('GET', `/api/v1/users/${id}`, {
550
+ queryParams: { 'as-of': asOf },
551
+ }),
552
+ /**
553
+ * Delete a user
554
+ * @param {string} id - The resource ID
555
+ */
556
+ delete: (id) =>
557
+ this._request('DELETE', `/api/v1/users/${id}`),
558
+ /**
559
+ * Modify a user. Admins may change the username, password, and admin
560
+ * status of any user. All other users may only modify their own username
561
+ * or password.
562
+ * @param {string} id - The resource ID
563
+ * @param {string} [password] - New password
564
+ * @param {string} [username] - New username
565
+ * @param {boolean} [isAdmin] - New admin status
566
+ */
567
+ update: (id, password, username, isAdmin) =>
568
+ this._request('PATCH', `/api/v1/users/${id}`, {
569
+ body: bodyOf({ password, username, 'is-admin': isAdmin }),
570
+ }),
571
+ };
572
+
573
+ this.apiTokens = {
574
+ /**
575
+ * List a user's named API tokens. Never includes the signed token
576
+ * string itself — that is only returned once, by create().
577
+ * Transparently follows pagination cursors and returns the full flat array.
578
+ * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
579
+ * @param {string} userId - The user ID who owns the tokens
580
+ */
581
+ list: (userId) =>
582
+ listAll(this, `/api/v1/users/${userId}/tokens`),
583
+ /**
584
+ * Fetch a single page of a user's named API tokens.
585
+ * @param {string} userId - The user ID who owns the tokens
586
+ * @param {object} [opts]
587
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
588
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
589
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
590
+ */
591
+ listPage: (userId, { limit, cursor } = {}) =>
592
+ listPage(this, `/api/v1/users/${userId}/tokens`, { limit, cursor }),
593
+ /**
594
+ * Async-iterate a user's named API tokens page by page; yields each
595
+ * page's entries array.
596
+ * @param {string} userId - The user ID who owns the tokens
597
+ * @param {object} [opts]
598
+ * @param {number} [opts.pageSize] - Per-request page size
599
+ * Cannot be used inside a batch (auto-paginates across requests); throws on first iteration if called while batching — use listPage() for a single page in a batch.
600
+ * @returns {AsyncGenerator<Array>}
601
+ */
602
+ iterPages: (userId, { pageSize } = {}) =>
603
+ iterPages(this, `/api/v1/users/${userId}/tokens`, { pageSize }),
604
+ /**
605
+ * Mint a named API token for a user. The returned `token` is the signed
606
+ * credential and is shown ONLY here — store it immediately. API tokens
607
+ * do not expire and survive password changes / logout; revoke to kill.
608
+ * @param {string} userId - The user ID who will own the token
609
+ * @param {string} name - A human label, e.g. "Stanza Parser"
610
+ * @returns {Promise<{id: string, name: string, token: string}>}
611
+ */
612
+ create: (userId, name) =>
613
+ this._request('POST', `/api/v1/users/${userId}/tokens`, {
614
+ body: bodyOf({ name }),
615
+ }),
616
+ /**
617
+ * Revoke a named API token (soft-revoke; idempotent).
618
+ * @param {string} userId - The user ID who owns the token
619
+ * @param {string} tokenId - The token ID to revoke
620
+ */
621
+ revoke: (userId, tokenId) =>
622
+ this._request('DELETE', `/api/v1/users/${userId}/tokens/${tokenId}`),
623
+ };
624
+
625
+ this.tokenLayers = {
626
+ /**
627
+ * Shift a token layer's display order.
628
+ * @param {string} tokenLayerId - The token layer ID
629
+ * @param {string} direction - The direction ("up" or "down")
630
+ */
631
+ shift: (tokenLayerId, direction) =>
632
+ this._request('POST', `/api/v1/token-layers/${tokenLayerId}/shift`, {
633
+ body: bodyOf({ direction }),
634
+ }),
635
+ /**
636
+ * Create a new token layer.
637
+ * @param {string} textLayerId - The text layer ID
638
+ * @param {string} name - The name
639
+ * @param {string} [overlapMode] - Per-layer, immutable token invariant: "any" (default), "non-overlapping", or "partitioning". On partitioning layers, single token create/update/delete are rejected; use bulkCreate plus split/merge/shift.
640
+ * @param {string} [parentTokenLayerId] - Optional immutable parent token layer. Tokens in this layer must nest within a parent-layer token; the parent layer must be in the same text layer and be "non-overlapping" or "partitioning" (an "any" parent is rejected). A nested layer may be "any" or "non-overlapping" but not "partitioning" (partitioning is only for root layers), e.g. words (non-overlapping, parent=sentences) within sentences (partitioning).
641
+ */
642
+ create: (textLayerId, name, overlapMode, parentTokenLayerId) =>
643
+ this._request('POST', '/api/v1/token-layers', {
644
+ body: bodyOf({ 'text-layer-id': textLayerId, name, 'overlap-mode': overlapMode, 'parent-token-layer-id': parentTokenLayerId }),
645
+ }),
646
+ /**
647
+ * Set a configuration value for a layer in an editor namespace.
648
+ * @param {string} tokenLayerId - The token layer ID
649
+ * @param {string} namespace - The config namespace
650
+ * @param {string} configKey - The config key
651
+ * @param {any} configValue - Configuration value to set
652
+ */
653
+ setConfig: (tokenLayerId, namespace, configKey, configValue) =>
654
+ this._request('PUT', `/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`, {
655
+ rawBody: configValue, skipResponseTransform: true,
656
+ }),
657
+ /**
658
+ * Remove a configuration value for a layer.
659
+ * @param {string} tokenLayerId - The token layer ID
660
+ * @param {string} namespace - The config namespace
661
+ * @param {string} configKey - The config key
662
+ */
663
+ deleteConfig: (tokenLayerId, namespace, configKey) =>
664
+ this._request('DELETE', `/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`, {
665
+ skipResponseTransform: true,
666
+ }),
667
+ /**
668
+ * Get a token layer by ID.
669
+ * @param {string} tokenLayerId - The token layer ID
670
+ * @param {string} [asOf] - Temporal query timestamp
671
+ */
672
+ get: (tokenLayerId, asOf) =>
673
+ this._request('GET', `/api/v1/token-layers/${tokenLayerId}`, {
674
+ queryParams: { 'as-of': asOf },
675
+ }),
676
+ /**
677
+ * Delete a token layer.
678
+ * @param {string} tokenLayerId - The token layer ID
679
+ */
680
+ delete: (tokenLayerId) =>
681
+ this._request('DELETE', `/api/v1/token-layers/${tokenLayerId}`),
682
+ /**
683
+ * Update a token layer's name.
684
+ * @param {string} tokenLayerId - The token layer ID
685
+ * @param {string} name - The name
686
+ */
687
+ update: (tokenLayerId, name) =>
688
+ this._request('PATCH', `/api/v1/token-layers/${tokenLayerId}`, {
689
+ body: bodyOf({ name }),
690
+ }),
691
+ };
692
+
693
+ this.documents = {
694
+ /**
695
+ * Check the lock status of a document.
696
+ * @param {string} documentId - The document ID
697
+ * @param {string} [asOf] - Temporal query timestamp
698
+ */
699
+ checkLock: (documentId, asOf) =>
700
+ this._request('GET', `/api/v1/documents/${documentId}/lock`, {
701
+ queryParams: { 'as-of': asOf },
702
+ }),
703
+ /**
704
+ * Acquire or refresh a document lock
705
+ * @param {string} documentId - The document ID
706
+ */
707
+ acquireLock: (documentId) =>
708
+ this._request('POST', `/api/v1/documents/${documentId}/lock`),
709
+ /**
710
+ * Release a document lock
711
+ * @param {string} documentId - The document ID
712
+ */
713
+ releaseLock: (documentId) =>
714
+ this._request('DELETE', `/api/v1/documents/${documentId}/lock`),
715
+ /**
716
+ * Get media file for a document
717
+ * @param {string} documentId - The document ID
718
+ * @param {string} [asOf] - Temporal query timestamp
719
+ */
720
+ getMedia: (documentId, asOf) =>
721
+ this._request('GET', `/api/v1/documents/${documentId}/media`, {
722
+ queryParams: { 'as-of': asOf },
723
+ noBatch: true,
724
+ binaryResponse: true,
725
+ }),
726
+ /**
727
+ * Upload a media file for a document. Uses Apache Tika for content validation.
728
+ * @param {string} documentId - The document ID
729
+ * @param {File} file - The file to upload
730
+ */
731
+ uploadMedia: (documentId, file) => {
732
+ const fd = new FormData();
733
+ fd.append('file', file);
734
+ return this._request('PUT', `/api/v1/documents/${documentId}/media`, {
735
+ body: fd, formData: true, noBatch: true,
736
+ });
737
+ },
738
+ /**
739
+ * Delete media file for a document
740
+ * @param {string} documentId - The document ID
741
+ */
742
+ deleteMedia: (documentId) =>
743
+ this._request('DELETE', `/api/v1/documents/${documentId}/media`, {
744
+ noBatch: true,
745
+ }),
746
+ /**
747
+ * Replace all metadata for a document.
748
+ * @param {string} documentId - The document ID
749
+ * @param {any} body - The request body
750
+ */
751
+ setMetadata: (documentId, body) =>
752
+ this._request('PUT', `/api/v1/documents/${documentId}/metadata`, {
753
+ rawBody: body, skipResponseTransform: true,
754
+ }),
755
+ /**
756
+ * Remove all metadata from a document.
757
+ * @param {string} documentId - The document ID
758
+ */
759
+ deleteMetadata: (documentId) =>
760
+ this._request('DELETE', `/api/v1/documents/${documentId}/metadata`, {
761
+ skipResponseTransform: true,
762
+ }),
763
+ /**
764
+ * Get audit log for a document. Transparently follows pagination cursors
765
+ * and returns the full flat array.
766
+ * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
767
+ * @param {string} documentId - The document ID
768
+ * @param {string} [startTime] - Start of time range
769
+ * @param {string} [endTime] - End of time range
770
+ * @param {string} [asOf] - Temporal query timestamp
771
+ */
772
+ audit: (documentId, startTime, endTime, asOf) =>
773
+ listAll(this, `/api/v1/documents/${documentId}/audit`, {
774
+ query: { 'start-time': startTime, 'end-time': endTime, 'as-of': asOf },
775
+ }),
776
+ /**
777
+ * Get a document. Set `includeBody` to true to include all data.
778
+ * @param {string} documentId - The document ID
779
+ * @param {boolean} [includeBody] - Include document body data
780
+ * @param {string} [asOf] - Temporal query timestamp
781
+ */
782
+ get: (documentId, includeBody, asOf) =>
783
+ this._request('GET', `/api/v1/documents/${documentId}`, {
784
+ queryParams: { 'include-body': includeBody, 'as-of': asOf },
785
+ }),
786
+ /**
787
+ * Delete a document and all data contained.
788
+ * @param {string} documentId - The document ID
789
+ */
790
+ delete: (documentId) =>
791
+ this._request('DELETE', `/api/v1/documents/${documentId}`),
792
+ /**
793
+ * Update a document's name.
794
+ * @param {string} documentId - The document ID
795
+ * @param {string} name - The name
796
+ */
797
+ update: (documentId, name) =>
798
+ this._request('PATCH', `/api/v1/documents/${documentId}`, {
799
+ body: bodyOf({ name }),
800
+ }),
801
+ /**
802
+ * Create a new document in a project.
803
+ * @param {string} projectId - The project ID
804
+ * @param {string} name - The name
805
+ * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
806
+ */
807
+ create: (projectId, name, metadata) =>
808
+ this._request('POST', '/api/v1/documents', {
809
+ body: bodyOf({ 'project-id': projectId, name, metadata }),
810
+ }),
811
+ };
812
+
813
+ this.projects = {
814
+ /**
815
+ * Set a user's access level to read and write for this project.
816
+ * @param {string} id - The resource ID
817
+ * @param {string} userId - The user ID
818
+ */
819
+ addWriter: (id, userId) =>
820
+ this._request('POST', `/api/v1/projects/${id}/writers/${userId}`),
821
+ /**
822
+ * Remove a user's writer privileges for this project.
823
+ * @param {string} id - The resource ID
824
+ * @param {string} userId - The user ID
825
+ */
826
+ removeWriter: (id, userId) =>
827
+ this._request('DELETE', `/api/v1/projects/${id}/writers/${userId}`),
828
+ /**
829
+ * Set a user's access level to read-only for this project.
830
+ * @param {string} id - The resource ID
831
+ * @param {string} userId - The user ID
832
+ */
833
+ addReader: (id, userId) =>
834
+ this._request('POST', `/api/v1/projects/${id}/readers/${userId}`),
835
+ /**
836
+ * Remove a user's reader privileges for this project.
837
+ * @param {string} id - The resource ID
838
+ * @param {string} userId - The user ID
839
+ */
840
+ removeReader: (id, userId) =>
841
+ this._request('DELETE', `/api/v1/projects/${id}/readers/${userId}`),
842
+ /**
843
+ * Set a configuration value for a project in an editor namespace.
844
+ * @param {string} id - The resource ID
845
+ * @param {string} namespace - The config namespace
846
+ * @param {string} configKey - The config key
847
+ * @param {any} configValue - Configuration value to set
848
+ */
849
+ setConfig: (id, namespace, configKey, configValue) =>
850
+ this._request('PUT', `/api/v1/projects/${id}/config/${namespace}/${configKey}`, {
851
+ rawBody: configValue, skipResponseTransform: true,
852
+ }),
853
+ /**
854
+ * Remove a configuration value for a project.
855
+ * @param {string} id - The resource ID
856
+ * @param {string} namespace - The config namespace
857
+ * @param {string} configKey - The config key
858
+ */
859
+ deleteConfig: (id, namespace, configKey) =>
860
+ this._request('DELETE', `/api/v1/projects/${id}/config/${namespace}/${configKey}`, {
861
+ skipResponseTransform: true,
862
+ }),
863
+ /**
864
+ * Assign a user as a maintainer for this project.
865
+ * @param {string} id - The resource ID
866
+ * @param {string} userId - The user ID
867
+ */
868
+ addMaintainer: (id, userId) =>
869
+ this._request('POST', `/api/v1/projects/${id}/maintainers/${userId}`),
870
+ /**
871
+ * Remove a user's maintainer privileges for this project.
872
+ * @param {string} id - The resource ID
873
+ * @param {string} userId - The user ID
874
+ */
875
+ removeMaintainer: (id, userId) =>
876
+ this._request('DELETE', `/api/v1/projects/${id}/maintainers/${userId}`),
877
+ /**
878
+ * Get audit log for a project. Transparently follows pagination cursors
879
+ * and returns the full flat array.
880
+ * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
881
+ * @param {string} projectId - The project ID
882
+ * @param {string} [startTime] - Start of time range
883
+ * @param {string} [endTime] - End of time range
884
+ * @param {string} [asOf] - Temporal query timestamp
885
+ */
886
+ audit: (projectId, startTime, endTime, asOf) =>
887
+ listAll(this, `/api/v1/projects/${projectId}/audit`, {
888
+ query: { 'start-time': startTime, 'end-time': endTime, 'as-of': asOf },
889
+ }),
890
+ /**
891
+ * Link a vocabulary to a project.
892
+ * @param {string} id - The resource ID
893
+ * @param {string} vocabId - The vocab layer ID
894
+ */
895
+ linkVocab: (id, vocabId) =>
896
+ this._request('POST', `/api/v1/projects/${id}/vocabs/${vocabId}`),
897
+ /**
898
+ * Unlink a vocabulary from a project.
899
+ * @param {string} id - The resource ID
900
+ * @param {string} vocabId - The vocab layer ID
901
+ */
902
+ unlinkVocab: (id, vocabId) =>
903
+ this._request('DELETE', `/api/v1/projects/${id}/vocabs/${vocabId}`),
904
+ /**
905
+ * Get a project by ID. To fetch the project's documents, use
906
+ * listDocuments(id) — the include-documents flag has been removed.
907
+ * @param {string} id - The resource ID
908
+ * @param {string} [asOf] - Temporal query timestamp
909
+ */
910
+ get: (id, asOf) =>
911
+ this._request('GET', `/api/v1/projects/${id}`, {
912
+ queryParams: { 'as-of': asOf },
913
+ }),
914
+ /**
915
+ * List all documents in a project. Transparently follows pagination
916
+ * cursors and returns the full flat array.
917
+ * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
918
+ *
919
+ * Note: this endpoint does not support temporal (`as-of`) queries; the
920
+ * server rejects `?as-of=` on the documents-list route with a 400.
921
+ * @param {string} id - The project ID
922
+ */
923
+ listDocuments: (id) =>
924
+ listAll(this, `/api/v1/projects/${id}/documents`),
925
+ /**
926
+ * Fetch a single page of a project's documents.
927
+ *
928
+ * Note: this endpoint does not support temporal (`as-of`) queries; the
929
+ * server rejects `?as-of=` on the documents-list route with a 400.
930
+ * @param {string} id - The project ID
931
+ * @param {object} [opts]
932
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
933
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
934
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
935
+ */
936
+ listDocumentsPage: (id, { limit, cursor } = {}) =>
937
+ listPage(this, `/api/v1/projects/${id}/documents`, { limit, cursor }),
938
+ /**
939
+ * Async-iterate a project's documents page by page; yields each page's
940
+ * entries array.
941
+ *
942
+ * Note: this endpoint does not support temporal (`as-of`) queries; the
943
+ * server rejects `?as-of=` on the documents-list route with a 400.
944
+ * @param {string} id - The project ID
945
+ * @param {object} [opts]
946
+ * @param {number} [opts.pageSize] - Per-request page size
947
+ * Cannot be used inside a batch (auto-paginates across requests); throws on first iteration if called while batching — use listPage() for a single page in a batch.
948
+ * @returns {AsyncGenerator<Array>}
949
+ */
950
+ iterDocuments: (id, { pageSize } = {}) =>
951
+ iterPages(this, `/api/v1/projects/${id}/documents`, { pageSize }),
952
+ /**
953
+ * Delete a project.
954
+ * @param {string} id - The resource ID
955
+ */
956
+ delete: (id) =>
957
+ this._request('DELETE', `/api/v1/projects/${id}`),
958
+ /**
959
+ * Update a project's name.
960
+ * @param {string} id - The resource ID
961
+ * @param {string} name - The name
962
+ */
963
+ update: (id, name) =>
964
+ this._request('PATCH', `/api/v1/projects/${id}`, {
965
+ body: bodyOf({ name }),
966
+ }),
967
+ /**
968
+ * List all projects accessible to user. Transparently follows pagination
969
+ * cursors and returns the full flat array.
970
+ * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
971
+ * @param {string} [asOf] - Temporal query timestamp
972
+ */
973
+ list: (asOf) =>
974
+ listAll(this, '/api/v1/projects', { query: { 'as-of': asOf } }),
975
+ /**
976
+ * Fetch a single page of projects.
977
+ * @param {object} [opts]
978
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
979
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
980
+ * @param {string} [opts.asOf] - Temporal query timestamp
981
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
982
+ */
983
+ listPage: ({ limit, cursor, asOf } = {}) =>
984
+ listPage(this, '/api/v1/projects', { limit, cursor, query: { 'as-of': asOf } }),
985
+ /**
986
+ * Async-iterate projects page by page; yields each page's entries array.
987
+ * @param {object} [opts]
988
+ * @param {number} [opts.pageSize] - Per-request page size
989
+ * @param {string} [opts.asOf] - Temporal query timestamp
990
+ * Cannot be used inside a batch (auto-paginates across requests); throws on first iteration if called while batching — use listPage() for a single page in a batch.
991
+ * @returns {AsyncGenerator<Array>}
992
+ */
993
+ iterPages: ({ pageSize, asOf } = {}) =>
994
+ iterPages(this, '/api/v1/projects', { pageSize, query: { 'as-of': asOf } }),
995
+ /**
996
+ * Create a new project. Note: this also registers the user as a maintainer.
997
+ * @param {string} name - The name
998
+ */
999
+ create: (name) =>
1000
+ this._request('POST', '/api/v1/projects', {
1001
+ body: bodyOf({ name }),
1002
+ }),
1003
+ };
1004
+
1005
+ this.textLayers = {
1006
+ /**
1007
+ * Set a configuration value for a layer in an editor namespace.
1008
+ * @param {string} textLayerId - The text layer ID
1009
+ * @param {string} namespace - The config namespace
1010
+ * @param {string} configKey - The config key
1011
+ * @param {any} configValue - Configuration value to set
1012
+ */
1013
+ setConfig: (textLayerId, namespace, configKey, configValue) =>
1014
+ this._request('PUT', `/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`, {
1015
+ rawBody: configValue, skipResponseTransform: true,
1016
+ }),
1017
+ /**
1018
+ * Remove a configuration value for a layer.
1019
+ * @param {string} textLayerId - The text layer ID
1020
+ * @param {string} namespace - The config namespace
1021
+ * @param {string} configKey - The config key
1022
+ */
1023
+ deleteConfig: (textLayerId, namespace, configKey) =>
1024
+ this._request('DELETE', `/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`, {
1025
+ skipResponseTransform: true,
1026
+ }),
1027
+ /**
1028
+ * Get a text layer by ID.
1029
+ * @param {string} textLayerId - The text layer ID
1030
+ * @param {string} [asOf] - Temporal query timestamp
1031
+ */
1032
+ get: (textLayerId, asOf) =>
1033
+ this._request('GET', `/api/v1/text-layers/${textLayerId}`, {
1034
+ queryParams: { 'as-of': asOf },
1035
+ }),
1036
+ /**
1037
+ * Delete a text layer.
1038
+ * @param {string} textLayerId - The text layer ID
1039
+ */
1040
+ delete: (textLayerId) =>
1041
+ this._request('DELETE', `/api/v1/text-layers/${textLayerId}`),
1042
+ /**
1043
+ * Update a text layer's name.
1044
+ * @param {string} textLayerId - The text layer ID
1045
+ * @param {string} name - The name
1046
+ */
1047
+ update: (textLayerId, name) =>
1048
+ this._request('PATCH', `/api/v1/text-layers/${textLayerId}`, {
1049
+ body: bodyOf({ name }),
1050
+ }),
1051
+ /**
1052
+ * Shift a text layer's display order within the project.
1053
+ * @param {string} textLayerId - The text layer ID
1054
+ * @param {string} direction - The direction ("up" or "down")
1055
+ */
1056
+ shift: (textLayerId, direction) =>
1057
+ this._request('POST', `/api/v1/text-layers/${textLayerId}/shift`, {
1058
+ body: bodyOf({ direction }),
1059
+ }),
1060
+ /**
1061
+ * Create a new text layer for a project.
1062
+ * @param {string} projectId - The project ID
1063
+ * @param {string} name - The name
1064
+ */
1065
+ create: (projectId, name) =>
1066
+ this._request('POST', '/api/v1/text-layers', {
1067
+ body: bodyOf({ 'project-id': projectId, name }),
1068
+ }),
1069
+ };
1070
+
1071
+ this.vocabItems = {
1072
+ /**
1073
+ * Replace all metadata for a vocab item.
1074
+ * @param {string} id - The resource ID
1075
+ * @param {any} body - The request body
1076
+ */
1077
+ setMetadata: (id, body) =>
1078
+ this._request('PUT', `/api/v1/vocab-items/${id}/metadata`, {
1079
+ rawBody: body, skipResponseTransform: true,
1080
+ }),
1081
+ /**
1082
+ * Remove all metadata from a vocab item.
1083
+ * @param {string} id - The resource ID
1084
+ */
1085
+ deleteMetadata: (id) =>
1086
+ this._request('DELETE', `/api/v1/vocab-items/${id}/metadata`, {
1087
+ skipResponseTransform: true,
1088
+ }),
1089
+ /**
1090
+ * Create a new vocab item
1091
+ * @param {string} vocabLayerId - The vocab layer ID
1092
+ * @param {string} form - The vocab item form
1093
+ * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
1094
+ */
1095
+ create: (vocabLayerId, form, metadata) =>
1096
+ this._request('POST', '/api/v1/vocab-items', {
1097
+ body: bodyOf({ 'vocab-layer-id': vocabLayerId, form, metadata }),
1098
+ }),
1099
+ /**
1100
+ * Get a vocab item by ID
1101
+ * @param {string} id - The resource ID
1102
+ * @param {string} [asOf] - Temporal query timestamp
1103
+ */
1104
+ get: (id, asOf) =>
1105
+ this._request('GET', `/api/v1/vocab-items/${id}`, {
1106
+ queryParams: { 'as-of': asOf },
1107
+ }),
1108
+ /**
1109
+ * Delete a vocab item
1110
+ * @param {string} id - The resource ID
1111
+ */
1112
+ delete: (id) =>
1113
+ this._request('DELETE', `/api/v1/vocab-items/${id}`),
1114
+ /**
1115
+ * Update a vocab item's form
1116
+ * @param {string} id - The resource ID
1117
+ * @param {string} form - The vocab item form
1118
+ */
1119
+ update: (id, form) =>
1120
+ this._request('PATCH', `/api/v1/vocab-items/${id}`, {
1121
+ body: bodyOf({ form }),
1122
+ }),
1123
+ };
1124
+
1125
+ this.relationLayers = {
1126
+ /**
1127
+ * Shift a relation layer's display order.
1128
+ * @param {string} relationLayerId - The relation layer ID
1129
+ * @param {string} direction - The direction ("up" or "down")
1130
+ */
1131
+ shift: (relationLayerId, direction) =>
1132
+ this._request('POST', `/api/v1/relation-layers/${relationLayerId}/shift`, {
1133
+ body: bodyOf({ direction }),
1134
+ }),
1135
+ /**
1136
+ * Create a new relation layer.
1137
+ * @param {string} spanLayerId - The span layer ID
1138
+ * @param {string} name - The name
1139
+ */
1140
+ create: (spanLayerId, name) =>
1141
+ this._request('POST', '/api/v1/relation-layers', {
1142
+ body: bodyOf({ 'span-layer-id': spanLayerId, name }),
1143
+ }),
1144
+ /**
1145
+ * Set a configuration value for a layer in an editor namespace.
1146
+ * @param {string} relationLayerId - The relation layer ID
1147
+ * @param {string} namespace - The config namespace
1148
+ * @param {string} configKey - The config key
1149
+ * @param {any} configValue - Configuration value to set
1150
+ */
1151
+ setConfig: (relationLayerId, namespace, configKey, configValue) =>
1152
+ this._request('PUT', `/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`, {
1153
+ rawBody: configValue, skipResponseTransform: true,
1154
+ }),
1155
+ /**
1156
+ * Remove a configuration value for a layer.
1157
+ * @param {string} relationLayerId - The relation layer ID
1158
+ * @param {string} namespace - The config namespace
1159
+ * @param {string} configKey - The config key
1160
+ */
1161
+ deleteConfig: (relationLayerId, namespace, configKey) =>
1162
+ this._request('DELETE', `/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`, {
1163
+ skipResponseTransform: true,
1164
+ }),
1165
+ /**
1166
+ * Get a relation layer by ID.
1167
+ * @param {string} relationLayerId - The relation layer ID
1168
+ * @param {string} [asOf] - Temporal query timestamp
1169
+ */
1170
+ get: (relationLayerId, asOf) =>
1171
+ this._request('GET', `/api/v1/relation-layers/${relationLayerId}`, {
1172
+ queryParams: { 'as-of': asOf },
1173
+ }),
1174
+ /**
1175
+ * Delete a relation layer.
1176
+ * @param {string} relationLayerId - The relation layer ID
1177
+ */
1178
+ delete: (relationLayerId) =>
1179
+ this._request('DELETE', `/api/v1/relation-layers/${relationLayerId}`),
1180
+ /**
1181
+ * Update a relation layer's name.
1182
+ * @param {string} relationLayerId - The relation layer ID
1183
+ * @param {string} name - The name
1184
+ */
1185
+ update: (relationLayerId, name) =>
1186
+ this._request('PATCH', `/api/v1/relation-layers/${relationLayerId}`, {
1187
+ body: bodyOf({ name }),
1188
+ }),
1189
+ };
1190
+
1191
+ this.tokens = {
1192
+ /**
1193
+ * Create a new token in a token layer. Tokens define text substrings
1194
+ * using begin and end offsets. Tokens may be zero-width and may overlap.
1195
+ * For tokens sharing the same begin, precedence controls the linear
1196
+ * ordering.
1197
+ * @param {string} tokenLayerId - The token layer ID
1198
+ * @param {string} text - The text ID
1199
+ * @param {number} begin - Start offset (inclusive)
1200
+ * @param {number} end - End offset (exclusive)
1201
+ * @param {number} [precedence] - Ordering precedence
1202
+ * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
1203
+ */
1204
+ create: (tokenLayerId, text, begin, end, precedence, metadata) =>
1205
+ this._request('POST', '/api/v1/tokens', {
1206
+ body: bodyOf({ 'token-layer-id': tokenLayerId, text, begin, end, precedence, metadata }),
1207
+ }),
1208
+ /**
1209
+ * Get a token.
1210
+ * @param {string} tokenId - The token ID
1211
+ * @param {string} [asOf] - Temporal query timestamp
1212
+ */
1213
+ get: (tokenId, asOf) =>
1214
+ this._request('GET', `/api/v1/tokens/${tokenId}`, {
1215
+ queryParams: { 'as-of': asOf },
1216
+ }),
1217
+ /**
1218
+ * Delete a token and remove it from any spans. If this causes a span to
1219
+ * have no remaining tokens, the span will also be deleted.
1220
+ * @param {string} tokenId - The token ID
1221
+ */
1222
+ delete: (tokenId) =>
1223
+ this._request('DELETE', `/api/v1/tokens/${tokenId}`),
1224
+ /**
1225
+ * Update a token.
1226
+ * @param {string} tokenId - The token ID
1227
+ * @param {number} [begin] - New start offset
1228
+ * @param {number} [end] - New end offset
1229
+ * @param {?number} [precedence] - Ordering precedence. Omit (undefined)
1230
+ * to leave unchanged; pass a number to set; pass null explicitly to
1231
+ * CLEAR it (revert to no explicit ordering). bodyOf keeps null but
1232
+ * drops undefined, so the three cases map correctly to the server.
1233
+ */
1234
+ update: (tokenId, begin, end, precedence) =>
1235
+ this._request('PATCH', `/api/v1/tokens/${tokenId}`, {
1236
+ body: bodyOf({ begin, end, precedence }),
1237
+ }),
1238
+ /**
1239
+ * Create multiple tokens in a single operation.
1240
+ * @param {Array} body - The request body
1241
+ */
1242
+ bulkCreate: (body) =>
1243
+ this._request('POST', '/api/v1/tokens/bulk', { body }),
1244
+ /**
1245
+ * Delete multiple tokens in a single operation. Provide an array of IDs.
1246
+ * @param {Array} body - The request body
1247
+ */
1248
+ bulkDelete: (body) =>
1249
+ this._request('DELETE', '/api/v1/tokens/bulk', { body }),
1250
+ /**
1251
+ * Split a token at a character offset. The original token becomes the left half
1252
+ * (keeps its ID, spans, vocab-links); the new right token's ID is returned.
1253
+ * @param {string} tokenId - The token ID
1254
+ * @param {number} position - Offset to split at (strictly between begin and end)
1255
+ */
1256
+ split: (tokenId, position) =>
1257
+ this._request('POST', `/api/v1/tokens/${tokenId}/split`, {
1258
+ body: bodyOf({ position }),
1259
+ }),
1260
+ /**
1261
+ * Merge two tokens. The left token (smaller begin) survives with the combined
1262
+ * extent; the right is deleted and its spans/vocab-links are reparented to the left.
1263
+ * On partitioning layers the tokens must be adjacent; on non-overlapping layers the
1264
+ * merged extent must not engulf a third token.
1265
+ * @param {string} tokenId - The anchor token ID
1266
+ * @param {string} otherTokenId - The other token to merge in
1267
+ */
1268
+ merge: (tokenId, otherTokenId) =>
1269
+ this._request('POST', `/api/v1/tokens/${tokenId}/merge`, {
1270
+ body: bodyOf({ 'other-token-id': otherTokenId }),
1271
+ }),
1272
+ /**
1273
+ * Shift a token's boundary. On partitioning layers the adjacent token is
1274
+ * auto-adjusted to preserve the partition; on non-overlapping layers a shift that
1275
+ * would create an overlap is rejected.
1276
+ * @param {string} tokenId - The token ID
1277
+ * @param {number} [begin] - New start offset
1278
+ * @param {number} [end] - New end offset
1279
+ */
1280
+ shift: (tokenId, begin, end) =>
1281
+ this._request('POST', `/api/v1/tokens/${tokenId}/shift`, {
1282
+ body: bodyOf({ begin, end }),
1283
+ }),
1284
+ /**
1285
+ * Replace all metadata for a token.
1286
+ * @param {string} tokenId - The token ID
1287
+ * @param {any} body - The request body
1288
+ */
1289
+ setMetadata: (tokenId, body) =>
1290
+ this._request('PUT', `/api/v1/tokens/${tokenId}/metadata`, {
1291
+ rawBody: body, skipResponseTransform: true,
1292
+ }),
1293
+ /**
1294
+ * Remove all metadata from a token.
1295
+ * @param {string} tokenId - The token ID
1296
+ */
1297
+ deleteMetadata: (tokenId) =>
1298
+ this._request('DELETE', `/api/v1/tokens/${tokenId}/metadata`, {
1299
+ skipResponseTransform: true,
1300
+ }),
1301
+ };
1302
+
1303
+ this.messages = {
1304
+ /**
1305
+ * Open a Server-Sent Events stream for a project.
1306
+ * @param {string} projectId - The UUID of the project to listen to
1307
+ * @param {function} onEvent - Callback function that receives (eventType, data). If it returns true, listening will stop.
1308
+ * @param {string} [path] - Stream path under baseUrl (defaults to the project /listen bus; service channels pass their own).
1309
+ * @returns {Object} SSE connection object with .close() and .getStats() methods
1310
+ */
1311
+ listen: (projectId, onEvent, path) =>
1312
+ createSSEConnection(this, projectId, onEvent, path),
1313
+
1314
+ /**
1315
+ * Send a message to project listeners
1316
+ * @param {string} projectId - The UUID of the project to send to
1317
+ * @param {any} data - The message data to send
1318
+ * @returns {Promise<any>} Response from the send operation
1319
+ */
1320
+ sendMessage: (projectId, data) =>
1321
+ this._request('POST', `/api/v1/projects/${projectId}/message`, {
1322
+ body: { body: data },
1323
+ }),
1324
+
1325
+ /**
1326
+ * Discover the services currently connected to a project (synchronous GET).
1327
+ * @param {string} projectId - The UUID of the project to query
1328
+ * @param {number} [timeout] - Ignored; kept for back-compat
1329
+ * @returns {Promise<Array>} Array of discovered service information
1330
+ */
1331
+ discoverServices: (projectId, timeout) =>
1332
+ discoverServices(this, projectId, timeout),
1333
+
1334
+ /**
1335
+ * Register as a service and handle incoming work requests.
1336
+ * @param {string} projectId - The UUID of the project to serve
1337
+ * @param {Object} serviceInfo - Service information {serviceId, serviceName, description}
1338
+ * @param {function} onServiceRequest - Callback (data, responseHelper)
1339
+ * @param {Object} [extras] - Optional additional service metadata
1340
+ * @returns {Object} Service registration object with .stop() method
1341
+ */
1342
+ serve: (projectId, serviceInfo, onServiceRequest, extras) =>
1343
+ serve(this, projectId, serviceInfo, onServiceRequest, extras),
1344
+
1345
+ /**
1346
+ * Request a service to perform work and await its result.
1347
+ * @param {string} projectId - The UUID of the project
1348
+ * @param {string} serviceId - The ID of the service to request
1349
+ * @param {any} data - The request data
1350
+ * @param {number} [timeout] - Timeout in milliseconds (default: 10000)
1351
+ * @param {function} [onProgress] - Called with each progress payload {percent, message}
1352
+ * @returns {Promise<any>} Service response
1353
+ */
1354
+ requestService: (projectId, serviceId, data, timeout, onProgress) =>
1355
+ requestService(this, projectId, serviceId, data, timeout, onProgress),
1356
+ };
1357
+
1358
+ /**
1359
+ * Run a query over every project you can read.
1360
+ *
1361
+ * `body` is the query AST. Its keys follow the usual client convention
1362
+ * (camelCase, e.g. `scope.projectIds`) and are converted to the wire
1363
+ * format automatically; clause heads and variables are plain strings you
1364
+ * write literally (e.g. `'span'`, `'?s1'`, `'vocab-link'`). Example:
1365
+ *
1366
+ * await client.query({
1367
+ * find: ['?s1', '?s2'],
1368
+ * where: [
1369
+ * ['span', '?s1', { layer: 'pos', value: 'NOUN' }],
1370
+ * ['span', '?s2', { layer: 'pos', value: 'VERB' }],
1371
+ * ['covers', '?s1', '?t1'], ['covers', '?s2', '?t2'],
1372
+ * ['precedes', '?t1', '?t2'],
1373
+ * ],
1374
+ * return: 'entities', // 'ids' (default) | 'entities' | 'count'
1375
+ * limit: 100,
1376
+ * });
1377
+ *
1378
+ * @param {Object} body - The query AST ({find, where, scope?, limit?, return?}).
1379
+ * @returns {Promise<Object>} For 'ids'/'entities': {columns, results, count, truncated}.
1380
+ * For 'count': {return: 'count', count}. Entity cells are full entity objects
1381
+ * (same shape as the GET endpoints).
1382
+ */
1383
+ this.query = (body) =>
1384
+ this._request('POST', '/api/v1/query', { body });
1385
+ }
1386
+
1387
+ // --- Core methods ---
1388
+
1389
+ async _request(method, path, options = {}) {
1390
+ return makeRequest(this, method, path, options);
1391
+ }
1392
+
1393
+ /**
1394
+ * Enter strict mode for a specific document, requiring document version
1395
+ * headers so that conflicting concurrent writes are rejected.
1396
+ * @param {string} documentId - The ID of the document to track versions for
1397
+ */
1398
+ enterStrictMode(documentId) {
1399
+ this.strictModeDocumentId = documentId;
1400
+ }
1401
+
1402
+ /** Exit strict mode and stop tracking document versions for writes. */
1403
+ exitStrictMode() {
1404
+ this.strictModeDocumentId = null;
1405
+ }
1406
+
1407
+ /** Begin a batch of operations. Subsequent API calls will be queued. */
1408
+ beginBatch() {
1409
+ this.isBatching = true;
1410
+ this.batchOperations = [];
1411
+ }
1412
+
1413
+ /**
1414
+ * Submit all queued batch operations as a single batch request, executed
1415
+ * atomically. If any operation fails, all changes are rolled back.
1416
+ * @returns {Promise<Array>} Array of results corresponding to each operation
1417
+ */
1418
+ async submitBatch() {
1419
+ if (!this.isBatching) {
1420
+ throw new Error('No active batch. Call beginBatch() first.');
1421
+ }
1422
+
1423
+ if (this.batchOperations.length === 0) {
1424
+ this.isBatching = false;
1425
+ return [];
1426
+ }
1427
+
1428
+ try {
1429
+ let url = `${this.baseUrl}/api/v1/batch`;
1430
+ const body = this.batchOperations.map(op => ({
1431
+ path: op.path,
1432
+ method: op.method.toUpperCase(),
1433
+ ...(op.body && { body: op.body }),
1434
+ }));
1435
+
1436
+ const fetchOptions = {
1437
+ method: 'POST',
1438
+ headers: {
1439
+ 'Authorization': `Bearer ${this.token}`,
1440
+ 'Content-Type': 'application/json',
1441
+ },
1442
+ body: JSON.stringify(body),
1443
+ };
1444
+ const signal = timeoutSignal(this.timeout);
1445
+ if (signal) fetchOptions.signal = signal;
1446
+
1447
+ try {
1448
+ const response = await fetch(url, fetchOptions);
1449
+ if (!response.ok) {
1450
+ throw makeHttpError(response, await parseErrorBody(response), url, 'POST');
1451
+ }
1452
+
1453
+ const results = await response.json();
1454
+
1455
+ // Extract document versions from each batch response
1456
+ for (const result of results) {
1457
+ if (result.headers && result.headers['X-Document-Versions']) {
1458
+ try {
1459
+ const versionsMap = JSON.parse(result.headers['X-Document-Versions']);
1460
+ if (typeof versionsMap === 'object' && versionsMap !== null) {
1461
+ // Clone once per response, then merge — not once per entry.
1462
+ this.documentVersions = { ...this.documentVersions, ...versionsMap };
1463
+ }
1464
+ } catch (e) {
1465
+ console.warn('Failed to parse document versions header from batch response:', e);
1466
+ }
1467
+ }
1468
+ }
1469
+
1470
+ return results.map(result => transformResponse(result));
1471
+ } catch (error) {
1472
+ if (error.status) throw error;
1473
+ throw makeNetworkError(error, url, 'POST');
1474
+ }
1475
+ } finally {
1476
+ this.isBatching = false;
1477
+ this.batchOperations = [];
1478
+ }
1479
+ }
1480
+
1481
+ /** Abort the current batch without executing any operations. */
1482
+ abortBatch() {
1483
+ this.isBatching = false;
1484
+ this.batchOperations = [];
1485
+ }
1486
+
1487
+ /**
1488
+ * Check if currently in batch mode.
1489
+ * @returns {boolean}
1490
+ */
1491
+ isBatchMode() {
1492
+ return this.isBatching;
1493
+ }
1494
+
1495
+ /**
1496
+ * Authenticate and return a new client instance with token. This is the
1497
+ * single auth entry point — there is no `client.login` resource.
1498
+ * @param {string} baseUrl - The base URL for the API
1499
+ * @param {string} userId - User ID for authentication
1500
+ * @param {string} password - Password for authentication
1501
+ * @param {object} [options] - Client options forwarded to the constructor (e.g. { timeout })
1502
+ * @returns {Promise<PlaidClient>} - Authenticated client instance
1503
+ */
1504
+ static async login(baseUrl, userId, password, options = {}) {
1505
+ baseUrl = baseUrl.replace(/\/$/, '');
1506
+ const url = `${baseUrl}/api/v1/login`;
1507
+ try {
1508
+ const fetchOptions = {
1509
+ method: 'POST',
1510
+ headers: { 'Content-Type': 'application/json' },
1511
+ body: JSON.stringify({ 'user-id': userId, password }),
1512
+ };
1513
+ const signal = timeoutSignal(options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS);
1514
+ if (signal) fetchOptions.signal = signal;
1515
+
1516
+ const response = await fetch(url, fetchOptions);
1517
+ if (!response.ok) {
1518
+ throw makeHttpError(response, await parseErrorBody(response), url, 'POST');
1519
+ }
1520
+
1521
+ const data = await response.json();
1522
+ const token = data.token || '';
1523
+ return new PlaidClient(baseUrl, token, options);
1524
+ } catch (error) {
1525
+ if (error.status) throw error;
1526
+ throw makeNetworkError(error, url, 'POST');
1527
+ }
1528
+ }
1529
+ }
1530
+
1531
+ export default PlaidClient;
1532
+ export { PlaidClient };