@empyria/common 0.2.0 → 0.3.1
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 -8
- package/lib/Ata.js +215 -19
- package/package.json +2 -2
- package/test/Ata.test.js +156 -0
package/README.md
CHANGED
|
@@ -41,14 +41,14 @@ import { defineSchema } from '@empyria/common/lib/Ata.js'
|
|
|
41
41
|
|
|
42
42
|
## Modules
|
|
43
43
|
|
|
44
|
-
| Module | Purpose
|
|
45
|
-
| -------------------------------------- |
|
|
46
|
-
| [index.js](./index.js) | Package entry point; re-exports every module below plus Moleculer meta helpers (`moleculerMeta`, `moleculerEmpyriaMeta`) and workflow ID helpers (`workflowName`, `workflowId`, `identifyWorkflow`).
|
|
47
|
-
| [lib/Errors.js](./lib/Errors.js) | `EmpyriaError`, the `ErrorCreator` factory, and `BaseErrors` — Empyria's catalog of pre-built, templated errors.
|
|
48
|
-
| [lib/Env.js](./lib/Env.js) | `createEnv` — builds a service's environment-variable JSON schema from Empyria-wide defaults; pass `{ closed: true }` to reject undeclared vars and bind `{ coerceTypes, removeAdditional }` to the schema.
|
|
49
|
-
| [lib/Ata.js](./lib/Ata.js) | JSON schema building blocks (`string`, `number`, `bool`, `enumType`, `
|
|
50
|
-
| [lib/Codogram.js](./lib/Codogram.js) | `generateCodogram` — one-time challenge codes (PIN, alphanumeric, alphabetic) for SMS/TOTP-style verification flows.
|
|
51
|
-
| [lib/Defaulter.js](./lib/Defaulter.js) | `defaultsFromSchema` — builds a fully-populated default object for a JSON schema.
|
|
44
|
+
| Module | Purpose |
|
|
45
|
+
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
46
|
+
| [index.js](./index.js) | Package entry point; re-exports every module below plus Moleculer meta helpers (`moleculerMeta`, `moleculerEmpyriaMeta`) and workflow ID helpers (`workflowName`, `workflowId`, `identifyWorkflow`). |
|
|
47
|
+
| [lib/Errors.js](./lib/Errors.js) | `EmpyriaError`, the `ErrorCreator` factory, and `BaseErrors` — Empyria's catalog of pre-built, templated errors. |
|
|
48
|
+
| [lib/Env.js](./lib/Env.js) | `createEnv` — builds a service's environment-variable JSON schema from Empyria-wide defaults; pass `{ closed: true }` to reject undeclared vars and bind `{ coerceTypes, removeAdditional }` to the schema. |
|
|
49
|
+
| [lib/Ata.js](./lib/Ata.js) | JSON schema building blocks (`string`, `number`, `integer`, `bool`, `enumType`, `constant`, `nullable`, `array`, `discriminated`, `email`, `isoDateTime`, ...) and validation — throwing (`defineSchema`, `createValidator`, `validate`), transform-intent aliases (`parse`, `createParser`), or non-throwing/structured (`createChecker`, `checkSchema`, now returning the parsed `data`) — built on [ata-validator](https://www.npmjs.com/package/ata-validator). See _Schema builders_ and _Validation options_ below. |
|
|
50
|
+
| [lib/Codogram.js](./lib/Codogram.js) | `generateCodogram` — one-time challenge codes (PIN, alphanumeric, alphabetic) for SMS/TOTP-style verification flows. |
|
|
51
|
+
| [lib/Defaulter.js](./lib/Defaulter.js) | `defaultsFromSchema` — builds a fully-populated default object for a JSON schema. |
|
|
52
52
|
|
|
53
53
|
Every exported function is documented with JSDoc directly in its source file — hovering
|
|
54
54
|
a function in VSCode or Zed shows its parameters and return type without any extra
|
|
@@ -90,6 +90,37 @@ copy when the caller must keep its input pristine (as the `env.js` files do with
|
|
|
90
90
|
intent is to transform rather than merely assert. `createChecker` / `checkSchema` now also
|
|
91
91
|
return `data` (the parsed value on success, `undefined` on failure).
|
|
92
92
|
|
|
93
|
+
## Schema builders
|
|
94
|
+
|
|
95
|
+
Each returns a plain JSON Schema fragment; compose them in `defineSchema(map, opts)`.
|
|
96
|
+
|
|
97
|
+
| Builder | Produces |
|
|
98
|
+
| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
99
|
+
| `bool(default?)` | `{ type: 'boolean' }` |
|
|
100
|
+
| `string(default?, minLength \| { minLength, maxLength, pattern, format, description })` | `{ type: 'string', … }` |
|
|
101
|
+
| `number(default?, { min, max, exclusiveMin, exclusiveMax, multipleOf, description })` | `{ type: 'number', … }` |
|
|
102
|
+
| `integer(default?, …)` | `{ type: 'integer', … }` — use for ports/counts; rejects `3.5` |
|
|
103
|
+
| `enumType(list, default?, { type, description })` | `{ enum, type? }` |
|
|
104
|
+
| `constant(value, { description })` | `{ const: value }` |
|
|
105
|
+
| `nullable(schema)` | `type: [t, 'null']`, or `{ anyOf: [schema, { type: 'null' }] }` |
|
|
106
|
+
| `array(items?, { minItems, maxItems, uniqueItems, default, description })` | `{ type: 'array', … }` |
|
|
107
|
+
| `email() / password() / ip4() / ip6()` | `{ type: 'string', format: 'empyria-*' }` (see `EMPYRIA_FORMATS`) |
|
|
108
|
+
| `isoDateTime() / isoDate() / isoTime() / duration() / uri() / uuid()` | `{ type: 'string', format: … }` (ata built-in formats) |
|
|
109
|
+
| `logLevel() / nodeEnv() / tmpFile() / logFile()` | domain presets |
|
|
110
|
+
| `discriminated(prop, { value: schema, … }, { description })` | JSON Schema **v1** `propertyDependencies` — an object whose shape depends on `prop`'s value |
|
|
111
|
+
|
|
112
|
+
### Dialect
|
|
113
|
+
|
|
114
|
+
`defineSchema(map, { dialect: 'v1' \| '2020-12' \| 'draft-07' \| <uri> })` stamps `$schema`
|
|
115
|
+
(`ata-validator` reads it to pick the dialect). `setDefaultDialect('v1')` / `getDefaultDialect()`
|
|
116
|
+
set a process-wide default a per-call `dialect` overrides. v1 differs from 2020-12 only in
|
|
117
|
+
`$dynamicRef` scoping and unlocks `propertyDependencies`; `discriminated()` works on any
|
|
118
|
+
dialect (ata supports the keyword as an extension) but `dialect: 'v1'` makes it spec-legitimate.
|
|
119
|
+
|
|
120
|
+
`defineSchema(map, { strict: 'unevaluated' })` sets `unevaluatedProperties: false` while
|
|
121
|
+
leaving `additionalProperties` open — the composable form of strict, for schemas meant to be
|
|
122
|
+
`allOf`-combined.
|
|
123
|
+
|
|
93
124
|
## Scripts
|
|
94
125
|
|
|
95
126
|
```bash
|
package/lib/Ata.js
CHANGED
|
@@ -63,6 +63,42 @@ export const EMPYRIA_FORMATS = {
|
|
|
63
63
|
/** Non-enumerable key under which {@link defineSchema} stashes a schema's bound options. */
|
|
64
64
|
const SCHEMA_OPTIONS = Symbol('ataOptions')
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Known JSON Schema dialect URIs, keyed by short name. `defineSchema({ dialect })` accepts a
|
|
68
|
+
* short name or a raw URI and stamps it as the schema's `$schema`. `ata-validator` reads
|
|
69
|
+
* `$schema` to pick the dialect; `'v1'` (the forthcoming IETF standard) differs from
|
|
70
|
+
* `'2020-12'` only in `$dynamicRef` scoping and unlocks `propertyDependencies` (see
|
|
71
|
+
* {@link discriminated}).
|
|
72
|
+
*/
|
|
73
|
+
export const DIALECTS = {
|
|
74
|
+
'2020-12': 'https://json-schema.org/draft/2020-12/schema',
|
|
75
|
+
'draft-07': 'http://json-schema.org/draft-07/schema#',
|
|
76
|
+
v1: 'https://json-schema.org/v1',
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Process-wide default dialect for {@link defineSchema}; `null` = emit no `$schema`. */
|
|
80
|
+
let defaultDialect = null
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Sets the process-wide default dialect for {@link defineSchema} — the `$schema` it stamps
|
|
84
|
+
* when a call passes no `dialect` of its own. Call once at startup to move a service to a
|
|
85
|
+
* dialect (e.g. `'v1'`) without touching every schema.
|
|
86
|
+
* @param {('2020-12'|'draft-07'|'v1'|string|null)} [dialect=null] - Short name, raw URI, or
|
|
87
|
+
* `null` to go back to emitting no `$schema`.
|
|
88
|
+
* @returns {void}
|
|
89
|
+
*/
|
|
90
|
+
export function setDefaultDialect(dialect = null) {
|
|
91
|
+
defaultDialect = dialect
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns the process-wide default dialect (see {@link setDefaultDialect}).
|
|
96
|
+
* @returns {string|null}
|
|
97
|
+
*/
|
|
98
|
+
export function getDefaultDialect() {
|
|
99
|
+
return defaultDialect
|
|
100
|
+
}
|
|
101
|
+
|
|
66
102
|
/** Process-wide default `ata-validator` options; the base layer for every call here. */
|
|
67
103
|
let libraryDefaults = {}
|
|
68
104
|
|
|
@@ -137,12 +173,17 @@ export function cloneBySchema(schema) {
|
|
|
137
173
|
* Builds a JSON schema object from a flat map of property schemas.
|
|
138
174
|
* @param {Object} schemaDef - Map of property name to its JSON schema.
|
|
139
175
|
* @param {Object} [options={}]
|
|
140
|
-
* @param {boolean} [options.strict] -
|
|
141
|
-
* (`additionalProperties: false`)
|
|
142
|
-
* *strip* unknown keys instead of rejecting
|
|
143
|
-
*
|
|
176
|
+
* @param {(boolean|'unevaluated')} [options.strict] - `true` disallows properties not listed
|
|
177
|
+
* in `schemaDef` (`additionalProperties: false`); combine with
|
|
178
|
+
* `validation: { removeAdditional: true }` to *strip* unknown keys instead of rejecting
|
|
179
|
+
* them. `'unevaluated'` sets `unevaluatedProperties: false` and leaves `additionalProperties`
|
|
180
|
+
* open — the composable form of strict, for a schema meant to be `allOf`-combined. Ignored
|
|
181
|
+
* when `options.additionalProperties` is given.
|
|
144
182
|
* @param {boolean|Object} [options.additionalProperties] - Explicit `additionalProperties`
|
|
145
183
|
* value (a boolean, or a sub-schema for the extra keys). Overrides `options.strict`.
|
|
184
|
+
* @param {('2020-12'|'draft-07'|'v1'|string)} [options.dialect] - Stamp this dialect as the
|
|
185
|
+
* schema's `$schema` (short name from {@link DIALECTS}, or a raw URI). Defaults to
|
|
186
|
+
* {@link getDefaultDialect}; when both are unset no `$schema` is emitted.
|
|
146
187
|
* @param {boolean} [options.optional] - If true, a property whose own schema carries
|
|
147
188
|
* `optional: true` is excluded from `required` (that marker is then stripped from the
|
|
148
189
|
* property itself — never leaked into the resulting schema, since it isn't a real JSON
|
|
@@ -168,14 +209,19 @@ export function defineSchema(schemaDef, options = {}) {
|
|
|
168
209
|
required.push(key)
|
|
169
210
|
}
|
|
170
211
|
}
|
|
212
|
+
const unevaluated = options.strict === 'unevaluated'
|
|
171
213
|
const schema = {
|
|
172
214
|
type: 'object',
|
|
173
215
|
properties,
|
|
174
216
|
required,
|
|
175
217
|
additionalProperties: defined(options.additionalProperties)
|
|
176
218
|
? options.additionalProperties
|
|
177
|
-
: !options.strict,
|
|
219
|
+
: !options.strict || unevaluated,
|
|
178
220
|
}
|
|
221
|
+
if (unevaluated) schema.unevaluatedProperties = false
|
|
222
|
+
|
|
223
|
+
const dialect = options.dialect ?? defaultDialect
|
|
224
|
+
if (dialect) schema.$schema = DIALECTS[dialect] ?? dialect
|
|
179
225
|
|
|
180
226
|
if (options.validation) {
|
|
181
227
|
Object.defineProperty(schema, SCHEMA_OPTIONS, {
|
|
@@ -327,15 +373,26 @@ export function bool(defaultValue) {
|
|
|
327
373
|
/**
|
|
328
374
|
* JSON schema for a string property.
|
|
329
375
|
* @param {string} [defaultValue] - Default value, if any.
|
|
330
|
-
* @param {number} [
|
|
331
|
-
*
|
|
376
|
+
* @param {number|Object} [opts] - A bare number is shorthand for `{ minLength }` (kept for
|
|
377
|
+
* back-compat). As an object:
|
|
378
|
+
* @param {number} [opts.minLength] - Minimum length (`0` is honoured, unlike the old
|
|
379
|
+
* number-shorthand which dropped non-positive values).
|
|
380
|
+
* @param {number} [opts.maxLength] - Maximum length.
|
|
381
|
+
* @param {string} [opts.pattern] - `pattern` regex (string form, unanchored per JSON Schema).
|
|
382
|
+
* @param {string} [opts.format] - `format` name (e.g. an {@link EMPYRIA_FORMATS} key or a
|
|
383
|
+
* built-in like `'uri'`; see {@link isoDateTime} & co. for ready-made wrappers).
|
|
384
|
+
* @param {string} [opts.description]
|
|
385
|
+
* @returns {Object} `{ type: 'string', default?, minLength?, maxLength?, pattern?, format?, description? }`.
|
|
332
386
|
*/
|
|
333
|
-
export function string(defaultValue,
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
}
|
|
387
|
+
export function string(defaultValue, opts) {
|
|
388
|
+
const o = typeof opts === 'number' ? { minLength: opts } : (opts ?? {})
|
|
389
|
+
const schema = { type: 'string' }
|
|
337
390
|
if (defined(defaultValue)) schema.default = `${defaultValue}`
|
|
338
|
-
if (minLength
|
|
391
|
+
if (defined(o.minLength) && o.minLength >= 0) schema.minLength = o.minLength
|
|
392
|
+
if (defined(o.maxLength)) schema.maxLength = o.maxLength
|
|
393
|
+
if (defined(o.pattern)) schema.pattern = o.pattern
|
|
394
|
+
if (defined(o.format)) schema.format = o.format
|
|
395
|
+
if (defined(o.description)) schema.description = o.description
|
|
339
396
|
return schema
|
|
340
397
|
}
|
|
341
398
|
|
|
@@ -343,13 +400,19 @@ export function string(defaultValue, minLength) {
|
|
|
343
400
|
* JSON schema for an enumerated property.
|
|
344
401
|
* @param {Array} list - Allowed values.
|
|
345
402
|
* @param {*} [defaultValue] - Default value, if any.
|
|
346
|
-
* @
|
|
403
|
+
* @param {Object} [opts]
|
|
404
|
+
* @param {string} [opts.type] - Optional `type` alongside `enum` (helps tooling / type
|
|
405
|
+
* inference; validation is unchanged since `enum` already pins the values).
|
|
406
|
+
* @param {string} [opts.description]
|
|
407
|
+
* @returns {Object} `{ enum: list, type?, default?, description? }`.
|
|
347
408
|
*/
|
|
348
|
-
export function enumType(list, defaultValue) {
|
|
409
|
+
export function enumType(list, defaultValue, opts = {}) {
|
|
349
410
|
const schema = {
|
|
350
411
|
enum: list,
|
|
351
412
|
}
|
|
413
|
+
if (defined(opts.type)) schema.type = opts.type
|
|
352
414
|
if (defined(defaultValue)) schema.default = defaultValue
|
|
415
|
+
if (defined(opts.description)) schema.description = opts.description
|
|
353
416
|
return schema
|
|
354
417
|
}
|
|
355
418
|
|
|
@@ -389,18 +452,151 @@ export function nodeEnv() {
|
|
|
389
452
|
}
|
|
390
453
|
|
|
391
454
|
/**
|
|
392
|
-
*
|
|
455
|
+
* @param {'number'|'integer'} type
|
|
456
|
+
* @param {number} [defaultValue]
|
|
457
|
+
* @param {Object} [opts] - `min`/`max` (inclusive → `minimum`/`maximum`),
|
|
458
|
+
* `exclusiveMin`/`exclusiveMax`, `multipleOf`, `description`.
|
|
459
|
+
* @returns {Object}
|
|
460
|
+
*/
|
|
461
|
+
function numeric(type, defaultValue, opts = {}) {
|
|
462
|
+
const schema = { type }
|
|
463
|
+
if (defined(defaultValue)) schema.default = Number(defaultValue)
|
|
464
|
+
if (defined(opts.min)) schema.minimum = opts.min
|
|
465
|
+
if (defined(opts.max)) schema.maximum = opts.max
|
|
466
|
+
if (defined(opts.exclusiveMin)) schema.exclusiveMinimum = opts.exclusiveMin
|
|
467
|
+
if (defined(opts.exclusiveMax)) schema.exclusiveMaximum = opts.exclusiveMax
|
|
468
|
+
if (defined(opts.multipleOf)) schema.multipleOf = opts.multipleOf
|
|
469
|
+
if (defined(opts.description)) schema.description = opts.description
|
|
470
|
+
return schema
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* JSON schema for a numeric property (`type: 'number'` — accepts integers and reals).
|
|
393
475
|
* @param {number} [defaultValue] - Default value, if any.
|
|
394
|
-
* @
|
|
476
|
+
* @param {Object} [opts] - `min`, `max`, `exclusiveMin`, `exclusiveMax`, `multipleOf`,
|
|
477
|
+
* `description` (see {@link numeric}).
|
|
478
|
+
* @returns {Object} `{ type: 'number', default?, minimum?, maximum?, ... }`.
|
|
395
479
|
*/
|
|
396
|
-
export function number(defaultValue) {
|
|
480
|
+
export function number(defaultValue, opts) {
|
|
481
|
+
return numeric('number', defaultValue, opts)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* JSON schema for an integer property (`type: 'integer'` — rejects `3.5`, and with
|
|
486
|
+
* `coerceTypes` rejects `"3.5"`). Prefer this over {@link number} for ports, counts, lengths.
|
|
487
|
+
* @param {number} [defaultValue] - Default value, if any.
|
|
488
|
+
* @param {Object} [opts] - Same as {@link number}.
|
|
489
|
+
* @returns {Object} `{ type: 'integer', default?, minimum?, maximum?, ... }`.
|
|
490
|
+
*/
|
|
491
|
+
export function integer(defaultValue, opts) {
|
|
492
|
+
return numeric('integer', defaultValue, opts)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* JSON schema for a constant value (`const`). Useful as a discriminator tag or a pinned
|
|
497
|
+
* config value.
|
|
498
|
+
* @param {*} value - The only accepted value (compared structurally).
|
|
499
|
+
* @param {Object} [opts] - `description`.
|
|
500
|
+
* @returns {Object} `{ const: value, description? }`.
|
|
501
|
+
*/
|
|
502
|
+
export function constant(value, opts = {}) {
|
|
503
|
+
const schema = { const: value }
|
|
504
|
+
if (defined(opts.description)) schema.description = opts.description
|
|
505
|
+
return schema
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Wraps a schema so `null` is also accepted. A schema with a single string `type` gets
|
|
510
|
+
* `type: [t, 'null']`; anything else (an `enum`, a `$ref`, a composed schema) is wrapped as
|
|
511
|
+
* `{ anyOf: [schema, { type: 'null' }] }`.
|
|
512
|
+
* @param {Object} schema - The schema to make nullable.
|
|
513
|
+
* @returns {Object}
|
|
514
|
+
*/
|
|
515
|
+
export function nullable(schema) {
|
|
516
|
+
if (schema && typeof schema.type === 'string') {
|
|
517
|
+
return { ...schema, type: [schema.type, 'null'] }
|
|
518
|
+
}
|
|
519
|
+
return { anyOf: [schema, { type: 'null' }] }
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* JSON schema for an array property.
|
|
524
|
+
* @param {Object} [items] - Schema every element must satisfy. Omit for "any element".
|
|
525
|
+
* @param {Object} [opts] - `minItems`, `maxItems`, `uniqueItems` (boolean), `default`,
|
|
526
|
+
* `description`.
|
|
527
|
+
* @returns {Object} `{ type: 'array', items?, minItems?, maxItems?, uniqueItems?, default?, description? }`.
|
|
528
|
+
*/
|
|
529
|
+
export function array(items, opts = {}) {
|
|
530
|
+
const schema = { type: 'array' }
|
|
531
|
+
if (defined(items)) schema.items = items
|
|
532
|
+
if (defined(opts.minItems)) schema.minItems = opts.minItems
|
|
533
|
+
if (defined(opts.maxItems)) schema.maxItems = opts.maxItems
|
|
534
|
+
if (opts.uniqueItems) schema.uniqueItems = true
|
|
535
|
+
if (defined(opts.default)) schema.default = opts.default
|
|
536
|
+
if (defined(opts.description)) schema.description = opts.description
|
|
537
|
+
return schema
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* JSON Schema v1 `propertyDependencies`: an object that takes a different shape depending on
|
|
542
|
+
* the value of a discriminator property. Replaces the `oneOf` + `if`/`then`/`const` pattern.
|
|
543
|
+
* Validates on every `ata-validator` engine (supported since 1.5.0); pair with
|
|
544
|
+
* `defineSchema`'s `dialect: 'v1'` — or `setDefaultDialect('v1')` — for a spec-legitimate
|
|
545
|
+
* `$schema`.
|
|
546
|
+
* The result is open (`additionalProperties` unset): `additionalProperties: false` cannot be
|
|
547
|
+
* used alongside `propertyDependencies` — it does not see through the branches and would
|
|
548
|
+
* reject every branch key. For a closed union, give each branch schema its own
|
|
549
|
+
* `unevaluatedProperties: false` and add one at this level too.
|
|
550
|
+
* @param {string} property - The discriminator property name.
|
|
551
|
+
* @param {Record<string, Object>} mapping - Discriminator value → the schema the rest of the
|
|
552
|
+
* object must then satisfy.
|
|
553
|
+
* @param {Object} [opts]
|
|
554
|
+
* @param {string} [opts.description]
|
|
555
|
+
* @returns {Object} `{ type: 'object', properties: { [property]: { enum } }, required, propertyDependencies }`.
|
|
556
|
+
*/
|
|
557
|
+
export function discriminated(property, mapping, opts = {}) {
|
|
397
558
|
const schema = {
|
|
398
|
-
type: '
|
|
559
|
+
type: 'object',
|
|
560
|
+
properties: { [property]: { enum: Object.keys(mapping) } },
|
|
561
|
+
required: [property],
|
|
562
|
+
propertyDependencies: { [property]: mapping },
|
|
399
563
|
}
|
|
400
|
-
if (defined(
|
|
564
|
+
if (defined(opts.description)) schema.description = opts.description
|
|
401
565
|
return schema
|
|
402
566
|
}
|
|
403
567
|
|
|
568
|
+
const stringFormat = (format, description) => ({ type: 'string', format, description })
|
|
569
|
+
|
|
570
|
+
/** JSON schema for an RFC 3339 date-time string (`format: 'date-time'`). */
|
|
571
|
+
export function isoDateTime() {
|
|
572
|
+
return stringFormat('date-time', 'ISO 8601 / RFC 3339 date-time')
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** JSON schema for an RFC 3339 full-date string (`format: 'date'`). */
|
|
576
|
+
export function isoDate() {
|
|
577
|
+
return stringFormat('date', 'ISO 8601 / RFC 3339 date')
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** JSON schema for an RFC 3339 full-time string (`format: 'time'`). */
|
|
581
|
+
export function isoTime() {
|
|
582
|
+
return stringFormat('time', 'ISO 8601 / RFC 3339 time')
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** JSON schema for an ISO 8601 duration string (`format: 'duration'`, e.g. `P1DT2H`). */
|
|
586
|
+
export function duration() {
|
|
587
|
+
return stringFormat('duration', 'ISO 8601 duration')
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** JSON schema for a URI string (`format: 'uri'`). */
|
|
591
|
+
export function uri() {
|
|
592
|
+
return stringFormat('uri', 'URI')
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** JSON schema for a UUID string (`format: 'uuid'`). */
|
|
596
|
+
export function uuid() {
|
|
597
|
+
return stringFormat('uuid', 'UUID')
|
|
598
|
+
}
|
|
599
|
+
|
|
404
600
|
/**
|
|
405
601
|
* JSON schema for an email-address string, checked against the `email` format
|
|
406
602
|
* ({@link EMPYRIA_FORMATS}, which mirrors {@link P_EMAIL}).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empyria/common",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Shared backend goodies for Empyria",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Imre Fazekas <imre.fazekas@icloud.com>",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@empyria/classification": "0.1.6",
|
|
23
|
-
"ata-validator": "1.
|
|
23
|
+
"ata-validator": "1.18.0",
|
|
24
24
|
"clerobee": "1.0.8",
|
|
25
25
|
"ms": "2.1.3"
|
|
26
26
|
},
|
package/test/Ata.test.js
CHANGED
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
schemaOptions,
|
|
16
16
|
setAtaDefaults,
|
|
17
17
|
getAtaDefaults,
|
|
18
|
+
setDefaultDialect,
|
|
19
|
+
getDefaultDialect,
|
|
20
|
+
DIALECTS,
|
|
18
21
|
EMPYRIA_FORMATS,
|
|
19
22
|
bool,
|
|
20
23
|
string,
|
|
@@ -24,6 +27,17 @@ import {
|
|
|
24
27
|
logFile,
|
|
25
28
|
nodeEnv,
|
|
26
29
|
number,
|
|
30
|
+
integer,
|
|
31
|
+
constant,
|
|
32
|
+
nullable,
|
|
33
|
+
array,
|
|
34
|
+
discriminated,
|
|
35
|
+
isoDateTime,
|
|
36
|
+
isoDate,
|
|
37
|
+
isoTime,
|
|
38
|
+
duration,
|
|
39
|
+
uri,
|
|
40
|
+
uuid,
|
|
27
41
|
email,
|
|
28
42
|
password,
|
|
29
43
|
ip4,
|
|
@@ -329,3 +343,145 @@ describe('parse / createParser', () => {
|
|
|
329
343
|
expect(typeof createValidator).toBe('function')
|
|
330
344
|
})
|
|
331
345
|
})
|
|
346
|
+
|
|
347
|
+
describe('scalar builders — constraints', () => {
|
|
348
|
+
test('string accepts a number shorthand or an options object', () => {
|
|
349
|
+
expect(string('x', 3)).toEqual({ type: 'string', default: 'x', minLength: 3 })
|
|
350
|
+
expect(
|
|
351
|
+
string(undefined, { minLength: 0, maxLength: 8, pattern: '^a', format: 'uri' }),
|
|
352
|
+
).toEqual({
|
|
353
|
+
type: 'string',
|
|
354
|
+
minLength: 0,
|
|
355
|
+
maxLength: 8,
|
|
356
|
+
pattern: '^a',
|
|
357
|
+
format: 'uri',
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
test('number/integer carry bounds; integer types as integer', () => {
|
|
362
|
+
expect(number(5)).toEqual({ type: 'number', default: 5 })
|
|
363
|
+
expect(integer(8100, { min: 1, max: 65535 })).toEqual({
|
|
364
|
+
type: 'integer',
|
|
365
|
+
default: 8100,
|
|
366
|
+
minimum: 1,
|
|
367
|
+
maximum: 65535,
|
|
368
|
+
})
|
|
369
|
+
expect(integer(undefined, { exclusiveMin: 0, multipleOf: 2 })).toEqual({
|
|
370
|
+
type: 'integer',
|
|
371
|
+
exclusiveMinimum: 0,
|
|
372
|
+
multipleOf: 2,
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
test('an integer schema rejects a real (and, coerced, a non-integer string)', () => {
|
|
377
|
+
const schema = defineSchema({ port: integer() })
|
|
378
|
+
expect(() => validate(schema, { port: 3.5 })).toThrow(EmpyriaError)
|
|
379
|
+
expect(() => validate(schema, { port: '3.5' }, { coerceTypes: true })).toThrow(EmpyriaError)
|
|
380
|
+
expect(validate(schema, { port: '3000' }, { coerceTypes: true })).toEqual({ port: 3000 })
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
test('enumType can carry a type alongside enum', () => {
|
|
384
|
+
expect(enumType(['a', 'b'], 'a')).toEqual({ enum: ['a', 'b'], default: 'a' })
|
|
385
|
+
expect(enumType(['a', 'b'], undefined, { type: 'string' })).toEqual({
|
|
386
|
+
type: 'string',
|
|
387
|
+
enum: ['a', 'b'],
|
|
388
|
+
})
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
test('constant / nullable', () => {
|
|
392
|
+
expect(constant('circle')).toEqual({ const: 'circle' })
|
|
393
|
+
expect(nullable(string('x'))).toEqual({ type: ['string', 'null'], default: 'x' })
|
|
394
|
+
expect(nullable(enumType(['a']))).toEqual({ anyOf: [{ enum: ['a'] }, { type: 'null' }] })
|
|
395
|
+
const schema = defineSchema({ note: nullable(string()) })
|
|
396
|
+
expect(validate(schema, { note: null })).toEqual({ note: null })
|
|
397
|
+
expect(validate(schema, { note: 'hi' })).toEqual({ note: 'hi' })
|
|
398
|
+
expect(() => validate(schema, { note: 3 })).toThrow(EmpyriaError)
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
test('array builder', () => {
|
|
402
|
+
expect(array(string(), { minItems: 1, uniqueItems: true })).toEqual({
|
|
403
|
+
type: 'array',
|
|
404
|
+
items: { type: 'string' },
|
|
405
|
+
minItems: 1,
|
|
406
|
+
uniqueItems: true,
|
|
407
|
+
})
|
|
408
|
+
const schema = defineSchema({ tags: array(string()) })
|
|
409
|
+
expect(validate(schema, { tags: ['a', 'b'] })).toEqual({ tags: ['a', 'b'] })
|
|
410
|
+
expect(() => validate(schema, { tags: ['a', 2] })).toThrow(EmpyriaError)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
test('format wrappers', () => {
|
|
414
|
+
expect(isoDateTime()).toEqual({
|
|
415
|
+
type: 'string',
|
|
416
|
+
format: 'date-time',
|
|
417
|
+
description: expect.any(String),
|
|
418
|
+
})
|
|
419
|
+
expect([isoDate(), isoTime(), duration(), uri(), uuid()].map((s) => s.format)).toEqual([
|
|
420
|
+
'date',
|
|
421
|
+
'time',
|
|
422
|
+
'duration',
|
|
423
|
+
'uri',
|
|
424
|
+
'uuid',
|
|
425
|
+
])
|
|
426
|
+
const schema = defineSchema({ at: isoDateTime() })
|
|
427
|
+
expect(validate(schema, { at: '2026-09-10T09:28:09Z' })).toEqual({
|
|
428
|
+
at: '2026-09-10T09:28:09Z',
|
|
429
|
+
})
|
|
430
|
+
expect(() => validate(schema, { at: 'not-a-date' })).toThrow(EmpyriaError)
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
describe('defineSchema — dialect & unevaluated strict', () => {
|
|
435
|
+
test('dialect stamps $schema from a short name or a raw URI', () => {
|
|
436
|
+
expect(defineSchema({ a: string() }, { dialect: 'v1' }).$schema).toBe(
|
|
437
|
+
'https://json-schema.org/v1',
|
|
438
|
+
)
|
|
439
|
+
expect(defineSchema({ a: string() }, { dialect: 'https://example.test/s' }).$schema).toBe(
|
|
440
|
+
'https://example.test/s',
|
|
441
|
+
)
|
|
442
|
+
expect(defineSchema({ a: string() }).$schema).toBeUndefined()
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
test('setDefaultDialect applies when a call passes no dialect', () => {
|
|
446
|
+
expect(getDefaultDialect()).toBeNull()
|
|
447
|
+
setDefaultDialect('v1')
|
|
448
|
+
try {
|
|
449
|
+
expect(defineSchema({ a: string() }).$schema).toBe('https://json-schema.org/v1')
|
|
450
|
+
expect(defineSchema({ a: string() }, { dialect: '2020-12' }).$schema).toBe(
|
|
451
|
+
'https://json-schema.org/draft/2020-12/schema',
|
|
452
|
+
)
|
|
453
|
+
} finally {
|
|
454
|
+
setDefaultDialect()
|
|
455
|
+
}
|
|
456
|
+
expect(getDefaultDialect()).toBeNull()
|
|
457
|
+
expect(defineSchema({ a: string() }).$schema).toBeUndefined()
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
test("strict: 'unevaluated' keeps additionalProperties open but sets unevaluatedProperties:false", () => {
|
|
461
|
+
const schema = defineSchema({ a: string() }, { strict: 'unevaluated' })
|
|
462
|
+
expect(schema.additionalProperties).toBe(true)
|
|
463
|
+
expect(schema.unevaluatedProperties).toBe(false)
|
|
464
|
+
})
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
describe('discriminated (propertyDependencies)', () => {
|
|
468
|
+
const shape = discriminated('kind', {
|
|
469
|
+
circle: defineSchema({ radius: number() }),
|
|
470
|
+
square: defineSchema({ side: number() }),
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
test('selects the branch by the discriminator value', () => {
|
|
474
|
+
expect(validate(shape, { kind: 'circle', radius: 2 })).toEqual({
|
|
475
|
+
kind: 'circle',
|
|
476
|
+
radius: 2,
|
|
477
|
+
})
|
|
478
|
+
expect(validate(shape, { kind: 'square', side: 3 })).toEqual({ kind: 'square', side: 3 })
|
|
479
|
+
expect(() => validate(shape, { kind: 'circle', side: 3 })).toThrow(EmpyriaError)
|
|
480
|
+
expect(() => validate(shape, { kind: 'triangle' })).toThrow(EmpyriaError)
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
test('pairs with a v1 $schema via dialect', () => {
|
|
484
|
+
const v1shape = { ...shape, $schema: DIALECTS.v1 }
|
|
485
|
+
expect(validate(v1shape, { kind: 'square', side: 3 })).toEqual({ kind: 'square', side: 3 })
|
|
486
|
+
})
|
|
487
|
+
})
|