@larc-iu/plaid-client 0.2.0-alpha.12 → 0.2.0-alpha.14
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 +132 -3
- package/package.json +1 -1
- package/src/documentLock.js +299 -0
- package/src/http.js +87 -14
- package/src/index.js +268 -47
- package/src/pagination.js +23 -35
- package/src/services.js +145 -21
package/src/pagination.js
CHANGED
|
@@ -10,6 +10,11 @@
|
|
|
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 every request these helpers make carries `bypassBatch`
|
|
15
|
+
* (see the three classes at the top of http.js). That is what lets them work
|
|
16
|
+
* while a batch is open on the same client: each page is answered from the
|
|
17
|
+
* pool, in order, against the state the batch has not committed yet.
|
|
13
18
|
*/
|
|
14
19
|
|
|
15
20
|
// Merge caller query params with paging params, dropping undefined/null so we
|
|
@@ -21,16 +26,23 @@ function buildQueryParams(query, limit, cursor) {
|
|
|
21
26
|
return params;
|
|
22
27
|
}
|
|
23
28
|
|
|
29
|
+
// Every collection endpoint answers with the envelope. A bare array or any
|
|
30
|
+
// other shape is a broken endpoint, so name it and stop rather than return a
|
|
31
|
+
// silently truncated result.
|
|
32
|
+
function entriesOf(response, path) {
|
|
33
|
+
if (!response || !Array.isArray(response.entries)) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`GET ${path} did not return a paginated envelope (no 'entries' array)`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return response.entries;
|
|
39
|
+
}
|
|
40
|
+
|
|
24
41
|
/**
|
|
25
42
|
* Fetch every page and return the full flat array of entries, transparently
|
|
26
43
|
* following `nextCursor` until it is null. This is what `.list()` calls so it
|
|
27
44
|
* stays backward compatible with the pre-pagination (bare-array) contract.
|
|
28
45
|
*
|
|
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
46
|
* @param {object} client - PlaidClient instance
|
|
35
47
|
* @param {string} path - API path, e.g. '/api/v1/projects'
|
|
36
48
|
* @param {object} [opts]
|
|
@@ -39,31 +51,15 @@ function buildQueryParams(query, limit, cursor) {
|
|
|
39
51
|
* @returns {Promise<Array>} The concatenated entries across all pages
|
|
40
52
|
*/
|
|
41
53
|
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
54
|
const all = [];
|
|
48
55
|
let cursor = null;
|
|
49
56
|
let prevCursor;
|
|
50
57
|
do {
|
|
51
58
|
const response = await client._request('GET', path, {
|
|
52
59
|
queryParams: buildQueryParams(query, pageSize, cursor),
|
|
60
|
+
bypassBatch: true,
|
|
53
61
|
});
|
|
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
|
-
}
|
|
62
|
+
all.push(...entriesOf(response, path));
|
|
67
63
|
prevCursor = cursor;
|
|
68
64
|
cursor = response.nextCursor;
|
|
69
65
|
// Guard against a buggy server/proxy that returns a constant non-null
|
|
@@ -91,6 +87,7 @@ export async function listAll(client, path, { pageSize = 1000, query = {} } = {}
|
|
|
91
87
|
export async function listPage(client, path, { limit, cursor, query = {} } = {}) {
|
|
92
88
|
return client._request('GET', path, {
|
|
93
89
|
queryParams: buildQueryParams(query, limit, cursor),
|
|
90
|
+
bypassBatch: true,
|
|
94
91
|
});
|
|
95
92
|
}
|
|
96
93
|
|
|
@@ -98,11 +95,6 @@ export async function listPage(client, path, { limit, cursor, query = {} } = {})
|
|
|
98
95
|
* Async generator yielding each page's entries array in turn, following
|
|
99
96
|
* `nextCursor` until it is null.
|
|
100
97
|
*
|
|
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
98
|
* @param {object} client - PlaidClient instance
|
|
107
99
|
* @param {string} path - API path
|
|
108
100
|
* @param {object} [opts]
|
|
@@ -111,18 +103,14 @@ export async function listPage(client, path, { limit, cursor, query = {} } = {})
|
|
|
111
103
|
* @yields {Array} The entries array for each page
|
|
112
104
|
*/
|
|
113
105
|
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
106
|
let cursor = null;
|
|
120
107
|
let prevCursor;
|
|
121
108
|
do {
|
|
122
109
|
const response = await client._request('GET', path, {
|
|
123
110
|
queryParams: buildQueryParams(query, pageSize, cursor),
|
|
111
|
+
bypassBatch: true,
|
|
124
112
|
});
|
|
125
|
-
const entries = (response
|
|
113
|
+
const entries = entriesOf(response, path);
|
|
126
114
|
// Suppress the trailing empty page that the server emits when a collection's
|
|
127
115
|
// size is an exact multiple of the page size (a final full page with a
|
|
128
116
|
// non-null cursor, then an empty page). Still follow the cursor below.
|
|
@@ -130,7 +118,7 @@ export async function* iterPages(client, path, { pageSize = 1000, query = {} } =
|
|
|
130
118
|
yield entries;
|
|
131
119
|
}
|
|
132
120
|
prevCursor = cursor;
|
|
133
|
-
cursor = response
|
|
121
|
+
cursor = response.nextCursor;
|
|
134
122
|
// Guard against a buggy server/proxy that returns a constant non-null
|
|
135
123
|
// cursor, which would otherwise loop forever.
|
|
136
124
|
if (cursor !== null && cursor !== undefined && cursor === prevCursor) {
|
package/src/services.js
CHANGED
|
@@ -10,19 +10,96 @@
|
|
|
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
|
|
22
92
|
* @returns {Promise<Array>} [{serviceId, serviceName, description, extras, online, lastSeenAt}]
|
|
23
93
|
*/
|
|
24
94
|
export function discoverServices(client, projectId) {
|
|
25
|
-
|
|
95
|
+
// bypassBatch: the registry is read by whoever asked, not by whatever batch
|
|
96
|
+
// happens to be open on this shared client. The assistant's availability
|
|
97
|
+
// probe polls for 49 seconds after a mount and landed inside a Grew rewrite
|
|
98
|
+
// or an import, where the answer was `{batched: true}`, cached as the
|
|
99
|
+
// service list, and every later render threw on it until a reload.
|
|
100
|
+
return client._request('GET', `/api/v1/projects/${projectId}/services`, {
|
|
101
|
+
bypassBatch: true,
|
|
102
|
+
});
|
|
26
103
|
}
|
|
27
104
|
|
|
28
105
|
/**
|
|
@@ -39,6 +116,31 @@ export function discardService(client, projectId, serviceId) {
|
|
|
39
116
|
return client._request('DELETE', `/api/v1/projects/${projectId}/services/${encodeURIComponent(serviceId)}`);
|
|
40
117
|
}
|
|
41
118
|
|
|
119
|
+
/**
|
|
120
|
+
* POST a progress, result or error event for an in-flight request; the server
|
|
121
|
+
* relays it to the waiting requester. The Python twin is `_report_event`.
|
|
122
|
+
*
|
|
123
|
+
* bypassBatch: these are out-of-band signals to whoever is waiting, not writes
|
|
124
|
+
* to the project. A service reports them from inside its own
|
|
125
|
+
* `client.batched()` block, where queueing them would hold every one back
|
|
126
|
+
* until submit, deliver none at all if the batch aborts, and take slots in the
|
|
127
|
+
* batch's results. The requester would hear nothing and wait out its idle
|
|
128
|
+
* timeout on work that had already finished.
|
|
129
|
+
*
|
|
130
|
+
* @param {Object} client - PlaidClient instance
|
|
131
|
+
* @param {string} projectId - Project UUID
|
|
132
|
+
* @param {string} requestId - The request id
|
|
133
|
+
* @param {Object} body - The event payload
|
|
134
|
+
* @returns {Promise<any>}
|
|
135
|
+
*/
|
|
136
|
+
export function reportRequestEvent(client, projectId, requestId, body) {
|
|
137
|
+
return client._request(
|
|
138
|
+
'POST',
|
|
139
|
+
`/api/v1/projects/${projectId}/service-requests/${encodeURIComponent(requestId)}/events`,
|
|
140
|
+
{ body, bypassBatch: true },
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
42
144
|
/**
|
|
43
145
|
* Register a service and handle incoming work requests.
|
|
44
146
|
*
|
|
@@ -86,12 +188,10 @@ export function serve(client, projectId, serviceInfo, onServiceRequest, extras =
|
|
|
86
188
|
};
|
|
87
189
|
|
|
88
190
|
const reportEvent = (requestId, body) =>
|
|
89
|
-
client
|
|
90
|
-
|
|
91
|
-
.
|
|
92
|
-
|
|
93
|
-
console.warn('Failed to report request event:', error.message || error);
|
|
94
|
-
});
|
|
191
|
+
reportRequestEvent(client, projectId, requestId, body).catch((error) => {
|
|
192
|
+
// 404 just means the requester already went away; nothing to do.
|
|
193
|
+
console.warn('Failed to report request event:', error.message || error);
|
|
194
|
+
});
|
|
95
195
|
|
|
96
196
|
const serviceRegistration = {
|
|
97
197
|
stop: () => {
|
|
@@ -184,9 +284,17 @@ export function serve(client, projectId, serviceInfo, onServiceRequest, extras =
|
|
|
184
284
|
data: { ...(data || {}), stopped: true },
|
|
185
285
|
});
|
|
186
286
|
},
|
|
287
|
+
// Every failure a requester is shown goes out through here, whoever
|
|
288
|
+
// reports it, so the scrub lives here rather than only in `settle`: a
|
|
289
|
+
// service that catches its own error and calls `helper.error(err)`
|
|
290
|
+
// would otherwise put the endpoint the client called on the requester's
|
|
291
|
+
// screen. Already-sanitized text passes through unchanged.
|
|
187
292
|
error: (error) => {
|
|
188
293
|
finished();
|
|
189
|
-
return reportEvent(requestId, {
|
|
294
|
+
return reportEvent(requestId, {
|
|
295
|
+
status: 'error',
|
|
296
|
+
data: { error: requesterMessage(error) },
|
|
297
|
+
});
|
|
190
298
|
},
|
|
191
299
|
};
|
|
192
300
|
|
|
@@ -196,7 +304,11 @@ export function serve(client, projectId, serviceInfo, onServiceRequest, extras =
|
|
|
196
304
|
if (error instanceof ServiceCancelled || error?.name === 'ServiceCancelled') {
|
|
197
305
|
responseHelper.stopped();
|
|
198
306
|
} else {
|
|
199
|
-
|
|
307
|
+
// What reaches the requester is the sanitized half: the raw message
|
|
308
|
+
// names the endpoint the client called, which is the operator's
|
|
309
|
+
// business. The console keeps the whole error.
|
|
310
|
+
console.error(`Service ${serviceId} failed request ${requestId}:`, error);
|
|
311
|
+
responseHelper.error(serviceErrorMessage(error, serviceName));
|
|
200
312
|
}
|
|
201
313
|
};
|
|
202
314
|
try {
|
|
@@ -321,16 +433,6 @@ export function attachServiceRequest(client, projectId, requestId, timeout = 100
|
|
|
321
433
|
);
|
|
322
434
|
}
|
|
323
435
|
|
|
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
436
|
/**
|
|
335
437
|
* Thrown inside a handler when the requester has asked it to stop.
|
|
336
438
|
*
|
|
@@ -380,8 +482,30 @@ export function createCancelScope(isCancelled) {
|
|
|
380
482
|
};
|
|
381
483
|
}
|
|
382
484
|
|
|
485
|
+
/**
|
|
486
|
+
* Ask the service to stop a request made earlier (by this user). The request
|
|
487
|
+
* still ends with whatever the service then reports, on the stream of whoever
|
|
488
|
+
* is awaiting it. Rejects with 404 if unknown or expired, 409 once finished.
|
|
489
|
+
*
|
|
490
|
+
* Goes over the wire even while a batch is open on the client. A DELETE, but
|
|
491
|
+
* not a write: it signals a running service out of band and changes no project
|
|
492
|
+
* data. The Stop button lives in the app's chrome, on the same client an
|
|
493
|
+
* import or a bulk edit is batching on, and a stop that waits for that batch to
|
|
494
|
+
* submit has stopped nothing (and if the batch aborts, it never arrives at
|
|
495
|
+
* all), while the queued DELETE shifts every result index the batch's caller
|
|
496
|
+
* reads back.
|
|
497
|
+
*
|
|
498
|
+
* @param {Object} client - PlaidClient instance
|
|
499
|
+
* @param {string} projectId - Project UUID
|
|
500
|
+
* @param {string} requestId - The request id
|
|
501
|
+
* @returns {Promise<void>}
|
|
502
|
+
*/
|
|
383
503
|
export function cancelServiceRequest(client, projectId, requestId) {
|
|
384
|
-
return client._request(
|
|
504
|
+
return client._request(
|
|
505
|
+
'DELETE',
|
|
506
|
+
`/api/v1/projects/${projectId}/service-requests/${encodeURIComponent(requestId)}`,
|
|
507
|
+
{ bypassBatch: true },
|
|
508
|
+
);
|
|
385
509
|
}
|
|
386
510
|
|
|
387
511
|
/**
|