@larc-iu/plaid-client 0.0.0 → 0.1.0-alpha.10
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 +111 -2
- package/package.json +1 -1
- package/src/codepoint.js +73 -0
- package/src/index.js +135 -23
- package/src/roles.js +57 -0
- package/src/serviceSchema.js +206 -0
- package/src/transforms.js +1 -1
package/index.d.ts
CHANGED
|
@@ -4,18 +4,56 @@ interface Page<T = any> {
|
|
|
4
4
|
nextCursor: string | null;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
/** One choice for an `enum` / `multiselect` service parameter. */
|
|
8
|
+
interface ServiceParamOption {
|
|
9
|
+
value: string;
|
|
10
|
+
label: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A single user-controllable argument a service advertises. */
|
|
14
|
+
interface ServiceParam {
|
|
15
|
+
/** Key the value is sent under in the request payload. */
|
|
16
|
+
key: string;
|
|
17
|
+
label: string;
|
|
18
|
+
type: 'string' | 'number' | 'boolean' | 'enum' | 'multiselect';
|
|
19
|
+
description?: string;
|
|
20
|
+
default?: any;
|
|
21
|
+
required?: boolean;
|
|
22
|
+
/** Required for `enum` / `multiselect`. */
|
|
23
|
+
options?: ServiceParamOption[];
|
|
24
|
+
/** `number` only. */
|
|
25
|
+
min?: number;
|
|
26
|
+
max?: number;
|
|
27
|
+
step?: number;
|
|
28
|
+
/** `string` only. */
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
multiline?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A service's standardized self-description (lives in `extras`). */
|
|
34
|
+
interface ServiceExtras {
|
|
35
|
+
schemaVersion?: number;
|
|
36
|
+
/** Tasks this service serves; from the TASKS vocabulary. */
|
|
37
|
+
tasks?: string[];
|
|
38
|
+
/** Rich human description (markdown), beyond the short `description`. */
|
|
39
|
+
summary?: string;
|
|
40
|
+
/** Ordered parameter schema, rendered into a form by the UI. */
|
|
41
|
+
parameters?: ServiceParam[];
|
|
42
|
+
[key: string]: any;
|
|
43
|
+
}
|
|
44
|
+
|
|
7
45
|
interface ServiceInfo {
|
|
8
46
|
serviceId: string;
|
|
9
47
|
serviceName: string;
|
|
10
48
|
description: string;
|
|
11
|
-
extras?:
|
|
49
|
+
extras?: ServiceExtras;
|
|
12
50
|
}
|
|
13
51
|
|
|
14
52
|
interface DiscoveredService {
|
|
15
53
|
serviceId: string;
|
|
16
54
|
serviceName: string;
|
|
17
55
|
description: string;
|
|
18
|
-
extras:
|
|
56
|
+
extras: ServiceExtras;
|
|
19
57
|
}
|
|
20
58
|
|
|
21
59
|
interface ServiceRegistration {
|
|
@@ -40,6 +78,7 @@ interface VocabLinksBundle {
|
|
|
40
78
|
create(vocabItem: string, tokens: any[], metadata?: any): Promise<any>;
|
|
41
79
|
setMetadata(id: string, body: any): Promise<any>;
|
|
42
80
|
deleteMetadata(id: string): Promise<any>;
|
|
81
|
+
patchMetadata(id: string, body: any): Promise<any>;
|
|
43
82
|
get(id: string, asOf?: string): Promise<any>;
|
|
44
83
|
delete(id: string): Promise<any>;
|
|
45
84
|
}
|
|
@@ -61,6 +100,7 @@ interface VocabLayersBundle {
|
|
|
61
100
|
interface RelationsBundle {
|
|
62
101
|
setMetadata(relationId: string, body: any): Promise<any>;
|
|
63
102
|
deleteMetadata(relationId: string): Promise<any>;
|
|
103
|
+
patchMetadata(relationId: string, body: any): Promise<any>;
|
|
64
104
|
setTarget(relationId: string, spanId: string): Promise<any>;
|
|
65
105
|
get(relationId: string, asOf?: string): Promise<any>;
|
|
66
106
|
delete(relationId: string): Promise<any>;
|
|
@@ -91,6 +131,7 @@ interface SpansBundle {
|
|
|
91
131
|
bulkDelete(body: any[]): Promise<any>;
|
|
92
132
|
setMetadata(spanId: string, body: any): Promise<any>;
|
|
93
133
|
deleteMetadata(spanId: string): Promise<any>;
|
|
134
|
+
patchMetadata(spanId: string, body: any): Promise<any>;
|
|
94
135
|
}
|
|
95
136
|
|
|
96
137
|
interface BatchBundle {
|
|
@@ -100,6 +141,7 @@ interface BatchBundle {
|
|
|
100
141
|
interface TextsBundle {
|
|
101
142
|
setMetadata(textId: string, body: any): Promise<any>;
|
|
102
143
|
deleteMetadata(textId: string): Promise<any>;
|
|
144
|
+
patchMetadata(textId: string, body: any): Promise<any>;
|
|
103
145
|
create(textLayerId: string, documentId: string, body: string, metadata?: any): Promise<any>;
|
|
104
146
|
get(textId: string, asOf?: string): Promise<any>;
|
|
105
147
|
delete(textId: string): Promise<any>;
|
|
@@ -144,6 +186,7 @@ interface DocumentsBundle {
|
|
|
144
186
|
deleteMedia(documentId: string): Promise<any>;
|
|
145
187
|
setMetadata(documentId: string, body: any): Promise<any>;
|
|
146
188
|
deleteMetadata(documentId: string): Promise<any>;
|
|
189
|
+
patchMetadata(documentId: string, body: any): Promise<any>;
|
|
147
190
|
audit(documentId: string, startTime?: string, endTime?: string, asOf?: string): Promise<any[]>;
|
|
148
191
|
get(documentId: string, includeBody?: boolean, asOf?: string): Promise<any>;
|
|
149
192
|
delete(documentId: string): Promise<any>;
|
|
@@ -198,6 +241,7 @@ interface TextLayersBundle {
|
|
|
198
241
|
interface VocabItemsBundle {
|
|
199
242
|
setMetadata(id: string, body: any): Promise<any>;
|
|
200
243
|
deleteMetadata(id: string): Promise<any>;
|
|
244
|
+
patchMetadata(id: string, body: any): Promise<any>;
|
|
201
245
|
create(vocabLayerId: string, form: string, metadata?: any): Promise<any>;
|
|
202
246
|
get(id: string, asOf?: string): Promise<any>;
|
|
203
247
|
delete(id: string): Promise<any>;
|
|
@@ -226,6 +270,7 @@ interface TokensBundle {
|
|
|
226
270
|
shift(tokenId: string, begin?: number, end?: number): Promise<any>;
|
|
227
271
|
setMetadata(tokenId: string, body: any): Promise<any>;
|
|
228
272
|
deleteMetadata(tokenId: string): Promise<any>;
|
|
273
|
+
patchMetadata(tokenId: string, body: any): Promise<any>;
|
|
229
274
|
}
|
|
230
275
|
|
|
231
276
|
interface PlaidClientOptions {
|
|
@@ -248,6 +293,9 @@ export declare class PlaidClient {
|
|
|
248
293
|
enterStrictMode(documentId: string): void;
|
|
249
294
|
exitStrictMode(): void;
|
|
250
295
|
|
|
296
|
+
// Query
|
|
297
|
+
query(body: any): Promise<any>;
|
|
298
|
+
|
|
251
299
|
vocabLinks: VocabLinksBundle;
|
|
252
300
|
vocabLayers: VocabLayersBundle;
|
|
253
301
|
relations: RelationsBundle;
|
|
@@ -268,3 +316,64 @@ export declare class PlaidClient {
|
|
|
268
316
|
}
|
|
269
317
|
|
|
270
318
|
export default PlaidClient;
|
|
319
|
+
|
|
320
|
+
// --- Unicode code-point helpers for text offsets ---------------------------
|
|
321
|
+
// Token begin/end offsets are 0-based Unicode code-point indices (not UTF-16).
|
|
322
|
+
/** Number of Unicode code points in `s` (not `s.length`). */
|
|
323
|
+
export function cpLength(s: string): number;
|
|
324
|
+
/** Substring of `s` by code-point indices [begin, end) (end optional). */
|
|
325
|
+
export function cpSlice(s: string, begin: number, end?: number): string;
|
|
326
|
+
/** Prebuilt slicer for many code-point slices of one string (spreads once). */
|
|
327
|
+
export function cpSlicer(s: string): (begin: number, end?: number) => string;
|
|
328
|
+
/** UTF-16 index -> code-point index in `s`. */
|
|
329
|
+
export function utf16ToCp(s: string, u: number): number;
|
|
330
|
+
/** Code-point index -> UTF-16 index in `s` (clamps past the end). */
|
|
331
|
+
export function cpToUtf16(s: string, cp: number): number;
|
|
332
|
+
/** Like indexOf, but the result and `fromCp` are code-point indices; -1 if absent. */
|
|
333
|
+
export function cpIndexOf(s: string, sub: string, fromCp?: number): number;
|
|
334
|
+
|
|
335
|
+
// --- Shared layer-role vocabulary (cross-app interoperability) --------------
|
|
336
|
+
// Substrate layers are tagged with a role at `config.plaid.role` (a scalar) so
|
|
337
|
+
// that different apps can share a project. See the manual, "Layer Interoperability".
|
|
338
|
+
/** The reserved config namespace for cross-app conventions. */
|
|
339
|
+
export const PLAID_NAMESPACE: 'plaid';
|
|
340
|
+
/** The config key, under `plaid`, holding a layer's role. */
|
|
341
|
+
export const ROLE_KEY: 'role';
|
|
342
|
+
/** The fixed role inventory; only these values are interoperable across apps. */
|
|
343
|
+
export const ROLES: {
|
|
344
|
+
readonly BASELINE: 'baseline';
|
|
345
|
+
readonly SENTENCE: 'sentence';
|
|
346
|
+
readonly WORD: 'word';
|
|
347
|
+
readonly SYNTACTIC_WORD: 'syntactic-word';
|
|
348
|
+
readonly MORPHEME: 'morpheme';
|
|
349
|
+
readonly TIME_ALIGNMENT: 'time-alignment';
|
|
350
|
+
};
|
|
351
|
+
/** The role recorded on a layer's `config`, or null if none. */
|
|
352
|
+
export function readRole(config?: object): string | null;
|
|
353
|
+
/** The first layer in `layers` carrying the given role, or null. */
|
|
354
|
+
export function findByRole<T extends { config?: object }>(layers: T[] | undefined, role: string): T | null;
|
|
355
|
+
|
|
356
|
+
// --- Service self-description helpers ----------------------------------------
|
|
357
|
+
// Standardize how a service advertises (in `extras`) the tasks it serves, a
|
|
358
|
+
// summary, and a parameter schema — so a UI can offer service selection, an
|
|
359
|
+
// argument form, and a summary at a fixed integration point. See the manual,
|
|
360
|
+
// "Describing a service".
|
|
361
|
+
/** The controlled task vocabulary — the fixed integration-point goals. */
|
|
362
|
+
export const TASKS: {
|
|
363
|
+
readonly TOKENIZE: 'tokenize';
|
|
364
|
+
readonly PARSE: 'parse';
|
|
365
|
+
readonly TRANSCRIBE: 'transcribe';
|
|
366
|
+
readonly LINK_VOCAB: 'link-vocab';
|
|
367
|
+
};
|
|
368
|
+
/** Whether a service serves a task (declared `extras.tasks`, legacy id-prefix fallback). */
|
|
369
|
+
export function servesTask(service: DiscoveredService, task: string): boolean;
|
|
370
|
+
/** The discovered services that serve `task`. */
|
|
371
|
+
export function filterServicesByTask(services: DiscoveredService[] | undefined, task: string): DiscoveredService[];
|
|
372
|
+
/** The parameter schema a service declares (ordered), or []. */
|
|
373
|
+
export function getParamSchema(service: DiscoveredService): ServiceParam[];
|
|
374
|
+
/** A service's human summary: `extras.summary`, else `description`, else ''. */
|
|
375
|
+
export function getServiceSummary(service: DiscoveredService): string;
|
|
376
|
+
/** Default form values keyed by param key. */
|
|
377
|
+
export function buildDefaultValues(schema: ServiceParam[]): Record<string, any>;
|
|
378
|
+
/** Coerce/validate raw form values against the schema. */
|
|
379
|
+
export function coerceParamValues(schema: ServiceParam[], raw: Record<string, any>): { values: Record<string, any>; errors: Record<string, string> };
|
package/package.json
CHANGED
package/src/codepoint.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unicode code-point helpers for working with Plaid text offsets.
|
|
3
|
+
*
|
|
4
|
+
* Plaid token offsets (`begin` / `end`) are 0-based indices in Unicode CODE
|
|
5
|
+
* POINTS (begin inclusive, end exclusive) — NOT UTF-16 code units. JavaScript
|
|
6
|
+
* strings are UTF-16, so `.length`, `.slice`, `.substring`, `s[i]`,
|
|
7
|
+
* `String.prototype.indexOf`, and `Intl.Segmenter`'s `index` all count UTF-16
|
|
8
|
+
* code units, which disagree with code points for astral characters
|
|
9
|
+
* (>= U+10000 — emoji, and SMP scripts such as Gothic, cuneiform, CJK Ext-B).
|
|
10
|
+
*
|
|
11
|
+
* Use these to slice a text body by token offsets, and to compute offsets for
|
|
12
|
+
* new tokens, in code points. The spread/`for…of` string iterator yields code
|
|
13
|
+
* points, which is what makes this work.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Number of Unicode code points in `s` (not `s.length`, which is UTF-16). */
|
|
17
|
+
export function cpLength(s) {
|
|
18
|
+
return [...s].length;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Substring of `s` by CODE-POINT indices [begin, end) (end optional = to end).
|
|
23
|
+
* Mirrors `String.prototype.slice` semantics but in code points.
|
|
24
|
+
*/
|
|
25
|
+
export function cpSlice(s, begin, end) {
|
|
26
|
+
return [...s].slice(begin, end).join('');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Prebuilt slicer for taking MANY code-point slices of the same string:
|
|
31
|
+
* spreads `s` into code points once, then each slice costs O(slice length).
|
|
32
|
+
* `cpSlice` spreads the whole string per call, which turns quadratic when a
|
|
33
|
+
* caller slices every token of a large text. Mirror of the server's
|
|
34
|
+
* `plaid.util.codepoint/cp-slicer`.
|
|
35
|
+
*/
|
|
36
|
+
export function cpSlicer(s) {
|
|
37
|
+
const chars = [...(s ?? '')];
|
|
38
|
+
return (begin, end) => chars.slice(begin, end).join('');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert a UTF-16 index `u` into `s` to a code-point index — i.e. how many
|
|
43
|
+
* code points precede `u`. Inverse of `cpToUtf16`. Useful for converting a
|
|
44
|
+
* DOM/`indexOf`/`Intl.Segmenter` (UTF-16) position into a code-point offset.
|
|
45
|
+
*/
|
|
46
|
+
export function utf16ToCp(s, u) {
|
|
47
|
+
return [...s.slice(0, u)].length;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Convert a code-point index `cp` into `s` to a UTF-16 index. Clamps to
|
|
52
|
+
* `s.length` when `cp` is past the end. Inverse of `utf16ToCp`.
|
|
53
|
+
*/
|
|
54
|
+
export function cpToUtf16(s, cp) {
|
|
55
|
+
if (cp <= 0) return 0;
|
|
56
|
+
let u = 0;
|
|
57
|
+
let c = 0;
|
|
58
|
+
for (const ch of s) {
|
|
59
|
+
if (c >= cp) break;
|
|
60
|
+
u += ch.length; // 1 for BMP, 2 for an astral code point (surrogate pair)
|
|
61
|
+
c += 1;
|
|
62
|
+
}
|
|
63
|
+
return u;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Like `String.prototype.indexOf`, but the returned index and `fromCp` are
|
|
68
|
+
* CODE-POINT indices. Returns -1 when `sub` is not found.
|
|
69
|
+
*/
|
|
70
|
+
export function cpIndexOf(s, sub, fromCp = 0) {
|
|
71
|
+
const u = s.indexOf(sub, cpToUtf16(s, fromCp));
|
|
72
|
+
return u < 0 ? -1 : utf16ToCp(s, u);
|
|
73
|
+
}
|
package/src/index.js
CHANGED
|
@@ -71,6 +71,15 @@ class PlaidClient {
|
|
|
71
71
|
this._request('DELETE', `/api/v1/vocab-links/${id}/metadata`, {
|
|
72
72
|
skipResponseTransform: true,
|
|
73
73
|
}),
|
|
74
|
+
/**
|
|
75
|
+
* Patch (shallow-merge) metadata for a vocab link. Keys present in the body are set or overwritten; keys not present are left untouched; a key whose value is null is deleted. Merging is top-level only (nested objects are replaced wholesale, not deep-merged), so a literal null cannot be stored as a value. An empty body changes no metadata.
|
|
76
|
+
* @param {string} id - The resource ID
|
|
77
|
+
* @param {any} body - The metadata patch
|
|
78
|
+
*/
|
|
79
|
+
patchMetadata: (id, body) =>
|
|
80
|
+
this._request('PATCH', `/api/v1/vocab-links/${id}/metadata`, {
|
|
81
|
+
rawBody: body, skipResponseTransform: true,
|
|
82
|
+
}),
|
|
74
83
|
/**
|
|
75
84
|
* Get a vocab link by ID
|
|
76
85
|
* @param {string} id - The resource ID
|
|
@@ -205,6 +214,15 @@ class PlaidClient {
|
|
|
205
214
|
this._request('DELETE', `/api/v1/relations/${relationId}/metadata`, {
|
|
206
215
|
skipResponseTransform: true,
|
|
207
216
|
}),
|
|
217
|
+
/**
|
|
218
|
+
* Patch (shallow-merge) metadata for a relation. Keys present in the body are set or overwritten; keys not present are left untouched; a key whose value is null is deleted. Merging is top-level only (nested objects are replaced wholesale, not deep-merged), so a literal null cannot be stored as a value. An empty body changes no metadata.
|
|
219
|
+
* @param {string} relationId - The relation ID
|
|
220
|
+
* @param {any} body - The metadata patch
|
|
221
|
+
*/
|
|
222
|
+
patchMetadata: (relationId, body) =>
|
|
223
|
+
this._request('PATCH', `/api/v1/relations/${relationId}/metadata`, {
|
|
224
|
+
rawBody: body, skipResponseTransform: true,
|
|
225
|
+
}),
|
|
208
226
|
/**
|
|
209
227
|
* Update the target span of a relation.
|
|
210
228
|
* @param {string} relationId - The relation ID
|
|
@@ -416,6 +434,15 @@ class PlaidClient {
|
|
|
416
434
|
this._request('DELETE', `/api/v1/spans/${spanId}/metadata`, {
|
|
417
435
|
skipResponseTransform: true,
|
|
418
436
|
}),
|
|
437
|
+
/**
|
|
438
|
+
* Patch (shallow-merge) metadata for a span. Keys present in the body are set or overwritten; keys not present are left untouched; a key whose value is null is deleted. Merging is top-level only (nested objects are replaced wholesale, not deep-merged), so a literal null cannot be stored as a value. An empty body changes no metadata.
|
|
439
|
+
* @param {string} spanId - The span ID
|
|
440
|
+
* @param {any} body - The metadata patch
|
|
441
|
+
*/
|
|
442
|
+
patchMetadata: (spanId, body) =>
|
|
443
|
+
this._request('PATCH', `/api/v1/spans/${spanId}/metadata`, {
|
|
444
|
+
rawBody: body, skipResponseTransform: true,
|
|
445
|
+
}),
|
|
419
446
|
};
|
|
420
447
|
|
|
421
448
|
this.batch = {
|
|
@@ -448,6 +475,15 @@ class PlaidClient {
|
|
|
448
475
|
this._request('DELETE', `/api/v1/texts/${textId}/metadata`, {
|
|
449
476
|
skipResponseTransform: true,
|
|
450
477
|
}),
|
|
478
|
+
/**
|
|
479
|
+
* Patch (shallow-merge) metadata for a text. Keys present in the body are set or overwritten; keys not present are left untouched; a key whose value is null is deleted. Merging is top-level only (nested objects are replaced wholesale, not deep-merged), so a literal null cannot be stored as a value. An empty body changes no metadata.
|
|
480
|
+
* @param {string} textId - The text ID
|
|
481
|
+
* @param {any} body - The metadata patch
|
|
482
|
+
*/
|
|
483
|
+
patchMetadata: (textId, body) =>
|
|
484
|
+
this._request('PATCH', `/api/v1/texts/${textId}/metadata`, {
|
|
485
|
+
rawBody: body, skipResponseTransform: true,
|
|
486
|
+
}),
|
|
451
487
|
/**
|
|
452
488
|
* Create a new text in a document's text layer. A text is a container for
|
|
453
489
|
* one long string in `body` for a given layer.
|
|
@@ -490,33 +526,37 @@ class PlaidClient {
|
|
|
490
526
|
|
|
491
527
|
this.users = {
|
|
492
528
|
/**
|
|
493
|
-
* List
|
|
494
|
-
* the full flat array.
|
|
529
|
+
* List (or search) users. Transparently follows pagination cursors and
|
|
530
|
+
* returns the full flat array. Admin-or-maintainer only.
|
|
495
531
|
* Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
|
|
496
|
-
* @param {
|
|
532
|
+
* @param {object} [opts]
|
|
533
|
+
* @param {string} [opts.q] - Filter to usernames containing this text (case-insensitive)
|
|
534
|
+
* @param {string} [opts.asOf] - Temporal query timestamp
|
|
497
535
|
*/
|
|
498
|
-
list: (asOf) =>
|
|
499
|
-
listAll(this, '/api/v1/users', { query: { 'as-of': asOf } }),
|
|
536
|
+
list: ({ q, asOf } = {}) =>
|
|
537
|
+
listAll(this, '/api/v1/users', { query: { q, 'as-of': asOf } }),
|
|
500
538
|
/**
|
|
501
|
-
* Fetch a single page of users.
|
|
539
|
+
* Fetch a single page of users (optionally filtered by `q`).
|
|
502
540
|
* @param {object} [opts]
|
|
541
|
+
* @param {string} [opts.q] - Filter to usernames containing this text (case-insensitive)
|
|
503
542
|
* @param {number} [opts.limit] - Page size (1..1000; server default 100)
|
|
504
543
|
* @param {string} [opts.cursor] - Opaque cursor from a previous page
|
|
505
544
|
* @param {string} [opts.asOf] - Temporal query timestamp
|
|
506
545
|
* @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
|
|
507
546
|
*/
|
|
508
|
-
listPage: ({ limit, cursor, asOf } = {}) =>
|
|
509
|
-
listPage(this, '/api/v1/users', { limit, cursor, query: { 'as-of': asOf } }),
|
|
547
|
+
listPage: ({ q, limit, cursor, asOf } = {}) =>
|
|
548
|
+
listPage(this, '/api/v1/users', { limit, cursor, query: { q, 'as-of': asOf } }),
|
|
510
549
|
/**
|
|
511
550
|
* Async-iterate users page by page; yields each page's entries array.
|
|
512
551
|
* @param {object} [opts]
|
|
552
|
+
* @param {string} [opts.q] - Filter to usernames containing this text (case-insensitive)
|
|
513
553
|
* @param {number} [opts.pageSize] - Per-request page size
|
|
514
554
|
* @param {string} [opts.asOf] - Temporal query timestamp
|
|
515
555
|
* Cannot be used inside a batch (auto-paginates across requests); throws on first iteration if called while batching — use listPage() for a single page in a batch.
|
|
516
556
|
* @returns {AsyncGenerator<Array>}
|
|
517
557
|
*/
|
|
518
|
-
iterPages: ({ pageSize, asOf } = {}) =>
|
|
519
|
-
iterPages(this, '/api/v1/users', { pageSize, query: { 'as-of': asOf } }),
|
|
558
|
+
iterPages: ({ q, pageSize, asOf } = {}) =>
|
|
559
|
+
iterPages(this, '/api/v1/users', { pageSize, query: { q, 'as-of': asOf } }),
|
|
520
560
|
/**
|
|
521
561
|
* Create a new user
|
|
522
562
|
* @param {string} username - The username
|
|
@@ -550,11 +590,23 @@ class PlaidClient {
|
|
|
550
590
|
queryParams: { 'as-of': asOf },
|
|
551
591
|
}),
|
|
552
592
|
/**
|
|
553
|
-
*
|
|
593
|
+
* Deactivate a user. Users are never hard-deleted: deactivation
|
|
594
|
+
* rejects their logins and tokens, strips their project memberships
|
|
595
|
+
* and vocab maintainerships, and revokes their API tokens. The user
|
|
596
|
+
* stays visible in listings with a deactivated-at timestamp.
|
|
597
|
+
* Reversible via activate(), which restores login only.
|
|
554
598
|
* @param {string} id - The resource ID
|
|
555
599
|
*/
|
|
556
600
|
delete: (id) =>
|
|
557
601
|
this._request('DELETE', `/api/v1/users/${id}`),
|
|
602
|
+
/**
|
|
603
|
+
* Reactivate a deactivated user, restoring their ability to log in.
|
|
604
|
+
* Project memberships, vocab maintainerships, and API tokens removed
|
|
605
|
+
* at deactivation are NOT restored — re-grant them deliberately.
|
|
606
|
+
* @param {string} id - The resource ID
|
|
607
|
+
*/
|
|
608
|
+
activate: (id) =>
|
|
609
|
+
this._request('POST', `/api/v1/users/${id}/activate`),
|
|
558
610
|
/**
|
|
559
611
|
* Modify a user. Admins may change the username, password, and admin
|
|
560
612
|
* status of any user. All other users may only modify their own username
|
|
@@ -760,6 +812,15 @@ class PlaidClient {
|
|
|
760
812
|
this._request('DELETE', `/api/v1/documents/${documentId}/metadata`, {
|
|
761
813
|
skipResponseTransform: true,
|
|
762
814
|
}),
|
|
815
|
+
/**
|
|
816
|
+
* Patch (shallow-merge) metadata for a document. Keys present in the body are set or overwritten; keys not present are left untouched; a key whose value is null is deleted. Merging is top-level only (nested objects are replaced wholesale, not deep-merged), so a literal null cannot be stored as a value. An empty body changes no metadata.
|
|
817
|
+
* @param {string} documentId - The document ID
|
|
818
|
+
* @param {any} body - The metadata patch
|
|
819
|
+
*/
|
|
820
|
+
patchMetadata: (documentId, body) =>
|
|
821
|
+
this._request('PATCH', `/api/v1/documents/${documentId}/metadata`, {
|
|
822
|
+
rawBody: body, skipResponseTransform: true,
|
|
823
|
+
}),
|
|
763
824
|
/**
|
|
764
825
|
* Get audit log for a document. Transparently follows pagination cursors
|
|
765
826
|
* and returns the full flat array.
|
|
@@ -1086,6 +1147,15 @@ class PlaidClient {
|
|
|
1086
1147
|
this._request('DELETE', `/api/v1/vocab-items/${id}/metadata`, {
|
|
1087
1148
|
skipResponseTransform: true,
|
|
1088
1149
|
}),
|
|
1150
|
+
/**
|
|
1151
|
+
* Patch (shallow-merge) metadata for a vocab item. Keys present in the body are set or overwritten; keys not present are left untouched; a key whose value is null is deleted. Merging is top-level only (nested objects are replaced wholesale, not deep-merged), so a literal null cannot be stored as a value. An empty body changes no metadata.
|
|
1152
|
+
* @param {string} id - The resource ID
|
|
1153
|
+
* @param {any} body - The metadata patch
|
|
1154
|
+
*/
|
|
1155
|
+
patchMetadata: (id, body) =>
|
|
1156
|
+
this._request('PATCH', `/api/v1/vocab-items/${id}/metadata`, {
|
|
1157
|
+
rawBody: body, skipResponseTransform: true,
|
|
1158
|
+
}),
|
|
1089
1159
|
/**
|
|
1090
1160
|
* Create a new vocab item
|
|
1091
1161
|
* @param {string} vocabLayerId - The vocab layer ID
|
|
@@ -1194,10 +1264,16 @@ class PlaidClient {
|
|
|
1194
1264
|
* using begin and end offsets. Tokens may be zero-width and may overlap.
|
|
1195
1265
|
* For tokens sharing the same begin, precedence controls the linear
|
|
1196
1266
|
* ordering.
|
|
1267
|
+
*
|
|
1268
|
+
* Offsets are 0-based indices in Unicode CODE POINTS (not UTF-16 code
|
|
1269
|
+
* units): a supplementary-plane character (emoji, SMP script) is one
|
|
1270
|
+
* position. JS strings are UTF-16, so do NOT use `str.length` /
|
|
1271
|
+
* `str.substring` to compute offsets — count code points instead
|
|
1272
|
+
* (e.g. `[...str].length`, or iterate with `codePointAt`).
|
|
1197
1273
|
* @param {string} tokenLayerId - The token layer ID
|
|
1198
1274
|
* @param {string} text - The text ID
|
|
1199
|
-
* @param {number} begin - Start offset (
|
|
1200
|
-
* @param {number} end - End offset (
|
|
1275
|
+
* @param {number} begin - Start offset, inclusive (Unicode code points)
|
|
1276
|
+
* @param {number} end - End offset, exclusive (Unicode code points)
|
|
1201
1277
|
* @param {number} [precedence] - Ordering precedence
|
|
1202
1278
|
* @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
|
|
1203
1279
|
*/
|
|
@@ -1224,8 +1300,8 @@ class PlaidClient {
|
|
|
1224
1300
|
/**
|
|
1225
1301
|
* Update a token.
|
|
1226
1302
|
* @param {string} tokenId - The token ID
|
|
1227
|
-
* @param {number} [begin] - New start offset
|
|
1228
|
-
* @param {number} [end] - New end offset
|
|
1303
|
+
* @param {number} [begin] - New start offset, inclusive (Unicode code points)
|
|
1304
|
+
* @param {number} [end] - New end offset, exclusive (Unicode code points)
|
|
1229
1305
|
* @param {?number} [precedence] - Ordering precedence. Omit (undefined)
|
|
1230
1306
|
* to leave unchanged; pass a number to set; pass null explicitly to
|
|
1231
1307
|
* CLEAR it (revert to no explicit ordering). bodyOf keeps null but
|
|
@@ -1248,10 +1324,10 @@ class PlaidClient {
|
|
|
1248
1324
|
bulkDelete: (body) =>
|
|
1249
1325
|
this._request('DELETE', '/api/v1/tokens/bulk', { body }),
|
|
1250
1326
|
/**
|
|
1251
|
-
* Split a token at a
|
|
1252
|
-
* (keeps its ID, spans, vocab-links); the new right token's ID is returned.
|
|
1327
|
+
* Split a token at a Unicode code-point offset. The original token becomes the
|
|
1328
|
+
* left half (keeps its ID, spans, vocab-links); the new right token's ID is returned.
|
|
1253
1329
|
* @param {string} tokenId - The token ID
|
|
1254
|
-
* @param {number} position -
|
|
1330
|
+
* @param {number} position - Code-point offset to split at (strictly between begin and end)
|
|
1255
1331
|
*/
|
|
1256
1332
|
split: (tokenId, position) =>
|
|
1257
1333
|
this._request('POST', `/api/v1/tokens/${tokenId}/split`, {
|
|
@@ -1274,8 +1350,8 @@ class PlaidClient {
|
|
|
1274
1350
|
* auto-adjusted to preserve the partition; on non-overlapping layers a shift that
|
|
1275
1351
|
* would create an overlap is rejected.
|
|
1276
1352
|
* @param {string} tokenId - The token ID
|
|
1277
|
-
* @param {number} [begin] - New start offset
|
|
1278
|
-
* @param {number} [end] - New end offset
|
|
1353
|
+
* @param {number} [begin] - New start offset, inclusive (Unicode code points)
|
|
1354
|
+
* @param {number} [end] - New end offset, exclusive (Unicode code points)
|
|
1279
1355
|
*/
|
|
1280
1356
|
shift: (tokenId, begin, end) =>
|
|
1281
1357
|
this._request('POST', `/api/v1/tokens/${tokenId}/shift`, {
|
|
@@ -1298,6 +1374,15 @@ class PlaidClient {
|
|
|
1298
1374
|
this._request('DELETE', `/api/v1/tokens/${tokenId}/metadata`, {
|
|
1299
1375
|
skipResponseTransform: true,
|
|
1300
1376
|
}),
|
|
1377
|
+
/**
|
|
1378
|
+
* Patch (shallow-merge) metadata for a token. Keys present in the body are set or overwritten; keys not present are left untouched; a key whose value is null is deleted. Merging is top-level only (nested objects are replaced wholesale, not deep-merged), so a literal null cannot be stored as a value. An empty body changes no metadata.
|
|
1379
|
+
* @param {string} tokenId - The token ID
|
|
1380
|
+
* @param {any} body - The metadata patch
|
|
1381
|
+
*/
|
|
1382
|
+
patchMetadata: (tokenId, body) =>
|
|
1383
|
+
this._request('PATCH', `/api/v1/tokens/${tokenId}/metadata`, {
|
|
1384
|
+
rawBody: body, skipResponseTransform: true,
|
|
1385
|
+
}),
|
|
1301
1386
|
};
|
|
1302
1387
|
|
|
1303
1388
|
this.messages = {
|
|
@@ -1366,8 +1451,8 @@ class PlaidClient {
|
|
|
1366
1451
|
* await client.query({
|
|
1367
1452
|
* find: ['?s1', '?s2'],
|
|
1368
1453
|
* where: [
|
|
1369
|
-
* ['span', '?s1', { layer:
|
|
1370
|
-
* ['span', '?s2', { layer:
|
|
1454
|
+
* ['span', '?s1', { layer: posLayerId, value: 'NOUN' }],
|
|
1455
|
+
* ['span', '?s2', { layer: posLayerId, value: 'VERB' }],
|
|
1371
1456
|
* ['covers', '?s1', '?t1'], ['covers', '?s2', '?t2'],
|
|
1372
1457
|
* ['precedes', '?t1', '?t2'],
|
|
1373
1458
|
* ],
|
|
@@ -1375,7 +1460,17 @@ class PlaidClient {
|
|
|
1375
1460
|
* limit: 100,
|
|
1376
1461
|
* });
|
|
1377
1462
|
*
|
|
1378
|
-
*
|
|
1463
|
+
* A `layer` is referenced by its id (its UUID) only — not by name or
|
|
1464
|
+
* path. To match a layer by name, bind it with a `*-layer` clause (e.g.
|
|
1465
|
+
* `['span-layer', '?sl', { name: 'pos' }]`) and use the variable.
|
|
1466
|
+
*
|
|
1467
|
+
* Optional keys: `scope` (restrict to projects by id, `{projectIds}`), `orderBy`
|
|
1468
|
+
* (sort rows), and `bindings` (substitute `?name` placeholders with literals).
|
|
1469
|
+
* `return` may also be an aggregate spec `{group, aggregates}`. See the query
|
|
1470
|
+
* language reference.
|
|
1471
|
+
*
|
|
1472
|
+
* @param {Object} body - The query AST ({find, where, scope?, limit?, orderBy?,
|
|
1473
|
+
* return?, bindings?}).
|
|
1379
1474
|
* @returns {Promise<Object>} For 'ids'/'entities': {columns, results, count, truncated}.
|
|
1380
1475
|
* For 'count': {return: 'count', count}. Entity cells are full entity objects
|
|
1381
1476
|
* (same shape as the GET endpoints).
|
|
@@ -1530,3 +1625,20 @@ class PlaidClient {
|
|
|
1530
1625
|
|
|
1531
1626
|
export default PlaidClient;
|
|
1532
1627
|
export { PlaidClient };
|
|
1628
|
+
|
|
1629
|
+
// Unicode code-point helpers for text offsets (token begin/end are code-point
|
|
1630
|
+
// indices). See ./codepoint.js.
|
|
1631
|
+
export { cpLength, cpSlice, cpSlicer, utf16ToCp, cpToUtf16, cpIndexOf } from './codepoint.js';
|
|
1632
|
+
export { PLAID_NAMESPACE, ROLE_KEY, ROLES, readRole, findByRole } from './roles.js';
|
|
1633
|
+
// Service self-description helpers: filter discovered services by task, read a
|
|
1634
|
+
// service's parameter schema/summary, and build/coerce form values. See
|
|
1635
|
+
// ./serviceSchema.js.
|
|
1636
|
+
export {
|
|
1637
|
+
TASKS,
|
|
1638
|
+
servesTask,
|
|
1639
|
+
filterServicesByTask,
|
|
1640
|
+
getParamSchema,
|
|
1641
|
+
getServiceSummary,
|
|
1642
|
+
buildDefaultValues,
|
|
1643
|
+
coerceParamValues,
|
|
1644
|
+
} from './serviceSchema.js';
|
package/src/roles.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared layer-role vocabulary for cross-app interoperability.
|
|
3
|
+
*
|
|
4
|
+
* Apps that share a Plaid project agree on the *substrate* — the text and token
|
|
5
|
+
* layers — by tagging each shared layer with a ROLE under the reserved `plaid`
|
|
6
|
+
* config namespace (`config.plaid.role`, a scalar). Annotations stay private to
|
|
7
|
+
* each app under that app's own namespace. See the Plaid manual, "Layer
|
|
8
|
+
* Interoperability". The role inventory is small and fixed:
|
|
9
|
+
*
|
|
10
|
+
* baseline the primary text layer
|
|
11
|
+
* sentence sentence token layer
|
|
12
|
+
* word orthographic-word token layer (CoNLL-U "token")
|
|
13
|
+
* syntactic-word grammatical words below the word (CoNLL-U "word" / MWT splits)
|
|
14
|
+
* morpheme morpheme token layer
|
|
15
|
+
* time-alignment media-timeline token layer
|
|
16
|
+
*
|
|
17
|
+
* Only these values are understood across apps; an app may store any string but
|
|
18
|
+
* loses interoperability for unknown values.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** The reserved config namespace for cross-app conventions. */
|
|
22
|
+
export const PLAID_NAMESPACE = 'plaid';
|
|
23
|
+
|
|
24
|
+
/** The config key, under `plaid`, holding a layer's role. */
|
|
25
|
+
export const ROLE_KEY = 'role';
|
|
26
|
+
|
|
27
|
+
/** The fixed role inventory. */
|
|
28
|
+
export const ROLES = Object.freeze({
|
|
29
|
+
BASELINE: 'baseline',
|
|
30
|
+
SENTENCE: 'sentence',
|
|
31
|
+
WORD: 'word',
|
|
32
|
+
SYNTACTIC_WORD: 'syntactic-word',
|
|
33
|
+
MORPHEME: 'morpheme',
|
|
34
|
+
TIME_ALIGNMENT: 'time-alignment',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The role recorded on a layer's `config`, or null if none.
|
|
39
|
+
* @param {object} [config] a layer's `config` object
|
|
40
|
+
* @returns {string|null}
|
|
41
|
+
*/
|
|
42
|
+
export function readRole(config) {
|
|
43
|
+
const v = config?.[PLAID_NAMESPACE]?.[ROLE_KEY];
|
|
44
|
+
return v == null ? null : v;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The first layer in `layers` carrying the given role, or null. The single
|
|
49
|
+
* "find a layer by its role" primitive — apps build their named finders
|
|
50
|
+
* (findWordTokenLayer, etc.) on top of this.
|
|
51
|
+
* @param {Array<{config?: object}>} [layers]
|
|
52
|
+
* @param {string} role
|
|
53
|
+
* @returns {object|null}
|
|
54
|
+
*/
|
|
55
|
+
export function findByRole(layers, role) {
|
|
56
|
+
return (layers || []).find(l => readRole(l?.config) === role) || null;
|
|
57
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service self-description: tasks, summary, and a parameter schema.
|
|
3
|
+
*
|
|
4
|
+
* The service framework's transport carries an opaque `extras` JSON map on every
|
|
5
|
+
* registered service (see services.js). This module standardizes what a service
|
|
6
|
+
* advertises in that map so apps can, at a fixed integration point (a "task"
|
|
7
|
+
* like tokenize / parse / transcribe):
|
|
8
|
+
*
|
|
9
|
+
* 1. SELECT one of several services that serve the task,
|
|
10
|
+
* 2. let the user SPECIFY arguments the service declares, and
|
|
11
|
+
* 3. show the user a service-provided SUMMARY.
|
|
12
|
+
*
|
|
13
|
+
* Shape of a service's `extras` (camelCase here; Python services author the
|
|
14
|
+
* snake_case equivalent and the client transform converts it — see the Plaid
|
|
15
|
+
* manual, "Describing a service"):
|
|
16
|
+
*
|
|
17
|
+
* {
|
|
18
|
+
* schemaVersion: 1,
|
|
19
|
+
* tasks: ["tokenize"], // controlled vocab; REPLACES tok:/asr: id prefixes
|
|
20
|
+
* summary: "## markdown …", // rich human description
|
|
21
|
+
* parameters: [ // ordered; rendered into a form
|
|
22
|
+
* { key, label, type, description?, default?, required?,
|
|
23
|
+
* options?: [{value, label}], // enum / multiselect
|
|
24
|
+
* min?, max?, step?, // number
|
|
25
|
+
* placeholder?, multiline? } // string
|
|
26
|
+
* ]
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* A parameter's `key` is a string VALUE, so it passes over the wire verbatim;
|
|
30
|
+
* the UI sends `{ [param.key]: value }` in the request data. Declare each `key`
|
|
31
|
+
* in your service's own convention (Python: snake_case; JS: camelCase) and read
|
|
32
|
+
* it back under that same key — request data is recased symmetrically, so a
|
|
33
|
+
* snake_case key round-trips unchanged to a JS UI and back.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** The controlled task vocabulary — the fixed integration-point goals. */
|
|
37
|
+
export const TASKS = Object.freeze({
|
|
38
|
+
TOKENIZE: 'tokenize',
|
|
39
|
+
PARSE: 'parse',
|
|
40
|
+
TRANSCRIBE: 'transcribe',
|
|
41
|
+
/** Create vocab links for unlinked tokens. Services should stamp link
|
|
42
|
+
* metadata with the provenance convention ({ prov: 'inferred', provSource:
|
|
43
|
+
* 'service:<id>' }) so UIs can distinguish machine links until confirmed. */
|
|
44
|
+
LINK_VOCAB: 'link-vocab',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Legacy id-prefix → task map, for services that have not yet migrated to a
|
|
49
|
+
* declared `tasks` array. Drop once all services advertise `tasks`.
|
|
50
|
+
*/
|
|
51
|
+
const LEGACY_TASK_PREFIXES = Object.freeze({
|
|
52
|
+
[TASKS.TOKENIZE]: 'tok:',
|
|
53
|
+
[TASKS.TRANSCRIBE]: 'asr:',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Does `service` serve `task`? Prefers the declared `extras.tasks` array; falls
|
|
58
|
+
* back to the legacy id-prefix convention for un-migrated services.
|
|
59
|
+
* @param {{serviceId?: string, extras?: {tasks?: string[]}}} service
|
|
60
|
+
* @param {string} task one of TASKS
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
export function servesTask(service, task) {
|
|
64
|
+
const declared = service?.extras?.tasks;
|
|
65
|
+
if (Array.isArray(declared) && declared.length) {
|
|
66
|
+
return declared.includes(task);
|
|
67
|
+
}
|
|
68
|
+
const prefix = LEGACY_TASK_PREFIXES[task];
|
|
69
|
+
return !!prefix && typeof service?.serviceId === 'string' && service.serviceId.startsWith(prefix);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The discovered services that serve `task`.
|
|
74
|
+
* @param {Array} services result of client.messages.discoverServices()
|
|
75
|
+
* @param {string} task one of TASKS
|
|
76
|
+
* @returns {Array}
|
|
77
|
+
*/
|
|
78
|
+
export function filterServicesByTask(services, task) {
|
|
79
|
+
return (services || []).filter((s) => servesTask(s, task));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The parameter schema a service declares (ordered array), or [].
|
|
84
|
+
* @param {{extras?: {parameters?: Array}}} service
|
|
85
|
+
* @returns {Array}
|
|
86
|
+
*/
|
|
87
|
+
export function getParamSchema(service) {
|
|
88
|
+
const params = service?.extras?.parameters;
|
|
89
|
+
return Array.isArray(params) ? params : [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A service's human summary: the rich `extras.summary`, else the short
|
|
94
|
+
* `description`, else ''.
|
|
95
|
+
* @param {{description?: string, extras?: {summary?: string}}} service
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
export function getServiceSummary(service) {
|
|
99
|
+
return service?.extras?.summary || service?.description || '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Valid option values for an enum/multiselect param. */
|
|
103
|
+
function optionValues(param) {
|
|
104
|
+
return Array.isArray(param?.options) ? param.options.map((o) => o.value) : [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A single parameter's default, honoring its declared `default` then falling
|
|
109
|
+
* back per type. For enum/multiselect the declared default is validated against
|
|
110
|
+
* `options` (an out-of-range declared default never escapes).
|
|
111
|
+
*/
|
|
112
|
+
function defaultForParam(param) {
|
|
113
|
+
const opts = optionValues(param);
|
|
114
|
+
if (param.type === 'enum') {
|
|
115
|
+
if (param.default != null && opts.includes(param.default)) return param.default;
|
|
116
|
+
return opts[0] ?? '';
|
|
117
|
+
}
|
|
118
|
+
if (param.type === 'multiselect') {
|
|
119
|
+
const arr = Array.isArray(param.default) ? param.default : [];
|
|
120
|
+
return opts.length ? arr.filter((x) => opts.includes(x)) : arr;
|
|
121
|
+
}
|
|
122
|
+
if (param.default !== undefined && param.default !== null) return param.default;
|
|
123
|
+
switch (param.type) {
|
|
124
|
+
case 'number': return typeof param.min === 'number' ? param.min : 0;
|
|
125
|
+
case 'boolean': return false;
|
|
126
|
+
case 'string':
|
|
127
|
+
default: return '';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Default values keyed by param key — the initial form state.
|
|
133
|
+
* @param {Array} schema getParamSchema(service)
|
|
134
|
+
* @returns {Object} { [key]: defaultValue }
|
|
135
|
+
*/
|
|
136
|
+
export function buildDefaultValues(schema) {
|
|
137
|
+
const out = {};
|
|
138
|
+
for (const param of schema || []) {
|
|
139
|
+
if (!param || !param.key) continue;
|
|
140
|
+
out[param.key] = defaultForParam(param);
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Coerce/validate raw form values against the schema. Returns the cleaned
|
|
147
|
+
* values (keyed by param key, ready to merge into the request payload) plus any
|
|
148
|
+
* validation errors keyed by param key. Unknown keys in `raw` are dropped.
|
|
149
|
+
* @param {Array} schema getParamSchema(service)
|
|
150
|
+
* @param {Object} raw current form values
|
|
151
|
+
* @returns {{values: Object, errors: Object}}
|
|
152
|
+
*/
|
|
153
|
+
export function coerceParamValues(schema, raw) {
|
|
154
|
+
const values = {};
|
|
155
|
+
const errors = {};
|
|
156
|
+
const src = raw || {};
|
|
157
|
+
for (const param of schema || []) {
|
|
158
|
+
if (!param || !param.key) continue;
|
|
159
|
+
const k = param.key;
|
|
160
|
+
let v = src[k];
|
|
161
|
+
if (v === undefined) v = defaultForParam(param);
|
|
162
|
+
|
|
163
|
+
switch (param.type) {
|
|
164
|
+
case 'number': {
|
|
165
|
+
// Blank / nullish counts as "missing" → the param's default (matches the
|
|
166
|
+
// Python client, where float('') raises and falls back to the default).
|
|
167
|
+
let n;
|
|
168
|
+
if (v === '' || v == null || (typeof v === 'string' && v.trim() === '')) {
|
|
169
|
+
n = defaultForParam(param);
|
|
170
|
+
} else {
|
|
171
|
+
n = typeof v === 'number' ? v : Number(v);
|
|
172
|
+
if (Number.isNaN(n)) n = defaultForParam(param);
|
|
173
|
+
}
|
|
174
|
+
if (typeof param.min === 'number') n = Math.max(param.min, n);
|
|
175
|
+
if (typeof param.max === 'number') n = Math.min(param.max, n);
|
|
176
|
+
v = n;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case 'boolean':
|
|
180
|
+
v = v === true || v === 'true';
|
|
181
|
+
break;
|
|
182
|
+
case 'enum': {
|
|
183
|
+
const opts = optionValues(param);
|
|
184
|
+
if (opts.length && !opts.includes(v)) v = defaultForParam(param);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case 'multiselect': {
|
|
188
|
+
const opts = optionValues(param);
|
|
189
|
+
const arr = Array.isArray(v) ? v : (v == null || v === '' ? [] : [v]);
|
|
190
|
+
v = opts.length ? arr.filter((x) => opts.includes(x)) : arr;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case 'string':
|
|
194
|
+
default:
|
|
195
|
+
v = v == null ? '' : String(v);
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (param.required) {
|
|
200
|
+
const empty = v === '' || v == null || (Array.isArray(v) && v.length === 0);
|
|
201
|
+
if (empty) errors[k] = `${param.label || k} is required`;
|
|
202
|
+
}
|
|
203
|
+
values[k] = v;
|
|
204
|
+
}
|
|
205
|
+
return { values, errors };
|
|
206
|
+
}
|
package/src/transforms.js
CHANGED
|
@@ -25,7 +25,7 @@ export function transformKeyFromCamel(key) {
|
|
|
25
25
|
// are never re-cased or namespace-stripped — a label like `case-marker` used as
|
|
26
26
|
// a map key survives intact. Everything else is API envelope and gets the
|
|
27
27
|
// usual case conversion.
|
|
28
|
-
const OPAQUE_KEYS = new Set(['metadata', 'config']);
|
|
28
|
+
const OPAQUE_KEYS = new Set(['metadata', 'config', 'bindings']);
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Recursively transform request object keys from camelCase to kebab-case.
|