@larc-iu/plaid-client 0.2.0-alpha.29 → 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.
Files changed (49) hide show
  1. package/package.json +3 -2
  2. package/src/apiAnnotations.d.ts +508 -0
  3. package/src/apiAnnotations.js +543 -0
  4. package/src/apiDocuments.d.ts +569 -0
  5. package/src/apiDocuments.js +551 -0
  6. package/src/apiLayers.d.ts +369 -0
  7. package/src/apiLayers.js +222 -0
  8. package/src/apiMessages.d.ts +293 -0
  9. package/src/apiMessages.js +265 -0
  10. package/src/apiPeople.d.ts +626 -0
  11. package/src/apiPeople.js +698 -0
  12. package/src/apiProject.d.ts +761 -0
  13. package/src/apiProject.js +774 -0
  14. package/src/apiServer.d.ts +455 -0
  15. package/src/apiServer.js +422 -0
  16. package/src/apiVocab.d.ts +456 -0
  17. package/src/apiVocab.js +425 -0
  18. package/src/batch.d.ts +54 -0
  19. package/src/batch.js +323 -0
  20. package/src/client.d.ts +1321 -0
  21. package/src/client.js +743 -0
  22. package/src/constraints.d.ts +8 -0
  23. package/src/constraints.js +14 -0
  24. package/src/documentLock.d.ts +18 -4
  25. package/src/documentLock.js +43 -9
  26. package/src/entryForms.d.ts +63 -0
  27. package/src/entryForms.js +152 -0
  28. package/src/events.js +2 -1
  29. package/src/families.d.ts +349 -0
  30. package/src/families.js +396 -0
  31. package/src/http.d.ts +39 -18
  32. package/src/http.js +127 -87
  33. package/src/index.d.ts +90 -4557
  34. package/src/index.js +113 -5172
  35. package/src/operations.d.ts +36 -0
  36. package/src/operations.js +164 -0
  37. package/src/provenance.d.ts +47 -4
  38. package/src/provenance.js +168 -12
  39. package/src/roles.d.ts +39 -0
  40. package/src/roles.js +44 -0
  41. package/src/serviceSchema.d.ts +19 -13
  42. package/src/serviceSchema.js +30 -15
  43. package/src/services.d.ts +20 -3
  44. package/src/services.js +45 -8
  45. package/src/sse.js +10 -1
  46. package/src/testing.d.ts +128 -0
  47. package/src/testing.js +379 -0
  48. package/src/types.d.ts +867 -0
  49. package/src/types.js +663 -0
@@ -22,6 +22,9 @@
22
22
  * @property {"Sentence" | "Word" | "Morpheme"} [scope] `field` only: the
23
23
  * scope of the project's annotation fields the value names one of. A
24
24
  * `field` value is a string everywhere else.
25
+ * @property {string} [role] `field` only: the lexicon role whose paired field,
26
+ * or the sentence role whose field, fills the parameter when nothing else
27
+ * does.
25
28
  */
26
29
  /**
27
30
  * A service's standardized self-description (lives in `extras`).
@@ -106,17 +109,9 @@ export function getServiceSummary(service: {
106
109
  * @returns {Record<string, any>} { [key]: defaultValue }
107
110
  */
108
111
  export function buildDefaultValues(schema: ServiceParam[]): Record<string, any>;
109
- /**
110
- * Coerce/validate raw form values against the schema. Returns the cleaned
111
- * values (keyed by param key, ready to merge into the request payload) plus any
112
- * validation errors keyed by param key. Unknown keys in `raw` are dropped.
113
- * @param {ServiceParam[]} schema getParamSchema(service)
114
- * @param {Record<string, any>} raw current form values
115
- * @returns {{values: Record<string, any>, errors: Record<string, string>}}
116
- */
117
- export function coerceParamValues(schema: ServiceParam[], raw: Record<string, any>): {
118
- values: Record<string, any>;
119
- errors: Record<string, string>;
112
+ export function coerceParamValues(schema: any, raw: any): {
113
+ values: {};
114
+ errors: {};
120
115
  };
121
116
  /**
122
117
  * Service self-description: tasks, summary, and a parameter schema.
@@ -143,13 +138,18 @@ export function coerceParamValues(schema: ServiceParam[], raw: Record<string, an
143
138
  * options?: [{value, label}], // enum / multiselect
144
139
  * min?, max?, step?, slider?, // number (slider: drag, don't type)
145
140
  * placeholder?, multiline?, // string
146
- * scope? } // field: 'Sentence' | 'Word' | 'Morpheme'
141
+ * scope?, role? } // field: 'Sentence' | 'Word' | 'Morpheme'
147
142
  * ]
148
143
  * }
149
144
  *
150
145
  * A `field` parameter names one of the project's annotation fields at its
151
146
  * `scope`. A UI that knows the project offers those fields to choose from; to
152
- * everything here, and to a UI that does not, it is a string.
147
+ * everything here, and to a UI that does not, it is a string. Its optional
148
+ * `role` names a lexicon role ("gloss") or, at Sentence scope, a sentence
149
+ * field's role ("translation", "literalTranslation", "note"): a UI that knows
150
+ * the project fills the parameter with the one field at `scope` whose stored
151
+ * Lexicon field holds that role, or whose stored role it is. A field is never
152
+ * chosen by its name.
153
153
  *
154
154
  * A parameter's `key` is a string VALUE, so it passes over the wire verbatim;
155
155
  * the UI sends `{ [param.key]: value }` in the request data. Declare each `key`
@@ -265,6 +265,12 @@ export type ServiceParam = {
265
265
  * `field` value is a string everywhere else.
266
266
  */
267
267
  scope?: "Sentence" | "Word" | "Morpheme";
268
+ /**
269
+ * `field` only: the lexicon role whose paired field,
270
+ * or the sentence role whose field, fills the parameter when nothing else
271
+ * does.
272
+ */
273
+ role?: string;
268
274
  };
269
275
  /**
270
276
  * A service's standardized self-description (lives in `extras`).
@@ -23,13 +23,18 @@
23
23
  * options?: [{value, label}], // enum / multiselect
24
24
  * min?, max?, step?, slider?, // number (slider: drag, don't type)
25
25
  * placeholder?, multiline?, // string
26
- * scope? } // field: 'Sentence' | 'Word' | 'Morpheme'
26
+ * scope?, role? } // field: 'Sentence' | 'Word' | 'Morpheme'
27
27
  * ]
28
28
  * }
29
29
  *
30
30
  * A `field` parameter names one of the project's annotation fields at its
31
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.
32
+ * everything here, and to a UI that does not, it is a string. Its optional
33
+ * `role` names a lexicon role ("gloss") or, at Sentence scope, a sentence
34
+ * field's role ("translation", "literalTranslation", "note"): a UI that knows
35
+ * the project fills the parameter with the one field at `scope` whose stored
36
+ * Lexicon field holds that role, or whose stored role it is. A field is never
37
+ * chosen by its name.
33
38
  *
34
39
  * A parameter's `key` is a string VALUE, so it passes over the wire verbatim;
35
40
  * the UI sends `{ [param.key]: value }` in the request data. Declare each `key`
@@ -117,6 +122,9 @@ export const TASKS = Object.freeze({
117
122
  * @property {"Sentence" | "Word" | "Morpheme"} [scope] `field` only: the
118
123
  * scope of the project's annotation fields the value names one of. A
119
124
  * `field` value is a string everywhere else.
125
+ * @property {string} [role] `field` only: the lexicon role whose paired field,
126
+ * or the sentence role whose field, fills the parameter when nothing else
127
+ * does.
120
128
  */
121
129
 
122
130
  /**
@@ -255,6 +263,20 @@ export function buildDefaultValues(schema) {
255
263
  * @param {Record<string, any>} raw current form values
256
264
  * @returns {{values: Record<string, any>, errors: Record<string, string>}}
257
265
  */
266
+ // A decimal number as a person types one: an optional sign, digits with an
267
+ // optional fraction (or a bare fraction), an optional exponent, with spaces
268
+ // around it allowed. ASCII whitespace only, as in Python (each language's \s
269
+ // holds characters the other's lacks).
270
+ const DECIMAL = /^[ \t\n\v\f\r]*[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?[ \t\n\v\f\r]*$/;
271
+
272
+ /** `v` as a finite number when it is one or a decimal string, else null. */
273
+ function numberOf(v) {
274
+ let n = null;
275
+ if (typeof v === "number") n = v;
276
+ else if (typeof v === "string" && DECIMAL.test(v)) n = Number(v);
277
+ return n !== null && Number.isFinite(n) ? n : null;
278
+ }
279
+
258
280
  export function coerceParamValues(schema, raw) {
259
281
  const values = {};
260
282
  const errors = {};
@@ -267,19 +289,12 @@ export function coerceParamValues(schema, raw) {
267
289
 
268
290
  switch (param.type) {
269
291
  case "number": {
270
- // Blank / nullish counts as "missing" → the param's default (matches the
271
- // Python client, where float('') raises and falls back to the default).
272
- let n;
273
- if (
274
- v === "" ||
275
- v == null ||
276
- (typeof v === "string" && v.trim() === "")
277
- ) {
278
- n = defaultForParam(param);
279
- } else {
280
- n = typeof v === "number" ? v : Number(v);
281
- if (Number.isNaN(n)) n = defaultForParam(param);
282
- }
292
+ // A finite number, or a string holding a decimal one; anything else
293
+ // (blank, a boolean, a list, hex, an underscore, Infinity, a value too
294
+ // large to be finite) is the param's default. The Python client's
295
+ // `coerce` reads the same rule.
296
+ let n = numberOf(v);
297
+ if (n === null) n = defaultForParam(param);
283
298
  if (typeof param.min === "number") n = Math.max(param.min, n);
284
299
  if (typeof param.max === "number") n = Math.min(param.max, n);
285
300
  v = n;
package/src/services.d.ts CHANGED
@@ -38,12 +38,15 @@ export function serviceErrorMessage(error: any, serviceName?: string, secrets?:
38
38
  * it, as the caller sees it: `runnerName` (the runner's display name),
39
39
  * `runByYou`, and `servesYou`, whether it would take the caller's requests on
40
40
  * this project. A delegating service serves members other than its runner
41
- * only where the runner is a maintainer or an admin. Goes over the wire even
42
- * while a batch is open on the client.
41
+ * only where the runner is a maintainer or an admin. Every entry whose id an
42
+ * account holds gives its display name in `claimedByName`. An admin also gets
43
+ * the account in `claimedBy` (see `releaseService`), and an entry with
44
+ * `seen: false` for each held id whose registration was forgotten. Goes over
45
+ * the wire even while a batch is open on the client.
43
46
  *
44
47
  * @param {Object} client - PlaidClient instance
45
48
  * @param {string} projectId - Project UUID
46
- * @returns {Promise<Array>} [{serviceId, serviceName, description, extras, online, lastSeenAt, runnerName, runByYou, servesYou}]
49
+ * @returns {Promise<Array>} [{serviceId, serviceName, description, extras, online, lastSeenAt, runnerName, runByYou, servesYou, claimedByName, claimedBy, seen}]
47
50
  */
48
51
  export function discoverServices(client: any, projectId: string): Promise<any[]>;
49
52
  /**
@@ -57,6 +60,20 @@ export function discoverServices(client: any, projectId: string): Promise<any[]>
57
60
  * @returns {Promise<void>}
58
61
  */
59
62
  export function discardService(client: any, projectId: string, serviceId: string): Promise<void>;
63
+ /**
64
+ * Release a service id from the account that first registered it on the
65
+ * project. A service id belongs to that account, and the server refuses any
66
+ * other account's registration (403) until an admin releases it. The next
67
+ * account to register it then claims it. Deactivating the account releases
68
+ * every id it holds. Admin-only; 409 while the service is connected, 404 when
69
+ * no account holds it.
70
+ *
71
+ * @param {Object} client - PlaidClient instance
72
+ * @param {string} projectId - Project UUID
73
+ * @param {string} serviceId - Service ID to release
74
+ * @returns {Promise<void>}
75
+ */
76
+ export function releaseService(client: any, projectId: string, serviceId: string): Promise<void>;
60
77
  /**
61
78
  * POST a progress, result or error event for an in-flight request; the server
62
79
  * relays it to the waiting requester. The Python twin is `_report_event`.
package/src/services.js CHANGED
@@ -37,9 +37,12 @@ export const UNKNOWN_OUTCOME =
37
37
  /** The server was never reached, so nothing was sent. */
38
38
  export const UNREACHABLE = 'The Plaid server could not be reached.';
39
39
 
40
- // A few reads travel as a POST, and saved nothing either way (the Python twin
41
- // and plaid-ui's errors.js keep the same list).
42
- const READ_POSTS = /\/api\/v1\/(?:query|login|invites\/lookup)(?:[?#]|$)/;
40
+ // A few reads travel as a POST (a query, a token count, a layer rule check,
41
+ // the values a rule would refuse, signing in, looking an invite up), and saved
42
+ // nothing either way. The Python twin and plaid-ui's errors.js keep the same
43
+ // list (readPosts.test.js holds the three equal).
44
+ const READ_POSTS =
45
+ /\/api\/v1\/(?:query|query\/token-counts|login|invites\/lookup|(?:token|span|relation)-layers\/[^/?#]+\/constraints\/(?:check|refused-values))(?:[?#]|$)/;
43
46
 
44
47
  // Node's fetch (undici) names a connection that never opened by its code: a
45
48
  // refused port, a name that did not resolve, a connect timeout. Nothing was
@@ -136,12 +139,15 @@ export function serviceErrorMessage(error, serviceName = '', secrets = []) {
136
139
  * it, as the caller sees it: `runnerName` (the runner's display name),
137
140
  * `runByYou`, and `servesYou`, whether it would take the caller's requests on
138
141
  * this project. A delegating service serves members other than its runner
139
- * only where the runner is a maintainer or an admin. Goes over the wire even
140
- * while a batch is open on the client.
142
+ * only where the runner is a maintainer or an admin. Every entry whose id an
143
+ * account holds gives its display name in `claimedByName`. An admin also gets
144
+ * the account in `claimedBy` (see `releaseService`), and an entry with
145
+ * `seen: false` for each held id whose registration was forgotten. Goes over
146
+ * the wire even while a batch is open on the client.
141
147
  *
142
148
  * @param {Object} client - PlaidClient instance
143
149
  * @param {string} projectId - Project UUID
144
- * @returns {Promise<Array>} [{serviceId, serviceName, description, extras, online, lastSeenAt, runnerName, runByYou, servesYou}]
150
+ * @returns {Promise<Array>} [{serviceId, serviceName, description, extras, online, lastSeenAt, runnerName, runByYou, servesYou, claimedByName, claimedBy, seen}]
145
151
  */
146
152
  export function discoverServices(client, projectId) {
147
153
  return client._request('GET', `/api/v1/projects/${projectId}/services`);
@@ -165,6 +171,27 @@ export function discardService(client, projectId, serviceId) {
165
171
  });
166
172
  }
167
173
 
174
+ /**
175
+ * Release a service id from the account that first registered it on the
176
+ * project. A service id belongs to that account, and the server refuses any
177
+ * other account's registration (403) until an admin releases it. The next
178
+ * account to register it then claims it. Deactivating the account releases
179
+ * every id it holds. Admin-only; 409 while the service is connected, 404 when
180
+ * no account holds it.
181
+ *
182
+ * @param {Object} client - PlaidClient instance
183
+ * @param {string} projectId - Project UUID
184
+ * @param {string} serviceId - Service ID to release
185
+ * @returns {Promise<void>}
186
+ */
187
+ export function releaseService(client, projectId, serviceId) {
188
+ // outOfBand: the registry is not project data (see the note at the top of
189
+ // http.js).
190
+ return client._request('DELETE', `/api/v1/projects/${projectId}/services/${encodeURIComponent(serviceId)}/owner`, {
191
+ outOfBand: true,
192
+ });
193
+ }
194
+
168
195
  /**
169
196
  * POST a progress, result or error event for an in-flight request; the server
170
197
  * relays it to the waiting requester. The Python twin is `_report_event`.
@@ -430,7 +457,14 @@ export function serve(client, projectId, serviceInfo, onServiceRequest, extras =
430
457
  // What reaches the requester is the sanitized half: the raw message
431
458
  // names the endpoint the client called, which is the operator's
432
459
  // business. The console keeps the whole error.
433
- console.error(`Service ${serviceId} failed request ${requestId}:`, error);
460
+ // Somebody else holding the document is a refusal a service expects,
461
+ // which the lock acquire words itself (from the server's 423): one
462
+ // line, not the error and its cause.
463
+ if (error?.status === 423) {
464
+ console.error(`Service ${serviceId} failed request ${requestId}: ${error.message}`);
465
+ } else {
466
+ console.error(`Service ${serviceId} failed request ${requestId}:`, error);
467
+ }
434
468
  responseHelper.error(serviceErrorMessage(error, serviceName));
435
469
  }
436
470
  };
@@ -468,7 +502,10 @@ export function serve(client, projectId, serviceInfo, onServiceRequest, extras =
468
502
  if (PERMANENT_STATUSES.has(status)) {
469
503
  noteDisconnected();
470
504
  serviceRegistration.stop();
471
- report('stopped', `HTTP ${status}`);
505
+ // The server's reason when it gave one (a service id another
506
+ // account holds says whose it is).
507
+ const why = connection.error?.responseData?.error;
508
+ report('stopped', typeof why === 'string' ? `HTTP ${status}: ${why}` : `HTTP ${status}`);
472
509
  return;
473
510
  }
474
511
  noteDisconnected();
package/src/sse.js CHANGED
@@ -145,8 +145,17 @@ export function createSSEConnection(client, projectId, onEvent, path) {
145
145
  // takes over.
146
146
  console.warn('Service registration rejected (409): another instance of this service is already connected; will retry');
147
147
  }
148
- const refused = new Error(`HTTP ${response.status} ${response.statusText}`);
148
+ // A refusal says why in its body (`{error}`), as on every other route.
149
+ let said = null;
150
+ try {
151
+ said = await response.json();
152
+ } catch {
153
+ said = null;
154
+ }
155
+ const why = typeof said?.error === 'string' ? `: ${said.error}` : '';
156
+ const refused = new Error(`HTTP ${response.status} ${response.statusText}${why}`);
149
157
  refused.status = response.status;
158
+ if (said && typeof said === 'object') refused.responseData = said;
150
159
  throw refused;
151
160
  }
152
161
 
@@ -0,0 +1,128 @@
1
+ /**
2
+ * A request as `answer` is asked it: the bundle call that made it (`kind` and
3
+ * `args`), and the request as the server reads it.
4
+ * @typedef {{kind?: string, args: any[], method: string, path: string,
5
+ * query: Record<string, string>, body: any, batched: boolean}} FakeRequest
6
+ */
7
+ /**
8
+ * A round trip as `requests` holds it and `onRequest` hears it: a request,
9
+ * or a batch request with its `operations`.
10
+ * @typedef {{method: string, path: string, query: Record<string, string>, body: any,
11
+ * kind?: string, args?: any[], operations?: object[]}} RecordedRequest
12
+ */
13
+ /**
14
+ * An error as the client throws it for a refusal: `status`, and the
15
+ * server's answer as `responseData`.
16
+ * @param {number} status
17
+ * @param {object} [responseData]
18
+ * @returns {Error & {status: number, responseData: object}}
19
+ */
20
+ export function refusal(status: number, responseData?: object): Error & {
21
+ status: number;
22
+ responseData: object;
23
+ };
24
+ /**
25
+ * What a request is answered with when `answer` gives nothing: the id a
26
+ * create minted (or a new one), the ids of a bulk create, an empty page for a
27
+ * read, and `{}` for anything else.
28
+ * @param {{kind?: string, method: string, body?: any}} request
29
+ * @returns {object}
30
+ */
31
+ export function defaultAnswer({ kind, method, body }: {
32
+ kind?: string;
33
+ method: string;
34
+ body?: any;
35
+ }): object;
36
+ /**
37
+ * A real PlaidClient on a recording transport (see the module's note).
38
+ *
39
+ * `answer(request)` answers each request, `request` being `{kind, args,
40
+ * method, path, query, body, batched}`: the bundle call that made it (`kind`
41
+ * and `args`, any batch refs in them filled in, undefined for a request no
42
+ * bundle call made), and the request as the server reads it. It returns the
43
+ * answer as the client hands it back (camelCase), or a promise of it, or
44
+ * undefined for `defaultAnswer`'s. To refuse, it throws (see `refusal`). A
45
+ * refusal of any op of a batch refuses the whole batch, as the server's
46
+ * transaction does.
47
+ *
48
+ * @param {{answer?: (request: FakeRequest) => any,
49
+ * onRequest?: (request: RecordedRequest) => void, baseUrl?: string}} [options]
50
+ * @returns {PlaidClient & {calls: Array<{kind: string, args: any[]}>,
51
+ * requests: RecordedRequest[], operations: Array<{message: string, kind?: string, ref?: string}>}}
52
+ */
53
+ export function fakeClient({ answer, onRequest, baseUrl, }?: {
54
+ answer?: (request: FakeRequest) => any;
55
+ onRequest?: (request: RecordedRequest) => void;
56
+ baseUrl?: string;
57
+ }): PlaidClient & {
58
+ calls: Array<{
59
+ kind: string;
60
+ args: any[];
61
+ }>;
62
+ requests: RecordedRequest[];
63
+ operations: Array<{
64
+ message: string;
65
+ kind?: string;
66
+ ref?: string;
67
+ }>;
68
+ };
69
+ /**
70
+ * Runs `f` and gives its value and the milliseconds it took. With `warm`,
71
+ * runs it that many times first, untimed, so the engine has compiled it.
72
+ * With `runs`, times that many runs and keeps the quickest, which a pause for
73
+ * garbage collection or another process touches least. Only for an `f` that
74
+ * can run again.
75
+ * @template T
76
+ * @param {() => T} f
77
+ * @param {{warm?: number, runs?: number}} [options]
78
+ * @returns {[T, number]}
79
+ */
80
+ export function timed<T>(f: () => T, { warm, runs }?: {
81
+ warm?: number;
82
+ runs?: number;
83
+ }): [T, number];
84
+ /**
85
+ * Throws unless `bigMs` is under `factor` times `smallMs`, two cases timed on
86
+ * the same machine in the same run. A loaded runner slows both, so the bound
87
+ * holds where a fixed number of milliseconds fails. Pick the cases so that
88
+ * the fault the test guards against grows the ratio well past `factor`: a
89
+ * scan of every row per row goes from 10 to 100 when the big case is ten
90
+ * times the small one. `smallMs` counts as at least `floorMs` (default 1),
91
+ * so a case too quick to time cannot make the bound nothing.
92
+ * @param {number} factor
93
+ * @param {number} smallMs
94
+ * @param {number} bigMs
95
+ * @param {{floorMs?: number, label?: string}} [options]
96
+ * @returns {number} `bigMs`
97
+ */
98
+ export function withinFactorOf(factor: number, smallMs: number, bigMs: number, { floorMs, label }?: {
99
+ floorMs?: number;
100
+ label?: string;
101
+ }): number;
102
+ /**
103
+ * A request as `answer` is asked it: the bundle call that made it (`kind` and
104
+ * `args`), and the request as the server reads it.
105
+ */
106
+ export type FakeRequest = {
107
+ kind?: string;
108
+ args: any[];
109
+ method: string;
110
+ path: string;
111
+ query: Record<string, string>;
112
+ body: any;
113
+ batched: boolean;
114
+ };
115
+ /**
116
+ * A round trip as `requests` holds it and `onRequest` hears it: a request,
117
+ * or a batch request with its `operations`.
118
+ */
119
+ export type RecordedRequest = {
120
+ method: string;
121
+ path: string;
122
+ query: Record<string, string>;
123
+ body: any;
124
+ kind?: string;
125
+ args?: any[];
126
+ operations?: object[];
127
+ };
128
+ import { PlaidClient } from "./client.js";