@larc-iu/plaid-client 0.2.0-alpha.13 → 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 +212 -14
- package/package.json +1 -1
- package/src/documentLock.js +299 -0
- package/src/http.js +143 -53
- package/src/index.js +387 -151
- package/src/pagination.js +19 -35
- package/src/serviceSchema.js +27 -1
- package/src/services.js +137 -21
package/src/pagination.js
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
* Each helper threads the client through to `client._request`, so auth
|
|
11
11
|
* headers, base URL, and response transforms all apply exactly as for any
|
|
12
12
|
* other request.
|
|
13
|
+
*
|
|
14
|
+
* A page is a read, so it goes over the wire even when the helper is handed a
|
|
15
|
+
* batch (see the note at the top of http.js): each page is answered from the
|
|
16
|
+
* pool, in order, against the state no batch has committed yet.
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
19
|
// Merge caller query params with paging params, dropping undefined/null so we
|
|
@@ -21,16 +25,23 @@ function buildQueryParams(query, limit, cursor) {
|
|
|
21
25
|
return params;
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
// Every collection endpoint answers with the envelope. A bare array or any
|
|
29
|
+
// other shape is a broken endpoint, so name it and stop rather than return a
|
|
30
|
+
// silently truncated result.
|
|
31
|
+
function entriesOf(response, path) {
|
|
32
|
+
if (!response || !Array.isArray(response.entries)) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`GET ${path} did not return a paginated envelope (no 'entries' array)`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return response.entries;
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
/**
|
|
25
41
|
* Fetch every page and return the full flat array of entries, transparently
|
|
26
42
|
* following `nextCursor` until it is null. This is what `.list()` calls so it
|
|
27
43
|
* stays backward compatible with the pre-pagination (bare-array) contract.
|
|
28
44
|
*
|
|
29
|
-
* NOTE: This auto-paginates and therefore CANNOT be used inside a batch — each
|
|
30
|
-
* page's request needs the previous page's `nextCursor`, which doesn't exist
|
|
31
|
-
* until the batch executes. It throws immediately when `client.isBatching` is
|
|
32
|
-
* true. Use `listPage` for a single page inside a batch.
|
|
33
|
-
*
|
|
34
45
|
* @param {object} client - PlaidClient instance
|
|
35
46
|
* @param {string} path - API path, e.g. '/api/v1/projects'
|
|
36
47
|
* @param {object} [opts]
|
|
@@ -39,11 +50,6 @@ function buildQueryParams(query, limit, cursor) {
|
|
|
39
50
|
* @returns {Promise<Array>} The concatenated entries across all pages
|
|
40
51
|
*/
|
|
41
52
|
export async function listAll(client, path, { pageSize = 1000, query = {} } = {}) {
|
|
42
|
-
if (client.isBatching) {
|
|
43
|
-
throw new Error(
|
|
44
|
-
`Cannot auto-paginate ${path} inside a batch: list methods follow cursors across multiple requests, which a batch cannot do. Use listPage() for a single page inside a batch, or call list() outside the batch.`,
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
53
|
const all = [];
|
|
48
54
|
let cursor = null;
|
|
49
55
|
let prevCursor;
|
|
@@ -51,19 +57,7 @@ export async function listAll(client, path, { pageSize = 1000, query = {} } = {}
|
|
|
51
57
|
const response = await client._request('GET', path, {
|
|
52
58
|
queryParams: buildQueryParams(query, pageSize, cursor),
|
|
53
59
|
});
|
|
54
|
-
|
|
55
|
-
// array. Treat it as a terminal full result with no further paging.
|
|
56
|
-
if (Array.isArray(response)) {
|
|
57
|
-
all.push(...response);
|
|
58
|
-
break;
|
|
59
|
-
}
|
|
60
|
-
if (response && Array.isArray(response.entries)) {
|
|
61
|
-
all.push(...response.entries);
|
|
62
|
-
} else {
|
|
63
|
-
throw new Error(
|
|
64
|
-
"Unexpected list response shape (no 'entries'); server may be incompatible.",
|
|
65
|
-
);
|
|
66
|
-
}
|
|
60
|
+
all.push(...entriesOf(response, path));
|
|
67
61
|
prevCursor = cursor;
|
|
68
62
|
cursor = response.nextCursor;
|
|
69
63
|
// Guard against a buggy server/proxy that returns a constant non-null
|
|
@@ -98,11 +92,6 @@ export async function listPage(client, path, { limit, cursor, query = {} } = {})
|
|
|
98
92
|
* Async generator yielding each page's entries array in turn, following
|
|
99
93
|
* `nextCursor` until it is null.
|
|
100
94
|
*
|
|
101
|
-
* NOTE: This auto-paginates and therefore CANNOT be used inside a batch — each
|
|
102
|
-
* page's request needs the previous page's `nextCursor`, which doesn't exist
|
|
103
|
-
* until the batch executes. It throws on first iteration when
|
|
104
|
-
* `client.isBatching` is true. Use `listPage` for a single page inside a batch.
|
|
105
|
-
*
|
|
106
95
|
* @param {object} client - PlaidClient instance
|
|
107
96
|
* @param {string} path - API path
|
|
108
97
|
* @param {object} [opts]
|
|
@@ -111,18 +100,13 @@ export async function listPage(client, path, { limit, cursor, query = {} } = {})
|
|
|
111
100
|
* @yields {Array} The entries array for each page
|
|
112
101
|
*/
|
|
113
102
|
export async function* iterPages(client, path, { pageSize = 1000, query = {} } = {}) {
|
|
114
|
-
if (client.isBatching) {
|
|
115
|
-
throw new Error(
|
|
116
|
-
`Cannot auto-paginate ${path} inside a batch: list methods follow cursors across multiple requests, which a batch cannot do. Use listPage() for a single page inside a batch, or call list() outside the batch.`,
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
103
|
let cursor = null;
|
|
120
104
|
let prevCursor;
|
|
121
105
|
do {
|
|
122
106
|
const response = await client._request('GET', path, {
|
|
123
107
|
queryParams: buildQueryParams(query, pageSize, cursor),
|
|
124
108
|
});
|
|
125
|
-
const entries = (response
|
|
109
|
+
const entries = entriesOf(response, path);
|
|
126
110
|
// Suppress the trailing empty page that the server emits when a collection's
|
|
127
111
|
// size is an exact multiple of the page size (a final full page with a
|
|
128
112
|
// non-null cursor, then an empty page). Still follow the cursor below.
|
|
@@ -130,7 +114,7 @@ export async function* iterPages(client, path, { pageSize = 1000, query = {} } =
|
|
|
130
114
|
yield entries;
|
|
131
115
|
}
|
|
132
116
|
prevCursor = cursor;
|
|
133
|
-
cursor = response
|
|
117
|
+
cursor = response.nextCursor;
|
|
134
118
|
// Guard against a buggy server/proxy that returns a constant non-null
|
|
135
119
|
// cursor, which would otherwise loop forever.
|
|
136
120
|
if (cursor !== null && cursor !== undefined && cursor === prevCursor) {
|
package/src/serviceSchema.js
CHANGED
|
@@ -22,10 +22,15 @@
|
|
|
22
22
|
* { key, label, type, description?, default?, required?,
|
|
23
23
|
* options?: [{value, label}], // enum / multiselect
|
|
24
24
|
* min?, max?, step?, slider?, // number (slider: drag, don't type)
|
|
25
|
-
* placeholder?, multiline
|
|
25
|
+
* placeholder?, multiline?, // string
|
|
26
|
+
* scope? } // field: 'Sentence' | 'Word' | 'Morpheme'
|
|
26
27
|
* ]
|
|
27
28
|
* }
|
|
28
29
|
*
|
|
30
|
+
* A `field` parameter names one of the project's annotation fields at its
|
|
31
|
+
* `scope`. A UI that knows the project offers those fields to choose from; to
|
|
32
|
+
* everything here, and to a UI that does not, it is a string.
|
|
33
|
+
*
|
|
29
34
|
* A parameter's `key` is a string VALUE, so it passes over the wire verbatim;
|
|
30
35
|
* the UI sends `{ [param.key]: value }` in the request data. Declare each `key`
|
|
31
36
|
* in your service's own convention (Python: snake_case; JS: camelCase) and read
|
|
@@ -50,6 +55,20 @@ export const TASKS = Object.freeze({
|
|
|
50
55
|
LINK_VOCAB: "link-vocab",
|
|
51
56
|
/** Propose an interlinear analysis (morpheme segmentation + glosses) for words. */
|
|
52
57
|
ANALYZE: "analyze",
|
|
58
|
+
/**
|
|
59
|
+
* Draft a meaning representation graph over the sentences of a document,
|
|
60
|
+
* for correction by hand. The request names a `scope` ('document' or
|
|
61
|
+
* 'sentence'), a `sentence` number when the scope is one sentence, and
|
|
62
|
+
* `overwrite`. See plaid-umr's drafting services.
|
|
63
|
+
*
|
|
64
|
+
* Apart from `analyze` on purpose. A task is the CONTRACT a spot offers a
|
|
65
|
+
* service against, and these two share almost none of it: an analyze
|
|
66
|
+
* request names the three token layers plus the language, metalanguage and
|
|
67
|
+
* orthography, and writes morphemes and glosses. Both claimed `analyze` for
|
|
68
|
+
* a while, so every app offering an analyze spot offered the other app's
|
|
69
|
+
* services, which could not answer the request they would have been given.
|
|
70
|
+
*/
|
|
71
|
+
DRAFT_GRAPH: "draft-graph",
|
|
53
72
|
/** Propose a free translation for each sentence. */
|
|
54
73
|
TRANSLATE: "translate",
|
|
55
74
|
/**
|
|
@@ -64,6 +83,13 @@ export const TASKS = Object.freeze({
|
|
|
64
83
|
DETECT_SPEECH: "detect-speech",
|
|
65
84
|
/** A conversational assistant over a project (chat turns; see plaid-agent). */
|
|
66
85
|
ASSIST: "assist",
|
|
86
|
+
/**
|
|
87
|
+
* Score one document's annotation against another document's of the same
|
|
88
|
+
* text (two annotators' copies), writing a report on the scored document's
|
|
89
|
+
* metadata and never an annotation. The request names the other document
|
|
90
|
+
* as `against`. See plaid-umr's AnCast service.
|
|
91
|
+
*/
|
|
92
|
+
COMPARE: "compare",
|
|
67
93
|
});
|
|
68
94
|
|
|
69
95
|
/**
|
package/src/services.js
CHANGED
|
@@ -10,12 +10,82 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { transformRequest, transformResponse } from './transforms.js';
|
|
12
12
|
|
|
13
|
+
// An absolute URL anywhere in an error message, with the phrase that introduces
|
|
14
|
+
// it (`... at http://host/api/v1/spans`). Client and transport errors name the
|
|
15
|
+
// endpoint they called, which is the service operator's business and not the
|
|
16
|
+
// requester's.
|
|
17
|
+
const URL_IN_TEXT = /(?:\s+(?:at|for url:?))?\s*\b[a-zA-Z][\w+.-]*:\/\/\S+/g;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What a requester is told when an error carries no message of its own.
|
|
21
|
+
* Naming the JS class instead would say nothing they can act on.
|
|
22
|
+
*/
|
|
23
|
+
export const UNKNOWN_FAILURE = 'The service could not finish this request.';
|
|
24
|
+
|
|
25
|
+
const redact = (text, secrets) => {
|
|
26
|
+
let out = text;
|
|
27
|
+
for (const secret of secrets || []) {
|
|
28
|
+
if (secret && String(secret).length >= 8) out = out.split(String(secret)).join('[redacted]');
|
|
29
|
+
}
|
|
30
|
+
return out.trim();
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* One line about a failure that is safe to show the person who asked.
|
|
35
|
+
*
|
|
36
|
+
* Strips absolute URLs (so an internal host never reaches a requester's screen)
|
|
37
|
+
* and any `secrets` given (a model provider's API key can come back inside its
|
|
38
|
+
* own error text). The whole error still goes to the operator's console: this
|
|
39
|
+
* is the requester's half only. The Python twin is `requester_message`.
|
|
40
|
+
*
|
|
41
|
+
* @param {any} error
|
|
42
|
+
* @param {string[]} [secrets]
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
export function requesterMessage(error, secrets = []) {
|
|
46
|
+
if (error && typeof error.status === 'number') {
|
|
47
|
+
if (!error.status) return 'The Plaid server could not be reached.';
|
|
48
|
+
let text = String(error.message || '');
|
|
49
|
+
if (error.url) text = text.split(` at ${error.url}`).join('').split(error.url).join('');
|
|
50
|
+
text = text.trim().replace(/[\s,:;]+$/, '');
|
|
51
|
+
return redact(text, secrets) || `HTTP ${error.status}`;
|
|
52
|
+
}
|
|
53
|
+
// An Error is read by its message and nothing else: `String(new TypeError(''))`
|
|
54
|
+
// is 'TypeError', which names the class to the requester and tells them
|
|
55
|
+
// nothing they can act on.
|
|
56
|
+
const raw =
|
|
57
|
+
error && typeof error === 'object' && 'message' in error
|
|
58
|
+
? String(error.message ?? '')
|
|
59
|
+
: String(error ?? '');
|
|
60
|
+
const text = raw.replace(URL_IN_TEXT, '').trim().replace(/[\s,:;]+$/, '');
|
|
61
|
+
return redact(text, secrets) || UNKNOWN_FAILURE;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* What the requester is told when a request fails, named by the service.
|
|
66
|
+
*
|
|
67
|
+
* Every path that reports a failed request goes through here, so a raw message
|
|
68
|
+
* cannot reach a requester from one of them. (Python's twin additionally lets a
|
|
69
|
+
* `ValueError` through unprefixed, as an authored refusal; JS has no
|
|
70
|
+
* distinguished refusal type, so every failure is named by the service.)
|
|
71
|
+
*
|
|
72
|
+
* @param {any} error
|
|
73
|
+
* @param {string} [serviceName]
|
|
74
|
+
* @param {string[]} [secrets]
|
|
75
|
+
* @returns {string}
|
|
76
|
+
*/
|
|
77
|
+
export function serviceErrorMessage(error, serviceName = '', secrets = []) {
|
|
78
|
+
const text = requesterMessage(error, secrets);
|
|
79
|
+
return serviceName ? `${serviceName}: ${text}` : text;
|
|
80
|
+
}
|
|
81
|
+
|
|
13
82
|
/**
|
|
14
83
|
* Discover the services seen on a project — a synchronous GET. Returns every
|
|
15
84
|
* service ever registered on the project: currently connected ones carry
|
|
16
85
|
* `online: true`; previously-seen offline ones carry `online: false` plus a
|
|
17
86
|
* `lastSeenAt` stamp. Callers that need a service they can actually submit
|
|
18
|
-
* work to should filter on `online`.
|
|
87
|
+
* work to should filter on `online`. Goes over the wire even while a batch is
|
|
88
|
+
* open on the client.
|
|
19
89
|
*
|
|
20
90
|
* @param {Object} client - PlaidClient instance
|
|
21
91
|
* @param {string} projectId - Project UUID
|
|
@@ -36,7 +106,36 @@ export function discoverServices(client, projectId) {
|
|
|
36
106
|
* @returns {Promise<void>}
|
|
37
107
|
*/
|
|
38
108
|
export function discardService(client, projectId, serviceId) {
|
|
39
|
-
|
|
109
|
+
// outOfBand: the registry is not project data (see the note at the top of
|
|
110
|
+
// http.js).
|
|
111
|
+
return client._request('DELETE', `/api/v1/projects/${projectId}/services/${encodeURIComponent(serviceId)}`, {
|
|
112
|
+
outOfBand: true,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* POST a progress, result or error event for an in-flight request; the server
|
|
118
|
+
* relays it to the waiting requester. The Python twin is `_report_event`.
|
|
119
|
+
*
|
|
120
|
+
* outOfBand: these are signals to whoever is waiting, not writes to the
|
|
121
|
+
* project (see the note at the top of http.js). A service may report them on
|
|
122
|
+
* the batch it is filling, where queueing them would hold every one back until
|
|
123
|
+
* submit, deliver none at all if the batch aborts, and take slots in the
|
|
124
|
+
* batch's results. The requester would hear nothing and wait out its idle
|
|
125
|
+
* timeout on work that had already finished.
|
|
126
|
+
*
|
|
127
|
+
* @param {Object} client - PlaidClient instance
|
|
128
|
+
* @param {string} projectId - Project UUID
|
|
129
|
+
* @param {string} requestId - The request id
|
|
130
|
+
* @param {Object} body - The event payload
|
|
131
|
+
* @returns {Promise<any>}
|
|
132
|
+
*/
|
|
133
|
+
export function reportRequestEvent(client, projectId, requestId, body) {
|
|
134
|
+
return client._request(
|
|
135
|
+
'POST',
|
|
136
|
+
`/api/v1/projects/${projectId}/service-requests/${encodeURIComponent(requestId)}/events`,
|
|
137
|
+
{ body, outOfBand: true },
|
|
138
|
+
);
|
|
40
139
|
}
|
|
41
140
|
|
|
42
141
|
/**
|
|
@@ -86,12 +185,10 @@ export function serve(client, projectId, serviceInfo, onServiceRequest, extras =
|
|
|
86
185
|
};
|
|
87
186
|
|
|
88
187
|
const reportEvent = (requestId, body) =>
|
|
89
|
-
client
|
|
90
|
-
|
|
91
|
-
.
|
|
92
|
-
|
|
93
|
-
console.warn('Failed to report request event:', error.message || error);
|
|
94
|
-
});
|
|
188
|
+
reportRequestEvent(client, projectId, requestId, body).catch((error) => {
|
|
189
|
+
// 404 just means the requester already went away; nothing to do.
|
|
190
|
+
console.warn('Failed to report request event:', error.message || error);
|
|
191
|
+
});
|
|
95
192
|
|
|
96
193
|
const serviceRegistration = {
|
|
97
194
|
stop: () => {
|
|
@@ -184,9 +281,17 @@ export function serve(client, projectId, serviceInfo, onServiceRequest, extras =
|
|
|
184
281
|
data: { ...(data || {}), stopped: true },
|
|
185
282
|
});
|
|
186
283
|
},
|
|
284
|
+
// Every failure a requester is shown goes out through here, whoever
|
|
285
|
+
// reports it, so the scrub lives here rather than only in `settle`: a
|
|
286
|
+
// service that catches its own error and calls `helper.error(err)`
|
|
287
|
+
// would otherwise put the endpoint the client called on the requester's
|
|
288
|
+
// screen. Already-sanitized text passes through unchanged.
|
|
187
289
|
error: (error) => {
|
|
188
290
|
finished();
|
|
189
|
-
return reportEvent(requestId, {
|
|
291
|
+
return reportEvent(requestId, {
|
|
292
|
+
status: 'error',
|
|
293
|
+
data: { error: requesterMessage(error) },
|
|
294
|
+
});
|
|
190
295
|
},
|
|
191
296
|
};
|
|
192
297
|
|
|
@@ -196,7 +301,11 @@ export function serve(client, projectId, serviceInfo, onServiceRequest, extras =
|
|
|
196
301
|
if (error instanceof ServiceCancelled || error?.name === 'ServiceCancelled') {
|
|
197
302
|
responseHelper.stopped();
|
|
198
303
|
} else {
|
|
199
|
-
|
|
304
|
+
// What reaches the requester is the sanitized half: the raw message
|
|
305
|
+
// names the endpoint the client called, which is the operator's
|
|
306
|
+
// business. The console keeps the whole error.
|
|
307
|
+
console.error(`Service ${serviceId} failed request ${requestId}:`, error);
|
|
308
|
+
responseHelper.error(serviceErrorMessage(error, serviceName));
|
|
200
309
|
}
|
|
201
310
|
};
|
|
202
311
|
try {
|
|
@@ -321,16 +430,6 @@ export function attachServiceRequest(client, projectId, requestId, timeout = 100
|
|
|
321
430
|
);
|
|
322
431
|
}
|
|
323
432
|
|
|
324
|
-
/**
|
|
325
|
-
* Ask the service to stop a request made earlier (by this user). The request
|
|
326
|
-
* still ends with whatever the service then reports, on the stream of whoever
|
|
327
|
-
* is awaiting it. Rejects with 404 if unknown or expired, 409 once finished.
|
|
328
|
-
*
|
|
329
|
-
* @param {Object} client - PlaidClient instance
|
|
330
|
-
* @param {string} projectId - Project UUID
|
|
331
|
-
* @param {string} requestId - The request id
|
|
332
|
-
* @returns {Promise<void>}
|
|
333
|
-
*/
|
|
334
433
|
/**
|
|
335
434
|
* Thrown inside a handler when the requester has asked it to stop.
|
|
336
435
|
*
|
|
@@ -380,8 +479,25 @@ export function createCancelScope(isCancelled) {
|
|
|
380
479
|
};
|
|
381
480
|
}
|
|
382
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Ask the service to stop a request made earlier (by this user). The request
|
|
484
|
+
* still ends with whatever the service then reports, on the stream of whoever
|
|
485
|
+
* is awaiting it. Rejects with 404 if unknown or expired, 409 once finished.
|
|
486
|
+
*
|
|
487
|
+
* outOfBand: a DELETE, but not a write. It signals a running service and
|
|
488
|
+
* changes no project data (see the note at the top of http.js).
|
|
489
|
+
*
|
|
490
|
+
* @param {Object} client - PlaidClient instance
|
|
491
|
+
* @param {string} projectId - Project UUID
|
|
492
|
+
* @param {string} requestId - The request id
|
|
493
|
+
* @returns {Promise<void>}
|
|
494
|
+
*/
|
|
383
495
|
export function cancelServiceRequest(client, projectId, requestId) {
|
|
384
|
-
return client._request(
|
|
496
|
+
return client._request(
|
|
497
|
+
'DELETE',
|
|
498
|
+
`/api/v1/projects/${projectId}/service-requests/${encodeURIComponent(requestId)}`,
|
|
499
|
+
{ outOfBand: true },
|
|
500
|
+
);
|
|
385
501
|
}
|
|
386
502
|
|
|
387
503
|
/**
|