@tai42/studio-sdk 22.4.1 → 24.0.0
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/dist/hooks/useApi.d.ts +194 -24
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/schema-form/SchemaForm.d.ts +10 -12
- package/dist/schema-form/SchemaForm.d.ts.map +1 -1
- package/dist/schema-form/SchemaForm.js.map +1 -1
- package/dist/schema-form/array-field.d.ts +9 -2
- package/dist/schema-form/array-field.d.ts.map +1 -1
- package/dist/schema-form/array-field.js +5 -3
- package/dist/schema-form/array-field.js.map +1 -1
- package/dist/schema-form/date-range-field.d.ts +30 -7
- package/dist/schema-form/date-range-field.d.ts.map +1 -1
- package/dist/schema-form/date-range-field.js +73 -15
- package/dist/schema-form/date-range-field.js.map +1 -1
- package/dist/schema-form/field-node.d.ts +7 -8
- package/dist/schema-form/field-node.d.ts.map +1 -1
- package/dist/schema-form/field-node.js +2 -1
- package/dist/schema-form/field-node.js.map +1 -1
- package/dist/schema-form/index.d.ts +2 -2
- package/dist/schema-form/index.d.ts.map +1 -1
- package/dist/schema-form/index.js +1 -1
- package/dist/schema-form/index.js.map +1 -1
- package/dist/schema-form/object-fields.d.ts +6 -8
- package/dist/schema-form/object-fields.d.ts.map +1 -1
- package/dist/schema-form/object-fields.js +15 -6
- package/dist/schema-form/object-fields.js.map +1 -1
- package/dist/schema-form/types.d.ts +29 -0
- package/dist/schema-form/types.d.ts.map +1 -1
- package/dist/schema-form/validate.d.ts +9 -8
- package/dist/schema-form/validate.d.ts.map +1 -1
- package/dist/schema-form/validate.js +104 -20
- package/dist/schema-form/validate.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +2 -0
- package/src/schema-form/SchemaForm.tsx +10 -9
- package/src/schema-form/array-field.tsx +19 -6
- package/src/schema-form/date-range-field.tsx +112 -25
- package/src/schema-form/field-node.tsx +11 -6
- package/src/schema-form/index.ts +2 -1
- package/src/schema-form/object-fields.tsx +47 -25
- package/src/schema-form/types.ts +30 -0
- package/src/schema-form/validate.ts +121 -24
|
@@ -10,7 +10,7 @@ import { classifySchema } from './classify';
|
|
|
10
10
|
import type { DateConstraints, FieldModel } from './field-model';
|
|
11
11
|
import { decodedByteSize, effectiveMaxBytes, overCapMessage } from './media';
|
|
12
12
|
import { resolveRef, scalarLabel } from './resolve';
|
|
13
|
-
import type { JsonSchema, SchemaFormErrors } from './types';
|
|
13
|
+
import type { FieldWords, JsonSchema, SchemaFormErrors } from './types';
|
|
14
14
|
import { activeVariantIndex } from './union';
|
|
15
15
|
import { isFieldVisible } from './visibility';
|
|
16
16
|
|
|
@@ -26,11 +26,13 @@ export interface ValidateOptions {
|
|
|
26
26
|
* Per-field words, keyed by the root object's exact property name. A field's
|
|
27
27
|
* `invalid` sentence replaces every shape error on that field and anything inside
|
|
28
28
|
* it (a format, pattern, length, bound, type or item error). A missing required
|
|
29
|
-
* value
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* value at the field or anywhere inside it shows the field's `required` sentence
|
|
30
|
+
* when set, else the SDK's own required message; `invalid` never covers it. A
|
|
31
|
+
* `pattern` that cannot compile keeps its own message, since that is a defect of the
|
|
32
|
+
* schema, not of the value. A field with no `invalid` sentence keeps the SDK's word
|
|
33
|
+
* for the failing shape.
|
|
32
34
|
*/
|
|
33
|
-
readonly fieldWording?: Readonly<Record<string,
|
|
35
|
+
readonly fieldWording?: Readonly<Record<string, FieldWords>>;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
/** The invariants every per-node validator shares while walking one value tree. */
|
|
@@ -54,6 +56,11 @@ interface ValidateCtx {
|
|
|
54
56
|
* each mapped to the root property it lies inside.
|
|
55
57
|
*/
|
|
56
58
|
readonly shapeErrorFields: Map<string, string>;
|
|
59
|
+
/**
|
|
60
|
+
* The paths whose error is a missing required value a field's `required` sentence
|
|
61
|
+
* may replace, each mapped to the root property it lies inside.
|
|
62
|
+
*/
|
|
63
|
+
readonly requiredFields: Map<string, string>;
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
/**
|
|
@@ -62,6 +69,7 @@ interface ValidateCtx {
|
|
|
62
69
|
*/
|
|
63
70
|
function shapeError(ctx: ValidateCtx, path: string, message: string): void {
|
|
64
71
|
ctx.errors[path] = message;
|
|
72
|
+
ctx.requiredFields.delete(path);
|
|
65
73
|
if (ctx.field === undefined) ctx.shapeErrorFields.delete(path);
|
|
66
74
|
else ctx.shapeErrorFields.set(path, ctx.field);
|
|
67
75
|
}
|
|
@@ -73,6 +81,7 @@ function shapeError(ctx: ValidateCtx, path: string, message: string): void {
|
|
|
73
81
|
function keptError(ctx: ValidateCtx, path: string, message: string): void {
|
|
74
82
|
ctx.errors[path] = message;
|
|
75
83
|
ctx.shapeErrorFields.delete(path);
|
|
84
|
+
ctx.requiredFields.delete(path);
|
|
76
85
|
}
|
|
77
86
|
|
|
78
87
|
/** The context for the child `name` of the value at `path`: a root child is its own field. */
|
|
@@ -250,7 +259,7 @@ function validateObject(
|
|
|
250
259
|
const childPath = joinPath(path, name);
|
|
251
260
|
const childValue = ownValue(value, name);
|
|
252
261
|
if (childValue === undefined) {
|
|
253
|
-
if (model.required.has(name)) markRequired(childPath, name, ctx);
|
|
262
|
+
if (model.required.has(name)) markRequired(childPath, name, childCtx(ctx, path, name));
|
|
254
263
|
continue;
|
|
255
264
|
}
|
|
256
265
|
walk(propSchema, childValue, childPath, childCtx(ctx, path, name));
|
|
@@ -258,15 +267,20 @@ function validateObject(
|
|
|
258
267
|
// Required keys naming a property that somehow isn't declared still count.
|
|
259
268
|
for (const name of model.required) {
|
|
260
269
|
if (!propMap.has(name) && ownValue(value, name) === undefined) {
|
|
261
|
-
markRequired(joinPath(path, name), name, ctx);
|
|
270
|
+
markRequired(joinPath(path, name), name, childCtx(ctx, path, name));
|
|
262
271
|
}
|
|
263
272
|
}
|
|
264
273
|
validateDateRanges(model, value, path, ctx);
|
|
265
274
|
}
|
|
266
275
|
|
|
267
|
-
/**
|
|
276
|
+
/**
|
|
277
|
+
* Record a missing required value at `path`. `ctx` is the missing child's own context,
|
|
278
|
+
* so inside a root property the field's `required` sentence may replace the message
|
|
279
|
+
* (never its `invalid` sentence).
|
|
280
|
+
*/
|
|
268
281
|
function markRequired(path: string, name: string, ctx: ValidateCtx): void {
|
|
269
282
|
keptError(ctx, path, `"${name}" is required`);
|
|
283
|
+
if (ctx.field !== undefined) ctx.requiredFields.set(path, ctx.field);
|
|
270
284
|
}
|
|
271
285
|
|
|
272
286
|
/** Days from `start` to `end` inclusive (both `YYYY-MM-DD`); 1 when they are equal. */
|
|
@@ -274,11 +288,51 @@ function inclusiveDayCount(start: string, end: string): number {
|
|
|
274
288
|
return Math.round((Date.parse(end) - Date.parse(start)) / 86_400_000) + 1;
|
|
275
289
|
}
|
|
276
290
|
|
|
291
|
+
const WRITTEN_DATE_RE = /^(\d{4}-\d{2}-\d{2})[Tt]/;
|
|
292
|
+
const TIME_RE = /^(\d{2}):(\d{2})(?::(\d{2}))?$/;
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* The written `YYYY-MM-DD` date of a range end's value: a `date` value as is, a
|
|
296
|
+
* `date-time` value's leading date. `undefined` when the value does not pass its own
|
|
297
|
+
* format check (that field's own validation reports it).
|
|
298
|
+
*/
|
|
299
|
+
function writtenDate(schema: JsonSchema, value: unknown): string | undefined {
|
|
300
|
+
if (typeof value !== 'string') return undefined;
|
|
301
|
+
if (schema.format === 'date') return formatError('date', value) === undefined ? value : undefined;
|
|
302
|
+
if (schema.format === 'date-time' && formatError('date-time', value) === undefined) {
|
|
303
|
+
return WRITTEN_DATE_RE.exec(value)?.[1];
|
|
304
|
+
}
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** The seconds into the day a `HH:MM[:SS]` value names, or `undefined` when it is not a real time. */
|
|
309
|
+
function secondsOfDay(value: unknown): number | undefined {
|
|
310
|
+
if (typeof value !== 'string') return undefined;
|
|
311
|
+
const match = TIME_RE.exec(value);
|
|
312
|
+
if (match === null) return undefined;
|
|
313
|
+
const [hours, minutes, seconds] = [match[1], match[2], match[3] ?? '0'].map(Number);
|
|
314
|
+
if (hours === undefined || minutes === undefined || seconds === undefined) return undefined;
|
|
315
|
+
if (hours > 23 || minutes > 59 || seconds > 59) return undefined;
|
|
316
|
+
return hours * 3600 + minutes * 60 + seconds;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** The `timeOf` time property paired with the date `dateName`, if any. */
|
|
320
|
+
function pairedTimeName(
|
|
321
|
+
dateName: string,
|
|
322
|
+
properties: readonly (readonly [string, JsonSchema])[],
|
|
323
|
+
): string | undefined {
|
|
324
|
+
return properties.find(([name, schema]) => schema.timeOf === dateName && name !== dateName)?.[0];
|
|
325
|
+
}
|
|
326
|
+
|
|
277
327
|
/**
|
|
278
|
-
* Enforce a date RANGE declared on an END field: the end is on or after its
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
328
|
+
* Enforce a date RANGE declared on an END field: the end is on or after its named
|
|
329
|
+
* `rangeStart` field, and the inclusive day span is within `minDays`/`maxDays`. Each end is
|
|
330
|
+
* read by its written date (a `date-time` end by its leading `YYYY-MM-DD`), so a mixed
|
|
331
|
+
* `date`/`date-time` pair is ordered by date; a pair of two `date-time` ends is left to
|
|
332
|
+
* the answer door, which orders it by instant. Two ends on the same date that each carry
|
|
333
|
+
* a readable `timeOf` time are then ordered by those times, the error on the END's time.
|
|
334
|
+
* Nothing is recombined. A missing or not-yet-valid endpoint is left to that field's own
|
|
335
|
+
* validation; a hidden end field is skipped.
|
|
282
336
|
*/
|
|
283
337
|
function validateDateRanges(
|
|
284
338
|
model: ModelOf<'object'>,
|
|
@@ -286,21 +340,55 @@ function validateDateRanges(
|
|
|
286
340
|
path: string,
|
|
287
341
|
ctx: ValidateCtx,
|
|
288
342
|
): void {
|
|
343
|
+
const propMap = new Map(model.properties);
|
|
289
344
|
for (const [name, propSchema] of model.properties) {
|
|
290
345
|
if (!isFieldVisible(propSchema, value)) continue;
|
|
291
346
|
const resolved = resolveRef(propSchema, ctx.root);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
if (
|
|
296
|
-
|
|
347
|
+
const startName = resolved.rangeStart;
|
|
348
|
+
if (typeof startName !== 'string') continue;
|
|
349
|
+
const startDeclared = propMap.get(startName);
|
|
350
|
+
if (startDeclared === undefined) continue;
|
|
351
|
+
const startSchema = resolveRef(startDeclared, ctx.root);
|
|
352
|
+
if (resolved.format === 'date-time' && startSchema.format === 'date-time') continue;
|
|
353
|
+
const endDate = writtenDate(resolved, ownValue(value, name));
|
|
354
|
+
const startDate = writtenDate(startSchema, ownValue(value, startName));
|
|
355
|
+
if (endDate === undefined || startDate === undefined) continue;
|
|
297
356
|
const childPath = joinPath(path, name);
|
|
298
357
|
if (ctx.errors[childPath] !== undefined) continue;
|
|
299
|
-
|
|
358
|
+
if (endDate === startDate && timeOrderError(model, value, path, ctx, startName, name)) continue;
|
|
359
|
+
const message = dateRangeMessage(resolved, startDate, endDate);
|
|
300
360
|
if (message !== undefined) shapeError(childCtx(ctx, path, name), childPath, message);
|
|
301
361
|
}
|
|
302
362
|
}
|
|
303
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Record the order error on the END's time when two same-date ends each carry a readable
|
|
366
|
+
* `timeOf` time and the END's is earlier; `true` when it was recorded. An absent or
|
|
367
|
+
* unreadable time leaves the dates to decide.
|
|
368
|
+
*/
|
|
369
|
+
function timeOrderError(
|
|
370
|
+
model: ModelOf<'object'>,
|
|
371
|
+
value: Record<string, unknown>,
|
|
372
|
+
path: string,
|
|
373
|
+
ctx: ValidateCtx,
|
|
374
|
+
startName: string,
|
|
375
|
+
endName: string,
|
|
376
|
+
): boolean {
|
|
377
|
+
const startTimeName = pairedTimeName(startName, model.properties);
|
|
378
|
+
const endTimeName = pairedTimeName(endName, model.properties);
|
|
379
|
+
if (startTimeName === undefined || endTimeName === undefined) return false;
|
|
380
|
+
const startSeconds = secondsOfDay(ownValue(value, startTimeName));
|
|
381
|
+
const endSeconds = secondsOfDay(ownValue(value, endTimeName));
|
|
382
|
+
if (startSeconds === undefined || endSeconds === undefined || endSeconds >= startSeconds) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
const timePath = joinPath(path, endTimeName);
|
|
386
|
+
if (ctx.errors[timePath] === undefined) {
|
|
387
|
+
shapeError(childCtx(ctx, path, endTimeName), timePath, 'must be on or after the start time');
|
|
388
|
+
}
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
|
|
304
392
|
/** The ordering/span violation on a range END field, or `undefined` when it holds. */
|
|
305
393
|
function dateRangeMessage(
|
|
306
394
|
resolved: JsonSchema,
|
|
@@ -489,18 +577,24 @@ function dispatchKind(model: FieldModel, value: unknown, path: string, ctx: Vali
|
|
|
489
577
|
|
|
490
578
|
/**
|
|
491
579
|
* Replace each shape error inside a root property whose words carry an `invalid`
|
|
492
|
-
* sentence with that sentence
|
|
580
|
+
* sentence with that sentence, and each missing required value inside a root property
|
|
581
|
+
* whose words carry a `required` sentence with that one; every other error keeps its
|
|
582
|
+
* own message.
|
|
493
583
|
*/
|
|
494
584
|
function applyFieldWording(
|
|
495
|
-
fieldWording: Readonly<Record<string,
|
|
585
|
+
fieldWording: Readonly<Record<string, FieldWords>>,
|
|
496
586
|
errors: Record<string, string>,
|
|
497
587
|
shapeErrorFields: ReadonlyMap<string, string>,
|
|
588
|
+
requiredFields: ReadonlyMap<string, string>,
|
|
498
589
|
): void {
|
|
499
590
|
for (const [path, field] of shapeErrorFields) {
|
|
500
|
-
|
|
501
|
-
const invalid = fieldWording[field]?.invalid;
|
|
591
|
+
const invalid = ownValue(fieldWording, field)?.invalid;
|
|
502
592
|
if (invalid !== undefined) errors[path] = invalid;
|
|
503
593
|
}
|
|
594
|
+
for (const [path, field] of requiredFields) {
|
|
595
|
+
const required = ownValue(fieldWording, field)?.required;
|
|
596
|
+
if (required !== undefined) errors[path] = required;
|
|
597
|
+
}
|
|
504
598
|
}
|
|
505
599
|
|
|
506
600
|
/**
|
|
@@ -509,7 +603,8 @@ function applyFieldWording(
|
|
|
509
603
|
* `SchemaForm` for display. `options.maxUploadBytes` mirrors the renderer's
|
|
510
604
|
* `maxUploadBytes` prop so a media field's byte cap is enforced identically here;
|
|
511
605
|
* `options.fieldWording` puts a field's own `invalid` sentence in place of the
|
|
512
|
-
* SDK's word for any shape error on that field
|
|
606
|
+
* SDK's word for any shape error on that field, and its own `required` sentence in
|
|
607
|
+
* place of the SDK's message for a missing required value at or inside that field.
|
|
513
608
|
*/
|
|
514
609
|
export function validateAgainstSchema(
|
|
515
610
|
schema: JsonSchema,
|
|
@@ -518,15 +613,17 @@ export function validateAgainstSchema(
|
|
|
518
613
|
): SchemaFormErrors {
|
|
519
614
|
const errors = Object.create(null) as Record<string, string>;
|
|
520
615
|
const shapeErrorFields = new Map<string, string>();
|
|
616
|
+
const requiredFields = new Map<string, string>();
|
|
521
617
|
walk(schema, value, '', {
|
|
522
618
|
root: schema,
|
|
523
619
|
errors,
|
|
524
620
|
maxUploadBytes: options?.maxUploadBytes,
|
|
525
621
|
field: undefined,
|
|
526
622
|
shapeErrorFields,
|
|
623
|
+
requiredFields,
|
|
527
624
|
});
|
|
528
625
|
if (options?.fieldWording !== undefined) {
|
|
529
|
-
applyFieldWording(options.fieldWording, errors, shapeErrorFields);
|
|
626
|
+
applyFieldWording(options.fieldWording, errors, shapeErrorFields, requiredFields);
|
|
530
627
|
}
|
|
531
628
|
// Hand back an ordinary object; its entries are defined as own keys, so a path such as
|
|
532
629
|
// `__proto__` stays an entry of the result.
|