@larc-iu/plaid-client 0.2.0-alpha.3 → 0.2.0-alpha.30
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/README.md +39 -0
- package/package.json +13 -6
- package/src/apiAnnotations.d.ts +508 -0
- package/src/apiAnnotations.js +543 -0
- package/src/apiDocuments.d.ts +569 -0
- package/src/apiDocuments.js +551 -0
- package/src/apiLayers.d.ts +369 -0
- package/src/apiLayers.js +222 -0
- package/src/apiMessages.d.ts +293 -0
- package/src/apiMessages.js +265 -0
- package/src/apiPeople.d.ts +626 -0
- package/src/apiPeople.js +698 -0
- package/src/apiProject.d.ts +761 -0
- package/src/apiProject.js +774 -0
- package/src/apiServer.d.ts +455 -0
- package/src/apiServer.js +422 -0
- package/src/apiVocab.d.ts +456 -0
- package/src/apiVocab.js +425 -0
- package/src/batch.d.ts +54 -0
- package/src/batch.js +323 -0
- package/src/batchRef.d.ts +30 -0
- package/src/batchRef.js +128 -0
- package/src/client.d.ts +1321 -0
- package/src/client.js +743 -0
- package/src/codepoint.d.ts +94 -0
- package/src/codepoint.js +131 -1
- package/src/constraints.d.ts +85 -0
- package/src/constraints.js +112 -0
- package/src/created.d.ts +2 -0
- package/src/created.js +29 -0
- package/src/documentLock.d.ts +172 -0
- package/src/documentLock.js +384 -0
- package/src/entryForms.d.ts +63 -0
- package/src/entryForms.js +152 -0
- package/src/events.d.ts +60 -0
- package/src/events.js +271 -0
- package/src/families.d.ts +349 -0
- package/src/families.js +396 -0
- package/src/http.d.ts +233 -0
- package/src/http.js +809 -93
- package/src/ids.d.ts +26 -0
- package/src/ids.js +96 -0
- package/src/index.d.ts +101 -0
- package/src/index.js +183 -2105
- package/src/metadataOps.d.ts +15 -0
- package/src/metadataOps.js +124 -0
- package/src/operations.d.ts +36 -0
- package/src/operations.js +164 -0
- package/src/pagination.d.ts +49 -0
- package/src/pagination.js +20 -53
- package/src/provenance.d.ts +243 -0
- package/src/provenance.js +458 -36
- package/src/replayed.d.ts +3 -0
- package/src/replayed.js +31 -0
- package/src/requester.d.ts +60 -0
- package/src/requester.js +74 -0
- package/src/roles.d.ts +104 -0
- package/src/roles.js +64 -2
- package/src/serviceSchema.d.ts +337 -0
- package/src/serviceSchema.js +177 -60
- package/src/services.d.ts +319 -0
- package/src/services.js +742 -58
- package/src/sse.d.ts +30 -0
- package/src/sse.js +80 -3
- package/src/testing.d.ts +128 -0
- package/src/testing.js +379 -0
- package/src/textEdits.d.ts +59 -0
- package/src/textEdits.js +234 -0
- package/src/transforms.d.ts +25 -0
- package/src/transforms.js +11 -1
- package/src/types.d.ts +867 -0
- package/src/types.js +663 -0
- package/index.d.ts +0 -532
package/src/client.js
ADDED
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PlaidClient: a client's state (token, versions, strict mode, the open
|
|
3
|
+
* operation, the server's facts), its bundles, its batches and logical
|
|
4
|
+
* operations, and the static entry points that make one (login, invites,
|
|
5
|
+
* health, info). The package root (index.js) re-exports it with the rest of
|
|
6
|
+
* the package.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { transformResponse } from "./transforms.js";
|
|
10
|
+
import {
|
|
11
|
+
makeRequest,
|
|
12
|
+
parseErrorBody,
|
|
13
|
+
makeHttpError,
|
|
14
|
+
makeNetworkError,
|
|
15
|
+
timeoutSignal,
|
|
16
|
+
DEFAULT_TIMEOUT_MS,
|
|
17
|
+
DEFAULT_BATCH_TIMEOUT_MS,
|
|
18
|
+
queryTimeout,
|
|
19
|
+
} from "./http.js";
|
|
20
|
+
import { normalizeUserId } from "./ids.js";
|
|
21
|
+
import { openBatch, postBatch } from "./batch.js";
|
|
22
|
+
import { messagesApi } from "./apiMessages.js";
|
|
23
|
+
import { commentsApi, eventsApi, guidelinesApi, operationGroupsApi, projectsApi } from "./apiProject.js";
|
|
24
|
+
import { adminApi, auditApi, serverApi } from "./apiServer.js";
|
|
25
|
+
import { apiTokensApi, authApi, invitesApi, userDataApi, usersApi } from "./apiPeople.js";
|
|
26
|
+
import { documentsApi, textsApi } from "./apiDocuments.js";
|
|
27
|
+
import { vocabItemsApi, vocabLayersApi } from "./apiVocab.js";
|
|
28
|
+
import { relationLayersApi, spanLayersApi, textLayersApi, tokenLayersApi } from "./apiLayers.js";
|
|
29
|
+
import { relationsApi, spansApi, tokensApi, vocabLinksApi } from "./apiAnnotations.js";
|
|
30
|
+
import {
|
|
31
|
+
beginOperation,
|
|
32
|
+
endOperation,
|
|
33
|
+
keySeed,
|
|
34
|
+
withOperation,
|
|
35
|
+
} from "./operations.js";
|
|
36
|
+
import { bodyOf } from "./families.js";
|
|
37
|
+
|
|
38
|
+
// ---- Types --------------------------------------------------------------
|
|
39
|
+
// The shapes the API answers with and takes are in types.js, named here so
|
|
40
|
+
// this module's JSDoc can use them.
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {import("./types.js").TextEditOp} TextEditOp
|
|
44
|
+
* @typedef {import("./types.js").TextGap} TextGap
|
|
45
|
+
* @typedef {import("./types.js").MetadataOp} MetadataOp
|
|
46
|
+
* @typedef {import("./types.js").LayerConstraint} LayerConstraint
|
|
47
|
+
* @typedef {import("./types.js").ConstraintWriteOptions} ConstraintWriteOptions
|
|
48
|
+
* @typedef {import("./types.js").ConstraintViolation} ConstraintViolation
|
|
49
|
+
* @typedef {import("./types.js").Requester} Requester
|
|
50
|
+
* @typedef {import("./types.js").BatchRef} BatchRef
|
|
51
|
+
* @typedef {import("./types.js").ServiceParam} ServiceParam
|
|
52
|
+
* @typedef {import("./types.js").ServiceInfo} ServiceInfo
|
|
53
|
+
* @typedef {import("./types.js").DiscoveredService} DiscoveredService
|
|
54
|
+
* @typedef {import("./types.js").ResponseHelper} ResponseHelper
|
|
55
|
+
* @typedef {import("./types.js").ServiceRegistration} ServiceRegistration
|
|
56
|
+
* @typedef {import("./types.js").ServiceStatusEvent} ServiceStatusEvent
|
|
57
|
+
* @typedef {import("./types.js").OperationGroupsBundle} OperationGroupsBundle
|
|
58
|
+
* @typedef {import("./types.js").PlaidClientOptions} PlaidClientOptions
|
|
59
|
+
* @typedef {import("./types.js").OperationKind} OperationKind
|
|
60
|
+
* @typedef {import("./types.js").KeySeed} KeySeed
|
|
61
|
+
* @typedef {import("./types.js").PlaidBatch} PlaidBatch
|
|
62
|
+
* @typedef {import("./types.js").AuditPageOptions} AuditPageOptions
|
|
63
|
+
* @typedef {import("./types.js").VocabItemRestoreSummary} VocabItemRestoreSummary
|
|
64
|
+
* @typedef {import("./types.js").UserDataEntry} UserDataEntry
|
|
65
|
+
* @typedef {import("./types.js").AdminUserDataEntry} AdminUserDataEntry
|
|
66
|
+
* @typedef {import("./types.js").ActivityTallyRow} ActivityTallyRow
|
|
67
|
+
* @typedef {import("./types.js").ConstraintRepair} ConstraintRepair
|
|
68
|
+
* @typedef {import("./types.js").ValueSetRule} ValueSetRule
|
|
69
|
+
* @typedef {import("./types.js").RefusedValues} RefusedValues
|
|
70
|
+
* @typedef {import("./types.js").CommentableType} CommentableType
|
|
71
|
+
* @typedef {import("./types.js").Comment} Comment
|
|
72
|
+
* @typedef {import("./types.js").Guideline} Guideline
|
|
73
|
+
* @typedef {import("./types.js").ClientEventType} ClientEventType
|
|
74
|
+
* @typedef {import("./types.js").ClientEvent} ClientEvent
|
|
75
|
+
* @typedef {import("./types.js").Invite} Invite
|
|
76
|
+
* @typedef {import("./types.js").ServerLimits} ServerLimits
|
|
77
|
+
* @typedef {import("./types.js").ServerHealth} ServerHealth
|
|
78
|
+
* @typedef {import("./types.js").RateLimitBucket} RateLimitBucket
|
|
79
|
+
* @typedef {import("./types.js").RateLimitSnapshot} RateLimitSnapshot
|
|
80
|
+
* @typedef {import("./types.js").BackupStatus} BackupStatus
|
|
81
|
+
* @typedef {import("./types.js").ServerReport} ServerReport
|
|
82
|
+
* @typedef {import("./types.js").LiveLog} LiveLog
|
|
83
|
+
* @typedef {import("./types.js").SSEConnection} SSEConnection
|
|
84
|
+
* @typedef {import("./types.js").SSEStats} SSEStats
|
|
85
|
+
* @typedef {import("./types.js").AtomicValue} AtomicValue
|
|
86
|
+
* @typedef {import("./types.js").Created} Created
|
|
87
|
+
* @typedef {import("./types.js").Project} Project
|
|
88
|
+
* @typedef {import("./types.js").ProjectTextLayer} ProjectTextLayer
|
|
89
|
+
* @typedef {import("./types.js").ProjectTokenLayer} ProjectTokenLayer
|
|
90
|
+
* @typedef {import("./types.js").ProjectSpanLayer} ProjectSpanLayer
|
|
91
|
+
* @typedef {import("./types.js").ProjectRelationLayer} ProjectRelationLayer
|
|
92
|
+
* @typedef {import("./types.js").ProjectVocab} ProjectVocab
|
|
93
|
+
* @typedef {import("./types.js").PlaidDocument} PlaidDocument
|
|
94
|
+
* @typedef {import("./types.js").DocumentTextLayer} DocumentTextLayer
|
|
95
|
+
* @typedef {import("./types.js").DocumentTokenLayer} DocumentTokenLayer
|
|
96
|
+
* @typedef {import("./types.js").DocumentSpanLayer} DocumentSpanLayer
|
|
97
|
+
* @typedef {import("./types.js").PlaidText} PlaidText
|
|
98
|
+
* @typedef {import("./types.js").TextReshape} TextReshape
|
|
99
|
+
* @typedef {import("./types.js").Token} Token
|
|
100
|
+
* @typedef {import("./types.js").Span} Span
|
|
101
|
+
* @typedef {import("./types.js").Relation} Relation
|
|
102
|
+
* @typedef {import("./types.js").VocabItem} VocabItem
|
|
103
|
+
* @typedef {import("./types.js").VocabLink} VocabLink
|
|
104
|
+
* @typedef {import("./types.js").VocabLayer} VocabLayer
|
|
105
|
+
* @typedef {import("./types.js").TextLayer} TextLayer
|
|
106
|
+
* @typedef {import("./types.js").TokenLayer} TokenLayer
|
|
107
|
+
* @typedef {import("./types.js").SpanLayer} SpanLayer
|
|
108
|
+
* @typedef {import("./types.js").RelationLayer} RelationLayer
|
|
109
|
+
* @typedef {import("./types.js").MetadataRecords} MetadataRecords
|
|
110
|
+
* @typedef {import("./types.js").LayerRecords} LayerRecords
|
|
111
|
+
* @typedef {import("./types.js").User} User
|
|
112
|
+
* @typedef {import("./types.js").ProjectListEntry} ProjectListEntry
|
|
113
|
+
* @typedef {import("./types.js").DocumentListEntry} DocumentListEntry
|
|
114
|
+
* @typedef {import("./types.js").ApiToken} ApiToken
|
|
115
|
+
* @typedef {import("./types.js").AuditUser} AuditUser
|
|
116
|
+
* @typedef {import("./types.js").AuditTarget} AuditTarget
|
|
117
|
+
* @typedef {import("./types.js").AuditOp} AuditOp
|
|
118
|
+
* @typedef {import("./types.js").AuditEntry} AuditEntry
|
|
119
|
+
* @typedef {import("./types.js").LockGrant} LockGrant
|
|
120
|
+
* @typedef {import("./types.js").DocumentRestoreSummary} DocumentRestoreSummary
|
|
121
|
+
* @typedef {import("./types.js").RestoreCounts} RestoreCounts
|
|
122
|
+
* @typedef {import("./types.js").OperationGroup} OperationGroup
|
|
123
|
+
* @typedef {import("./types.js").QueryResult} QueryResult
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @template [T=any]
|
|
128
|
+
* @typedef {import("./types.js").Page<T>} Page
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
// A request with no Authorization header: login, invite lookup and redemption
|
|
132
|
+
// (POST, with a body), /health and /api/v1/info (GET). Deliberately does not
|
|
133
|
+
// go through PlaidClient._request: these are called before any client exists,
|
|
134
|
+
// which is the whole point.
|
|
135
|
+
async function anonymousRequest(method, baseUrl, path, body, options = {}) {
|
|
136
|
+
const base = baseUrl.replace(/\/$/, "");
|
|
137
|
+
const url = `${base}${path}`;
|
|
138
|
+
try {
|
|
139
|
+
const fetchOptions =
|
|
140
|
+
method === "GET"
|
|
141
|
+
? { method }
|
|
142
|
+
: {
|
|
143
|
+
method,
|
|
144
|
+
headers: { "Content-Type": "application/json" },
|
|
145
|
+
body: JSON.stringify(body),
|
|
146
|
+
};
|
|
147
|
+
const signal = timeoutSignal(
|
|
148
|
+
options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS,
|
|
149
|
+
);
|
|
150
|
+
if (signal) fetchOptions.signal = signal;
|
|
151
|
+
|
|
152
|
+
const response = await fetch(url, fetchOptions);
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
throw makeHttpError(response, await parseErrorBody(response), url, method);
|
|
155
|
+
}
|
|
156
|
+
return transformResponse(await response.json());
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error.status) throw error;
|
|
159
|
+
throw makeNetworkError(error, url, method);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
class PlaidClient {
|
|
164
|
+
/**
|
|
165
|
+
* Create a new PlaidClient instance
|
|
166
|
+
* @param {string} baseUrl - The base URL for the API
|
|
167
|
+
* @param {string} token - The authentication token
|
|
168
|
+
* @param {PlaidClientOptions} [options] - Client options
|
|
169
|
+
*/
|
|
170
|
+
constructor(baseUrl, token, options = {}) {
|
|
171
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
172
|
+
this.token = token;
|
|
173
|
+
/** @type {number | null} */
|
|
174
|
+
this.timeout =
|
|
175
|
+
options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS;
|
|
176
|
+
// Batch submissions get a longer budget than a single request. Aborting a
|
|
177
|
+
// batch does NOT stop the server: it keeps running the transaction and
|
|
178
|
+
// holding the single SQLite write lock, so a client that gives up early
|
|
179
|
+
// just adds a retry on top of a write that is still in progress. Falls
|
|
180
|
+
// back to an explicit `timeout` when one was given.
|
|
181
|
+
/** @type {number | null} */
|
|
182
|
+
this.batchTimeout =
|
|
183
|
+
options.batchTimeout !== undefined
|
|
184
|
+
? options.batchTimeout
|
|
185
|
+
: options.timeout !== undefined
|
|
186
|
+
? options.timeout
|
|
187
|
+
: DEFAULT_BATCH_TIMEOUT_MS;
|
|
188
|
+
// Delays (ms) before sending a keyed write again when its answer was lost
|
|
189
|
+
// (no response, 502, 504). See the Idempotency-Key note in http.js.
|
|
190
|
+
/** @type {number[] | undefined} */
|
|
191
|
+
this.retryDelaysMs = options.retryDelaysMs;
|
|
192
|
+
/**
|
|
193
|
+
* The latest version seen for each document written through this client,
|
|
194
|
+
* keyed by document id.
|
|
195
|
+
* @type {Record<string, number>}
|
|
196
|
+
*/
|
|
197
|
+
this.documentVersions = {};
|
|
198
|
+
/**
|
|
199
|
+
* True once a response left its version list out
|
|
200
|
+
* (X-Document-Versions-Omitted, past fifty documents). Every version held
|
|
201
|
+
* was then forgotten, and the strict-mode document's is read again before
|
|
202
|
+
* the next write.
|
|
203
|
+
* @type {boolean | undefined}
|
|
204
|
+
*/
|
|
205
|
+
this.documentVersionsOmitted = undefined;
|
|
206
|
+
// The server's clock minus this machine's, in ms, from the last response
|
|
207
|
+
// with a Date header (null before one). See serverNow().
|
|
208
|
+
/** @type {number | null} */
|
|
209
|
+
this.serverClockOffsetMs = null;
|
|
210
|
+
/**
|
|
211
|
+
* The document strict mode is entered for, or null.
|
|
212
|
+
* @type {string | null}
|
|
213
|
+
*/
|
|
214
|
+
this.strictModeDocumentId = null;
|
|
215
|
+
// Set to a DocumentLockLost while a `documents.locked()` block's keep-alive
|
|
216
|
+
// has failed. Every write throws it until the block exits; see
|
|
217
|
+
// documentLock.js.
|
|
218
|
+
/** @type {import("./documentLock.js").DocumentLockLost | null} */
|
|
219
|
+
this.documentLockLost = null;
|
|
220
|
+
// The keeper of each `documents.locked()` block that renews on a beat, by
|
|
221
|
+
// document id: a write that lands on a held document renewed its lock,
|
|
222
|
+
// and the request layer tells that keeper.
|
|
223
|
+
this.lockKeepers = new Map();
|
|
224
|
+
// The open logical operation (audit-log group), or null. While set, every
|
|
225
|
+
// write is stamped with `?group-id=` (+ `group-message`) so the audit log
|
|
226
|
+
// folds them into ONE expandable entry. See beginOperation / withOperation.
|
|
227
|
+
// Shape: { id, message, depth, written, refined, frames }, where each of
|
|
228
|
+
// `frames` is { keys, count, minted, depth, owned } and the last numbers.
|
|
229
|
+
/**
|
|
230
|
+
* @type {{id: string, message: string | null, kind: OperationKind | null,
|
|
231
|
+
* ref: string | null, depth: number, written: boolean, refined: any,
|
|
232
|
+
* frames: object[]} | null}
|
|
233
|
+
*/
|
|
234
|
+
this.operationGroup = null;
|
|
235
|
+
// Optional callback fired once (per client) when any request returns HTTP
|
|
236
|
+
// 401 — i.e. the token is missing/expired/invalid. Apps use it to discard
|
|
237
|
+
// the stored token and route back to login. See makeRequest in http.js.
|
|
238
|
+
/** @type {((error: Error) => void) | null} */
|
|
239
|
+
this.onAuthError = options.onAuthError || null;
|
|
240
|
+
/** @private */
|
|
241
|
+
this._authErrorFired = false;
|
|
242
|
+
// Who hears that a request is being sent again (onRetry).
|
|
243
|
+
/** @private */
|
|
244
|
+
this._retryListeners = new Set();
|
|
245
|
+
// GET /info as last read (`server.info()`), and whether a stream of this
|
|
246
|
+
// client dropped since.
|
|
247
|
+
/** @private */
|
|
248
|
+
this._serverFacts = { info: null, stale: false };
|
|
249
|
+
// The key frame the last beginOperation opened, or null (withOperation).
|
|
250
|
+
/** @private @type {object | null} */
|
|
251
|
+
this._openedFrame = null;
|
|
252
|
+
|
|
253
|
+
this._installResources();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* A stream of this client ended without being closed here: the server may
|
|
258
|
+
* have restarted, with other limits.
|
|
259
|
+
* @private
|
|
260
|
+
*/
|
|
261
|
+
_noteStreamDropped() {
|
|
262
|
+
this._serverFacts.stale = true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* A stream of this client is open. After a drop the limits are read again,
|
|
267
|
+
* once however many streams reopen.
|
|
268
|
+
* @private
|
|
269
|
+
*/
|
|
270
|
+
_noteStreamOpened() {
|
|
271
|
+
if (!this._serverFacts.stale) return;
|
|
272
|
+
this.server.refresh().catch(() => {
|
|
273
|
+
// The figures known before stand, and the next open asks again.
|
|
274
|
+
this._serverFacts.stale = true;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Hear every time a request is sent again: after a 503 (the database was
|
|
280
|
+
* busy), or for a keyed write after its answer was lost (no response, 502,
|
|
281
|
+
* 504). The listener gets `{ attempt, retries, delay, error }` before the
|
|
282
|
+
* wait. A screen that writes through the client directly can say it is
|
|
283
|
+
* retrying while the server does not answer, though the browser is online.
|
|
284
|
+
* @param {(info: {attempt: number, retries: number, delay: number, error: Error}) => void} listener
|
|
285
|
+
* @returns {() => void} the unsubscribe
|
|
286
|
+
*/
|
|
287
|
+
onRetry(listener) {
|
|
288
|
+
this._retryListeners.add(listener);
|
|
289
|
+
return () => this._retryListeners.delete(listener);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Tell every `onRetry` listener. A listener that throws is passed over.
|
|
294
|
+
* @private
|
|
295
|
+
*/
|
|
296
|
+
_noteRetry(info) {
|
|
297
|
+
for (const listener of this._retryListeners) {
|
|
298
|
+
try {
|
|
299
|
+
listener(info);
|
|
300
|
+
} catch (_) {
|
|
301
|
+
/* a listener must not stop the retry */
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* The server's time now, as its last response's Date header put it (to the
|
|
308
|
+
* second), else this machine's. Judge a time the server stamped, such as an
|
|
309
|
+
* audit entry's `ts`, against this rather than `Date.now()`: a browser
|
|
310
|
+
* clock can be minutes off.
|
|
311
|
+
* @returns {Date}
|
|
312
|
+
*/
|
|
313
|
+
serverNow() {
|
|
314
|
+
return new Date(Date.now() + (this.serverClockOffsetMs ?? 0));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The API bundles (`client.documents`, `client.tokens`, ...). Installed on
|
|
319
|
+
* the client, and again on every batch it opens (see `batch()`): a batch is
|
|
320
|
+
* a view of the client with its own `_request`, so the same bundles built
|
|
321
|
+
* on it queue instead of sending.
|
|
322
|
+
* @private
|
|
323
|
+
*/
|
|
324
|
+
_installResources() {
|
|
325
|
+
this.documents = documentsApi(this);
|
|
326
|
+
this.texts = textsApi(this);
|
|
327
|
+
|
|
328
|
+
this.tokens = tokensApi(this);
|
|
329
|
+
this.spans = spansApi(this);
|
|
330
|
+
this.relations = relationsApi(this);
|
|
331
|
+
this.vocabLinks = vocabLinksApi(this);
|
|
332
|
+
|
|
333
|
+
this.textLayers = textLayersApi(this);
|
|
334
|
+
this.tokenLayers = tokenLayersApi(this);
|
|
335
|
+
this.spanLayers = spanLayersApi(this);
|
|
336
|
+
this.relationLayers = relationLayersApi(this);
|
|
337
|
+
|
|
338
|
+
this.vocabLayers = vocabLayersApi(this);
|
|
339
|
+
this.vocabItems = vocabItemsApi(this);
|
|
340
|
+
|
|
341
|
+
this.users = usersApi(this);
|
|
342
|
+
this.apiTokens = apiTokensApi(this);
|
|
343
|
+
this.invites = invitesApi(this);
|
|
344
|
+
this.auth = authApi(this);
|
|
345
|
+
this.userData = userDataApi(this);
|
|
346
|
+
|
|
347
|
+
this.projects = projectsApi(this);
|
|
348
|
+
this.guidelines = guidelinesApi(this);
|
|
349
|
+
this.comments = commentsApi(this);
|
|
350
|
+
this.events = eventsApi(this);
|
|
351
|
+
this.operationGroups = operationGroupsApi(this);
|
|
352
|
+
|
|
353
|
+
this.server = serverApi(this);
|
|
354
|
+
this.admin = adminApi(this);
|
|
355
|
+
this.audit = auditApi(this);
|
|
356
|
+
|
|
357
|
+
this.messages = messagesApi(this);
|
|
358
|
+
|
|
359
|
+
// outOfBand: a query is a read that travels as a POST (see the note at
|
|
360
|
+
// the top of http.js). Made on a batch it goes over the wire like any
|
|
361
|
+
// read, and it never joins a logical operation. It waits past core's own
|
|
362
|
+
// query limit, so a query too broad answers with core's 408, and a 503
|
|
363
|
+
// from core's full queue of large queries is not retried.
|
|
364
|
+
/**
|
|
365
|
+
* Run a query over every project you can read.
|
|
366
|
+
*
|
|
367
|
+
* `body` is the query AST. Its keys follow the usual client convention
|
|
368
|
+
* (camelCase, e.g. `scope.projectIds`) and are converted to the wire
|
|
369
|
+
* format automatically; clause heads and variables are plain strings you
|
|
370
|
+
* write literally (e.g. `'span'`, `'?s1'`, `'vocab-link'`). Example:
|
|
371
|
+
*
|
|
372
|
+
* await client.query({
|
|
373
|
+
* find: ['?s1', '?s2'],
|
|
374
|
+
* where: [
|
|
375
|
+
* ['span', '?s1', { layer: posLayerId, value: 'NOUN' }],
|
|
376
|
+
* ['span', '?s2', { layer: posLayerId, value: 'VERB' }],
|
|
377
|
+
* ['covers', '?s1', '?t1'], ['covers', '?s2', '?t2'],
|
|
378
|
+
* ['precedes', '?t1', '?t2'],
|
|
379
|
+
* ],
|
|
380
|
+
* return: 'entities', // 'ids' (default) | 'entities' | 'count'
|
|
381
|
+
* limit: 100,
|
|
382
|
+
* });
|
|
383
|
+
*
|
|
384
|
+
* A `layer` is referenced by its id (its UUID) only — not by name or
|
|
385
|
+
* path. To match a layer by name, bind it with a `*-layer` clause (e.g.
|
|
386
|
+
* `['span-layer', '?sl', { name: 'pos' }]`) and use the variable.
|
|
387
|
+
*
|
|
388
|
+
* A vocab link's provenance is on the link itself: bind it as a `link`
|
|
389
|
+
* entity to filter or group by it, e.g. the machine-made links nobody
|
|
390
|
+
* confirmed, with the tokens they sit on:
|
|
391
|
+
*
|
|
392
|
+
* ['link', '?l', { metadata: { prov: 'inferred' } }],
|
|
393
|
+
* ['not', ['link', '?l', { metadata: { provConfirmed: true } }]],
|
|
394
|
+
* ['link-token', '?l', '?t'], ['link-item', '?l', '?v']
|
|
395
|
+
*
|
|
396
|
+
* Optional keys: `scope` (restrict to projects by id, `{projectIds}`), `orderBy`
|
|
397
|
+
* (sort rows), and `bindings` (substitute `?name` placeholders with literals).
|
|
398
|
+
* `return` may also be an aggregate spec `{group, aggregates}`. See the query
|
|
399
|
+
* language reference.
|
|
400
|
+
*
|
|
401
|
+
* @param {Object} body - The query AST ({find, where, scope?, limit?, orderBy?,
|
|
402
|
+
* return?, bindings?}).
|
|
403
|
+
* @returns {Promise<QueryResult>} For 'ids'/'entities': {columns, results, count, truncated}.
|
|
404
|
+
* For 'count': {return: 'count', count, truncated}, where `truncated` says
|
|
405
|
+
* the count stopped at the server's cap. Entity cells are full entity
|
|
406
|
+
* objects (same shape as the GET endpoints).
|
|
407
|
+
*/
|
|
408
|
+
this.query = (body) =>
|
|
409
|
+
this._request("POST", "/api/v1/query", {
|
|
410
|
+
body,
|
|
411
|
+
outOfBand: true,
|
|
412
|
+
timeout: queryTimeout(this),
|
|
413
|
+
noBusyRetry: true,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// --- Core methods ---
|
|
418
|
+
|
|
419
|
+
/** @private */
|
|
420
|
+
async _request(method, path, options = {}) {
|
|
421
|
+
return makeRequest(this, method, path, options);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Enter strict mode for a specific document, requiring document version
|
|
426
|
+
* headers so that conflicting concurrent writes are rejected.
|
|
427
|
+
* @param {string} documentId - The ID of the document to track versions for
|
|
428
|
+
*/
|
|
429
|
+
enterStrictMode(documentId) {
|
|
430
|
+
this.strictModeDocumentId = documentId;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Exit strict mode and stop tracking document versions for writes. */
|
|
434
|
+
exitStrictMode() {
|
|
435
|
+
this.strictModeDocumentId = null;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Begin a LOGICAL OPERATION: a user-meaningful action ("Merge morphemes",
|
|
440
|
+
* "Re-transcribe") implemented as many low-level writes, possibly across
|
|
441
|
+
* several batches and even a service round-trip. Until `endOperation()`,
|
|
442
|
+
* every write is stamped with a client-minted `?group-id=` (and the message)
|
|
443
|
+
* so the audit log shows the whole run as ONE expandable entry labeled
|
|
444
|
+
* `message`, with each write's own description underneath.
|
|
445
|
+
*
|
|
446
|
+
* Grouping is orthogonal to batches: a batch is a transaction boundary, an
|
|
447
|
+
* operation is an intent boundary. An operation is NOT atomic — if write 3
|
|
448
|
+
* of 5 fails, writes 1–2 stay committed (and logged under the group). Use a
|
|
449
|
+
* batch inside the operation for any step that must be all-or-nothing.
|
|
450
|
+
*
|
|
451
|
+
* Nesting flattens: a `beginOperation` while one is open is a no-op that
|
|
452
|
+
* joins the outer operation (the outer label wins), and the matching
|
|
453
|
+
* `endOperation` is likewise a no-op. The label is recorded on the FIRST
|
|
454
|
+
* write, so an operation that is never ended (crash, closed tab) is still
|
|
455
|
+
* labeled in the log.
|
|
456
|
+
*
|
|
457
|
+
* "Every write" is the writes of project data. Reads never join, and neither
|
|
458
|
+
* do the out-of-band signals shaped like a write (a document lock taken or
|
|
459
|
+
* renewed, a stopped service request, a service reporting itself, an admin
|
|
460
|
+
* control), and neither does a broadcast message (`messages.sendMessage`):
|
|
461
|
+
* none of them is audited, so there would be nothing under the label.
|
|
462
|
+
*
|
|
463
|
+
* `kind` says what kind of operation this is, for a program reading the
|
|
464
|
+
* log: one of `assistant-plan`, `service-run`, `import`, `bulk-edit`,
|
|
465
|
+
* `guess-adoption`, `repair` or `review` (the server refuses any other).
|
|
466
|
+
* Any other kind throws here, before anything is written.
|
|
467
|
+
* `ref` is a short string naming what the operation came from, in the
|
|
468
|
+
* shape its kind documents (the core manual, "Kinds of operation"). Both
|
|
469
|
+
* are recorded from the first write like the label, and a nested operation
|
|
470
|
+
* keeps the outer one's.
|
|
471
|
+
*
|
|
472
|
+
* `keys` (from `keySeed()`) makes a run of the operation send the same
|
|
473
|
+
* requests as an earlier run with the same keys: the nth keyed request that
|
|
474
|
+
* joins it takes the Idempotency-Key `<seed>.<n>` and the document-version
|
|
475
|
+
* its first run claimed, so a request that landed is answered from its
|
|
476
|
+
* first send and writes nothing again. The count starts at 0 at each
|
|
477
|
+
* outermost begin. A nested operation joins the outer one's keys, unless it
|
|
478
|
+
* brings its own: then it numbers from 0 under its own seed until its
|
|
479
|
+
* matching end, and the outer numbering resumes after it.
|
|
480
|
+
*
|
|
481
|
+
* @param {string} message - Human label for the operation.
|
|
482
|
+
* @param {{groupId?: string, kind?: OperationKind, ref?: string, keys?: KeySeed, minted?: Iterable<string>}} [opts] - Optional `{ groupId, kind, ref, keys, minted }`. `groupId` adopts an existing group id instead of minting one (a service joining the requester's operation; `requestService` propagates an open operation to the service automatically). Python's is `group_id`. `kind`, `ref` and `keys` are described above. `minted` is the ids the operation mints for what it creates: a create refused 409 id-taken for one of them was made by an earlier send of the operation, and answers as made.
|
|
483
|
+
* @returns {string} The operation's group id.
|
|
484
|
+
*/
|
|
485
|
+
beginOperation(message, { groupId, kind, ref, keys, minted } = {}) {
|
|
486
|
+
return beginOperation(this, message, { groupId, kind, ref, keys, minted });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* A seed for the Idempotency-Keys of a logical operation that may be run
|
|
491
|
+
* again from the top (see beginOperation's `keys`). Keep it with the work
|
|
492
|
+
* it belongs to and pass it to every run.
|
|
493
|
+
* @returns {KeySeed}
|
|
494
|
+
*/
|
|
495
|
+
keySeed() {
|
|
496
|
+
return keySeed();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* End the current logical operation. With no argument this is purely local
|
|
501
|
+
* (no request). Pass a refined `message` to relabel the group now that the
|
|
502
|
+
* outcome is known (e.g. `endOperation('Merged 3 morphemes')`) — that sends
|
|
503
|
+
* one PATCH, skipped if the operation never wrote anything. A refine from a
|
|
504
|
+
* nested (flattened) `endOperation` is ignored; the outer label wins.
|
|
505
|
+
* @param {string} [message] - Optional refined label.
|
|
506
|
+
* @returns {Promise<void>}
|
|
507
|
+
*/
|
|
508
|
+
async endOperation(message) {
|
|
509
|
+
return endOperation(this, message);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Run `fn` as one logical operation (see beginOperation), ending it when
|
|
514
|
+
* `fn` settles — including on throw. `fn` receives a `setMessage(msg)`
|
|
515
|
+
* callback to refine the label once the outcome is known.
|
|
516
|
+
*
|
|
517
|
+
* await client.withOperation('Merge morphemes', async (setMessage) => {
|
|
518
|
+
* await client.batched(async (b) => { ... });
|
|
519
|
+
* setMessage(`Merged ${n} morphemes`);
|
|
520
|
+
* });
|
|
521
|
+
*
|
|
522
|
+
* The kind and reference (see beginOperation) come after `fn`:
|
|
523
|
+
*
|
|
524
|
+
* await client.withOperation('Import ELAN corpus', run, { kind: 'import', ref: 'format:elan' });
|
|
525
|
+
*
|
|
526
|
+
* @template T
|
|
527
|
+
* @param {string} message - Human label for the operation.
|
|
528
|
+
* @param {(setMessage: (msg: string) => void) => T | Promise<T>} fn - The work to run; receives `setMessage(msg)` to refine the label once the outcome is known.
|
|
529
|
+
* @param {{kind?: OperationKind, ref?: string, groupId?: string, keys?: KeySeed, minted?: Iterable<string>}} [opts] - Optional `{ kind, ref, groupId, keys, minted }`, as for beginOperation. `kind` is one of `assistant-plan`, `service-run`, `import`, `bulk-edit`, `guess-adoption`, `repair` or `review`, and any other throws before `fn` runs.
|
|
530
|
+
* @returns {Promise<T>} Whatever `fn` resolves to.
|
|
531
|
+
*/
|
|
532
|
+
async withOperation(message, fn, { kind, ref, groupId, keys, minted } = {}) {
|
|
533
|
+
return withOperation(this, message, fn, { kind, ref, groupId, keys, minted });
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Open a batch: a view of this client with the same bundles, on which every
|
|
539
|
+
* write of project data queues instead of going out. `submit()` sends the
|
|
540
|
+
* queued operations as ONE atomic request (larger than the server's cap,
|
|
541
|
+
* as consecutive requests with the results concatenated in queue order,
|
|
542
|
+
* each atomic on its own, so a failure in a later one leaves the earlier
|
|
543
|
+
* ones committed, and the error it throws carries `committed`, the count
|
|
544
|
+
* saved, and `committedResults`, their results) and
|
|
545
|
+
* resolves to one result per operation; `abort()` drops them. A call made
|
|
546
|
+
* on the client itself is never touched by an open batch, and a read or an
|
|
547
|
+
* out-of-band signal made on the batch goes over the wire now (see the note
|
|
548
|
+
* at the top of `http.js`).
|
|
549
|
+
*
|
|
550
|
+
* const b = client.batch();
|
|
551
|
+
* b.tokens.bulkCreate(sentenceOps);
|
|
552
|
+
* b.tokens.bulkCreate(wordOps);
|
|
553
|
+
* const [sentRes, wordRes] = await b.submit();
|
|
554
|
+
*
|
|
555
|
+
* Server-side a batch runs sequentially in one transaction: a child op sees
|
|
556
|
+
* parents created earlier in the same batch, and any op's failure rolls the
|
|
557
|
+
* whole batch back. A batch is not nestable. Prefer `batched()`, which
|
|
558
|
+
* submits or aborts for you.
|
|
559
|
+
*
|
|
560
|
+
* `timeout` (ms, or null for none) is how long each of this batch's
|
|
561
|
+
* requests may wait for its answer, in place of the client's
|
|
562
|
+
* `batchTimeout`, for this batch alone.
|
|
563
|
+
* @param {{timeout?: number | null}} [options]
|
|
564
|
+
* @returns {PlaidBatch} the batch view
|
|
565
|
+
*/
|
|
566
|
+
batch({ timeout } = {}) {
|
|
567
|
+
return openBatch(this, { timeout });
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* POST one batch request (at most MAX_BATCH_OPS operations). Resolves to
|
|
572
|
+
* one `{ status, headers, body }` per operation, with only the body
|
|
573
|
+
* recased: the headers keep the server's spelling. `key` is the request's
|
|
574
|
+
* Idempotency-Key: a batch whose answer is lost (no response, 502, 504) is
|
|
575
|
+
* sent again under it, and answered from the first send if that landed.
|
|
576
|
+
* `timeout` is this request's, in place of `batchTimeout` when given.
|
|
577
|
+
* See postBatch in http.js.
|
|
578
|
+
* @param {string} url
|
|
579
|
+
* @param {object[]} body
|
|
580
|
+
* @param {{key?: string, timeout?: number | null}} [options]
|
|
581
|
+
* @private
|
|
582
|
+
*/
|
|
583
|
+
async _postBatch(url, body, options) {
|
|
584
|
+
return postBatch(this, url, body, options);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Run `fn` with a batch, then submit all queued ops as `submit()` does (ONE
|
|
590
|
+
* atomic request up to MAX_BATCH_OPS operations, consecutive requests past
|
|
591
|
+
* it), or abort the batch if `fn` throws. `fn` receives the batch and makes its
|
|
592
|
+
* writes on it; it must NOT call `submit()` itself. Resolves to the batch
|
|
593
|
+
* results array (`[]` if `fn` queued nothing).
|
|
594
|
+
*
|
|
595
|
+
* const [sentRes, wordRes] = await client.batched(async (b) => {
|
|
596
|
+
* b.tokens.bulkCreate(sentenceOps);
|
|
597
|
+
* b.tokens.bulkCreate(wordOps);
|
|
598
|
+
* });
|
|
599
|
+
*
|
|
600
|
+
* A write made on `client` inside `fn` is not part of the batch: it goes
|
|
601
|
+
* over the wire at once, as it would anywhere else. See `batch()`, which
|
|
602
|
+
* also says what `timeout` does.
|
|
603
|
+
* @param {(batch: PlaidBatch) => (void | Promise<void>)} fn
|
|
604
|
+
* @param {{timeout?: number | null}} [options]
|
|
605
|
+
* @returns {Promise<any[]>}
|
|
606
|
+
*/
|
|
607
|
+
async batched(fn, { timeout } = {}) {
|
|
608
|
+
const batch = this.batch({ timeout });
|
|
609
|
+
try {
|
|
610
|
+
await fn(batch);
|
|
611
|
+
} catch (e) {
|
|
612
|
+
batch.abort();
|
|
613
|
+
throw e;
|
|
614
|
+
}
|
|
615
|
+
return batch.submit();
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Liveness, version and database size, with NO authentication and no client
|
|
620
|
+
* instance — for a launcher or status page checking whether a server is up
|
|
621
|
+
* before anyone logs in.
|
|
622
|
+
* @param {string} baseUrl - The API base URL
|
|
623
|
+
* @param {{timeout?: number | null}} [options]
|
|
624
|
+
* @returns {Promise<ServerHealth>}
|
|
625
|
+
*/
|
|
626
|
+
static async health(baseUrl, options = {}) {
|
|
627
|
+
return anonymousRequest("GET", baseUrl, "/health", undefined, options);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* The limits this server enforces, with NO authentication and no client
|
|
632
|
+
* instance. See the instance method for the shape.
|
|
633
|
+
* @param {string} baseUrl - The API base URL
|
|
634
|
+
* @param {{timeout?: number | null}} [options]
|
|
635
|
+
* @returns {Promise<{limits: ServerLimits}>}
|
|
636
|
+
*/
|
|
637
|
+
static async info(baseUrl, options = {}) {
|
|
638
|
+
return anonymousRequest("GET", baseUrl, "/api/v1/info", undefined, options);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Build the link to hand someone for an invite code. The server never sees
|
|
643
|
+
* an app URL, so the app that minted the invite is the one that names it.
|
|
644
|
+
* Both SPAs use HashRouter, so the code rides in the fragment — which also
|
|
645
|
+
* keeps it out of server access logs.
|
|
646
|
+
* @param {string} appUrl - Where the SPA lives, e.g. "https://plaid.example.org/igt/"
|
|
647
|
+
* @param {string} code - The code returned by invites.create()
|
|
648
|
+
* @returns {string}
|
|
649
|
+
*/
|
|
650
|
+
static inviteUrl(appUrl, code) {
|
|
651
|
+
return `${appUrl.replace(/#.*$/, "").replace(/\/$/, "")}/#/invite/${encodeURIComponent(code)}`;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Describe an invite code, with NO authentication — this is what a signup
|
|
656
|
+
* page calls before the redeemer has an account. Returns the kind of link
|
|
657
|
+
* ("signup" or "password-reset"), its status ("active", "used", "expired",
|
|
658
|
+
* "revoked", or "inactive" when its creator can no longer grant what it
|
|
659
|
+
* grants), and the project it grants access to, if any.
|
|
660
|
+
*
|
|
661
|
+
* Throws a 404-shaped error if the code is unknown. A known-but-dead code
|
|
662
|
+
* resolves normally with a non-"active" status, so the page can say why.
|
|
663
|
+
* @param {string} baseUrl - The API base URL
|
|
664
|
+
* @param {string} code - The invite code
|
|
665
|
+
* @param {object} [options]
|
|
666
|
+
* @returns {Promise<{kind: string, status: string, expiresAt: string, projectName?: string, projectRole?: string, email?: string, grantAdmin: boolean}>}
|
|
667
|
+
*/
|
|
668
|
+
static async lookupInvite(baseUrl, code, options = {}) {
|
|
669
|
+
return anonymousRequest("POST", baseUrl, "/api/v1/invites/lookup", { code }, options);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Redeem an invite code, with NO authentication.
|
|
674
|
+
*
|
|
675
|
+
* For a signup invite, pass `email` and `password` to create the account
|
|
676
|
+
* (and optionally `displayName`); the invite's grants are applied in the
|
|
677
|
+
* same transaction. For a password reset link, pass `password` only.
|
|
678
|
+
* Resolves to a logged-in client, exactly like login() — the redeemer just
|
|
679
|
+
* chose these credentials, so there is no reason to send them to a login
|
|
680
|
+
* form to retype them.
|
|
681
|
+
* @param {string} baseUrl - The API base URL
|
|
682
|
+
* @param {string} code - The invite code
|
|
683
|
+
* @param {object} credentials
|
|
684
|
+
* @param {string} [credentials.email] - The new account's email address,
|
|
685
|
+
* which becomes its id and login (signup only)
|
|
686
|
+
* @param {string} [credentials.displayName] - How the new user is shown in
|
|
687
|
+
* the UI; defaults to the local part of the email (signup only)
|
|
688
|
+
* @param {string} credentials.password - Desired password (min 8 characters)
|
|
689
|
+
* @param {object} [options]
|
|
690
|
+
* @returns {Promise<{client: PlaidClient, userId: string, kind: string}>}
|
|
691
|
+
*/
|
|
692
|
+
static async redeemInvite(
|
|
693
|
+
baseUrl,
|
|
694
|
+
code,
|
|
695
|
+
{ email, password, displayName } = {},
|
|
696
|
+
options = {},
|
|
697
|
+
) {
|
|
698
|
+
const data = await anonymousRequest(
|
|
699
|
+
"POST",
|
|
700
|
+
baseUrl,
|
|
701
|
+
"/api/v1/invites/redeem",
|
|
702
|
+
bodyOf({
|
|
703
|
+
code,
|
|
704
|
+
email: normalizeUserId(email),
|
|
705
|
+
password,
|
|
706
|
+
"display-name": displayName,
|
|
707
|
+
}),
|
|
708
|
+
options,
|
|
709
|
+
);
|
|
710
|
+
return {
|
|
711
|
+
client: new PlaidClient(
|
|
712
|
+
baseUrl.replace(/\/$/, ""),
|
|
713
|
+
data.token || "",
|
|
714
|
+
options,
|
|
715
|
+
),
|
|
716
|
+
userId: data.userId,
|
|
717
|
+
kind: data.kind,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Authenticate and return a new client instance with token. This is the
|
|
723
|
+
* single auth entry point — there is no `client.login` resource.
|
|
724
|
+
* @param {string} baseUrl - The base URL for the API
|
|
725
|
+
* @param {string} userId - User ID for authentication
|
|
726
|
+
* @param {string} password - Password for authentication
|
|
727
|
+
* @param {object} [options] - Client options forwarded to the constructor (e.g. { timeout })
|
|
728
|
+
* @returns {Promise<PlaidClient>} - Authenticated client instance
|
|
729
|
+
*/
|
|
730
|
+
static async login(baseUrl, userId, password, options = {}) {
|
|
731
|
+
baseUrl = baseUrl.replace(/\/$/, "");
|
|
732
|
+
const data = await anonymousRequest(
|
|
733
|
+
"POST",
|
|
734
|
+
baseUrl,
|
|
735
|
+
"/api/v1/login",
|
|
736
|
+
{ "user-id": normalizeUserId(userId), password },
|
|
737
|
+
options,
|
|
738
|
+
);
|
|
739
|
+
return new PlaidClient(baseUrl, data.token || "", options);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export { PlaidClient };
|