@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/index.d.ts ADDED
@@ -0,0 +1,270 @@
1
+ /** A single page of a cursor-paginated collection. */
2
+ interface Page<T = any> {
3
+ entries: T[];
4
+ nextCursor: string | null;
5
+ }
6
+
7
+ interface ServiceInfo {
8
+ serviceId: string;
9
+ serviceName: string;
10
+ description: string;
11
+ extras?: any;
12
+ }
13
+
14
+ interface DiscoveredService {
15
+ serviceId: string;
16
+ serviceName: string;
17
+ description: string;
18
+ extras: any;
19
+ }
20
+
21
+ interface ServiceRegistration {
22
+ stop(): void;
23
+ isRunning(): boolean;
24
+ serviceInfo: ServiceInfo & { extras: any };
25
+ }
26
+
27
+ interface ResponseHelper {
28
+ progress(percent: number, message: string): void;
29
+ complete(data: any): void;
30
+ error(error: string | Error): void;
31
+ }
32
+
33
+ interface SSEConnection {
34
+ close(): void;
35
+ getStats(): any;
36
+ readyState: number;
37
+ }
38
+
39
+ interface VocabLinksBundle {
40
+ create(vocabItem: string, tokens: any[], metadata?: any): Promise<any>;
41
+ setMetadata(id: string, body: any): Promise<any>;
42
+ deleteMetadata(id: string): Promise<any>;
43
+ get(id: string, asOf?: string): Promise<any>;
44
+ delete(id: string): Promise<any>;
45
+ }
46
+
47
+ interface VocabLayersBundle {
48
+ get(id: string, includeItems?: boolean, asOf?: string): Promise<any>;
49
+ delete(id: string): Promise<any>;
50
+ update(id: string, name: string): Promise<any>;
51
+ setConfig(id: string, namespace: string, configKey: string, configValue: any): Promise<any>;
52
+ deleteConfig(id: string, namespace: string, configKey: string): Promise<any>;
53
+ list(asOf?: string): Promise<any[]>;
54
+ listPage(opts?: { limit?: number; cursor?: string; asOf?: string }): Promise<Page>;
55
+ iterPages(opts?: { pageSize?: number; asOf?: string }): AsyncGenerator<any[]>;
56
+ create(name: string): Promise<any>;
57
+ addMaintainer(id: string, userId: string): Promise<any>;
58
+ removeMaintainer(id: string, userId: string): Promise<any>;
59
+ }
60
+
61
+ interface RelationsBundle {
62
+ setMetadata(relationId: string, body: any): Promise<any>;
63
+ deleteMetadata(relationId: string): Promise<any>;
64
+ setTarget(relationId: string, spanId: string): Promise<any>;
65
+ get(relationId: string, asOf?: string): Promise<any>;
66
+ delete(relationId: string): Promise<any>;
67
+ update(relationId: string, value: any): Promise<any>;
68
+ setSource(relationId: string, spanId: string): Promise<any>;
69
+ create(layerId: string, sourceId: string, targetId: string, value: any, metadata?: any): Promise<any>;
70
+ bulkCreate(body: any[]): Promise<any>;
71
+ bulkDelete(body: any[]): Promise<any>;
72
+ }
73
+
74
+ interface SpanLayersBundle {
75
+ setConfig(spanLayerId: string, namespace: string, configKey: string, configValue: any): Promise<any>;
76
+ deleteConfig(spanLayerId: string, namespace: string, configKey: string): Promise<any>;
77
+ get(spanLayerId: string, asOf?: string): Promise<any>;
78
+ delete(spanLayerId: string): Promise<any>;
79
+ update(spanLayerId: string, name: string): Promise<any>;
80
+ create(tokenLayerId: string, name: string): Promise<any>;
81
+ shift(spanLayerId: string, direction: string): Promise<any>;
82
+ }
83
+
84
+ interface SpansBundle {
85
+ setTokens(spanId: string, tokens: any[]): Promise<any>;
86
+ create(spanLayerId: string, tokens: any[], value: any, metadata?: any): Promise<any>;
87
+ get(spanId: string, asOf?: string): Promise<any>;
88
+ delete(spanId: string): Promise<any>;
89
+ update(spanId: string, value: any): Promise<any>;
90
+ bulkCreate(body: any[]): Promise<any>;
91
+ bulkDelete(body: any[]): Promise<any>;
92
+ setMetadata(spanId: string, body: any): Promise<any>;
93
+ deleteMetadata(spanId: string): Promise<any>;
94
+ }
95
+
96
+ interface BatchBundle {
97
+ submit(body: any[]): Promise<any>;
98
+ }
99
+
100
+ interface TextsBundle {
101
+ setMetadata(textId: string, body: any): Promise<any>;
102
+ deleteMetadata(textId: string): Promise<any>;
103
+ create(textLayerId: string, documentId: string, body: string, metadata?: any): Promise<any>;
104
+ get(textId: string, asOf?: string): Promise<any>;
105
+ delete(textId: string): Promise<any>;
106
+ update(textId: string, body: any): Promise<any>;
107
+ }
108
+
109
+ interface UsersBundle {
110
+ list(asOf?: string): Promise<any[]>;
111
+ listPage(opts?: { limit?: number; cursor?: string; asOf?: string }): Promise<Page>;
112
+ iterPages(opts?: { pageSize?: number; asOf?: string }): AsyncGenerator<any[]>;
113
+ create(username: string, password: string, isAdmin: boolean): Promise<any>;
114
+ audit(userId: string, startTime?: string, endTime?: string, asOf?: string): Promise<any[]>;
115
+ get(id: string, asOf?: string): Promise<any>;
116
+ delete(id: string): Promise<any>;
117
+ update(id: string, password?: string, username?: string, isAdmin?: boolean): Promise<any>;
118
+ }
119
+
120
+ interface ApiTokensBundle {
121
+ list(userId: string): Promise<any[]>;
122
+ listPage(userId: string, opts?: { limit?: number; cursor?: string }): Promise<Page>;
123
+ iterPages(userId: string, opts?: { pageSize?: number }): AsyncGenerator<any[]>;
124
+ create(userId: string, name: string): Promise<{ id: string; name: string; token: string }>;
125
+ revoke(userId: string, tokenId: string): Promise<any>;
126
+ }
127
+
128
+ interface TokenLayersBundle {
129
+ shift(tokenLayerId: string, direction: string): Promise<any>;
130
+ create(textLayerId: string, name: string, overlapMode?: string, parentTokenLayerId?: string): Promise<any>;
131
+ setConfig(tokenLayerId: string, namespace: string, configKey: string, configValue: any): Promise<any>;
132
+ deleteConfig(tokenLayerId: string, namespace: string, configKey: string): Promise<any>;
133
+ get(tokenLayerId: string, asOf?: string): Promise<any>;
134
+ delete(tokenLayerId: string): Promise<any>;
135
+ update(tokenLayerId: string, name: string): Promise<any>;
136
+ }
137
+
138
+ interface DocumentsBundle {
139
+ checkLock(documentId: string, asOf?: string): Promise<any>;
140
+ acquireLock(documentId: string): Promise<any>;
141
+ releaseLock(documentId: string): Promise<any>;
142
+ getMedia(documentId: string, asOf?: string): Promise<ArrayBuffer>;
143
+ uploadMedia(documentId: string, file: File): Promise<any>;
144
+ deleteMedia(documentId: string): Promise<any>;
145
+ setMetadata(documentId: string, body: any): Promise<any>;
146
+ deleteMetadata(documentId: string): Promise<any>;
147
+ audit(documentId: string, startTime?: string, endTime?: string, asOf?: string): Promise<any[]>;
148
+ get(documentId: string, includeBody?: boolean, asOf?: string): Promise<any>;
149
+ delete(documentId: string): Promise<any>;
150
+ update(documentId: string, name: string): Promise<any>;
151
+ create(projectId: string, name: string, metadata?: any): Promise<any>;
152
+ }
153
+
154
+ interface MessagesBundle {
155
+ sendMessage(projectId: string, data: any): Promise<any>;
156
+ listen(projectId: string, onEvent: (eventType: string, data: any) => void | boolean, path?: string): SSEConnection;
157
+ /** Discover the services currently connected to a project. `timeout` is ignored (kept for back-compat). */
158
+ discoverServices(projectId: string, timeout?: number): Promise<DiscoveredService[]>;
159
+ serve(projectId: string, serviceInfo: ServiceInfo, onServiceRequest: (data: any, responseHelper: ResponseHelper) => void, extras?: any): ServiceRegistration;
160
+ /** Submit work to a service; streams progress to `onProgress`, resolves with the result. */
161
+ requestService(projectId: string, serviceId: string, data: any, timeout?: number, onProgress?: (progress: any) => void): Promise<any>;
162
+ }
163
+
164
+ interface ProjectsBundle {
165
+ addWriter(id: string, userId: string): Promise<any>;
166
+ removeWriter(id: string, userId: string): Promise<any>;
167
+ addReader(id: string, userId: string): Promise<any>;
168
+ removeReader(id: string, userId: string): Promise<any>;
169
+ setConfig(id: string, namespace: string, configKey: string, configValue: any): Promise<any>;
170
+ deleteConfig(id: string, namespace: string, configKey: string): Promise<any>;
171
+ addMaintainer(id: string, userId: string): Promise<any>;
172
+ removeMaintainer(id: string, userId: string): Promise<any>;
173
+ audit(projectId: string, startTime?: string, endTime?: string, asOf?: string): Promise<any[]>;
174
+ linkVocab(id: string, vocabId: string): Promise<any>;
175
+ unlinkVocab(id: string, vocabId: string): Promise<any>;
176
+ get(id: string, asOf?: string): Promise<any>;
177
+ listDocuments(id: string): Promise<any[]>;
178
+ listDocumentsPage(id: string, opts?: { limit?: number; cursor?: string }): Promise<Page>;
179
+ iterDocuments(id: string, opts?: { pageSize?: number }): AsyncGenerator<any[]>;
180
+ delete(id: string): Promise<any>;
181
+ update(id: string, name: string): Promise<any>;
182
+ list(asOf?: string): Promise<any[]>;
183
+ listPage(opts?: { limit?: number; cursor?: string; asOf?: string }): Promise<Page>;
184
+ iterPages(opts?: { pageSize?: number; asOf?: string }): AsyncGenerator<any[]>;
185
+ create(name: string): Promise<any>;
186
+ }
187
+
188
+ interface TextLayersBundle {
189
+ setConfig(textLayerId: string, namespace: string, configKey: string, configValue: any): Promise<any>;
190
+ deleteConfig(textLayerId: string, namespace: string, configKey: string): Promise<any>;
191
+ get(textLayerId: string, asOf?: string): Promise<any>;
192
+ delete(textLayerId: string): Promise<any>;
193
+ update(textLayerId: string, name: string): Promise<any>;
194
+ shift(textLayerId: string, direction: string): Promise<any>;
195
+ create(projectId: string, name: string): Promise<any>;
196
+ }
197
+
198
+ interface VocabItemsBundle {
199
+ setMetadata(id: string, body: any): Promise<any>;
200
+ deleteMetadata(id: string): Promise<any>;
201
+ create(vocabLayerId: string, form: string, metadata?: any): Promise<any>;
202
+ get(id: string, asOf?: string): Promise<any>;
203
+ delete(id: string): Promise<any>;
204
+ update(id: string, form: string): Promise<any>;
205
+ }
206
+
207
+ interface RelationLayersBundle {
208
+ shift(relationLayerId: string, direction: string): Promise<any>;
209
+ create(spanLayerId: string, name: string): Promise<any>;
210
+ setConfig(relationLayerId: string, namespace: string, configKey: string, configValue: any): Promise<any>;
211
+ deleteConfig(relationLayerId: string, namespace: string, configKey: string): Promise<any>;
212
+ get(relationLayerId: string, asOf?: string): Promise<any>;
213
+ delete(relationLayerId: string): Promise<any>;
214
+ update(relationLayerId: string, name: string): Promise<any>;
215
+ }
216
+
217
+ interface TokensBundle {
218
+ create(tokenLayerId: string, text: string, begin: number, end: number, precedence?: number | null, metadata?: any): Promise<any>;
219
+ get(tokenId: string, asOf?: string): Promise<any>;
220
+ delete(tokenId: string): Promise<any>;
221
+ update(tokenId: string, begin?: number, end?: number, precedence?: number | null): Promise<any>;
222
+ bulkCreate(body: any[]): Promise<any>;
223
+ bulkDelete(body: any[]): Promise<any>;
224
+ split(tokenId: string, position: number): Promise<any>;
225
+ merge(tokenId: string, otherTokenId: string): Promise<any>;
226
+ shift(tokenId: string, begin?: number, end?: number): Promise<any>;
227
+ setMetadata(tokenId: string, body: any): Promise<any>;
228
+ deleteMetadata(tokenId: string): Promise<any>;
229
+ }
230
+
231
+ interface PlaidClientOptions {
232
+ /** Per-request timeout in ms (default 30000; 0 or null disables it). */
233
+ timeout?: number | null;
234
+ }
235
+
236
+ export declare class PlaidClient {
237
+ constructor(baseUrl: string, token: string, options?: PlaidClientOptions);
238
+ static login(baseUrl: string, userId: string, password: string, options?: PlaidClientOptions): Promise<PlaidClient>;
239
+ timeout: number | null;
240
+
241
+ // Batch control methods
242
+ beginBatch(): void;
243
+ submitBatch(): Promise<any[]>;
244
+ abortBatch(): void;
245
+ isBatchMode(): boolean;
246
+
247
+ // Strict mode methods
248
+ enterStrictMode(documentId: string): void;
249
+ exitStrictMode(): void;
250
+
251
+ vocabLinks: VocabLinksBundle;
252
+ vocabLayers: VocabLayersBundle;
253
+ relations: RelationsBundle;
254
+ spanLayers: SpanLayersBundle;
255
+ spans: SpansBundle;
256
+ batch: BatchBundle;
257
+ texts: TextsBundle;
258
+ users: UsersBundle;
259
+ apiTokens: ApiTokensBundle;
260
+ tokenLayers: TokenLayersBundle;
261
+ documents: DocumentsBundle;
262
+ messages: MessagesBundle;
263
+ projects: ProjectsBundle;
264
+ textLayers: TextLayersBundle;
265
+ vocabItems: VocabItemsBundle;
266
+ relationLayers: RelationLayersBundle;
267
+ tokens: TokensBundle;
268
+ }
269
+
270
+ export default PlaidClient;
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@larc-iu/plaid-client",
3
+ "version": "0.0.0",
4
+ "description": "JavaScript client for the Plaid annotation API",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/larc-iu/plaid.git",
9
+ "directory": "plaid-client-js"
10
+ },
11
+ "homepage": "https://github.com/larc-iu/plaid#readme",
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "main": "src/index.js",
16
+ "exports": {
17
+ ".": "./src/index.js"
18
+ },
19
+ "scripts": {
20
+ "test": "node --test"
21
+ },
22
+ "files": [
23
+ "src/",
24
+ "index.d.ts"
25
+ ]
26
+ }
package/src/http.js ADDED
@@ -0,0 +1,212 @@
1
+ import { transformRequest, transformResponse } from './transforms.js';
2
+
3
+ // Default per-request timeout (ms). Applied to every request unless the client
4
+ // is constructed with a different `timeout` (0 / null disables it). Note: this
5
+ // also bounds media up/downloads — bump it (or disable) for very large files.
6
+ export const DEFAULT_TIMEOUT_MS = 30000;
7
+
8
+ /**
9
+ * Extract and update document versions from response headers and body.
10
+ */
11
+ export function extractDocumentVersions(client, responseHeaders, responseBody = null) {
12
+ const docVersionsHeader = responseHeaders.get('X-Document-Versions');
13
+ if (docVersionsHeader) {
14
+ try {
15
+ const versionsMap = JSON.parse(docVersionsHeader);
16
+ if (typeof versionsMap === 'object' && versionsMap !== null) {
17
+ // Clone once, then assign — cloning inside the loop is O(n²) and pointless.
18
+ client.documentVersions = { ...client.documentVersions, ...versionsMap };
19
+ }
20
+ } catch (e) {
21
+ console.warn('Failed to parse document versions header:', e);
22
+ }
23
+ }
24
+
25
+ if (responseBody && typeof responseBody === 'object') {
26
+ if (responseBody['document/id'] && responseBody['document/version']) {
27
+ client.documentVersions = { ...client.documentVersions };
28
+ client.documentVersions[responseBody['document/id']] = responseBody['document/version'];
29
+ }
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Read a failed response's body as parsed JSON, falling back to text.
35
+ */
36
+ export async function parseErrorBody(response) {
37
+ try {
38
+ return await response.json();
39
+ } catch (_) {
40
+ return { message: await response.text().catch(() => 'Unable to read error response') };
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Create an enriched error from a failed HTTP response.
46
+ */
47
+ export function makeHttpError(response, errorData, url, method) {
48
+ const serverMessage = errorData?.error || errorData?.message || response.statusText || 'Unknown error';
49
+ const error = new Error(`HTTP ${response.status} ${serverMessage} at ${url}`);
50
+ error.status = response.status;
51
+ error.statusText = response.statusText;
52
+ error.url = url;
53
+ error.method = method;
54
+ error.responseData = errorData;
55
+ return error;
56
+ }
57
+
58
+ /**
59
+ * Create a network error (status 0). Timeout aborts get a clearer message.
60
+ */
61
+ export function makeNetworkError(originalError, url, method) {
62
+ const timedOut = originalError?.name === 'TimeoutError' || originalError?.name === 'AbortError';
63
+ const message = timedOut
64
+ ? `Request timed out at ${url}`
65
+ : `Network error: ${originalError.message} at ${url}`;
66
+ const error = new Error(message);
67
+ error.status = 0;
68
+ error.url = url;
69
+ error.method = method;
70
+ error.originalError = originalError;
71
+ return error;
72
+ }
73
+
74
+ /**
75
+ * Build a fetch AbortSignal that fires after `timeout` ms, or undefined when
76
+ * timeouts are disabled / unsupported.
77
+ */
78
+ export function timeoutSignal(timeout) {
79
+ if (timeout && timeout > 0 && typeof AbortSignal !== 'undefined' && AbortSignal.timeout) {
80
+ return AbortSignal.timeout(timeout);
81
+ }
82
+ return undefined;
83
+ }
84
+
85
+ /**
86
+ * Generic request method handling all fetch logic.
87
+ *
88
+ * Options:
89
+ * body - Object body, run through transformRequest
90
+ * rawBody - Body value passed directly (no transform). Mutually exclusive with body.
91
+ * formData - If true, body is FormData; skip Content-Type header
92
+ * queryParams - Object of query param key/values to append
93
+ * noBatch - If true, throw when in batch mode
94
+ * skipResponseTransform - Return raw parsed JSON (no transformResponse)
95
+ * noAuth - Skip Authorization header
96
+ * binaryResponse - Return arrayBuffer instead of JSON/text
97
+ */
98
+ export async function makeRequest(client, method, path, options = {}) {
99
+ const {
100
+ body,
101
+ rawBody,
102
+ formData,
103
+ queryParams,
104
+ noBatch,
105
+ skipResponseTransform,
106
+ noAuth,
107
+ binaryResponse,
108
+ } = options;
109
+
110
+ // Build URL
111
+ let url = `${client.baseUrl}${path}`;
112
+
113
+ // Append query params
114
+ if (queryParams) {
115
+ const params = new URLSearchParams();
116
+ for (const [key, value] of Object.entries(queryParams)) {
117
+ if (value !== undefined && value !== null) {
118
+ // URLSearchParams stringifies booleans to lowercase 'true'/'false',
119
+ // which the server's malli coercion requires (the Python client does
120
+ // this conversion explicitly).
121
+ params.append(key, value);
122
+ }
123
+ }
124
+ const qs = params.toString();
125
+ if (qs) url += `?${qs}`;
126
+ }
127
+
128
+ // Prepare request body
129
+ let requestBody;
130
+ if (formData) {
131
+ requestBody = body; // FormData passed directly
132
+ } else if (rawBody !== undefined) {
133
+ requestBody = rawBody;
134
+ } else if (body !== undefined) {
135
+ requestBody = transformRequest(body);
136
+ }
137
+
138
+ // Strict mode: append document-version for non-GET requests
139
+ if (client.strictModeDocumentId && method !== 'GET') {
140
+ const docId = client.strictModeDocumentId;
141
+ if (client.documentVersions[docId]) {
142
+ const docVersion = client.documentVersions[docId];
143
+ const separator = url.includes('?') ? '&' : '?';
144
+ url += `${separator}document-version=${encodeURIComponent(docVersion)}`;
145
+ }
146
+ }
147
+
148
+ // Batch mode
149
+ if (client.isBatching) {
150
+ if (noBatch) {
151
+ throw new Error(`This endpoint cannot be used in batch mode: ${path}`);
152
+ }
153
+ const operation = {
154
+ path: url.replace(client.baseUrl, ''),
155
+ method: method.toUpperCase(),
156
+ };
157
+ if (requestBody !== undefined) {
158
+ operation.body = requestBody;
159
+ }
160
+ client.batchOperations.push(operation);
161
+ return { batched: true };
162
+ }
163
+
164
+ // Build fetch options
165
+ const headers = {};
166
+ if (!noAuth) {
167
+ headers['Authorization'] = `Bearer ${client.token}`;
168
+ }
169
+ if (!formData) {
170
+ headers['Content-Type'] = 'application/json';
171
+ }
172
+
173
+ const fetchOptions = { method, headers };
174
+ if (requestBody !== undefined) {
175
+ fetchOptions.body = formData ? requestBody : JSON.stringify(requestBody);
176
+ }
177
+ const signal = timeoutSignal(client.timeout);
178
+ if (signal) fetchOptions.signal = signal;
179
+
180
+ try {
181
+ const response = await fetch(url, fetchOptions);
182
+
183
+ if (!response.ok) {
184
+ throw makeHttpError(response, await parseErrorBody(response), url, method);
185
+ }
186
+
187
+ // Binary response (getMedia)
188
+ if (binaryResponse) {
189
+ extractDocumentVersions(client, response.headers);
190
+ return await response.arrayBuffer();
191
+ }
192
+
193
+ // JSON or text response
194
+ const contentType = response.headers.get('content-type');
195
+ if (contentType && contentType.includes('application/json')) {
196
+ const data = await response.json();
197
+ extractDocumentVersions(client, response.headers, data);
198
+ if (skipResponseTransform) {
199
+ return data;
200
+ }
201
+ return transformResponse(data);
202
+ } else {
203
+ extractDocumentVersions(client, response.headers);
204
+ return await response.text();
205
+ }
206
+ } catch (error) {
207
+ if (error.status !== undefined) {
208
+ throw error;
209
+ }
210
+ throw makeNetworkError(error, url, method);
211
+ }
212
+ }