@forgehive/schema 0.1.3 → 0.2.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/README.md +207 -0
- package/dist/index.d.ts +79 -71
- package/dist/index.js +75 -182
- package/dist/test/custom-validations.test.js +45 -131
- package/dist/test/dates.test.js +41 -21
- package/dist/test/describe.test.d.ts +1 -0
- package/dist/test/describe.test.js +172 -0
- package/dist/test/from.test.d.ts +1 -0
- package/dist/test/from.test.js +162 -0
- package/dist/test/index.test.js +130 -30
- package/dist/test/optionals.test.js +22 -18
- package/dist/test/parse.test.d.ts +1 -0
- package/dist/test/parse.test.js +134 -0
- package/dist/test/record.test.js +47 -62
- package/package.json +2 -2
- package/src/index.ts +112 -250
- package/src/test/custom-validations.test.ts +45 -132
- package/src/test/dates.test.ts +45 -22
- package/src/test/describe.test.ts +190 -0
- package/src/test/from.test.ts +191 -0
- package/src/test/index.test.ts +148 -31
- package/src/test/optionals.test.ts +23 -18
- package/src/test/parse.test.ts +160 -0
- package/src/test/record.test.ts +48 -72
- package/tsconfig.json +2 -3
package/dist/test/dates.test.js
CHANGED
|
@@ -1,29 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const index_1 = require("../index");
|
|
4
|
+
// Dates are represented as ISO 8601 date-time strings (Schema.date() ->
|
|
5
|
+
// z.iso.datetime()), which are natively representable in JSON Schema.
|
|
4
6
|
describe('Schema single date field', () => {
|
|
5
|
-
const schema = index_1.Schema
|
|
6
|
-
createdAt:
|
|
7
|
+
const schema = new index_1.Schema({
|
|
8
|
+
createdAt: index_1.Schema.date()
|
|
7
9
|
});
|
|
8
|
-
it('should validate a valid date', () => {
|
|
10
|
+
it('should validate a valid ISO date-time string', () => {
|
|
9
11
|
const data = {
|
|
10
|
-
createdAt: new Date()
|
|
12
|
+
createdAt: new Date().toISOString()
|
|
11
13
|
};
|
|
12
14
|
expect(schema.validate(data)).toBe(true);
|
|
13
15
|
});
|
|
14
|
-
it('should validate a
|
|
16
|
+
it('should validate a fixed ISO date-time string', () => {
|
|
15
17
|
const data = {
|
|
16
|
-
createdAt:
|
|
18
|
+
createdAt: '2024-03-20T12:00:00Z'
|
|
17
19
|
};
|
|
18
20
|
expect(schema.validate(data)).toBe(true);
|
|
19
21
|
});
|
|
20
|
-
it('should reject invalid date', () => {
|
|
22
|
+
it('should reject an invalid date string', () => {
|
|
21
23
|
const data = {
|
|
22
24
|
createdAt: 'invalid-date'
|
|
23
25
|
};
|
|
24
26
|
expect(schema.validate(data)).toBe(false);
|
|
25
27
|
});
|
|
26
|
-
it('should reject
|
|
28
|
+
it('should reject a Date instance (runtime type is a string now)', () => {
|
|
29
|
+
const data = {
|
|
30
|
+
createdAt: new Date()
|
|
31
|
+
};
|
|
32
|
+
expect(schema.validate(data)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
it('should reject a non-date value', () => {
|
|
27
35
|
const data = {
|
|
28
36
|
createdAt: 123
|
|
29
37
|
};
|
|
@@ -31,28 +39,28 @@ describe('Schema single date field', () => {
|
|
|
31
39
|
});
|
|
32
40
|
});
|
|
33
41
|
describe('Schema array of dates', () => {
|
|
34
|
-
const schema = index_1.Schema
|
|
35
|
-
timestamps:
|
|
42
|
+
const schema = new index_1.Schema({
|
|
43
|
+
timestamps: index_1.Schema.array(index_1.Schema.date())
|
|
36
44
|
});
|
|
37
|
-
it('should validate an array of valid
|
|
45
|
+
it('should validate an array of valid ISO date-time strings', () => {
|
|
38
46
|
const data = {
|
|
39
|
-
timestamps: [new Date(), new Date()]
|
|
47
|
+
timestamps: [new Date().toISOString(), new Date().toISOString()]
|
|
40
48
|
};
|
|
41
49
|
expect(schema.validate(data)).toBe(true);
|
|
42
50
|
});
|
|
43
|
-
it('should validate an array of
|
|
51
|
+
it('should validate an array of fixed ISO date-time strings', () => {
|
|
44
52
|
const data = {
|
|
45
|
-
timestamps: [
|
|
53
|
+
timestamps: ['2024-03-20T12:00:00Z', '2024-03-21T12:00:00Z']
|
|
46
54
|
};
|
|
47
55
|
expect(schema.validate(data)).toBe(true);
|
|
48
56
|
});
|
|
49
|
-
it('should reject array with invalid date', () => {
|
|
57
|
+
it('should reject an array with an invalid date string', () => {
|
|
50
58
|
const data = {
|
|
51
|
-
timestamps: [new Date(), 'invalid-date']
|
|
59
|
+
timestamps: [new Date().toISOString(), 'invalid-date']
|
|
52
60
|
};
|
|
53
61
|
expect(schema.validate(data)).toBe(false);
|
|
54
62
|
});
|
|
55
|
-
it('should reject non-array value', () => {
|
|
63
|
+
it('should reject a non-array value', () => {
|
|
56
64
|
const data = {
|
|
57
65
|
timestamps: 'not-an-array'
|
|
58
66
|
};
|
|
@@ -60,15 +68,27 @@ describe('Schema array of dates', () => {
|
|
|
60
68
|
});
|
|
61
69
|
});
|
|
62
70
|
describe('Dates Schema description', () => {
|
|
63
|
-
it('should
|
|
71
|
+
it('should describe date fields as date-time formatted strings', () => {
|
|
64
72
|
const schema = new index_1.Schema({
|
|
65
73
|
createdAt: index_1.Schema.date(),
|
|
66
74
|
timestamps: index_1.Schema.array(index_1.Schema.date())
|
|
67
75
|
});
|
|
68
76
|
const description = schema.describe();
|
|
69
|
-
expect(description).
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
expect(description.properties?.createdAt).toMatchObject({
|
|
78
|
+
type: 'string',
|
|
79
|
+
format: 'date-time'
|
|
80
|
+
});
|
|
81
|
+
expect(description.properties?.timestamps).toMatchObject({
|
|
82
|
+
type: 'array',
|
|
83
|
+
items: { type: 'string', format: 'date-time' }
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
it('should round-trip date validation through describe/from', () => {
|
|
87
|
+
const schema = new index_1.Schema({
|
|
88
|
+
createdAt: index_1.Schema.date()
|
|
72
89
|
});
|
|
90
|
+
const clone = index_1.Schema.from(schema.describe());
|
|
91
|
+
expect(clone.validate({ createdAt: '2024-03-20T12:00:00Z' })).toBe(true);
|
|
92
|
+
expect(clone.validate({ createdAt: 'invalid-date' })).toBe(false);
|
|
73
93
|
});
|
|
74
94
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const index_1 = require("../index");
|
|
4
|
+
// `describe()` serializes a Schema to standard JSON Schema (draft 2020-12).
|
|
5
|
+
// Each test builds a Schema with the Schema.* helpers and asserts the EXACT
|
|
6
|
+
// JSON Schema it produces, so the wire format is obvious at a glance.
|
|
7
|
+
const $schema = 'https://json-schema.org/draft/2020-12/schema';
|
|
8
|
+
describe('Schema.describe() -> JSON Schema', () => {
|
|
9
|
+
it('basic types', () => {
|
|
10
|
+
const schema = new index_1.Schema({
|
|
11
|
+
name: index_1.Schema.string(),
|
|
12
|
+
age: index_1.Schema.number(),
|
|
13
|
+
active: index_1.Schema.boolean(),
|
|
14
|
+
});
|
|
15
|
+
expect(schema.describe()).toEqual({
|
|
16
|
+
$schema,
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
name: { type: 'string' },
|
|
20
|
+
age: { type: 'number' },
|
|
21
|
+
active: { type: 'boolean' },
|
|
22
|
+
},
|
|
23
|
+
required: ['name', 'age', 'active'],
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
it('string validations', () => {
|
|
28
|
+
const schema = new index_1.Schema({
|
|
29
|
+
username: index_1.Schema.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
|
|
30
|
+
});
|
|
31
|
+
expect(schema.describe()).toEqual({
|
|
32
|
+
$schema,
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
username: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
minLength: 3,
|
|
38
|
+
maxLength: 20,
|
|
39
|
+
pattern: '^[a-zA-Z0-9_]+$',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
required: ['username'],
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
it('number validations', () => {
|
|
47
|
+
const schema = new index_1.Schema({
|
|
48
|
+
age: index_1.Schema.number().min(18).max(100),
|
|
49
|
+
});
|
|
50
|
+
expect(schema.describe()).toEqual({
|
|
51
|
+
$schema,
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
age: { type: 'number', minimum: 18, maximum: 100 },
|
|
55
|
+
},
|
|
56
|
+
required: ['age'],
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
it('optional fields are omitted from `required`', () => {
|
|
61
|
+
const schema = new index_1.Schema({
|
|
62
|
+
name: index_1.Schema.string(),
|
|
63
|
+
age: index_1.Schema.number().optional(),
|
|
64
|
+
});
|
|
65
|
+
expect(schema.describe()).toEqual({
|
|
66
|
+
$schema,
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: {
|
|
69
|
+
name: { type: 'string' },
|
|
70
|
+
age: { type: 'number' },
|
|
71
|
+
},
|
|
72
|
+
required: ['name'], // age is optional, so it is not required
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
it('arrays', () => {
|
|
77
|
+
const schema = new index_1.Schema({
|
|
78
|
+
tags: index_1.Schema.array(index_1.Schema.string()),
|
|
79
|
+
scores: index_1.Schema.array(index_1.Schema.number()),
|
|
80
|
+
});
|
|
81
|
+
expect(schema.describe()).toEqual({
|
|
82
|
+
$schema,
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: {
|
|
85
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
86
|
+
scores: { type: 'array', items: { type: 'number' } },
|
|
87
|
+
},
|
|
88
|
+
required: ['tags', 'scores'],
|
|
89
|
+
additionalProperties: false,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
it('records', () => {
|
|
93
|
+
const schema = new index_1.Schema({
|
|
94
|
+
strings: index_1.Schema.stringRecord(),
|
|
95
|
+
mixed: index_1.Schema.mixedRecord(),
|
|
96
|
+
});
|
|
97
|
+
expect(schema.describe()).toEqual({
|
|
98
|
+
$schema,
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
strings: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
propertyNames: { type: 'string' },
|
|
104
|
+
additionalProperties: { type: 'string' },
|
|
105
|
+
},
|
|
106
|
+
mixed: {
|
|
107
|
+
type: 'object',
|
|
108
|
+
propertyNames: { type: 'string' },
|
|
109
|
+
additionalProperties: {
|
|
110
|
+
anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
required: ['strings', 'mixed'],
|
|
115
|
+
additionalProperties: false,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
it('nested objects', () => {
|
|
119
|
+
const schema = new index_1.Schema({
|
|
120
|
+
user: index_1.Schema.object({
|
|
121
|
+
name: index_1.Schema.string(),
|
|
122
|
+
age: index_1.Schema.number().optional(),
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
expect(schema.describe()).toEqual({
|
|
126
|
+
$schema,
|
|
127
|
+
type: 'object',
|
|
128
|
+
properties: {
|
|
129
|
+
user: {
|
|
130
|
+
type: 'object',
|
|
131
|
+
properties: {
|
|
132
|
+
name: { type: 'string' },
|
|
133
|
+
age: { type: 'number' },
|
|
134
|
+
},
|
|
135
|
+
required: ['name'],
|
|
136
|
+
additionalProperties: false,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
required: ['user'],
|
|
140
|
+
additionalProperties: false,
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
it('element descriptions', () => {
|
|
144
|
+
const schema = new index_1.Schema({
|
|
145
|
+
name: index_1.Schema.string().describe('The name of the user'),
|
|
146
|
+
});
|
|
147
|
+
expect(schema.describe()).toEqual({
|
|
148
|
+
$schema,
|
|
149
|
+
type: 'object',
|
|
150
|
+
properties: {
|
|
151
|
+
name: { type: 'string', description: 'The name of the user' },
|
|
152
|
+
},
|
|
153
|
+
required: ['name'],
|
|
154
|
+
additionalProperties: false,
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
it('string formats: email, uuid, url, date', () => {
|
|
158
|
+
const schema = new index_1.Schema({
|
|
159
|
+
email: index_1.Schema.email(),
|
|
160
|
+
id: index_1.Schema.uuid(),
|
|
161
|
+
site: index_1.Schema.url(),
|
|
162
|
+
when: index_1.Schema.date(),
|
|
163
|
+
});
|
|
164
|
+
// Format keywords are the stable part; the generated `pattern` strings are
|
|
165
|
+
// long and version-specific, so we assert the meaningful keys.
|
|
166
|
+
const properties = schema.describe().properties;
|
|
167
|
+
expect(properties?.email).toMatchObject({ type: 'string', format: 'email' });
|
|
168
|
+
expect(properties?.id).toMatchObject({ type: 'string', format: 'uuid' });
|
|
169
|
+
expect(properties?.site).toMatchObject({ type: 'string', format: 'uri' });
|
|
170
|
+
expect(properties?.when).toMatchObject({ type: 'string', format: 'date-time' });
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const index_1 = require("../index");
|
|
4
|
+
// `Schema.from()` rebuilds a Schema from standard JSON Schema. Each test feeds
|
|
5
|
+
// an EXPLICIT JSON Schema object and then validates concrete data, so the
|
|
6
|
+
// accepted input format and its effect are obvious at a glance.
|
|
7
|
+
describe('Schema.from(JSON Schema) -> validation', () => {
|
|
8
|
+
it('basic types', () => {
|
|
9
|
+
const jsonSchema = {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
name: { type: 'string' },
|
|
13
|
+
age: { type: 'number' },
|
|
14
|
+
active: { type: 'boolean' },
|
|
15
|
+
},
|
|
16
|
+
required: ['name', 'age', 'active'],
|
|
17
|
+
};
|
|
18
|
+
const schema = index_1.Schema.from(jsonSchema);
|
|
19
|
+
expect(schema.validate({ name: 'John', age: 30, active: true })).toBe(true);
|
|
20
|
+
expect(schema.validate({ name: 'John', age: '30', active: true })).toBe(false); // age not a number
|
|
21
|
+
expect(schema.validate({ name: 'John', age: 30 })).toBe(false); // active missing
|
|
22
|
+
});
|
|
23
|
+
it('string validations (min, max, pattern)', () => {
|
|
24
|
+
const jsonSchema = {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
username: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
minLength: 3,
|
|
30
|
+
maxLength: 20,
|
|
31
|
+
pattern: '^[a-zA-Z0-9_]+$',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ['username'],
|
|
35
|
+
};
|
|
36
|
+
const schema = index_1.Schema.from(jsonSchema);
|
|
37
|
+
expect(schema.validate({ username: 'john_doe' })).toBe(true);
|
|
38
|
+
expect(schema.validate({ username: 'ab' })).toBe(false); // too short
|
|
39
|
+
expect(schema.validate({ username: 'a'.repeat(21) })).toBe(false); // too long
|
|
40
|
+
expect(schema.validate({ username: 'john-doe' })).toBe(false); // pattern mismatch
|
|
41
|
+
});
|
|
42
|
+
it('number validations (minimum, maximum)', () => {
|
|
43
|
+
const jsonSchema = {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
age: { type: 'number', minimum: 18, maximum: 100 },
|
|
47
|
+
},
|
|
48
|
+
required: ['age'],
|
|
49
|
+
};
|
|
50
|
+
const schema = index_1.Schema.from(jsonSchema);
|
|
51
|
+
expect(schema.validate({ age: 25 })).toBe(true);
|
|
52
|
+
expect(schema.validate({ age: 18 })).toBe(true);
|
|
53
|
+
expect(schema.validate({ age: 17 })).toBe(false); // below minimum
|
|
54
|
+
expect(schema.validate({ age: 101 })).toBe(false); // above maximum
|
|
55
|
+
});
|
|
56
|
+
it('optional fields (absent from `required`)', () => {
|
|
57
|
+
const jsonSchema = {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
name: { type: 'string' },
|
|
61
|
+
age: { type: 'number' },
|
|
62
|
+
},
|
|
63
|
+
required: ['name'], // age is optional
|
|
64
|
+
};
|
|
65
|
+
const schema = index_1.Schema.from(jsonSchema);
|
|
66
|
+
expect(schema.validate({ name: 'John', age: 30 })).toBe(true);
|
|
67
|
+
expect(schema.validate({ name: 'John' })).toBe(true); // optional age omitted
|
|
68
|
+
expect(schema.validate({ age: 30 })).toBe(false); // required name omitted
|
|
69
|
+
});
|
|
70
|
+
it('arrays', () => {
|
|
71
|
+
const jsonSchema = {
|
|
72
|
+
type: 'object',
|
|
73
|
+
properties: {
|
|
74
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
75
|
+
},
|
|
76
|
+
required: ['tags'],
|
|
77
|
+
};
|
|
78
|
+
const schema = index_1.Schema.from(jsonSchema);
|
|
79
|
+
expect(schema.validate({ tags: ['a', 'b'] })).toBe(true);
|
|
80
|
+
expect(schema.validate({ tags: [] })).toBe(true);
|
|
81
|
+
expect(schema.validate({ tags: ['a', 2] })).toBe(false); // wrong item type
|
|
82
|
+
});
|
|
83
|
+
it('records (additionalProperties)', () => {
|
|
84
|
+
const jsonSchema = {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
data: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
propertyNames: { type: 'string' },
|
|
90
|
+
additionalProperties: { type: 'number' },
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
required: ['data'],
|
|
94
|
+
};
|
|
95
|
+
const schema = index_1.Schema.from(jsonSchema);
|
|
96
|
+
expect(schema.validate({ data: { a: 1, b: 2 } })).toBe(true);
|
|
97
|
+
expect(schema.validate({ data: { a: 'x' } })).toBe(false); // value not a number
|
|
98
|
+
});
|
|
99
|
+
it('mixed records (anyOf additionalProperties)', () => {
|
|
100
|
+
const jsonSchema = {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
data: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
propertyNames: { type: 'string' },
|
|
106
|
+
additionalProperties: {
|
|
107
|
+
anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
required: ['data'],
|
|
112
|
+
};
|
|
113
|
+
const schema = index_1.Schema.from(jsonSchema);
|
|
114
|
+
expect(schema.validate({ data: { a: 'x', b: 1, c: true } })).toBe(true);
|
|
115
|
+
expect(schema.validate({ data: { a: [1, 2] } })).toBe(false); // array not allowed
|
|
116
|
+
});
|
|
117
|
+
it('nested objects', () => {
|
|
118
|
+
const jsonSchema = {
|
|
119
|
+
type: 'object',
|
|
120
|
+
properties: {
|
|
121
|
+
user: {
|
|
122
|
+
type: 'object',
|
|
123
|
+
properties: {
|
|
124
|
+
name: { type: 'string', minLength: 2 },
|
|
125
|
+
age: { type: 'number' },
|
|
126
|
+
},
|
|
127
|
+
required: ['name'],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
required: ['user'],
|
|
131
|
+
};
|
|
132
|
+
const schema = index_1.Schema.from(jsonSchema);
|
|
133
|
+
expect(schema.validate({ user: { name: 'John', age: 30 } })).toBe(true);
|
|
134
|
+
expect(schema.validate({ user: { name: 'John' } })).toBe(true); // nested age optional
|
|
135
|
+
expect(schema.validate({ user: { name: 'J' } })).toBe(false); // name too short
|
|
136
|
+
expect(schema.validate({ user: { age: 30 } })).toBe(false); // nested name required
|
|
137
|
+
});
|
|
138
|
+
it('string format: email', () => {
|
|
139
|
+
const jsonSchema = {
|
|
140
|
+
type: 'object',
|
|
141
|
+
properties: {
|
|
142
|
+
email: { type: 'string', format: 'email' },
|
|
143
|
+
},
|
|
144
|
+
required: ['email'],
|
|
145
|
+
};
|
|
146
|
+
const schema = index_1.Schema.from(jsonSchema);
|
|
147
|
+
expect(schema.validate({ email: 'john@example.com' })).toBe(true);
|
|
148
|
+
expect(schema.validate({ email: 'not-an-email' })).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
it('string format: date-time', () => {
|
|
151
|
+
const jsonSchema = {
|
|
152
|
+
type: 'object',
|
|
153
|
+
properties: {
|
|
154
|
+
createdAt: { type: 'string', format: 'date-time' },
|
|
155
|
+
},
|
|
156
|
+
required: ['createdAt'],
|
|
157
|
+
};
|
|
158
|
+
const schema = index_1.Schema.from(jsonSchema);
|
|
159
|
+
expect(schema.validate({ createdAt: '2024-03-20T12:00:00Z' })).toBe(true);
|
|
160
|
+
expect(schema.validate({ createdAt: 'not-a-date' })).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
});
|
package/dist/test/index.test.js
CHANGED
|
@@ -26,15 +26,17 @@ describe('Schema basic types', () => {
|
|
|
26
26
|
expect(result).toEqual(true);
|
|
27
27
|
});
|
|
28
28
|
});
|
|
29
|
-
describe('Schema description', () => {
|
|
29
|
+
describe('Schema description (JSON Schema)', () => {
|
|
30
30
|
it('should describe a string', () => {
|
|
31
31
|
const schema = new index_1.default({
|
|
32
32
|
name: index_1.default.string(),
|
|
33
33
|
});
|
|
34
34
|
const result = schema.describe();
|
|
35
|
-
expect(result).
|
|
35
|
+
expect(result.type).toBe('object');
|
|
36
|
+
expect(result.properties).toEqual({
|
|
36
37
|
name: { type: 'string' },
|
|
37
38
|
});
|
|
39
|
+
expect(result.required).toEqual(['name']);
|
|
38
40
|
});
|
|
39
41
|
it('should describe a string and number', () => {
|
|
40
42
|
const schema = new index_1.default({
|
|
@@ -42,24 +44,26 @@ describe('Schema description', () => {
|
|
|
42
44
|
age: index_1.default.number(),
|
|
43
45
|
});
|
|
44
46
|
const result = schema.describe();
|
|
45
|
-
expect(result).toEqual({
|
|
47
|
+
expect(result.properties).toEqual({
|
|
46
48
|
name: { type: 'string' },
|
|
47
49
|
age: { type: 'number' },
|
|
48
50
|
});
|
|
51
|
+
expect(result.required).toEqual(['name', 'age']);
|
|
49
52
|
});
|
|
50
53
|
});
|
|
51
54
|
describe('Schema hydrate', () => {
|
|
52
|
-
it('should
|
|
53
|
-
const
|
|
54
|
-
name:
|
|
55
|
-
age:
|
|
56
|
-
};
|
|
57
|
-
const schema = index_1.default.from(
|
|
55
|
+
it('should rebuild a schema from its description', () => {
|
|
56
|
+
const original = new index_1.default({
|
|
57
|
+
name: index_1.default.string(),
|
|
58
|
+
age: index_1.default.number(),
|
|
59
|
+
});
|
|
60
|
+
const schema = index_1.default.from(original.describe());
|
|
58
61
|
const result = schema.describe();
|
|
59
|
-
expect(result).toEqual({
|
|
62
|
+
expect(result.properties).toEqual({
|
|
60
63
|
name: { type: 'string' },
|
|
61
64
|
age: { type: 'number' },
|
|
62
65
|
});
|
|
66
|
+
expect(result.required).toEqual(['name', 'age']);
|
|
63
67
|
});
|
|
64
68
|
});
|
|
65
69
|
describe('Schema validation errors', () => {
|
|
@@ -82,18 +86,27 @@ describe('Schema validation errors', () => {
|
|
|
82
86
|
expect(result).toBe(false);
|
|
83
87
|
});
|
|
84
88
|
});
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
89
|
+
describe('Schema optional fields', () => {
|
|
90
|
+
it('should validate with missing optional field', () => {
|
|
91
|
+
const schema = new index_1.default({
|
|
92
|
+
name: index_1.default.string(),
|
|
93
|
+
age: index_1.default.number().optional(),
|
|
94
|
+
});
|
|
95
|
+
const result = schema.validate({
|
|
96
|
+
name: 'World',
|
|
97
|
+
});
|
|
98
|
+
expect(result).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
it('should omit optional fields from the required list', () => {
|
|
101
|
+
const schema = new index_1.default({
|
|
102
|
+
name: index_1.default.string(),
|
|
103
|
+
age: index_1.default.number().optional(),
|
|
104
|
+
});
|
|
105
|
+
const result = schema.describe();
|
|
106
|
+
expect(result.required).toEqual(['name']);
|
|
107
|
+
expect(result.properties).toHaveProperty('age');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
97
110
|
describe('Schema arrays', () => {
|
|
98
111
|
it('should validate array of strings', () => {
|
|
99
112
|
const schema = new index_1.default({
|
|
@@ -137,29 +150,90 @@ describe('Schema arrays', () => {
|
|
|
137
150
|
scores: index_1.default.array(index_1.default.number()),
|
|
138
151
|
});
|
|
139
152
|
const result = schema.describe();
|
|
140
|
-
expect(result).toEqual({
|
|
153
|
+
expect(result.properties).toEqual({
|
|
141
154
|
tags: { type: 'array', items: { type: 'string' } },
|
|
142
155
|
scores: { type: 'array', items: { type: 'number' } },
|
|
143
156
|
});
|
|
144
157
|
});
|
|
145
158
|
it('should hydrate array schema from description', () => {
|
|
146
|
-
const
|
|
147
|
-
tags:
|
|
148
|
-
scores:
|
|
149
|
-
};
|
|
150
|
-
const schema = index_1.default.from(
|
|
159
|
+
const original = new index_1.default({
|
|
160
|
+
tags: index_1.default.array(index_1.default.string()),
|
|
161
|
+
scores: index_1.default.array(index_1.default.number()),
|
|
162
|
+
});
|
|
163
|
+
const schema = index_1.default.from(original.describe());
|
|
151
164
|
const result = schema.describe();
|
|
152
|
-
expect(result).toEqual({
|
|
165
|
+
expect(result.properties).toEqual({
|
|
153
166
|
tags: { type: 'array', items: { type: 'string' } },
|
|
154
167
|
scores: { type: 'array', items: { type: 'number' } },
|
|
155
168
|
});
|
|
156
169
|
});
|
|
157
170
|
});
|
|
171
|
+
describe('Schema nested objects', () => {
|
|
172
|
+
it('should validate and describe nested object fields', () => {
|
|
173
|
+
const schema = new index_1.default({
|
|
174
|
+
user: index_1.default.object({
|
|
175
|
+
name: index_1.default.string(),
|
|
176
|
+
age: index_1.default.number().optional(),
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
expect(schema.validate({ user: { name: 'John', age: 30 } })).toBe(true);
|
|
180
|
+
expect(schema.validate({ user: { name: 'John' } })).toBe(true);
|
|
181
|
+
expect(schema.validate({ user: { age: 30 } })).toBe(false);
|
|
182
|
+
const result = schema.describe();
|
|
183
|
+
expect(result.properties?.user).toMatchObject({
|
|
184
|
+
type: 'object',
|
|
185
|
+
properties: { name: { type: 'string' }, age: { type: 'number' } },
|
|
186
|
+
required: ['name'],
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
it('should round-trip a nested object schema', () => {
|
|
190
|
+
const original = new index_1.default({
|
|
191
|
+
user: index_1.default.object({
|
|
192
|
+
name: index_1.default.string().min(3),
|
|
193
|
+
}),
|
|
194
|
+
});
|
|
195
|
+
const clone = index_1.default.from(original.describe());
|
|
196
|
+
expect(clone.validate({ user: { name: 'John' } })).toBe(true);
|
|
197
|
+
expect(clone.validate({ user: { name: 'Jo' } })).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
it('should infer types through safeParse for an optional nested object', () => {
|
|
200
|
+
const base = new index_1.default({
|
|
201
|
+
name: index_1.default.string().describe('The name of the user'),
|
|
202
|
+
address: index_1.default.object({
|
|
203
|
+
street: index_1.default.string(),
|
|
204
|
+
city: index_1.default.string(),
|
|
205
|
+
state: index_1.default.string(),
|
|
206
|
+
zip: index_1.default.string(),
|
|
207
|
+
}).optional().describe('The address of the user'),
|
|
208
|
+
});
|
|
209
|
+
// Optional nested object omitted
|
|
210
|
+
const withoutAddress = base.safeParse({ name: 'World' });
|
|
211
|
+
expect(withoutAddress.success).toBe(true);
|
|
212
|
+
if (withoutAddress.success) {
|
|
213
|
+
// res.data.name is string, res.data.address is optional
|
|
214
|
+
const name = withoutAddress.data.name;
|
|
215
|
+
const street = withoutAddress.data.address?.street ?? '';
|
|
216
|
+
expect(name).toBe('World');
|
|
217
|
+
expect(street).toBe('');
|
|
218
|
+
}
|
|
219
|
+
// Optional nested object present
|
|
220
|
+
const withAddress = base.safeParse({
|
|
221
|
+
name: 'World',
|
|
222
|
+
address: { street: 'Main', city: 'Town', state: 'CA', zip: '00000' },
|
|
223
|
+
});
|
|
224
|
+
expect(withAddress.success).toBe(true);
|
|
225
|
+
if (withAddress.success) {
|
|
226
|
+
expect(withAddress.data.address?.street).toBe('Main');
|
|
227
|
+
}
|
|
228
|
+
// Partial nested object is rejected (street/city/... required when present)
|
|
229
|
+
expect(base.validate({ name: 'World', address: { street: 'Main' } })).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
158
232
|
describe('Schema custom validation', () => {
|
|
159
233
|
it('should validate with custom rules', () => {
|
|
160
234
|
const schema = new index_1.default({
|
|
161
235
|
age: index_1.default.number().min(0).max(120),
|
|
162
|
-
email: index_1.default.
|
|
236
|
+
email: index_1.default.email(),
|
|
163
237
|
});
|
|
164
238
|
const result = schema.validate({
|
|
165
239
|
age: 25,
|
|
@@ -167,4 +241,30 @@ describe('Schema custom validation', () => {
|
|
|
167
241
|
});
|
|
168
242
|
expect(result).toBe(true);
|
|
169
243
|
});
|
|
244
|
+
it('should serialize element descriptions', () => {
|
|
245
|
+
const schema = new index_1.default({
|
|
246
|
+
name: index_1.default.string().describe('the user name'),
|
|
247
|
+
});
|
|
248
|
+
const result = schema.describe();
|
|
249
|
+
expect(result.properties?.name).toMatchObject({
|
|
250
|
+
type: 'string',
|
|
251
|
+
description: 'the user name',
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
it('should keep element descriptions through the describe/from round-trip', () => {
|
|
255
|
+
const schema = new index_1.default({
|
|
256
|
+
name: index_1.default.string().describe('The name of the user'),
|
|
257
|
+
age: index_1.default.number().describe('The age of the user'),
|
|
258
|
+
});
|
|
259
|
+
const clone = index_1.default.from(schema.describe());
|
|
260
|
+
const result = clone.describe();
|
|
261
|
+
expect(result.properties?.name).toMatchObject({
|
|
262
|
+
type: 'string',
|
|
263
|
+
description: 'The name of the user',
|
|
264
|
+
});
|
|
265
|
+
expect(result.properties?.age).toMatchObject({
|
|
266
|
+
type: 'number',
|
|
267
|
+
description: 'The age of the user',
|
|
268
|
+
});
|
|
269
|
+
});
|
|
170
270
|
});
|