@forgehive/schema 0.1.4 → 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 +105 -59
- package/dist/index.d.ts +74 -71
- package/dist/index.js +68 -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 +105 -251
- 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/index.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Schema = void 0;
|
|
3
|
+
exports.Schema = exports.z = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
|
-
|
|
5
|
+
Object.defineProperty(exports, "z", { enumerable: true, get: function () { return zod_1.z; } });
|
|
6
|
+
/**
|
|
7
|
+
* A thin wrapper around a zod object schema. Fields are created with the static
|
|
8
|
+
* `Schema.*` helpers so call sites stay independent of the underlying validation
|
|
9
|
+
* library; the wrapper owns validation plus JSON Schema serialization
|
|
10
|
+
* (`describe`) and rehydration (`from`).
|
|
11
|
+
*/
|
|
6
12
|
class Schema {
|
|
7
13
|
constructor(fields) {
|
|
8
14
|
this.schema = zod_1.z.object(fields);
|
|
@@ -14,6 +20,13 @@ class Schema {
|
|
|
14
20
|
static string() {
|
|
15
21
|
return zod_1.z.string();
|
|
16
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Creates a number schema
|
|
25
|
+
* @returns A number schema
|
|
26
|
+
*/
|
|
27
|
+
static number() {
|
|
28
|
+
return zod_1.z.number();
|
|
29
|
+
}
|
|
17
30
|
/**
|
|
18
31
|
* Creates a boolean schema
|
|
19
32
|
* @returns A boolean schema
|
|
@@ -22,18 +35,49 @@ class Schema {
|
|
|
22
35
|
return zod_1.z.boolean();
|
|
23
36
|
}
|
|
24
37
|
/**
|
|
25
|
-
* Creates
|
|
26
|
-
*
|
|
38
|
+
* Creates an ISO 8601 date-time schema. The runtime value is a string
|
|
39
|
+
* (e.g. "2020-01-01T00:00:00Z"), which is natively representable in JSON Schema.
|
|
40
|
+
* @returns An ISO date-time schema
|
|
27
41
|
*/
|
|
28
|
-
static
|
|
29
|
-
return zod_1.z.
|
|
42
|
+
static date() {
|
|
43
|
+
return zod_1.z.iso.datetime();
|
|
30
44
|
}
|
|
31
45
|
/**
|
|
32
|
-
* Creates
|
|
33
|
-
* @returns
|
|
46
|
+
* Creates an email string schema (serializes to JSON Schema `format: "email"`).
|
|
47
|
+
* @returns An email schema
|
|
34
48
|
*/
|
|
35
|
-
static
|
|
36
|
-
return zod_1.z.
|
|
49
|
+
static email() {
|
|
50
|
+
return zod_1.z.email();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Creates a UUID string schema (serializes to JSON Schema `format: "uuid"`).
|
|
54
|
+
* @returns A UUID schema
|
|
55
|
+
*/
|
|
56
|
+
static uuid() {
|
|
57
|
+
return zod_1.z.uuid();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Creates a URL string schema (serializes to JSON Schema `format: "uri"`).
|
|
61
|
+
* @returns A URL schema
|
|
62
|
+
*/
|
|
63
|
+
static url() {
|
|
64
|
+
return zod_1.z.url();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Creates an array schema
|
|
68
|
+
* @param type The type of items in the array
|
|
69
|
+
* @returns An array schema
|
|
70
|
+
*/
|
|
71
|
+
static array(type) {
|
|
72
|
+
return zod_1.z.array(type);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Creates a nested object schema
|
|
76
|
+
* @param fields The fields of the object
|
|
77
|
+
* @returns An object schema
|
|
78
|
+
*/
|
|
79
|
+
static object(fields) {
|
|
80
|
+
return zod_1.z.object(fields);
|
|
37
81
|
}
|
|
38
82
|
/**
|
|
39
83
|
* Creates a record schema with string keys and string values
|
|
@@ -63,14 +107,6 @@ class Schema {
|
|
|
63
107
|
static mixedRecord() {
|
|
64
108
|
return zod_1.z.record(zod_1.z.string(), zod_1.z.union([zod_1.z.string(), zod_1.z.number(), zod_1.z.boolean()]));
|
|
65
109
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Creates an array schema
|
|
68
|
-
* @param type The type of items in the array
|
|
69
|
-
* @returns An array schema
|
|
70
|
-
*/
|
|
71
|
-
static array(type) {
|
|
72
|
-
return zod_1.z.array(type);
|
|
73
|
-
}
|
|
74
110
|
/**
|
|
75
111
|
* Infers the TypeScript type from a Schema instance
|
|
76
112
|
* @template S The Schema type
|
|
@@ -81,94 +117,21 @@ class Schema {
|
|
|
81
117
|
return {};
|
|
82
118
|
}
|
|
83
119
|
/**
|
|
84
|
-
* Creates a Schema instance from a description
|
|
85
|
-
*
|
|
120
|
+
* Creates a Schema instance from a JSON Schema description.
|
|
121
|
+
*
|
|
122
|
+
* Note: this relies on zod's `fromJSONSchema`, which zod considers
|
|
123
|
+
* semi-experimental. Round-trips of schemas produced by `describe()` are
|
|
124
|
+
* covered by the package tests.
|
|
125
|
+
*
|
|
126
|
+
* @param description A JSON Schema object describing an object schema
|
|
86
127
|
* @returns A new Schema instance
|
|
87
128
|
*/
|
|
88
129
|
static from(description) {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
let fieldSchema;
|
|
93
|
-
switch (fieldType) {
|
|
94
|
-
case 'string': {
|
|
95
|
-
let stringSchema = Schema.string();
|
|
96
|
-
if (field.validations) {
|
|
97
|
-
const validations = field.validations;
|
|
98
|
-
if (validations.email) {
|
|
99
|
-
stringSchema = stringSchema.email();
|
|
100
|
-
}
|
|
101
|
-
if (validations.minLength !== undefined) {
|
|
102
|
-
stringSchema = stringSchema.min(validations.minLength);
|
|
103
|
-
}
|
|
104
|
-
if (validations.maxLength !== undefined) {
|
|
105
|
-
stringSchema = stringSchema.max(validations.maxLength);
|
|
106
|
-
}
|
|
107
|
-
if (validations.regex !== undefined) {
|
|
108
|
-
stringSchema = stringSchema.regex(new RegExp(validations.regex));
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
fieldSchema = stringSchema;
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
case 'boolean':
|
|
115
|
-
fieldSchema = Schema.boolean();
|
|
116
|
-
break;
|
|
117
|
-
case 'number': {
|
|
118
|
-
let numberSchema = Schema.number();
|
|
119
|
-
if (field.validations) {
|
|
120
|
-
const validations = field.validations;
|
|
121
|
-
if (validations.min !== undefined) {
|
|
122
|
-
numberSchema = numberSchema.min(validations.min);
|
|
123
|
-
}
|
|
124
|
-
if (validations.max !== undefined) {
|
|
125
|
-
numberSchema = numberSchema.max(validations.max);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
fieldSchema = numberSchema;
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
case 'date':
|
|
132
|
-
fieldSchema = Schema.date();
|
|
133
|
-
break;
|
|
134
|
-
case 'stringRecord':
|
|
135
|
-
fieldSchema = Schema.stringRecord();
|
|
136
|
-
break;
|
|
137
|
-
case 'numberRecord':
|
|
138
|
-
fieldSchema = Schema.numberRecord();
|
|
139
|
-
break;
|
|
140
|
-
case 'booleanRecord':
|
|
141
|
-
fieldSchema = Schema.booleanRecord();
|
|
142
|
-
break;
|
|
143
|
-
case 'mixedRecord':
|
|
144
|
-
fieldSchema = Schema.mixedRecord();
|
|
145
|
-
break;
|
|
146
|
-
case 'array': {
|
|
147
|
-
const arrayField = field;
|
|
148
|
-
switch (arrayField.items.type) {
|
|
149
|
-
case 'string':
|
|
150
|
-
fieldSchema = Schema.array(Schema.string());
|
|
151
|
-
break;
|
|
152
|
-
case 'boolean':
|
|
153
|
-
fieldSchema = Schema.array(Schema.boolean());
|
|
154
|
-
break;
|
|
155
|
-
case 'number':
|
|
156
|
-
fieldSchema = Schema.array(Schema.number());
|
|
157
|
-
break;
|
|
158
|
-
case 'date':
|
|
159
|
-
fieldSchema = Schema.array(Schema.date());
|
|
160
|
-
break;
|
|
161
|
-
default:
|
|
162
|
-
throw new Error(`Unsupported array item type: ${arrayField.items.type}`);
|
|
163
|
-
}
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
default:
|
|
167
|
-
throw new Error(`Unsupported type: ${fieldType}`);
|
|
168
|
-
}
|
|
169
|
-
fields[key] = field.optional ? fieldSchema.optional() : fieldSchema;
|
|
130
|
+
const zodSchema = zod_1.z.fromJSONSchema(description);
|
|
131
|
+
if (!(zodSchema instanceof zod_1.z.ZodObject)) {
|
|
132
|
+
throw new Error('Schema.from expects a JSON Schema that describes an object');
|
|
170
133
|
}
|
|
171
|
-
return new Schema(
|
|
134
|
+
return new Schema(zodSchema.shape);
|
|
172
135
|
}
|
|
173
136
|
/**
|
|
174
137
|
* Validates the provided data against the schema
|
|
@@ -197,88 +160,11 @@ class Schema {
|
|
|
197
160
|
return this.schema.safeParse(data);
|
|
198
161
|
}
|
|
199
162
|
/**
|
|
200
|
-
*
|
|
201
|
-
* @returns
|
|
163
|
+
* Serializes the schema to JSON Schema (draft 2020-12).
|
|
164
|
+
* @returns A JSON Schema object describing the schema structure
|
|
202
165
|
*/
|
|
203
166
|
describe() {
|
|
204
|
-
|
|
205
|
-
const description = {};
|
|
206
|
-
for (const [key, value] of Object.entries(shape)) {
|
|
207
|
-
const isOptional = value instanceof zod_1.z.ZodOptional;
|
|
208
|
-
const baseValue = isOptional ? value.unwrap() : value;
|
|
209
|
-
if (baseValue instanceof zod_1.z.ZodString) {
|
|
210
|
-
const validations = {};
|
|
211
|
-
if (baseValue._def.checks) {
|
|
212
|
-
for (const check of baseValue._def.checks) {
|
|
213
|
-
if (check.kind === 'email') {
|
|
214
|
-
validations.email = true;
|
|
215
|
-
}
|
|
216
|
-
else if (check.kind === 'min') {
|
|
217
|
-
validations.minLength = check.value;
|
|
218
|
-
}
|
|
219
|
-
else if (check.kind === 'max') {
|
|
220
|
-
validations.maxLength = check.value;
|
|
221
|
-
}
|
|
222
|
-
else if (check.kind === 'regex') {
|
|
223
|
-
validations.regex = check.regex.toString().replace(/^\/|\/$/g, '').replace(/\\\//g, '/');
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
description[key] = Object.assign(Object.assign({ type: 'string' }, (isOptional && { optional: true })), (Object.keys(validations).length > 0 && { validations }));
|
|
228
|
-
}
|
|
229
|
-
else if (baseValue instanceof zod_1.z.ZodBoolean) {
|
|
230
|
-
description[key] = Object.assign({ type: 'boolean' }, (isOptional && { optional: true }));
|
|
231
|
-
}
|
|
232
|
-
else if (baseValue instanceof zod_1.z.ZodNumber) {
|
|
233
|
-
const validations = {};
|
|
234
|
-
if (baseValue._def.checks) {
|
|
235
|
-
for (const check of baseValue._def.checks) {
|
|
236
|
-
if (check.kind === 'min') {
|
|
237
|
-
validations.min = check.value;
|
|
238
|
-
}
|
|
239
|
-
else if (check.kind === 'max') {
|
|
240
|
-
validations.max = check.value;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
description[key] = Object.assign(Object.assign({ type: 'number' }, (isOptional && { optional: true })), (Object.keys(validations).length > 0 && { validations }));
|
|
245
|
-
}
|
|
246
|
-
else if (baseValue instanceof zod_1.z.ZodDate) {
|
|
247
|
-
description[key] = Object.assign({ type: 'date' }, (isOptional && { optional: true }));
|
|
248
|
-
}
|
|
249
|
-
else if (baseValue instanceof zod_1.z.ZodArray) {
|
|
250
|
-
const element = baseValue.element;
|
|
251
|
-
if (element instanceof zod_1.z.ZodString) {
|
|
252
|
-
description[key] = Object.assign({ type: 'array', items: { type: 'string' } }, (isOptional && { optional: true }));
|
|
253
|
-
}
|
|
254
|
-
else if (element instanceof zod_1.z.ZodBoolean) {
|
|
255
|
-
description[key] = Object.assign({ type: 'array', items: { type: 'boolean' } }, (isOptional && { optional: true }));
|
|
256
|
-
}
|
|
257
|
-
else if (element instanceof zod_1.z.ZodNumber) {
|
|
258
|
-
description[key] = Object.assign({ type: 'array', items: { type: 'number' } }, (isOptional && { optional: true }));
|
|
259
|
-
}
|
|
260
|
-
else if (element instanceof zod_1.z.ZodDate) {
|
|
261
|
-
description[key] = Object.assign({ type: 'array', items: { type: 'date' } }, (isOptional && { optional: true }));
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
else if (baseValue instanceof zod_1.z.ZodRecord) {
|
|
265
|
-
// Check the value type of the record
|
|
266
|
-
const valueType = baseValue._def.valueType;
|
|
267
|
-
if (valueType instanceof zod_1.z.ZodString) {
|
|
268
|
-
description[key] = Object.assign({ type: 'stringRecord' }, (isOptional && { optional: true }));
|
|
269
|
-
}
|
|
270
|
-
else if (valueType instanceof zod_1.z.ZodNumber) {
|
|
271
|
-
description[key] = Object.assign({ type: 'numberRecord' }, (isOptional && { optional: true }));
|
|
272
|
-
}
|
|
273
|
-
else if (valueType instanceof zod_1.z.ZodBoolean) {
|
|
274
|
-
description[key] = Object.assign({ type: 'booleanRecord' }, (isOptional && { optional: true }));
|
|
275
|
-
}
|
|
276
|
-
else if (valueType instanceof zod_1.z.ZodUnion) {
|
|
277
|
-
description[key] = Object.assign({ type: 'mixedRecord' }, (isOptional && { optional: true }));
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
return description;
|
|
167
|
+
return zod_1.z.toJSONSchema(this.schema, { target: 'draft-2020-12' });
|
|
282
168
|
}
|
|
283
169
|
/**
|
|
284
170
|
* Returns the underlying Zod schema object
|
|
@@ -3,21 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const index_1 = require("../index");
|
|
4
4
|
describe('Schema Custom Number Validations', () => {
|
|
5
5
|
it('should validate number min/max constraints', () => {
|
|
6
|
-
const schema = index_1.Schema
|
|
7
|
-
age:
|
|
8
|
-
|
|
9
|
-
validations: {
|
|
10
|
-
min: 18,
|
|
11
|
-
max: 100
|
|
12
|
-
}
|
|
13
|
-
},
|
|
14
|
-
score: {
|
|
15
|
-
type: 'number',
|
|
16
|
-
validations: {
|
|
17
|
-
min: 0,
|
|
18
|
-
max: 100
|
|
19
|
-
}
|
|
20
|
-
}
|
|
6
|
+
const schema = new index_1.Schema({
|
|
7
|
+
age: index_1.Schema.number().min(18).max(100),
|
|
8
|
+
score: index_1.Schema.number().min(0).max(100)
|
|
21
9
|
});
|
|
22
10
|
// Valid cases
|
|
23
11
|
expect(schema.validate({ age: 25, score: 85 })).toBe(true);
|
|
@@ -29,35 +17,22 @@ describe('Schema Custom Number Validations', () => {
|
|
|
29
17
|
expect(schema.validate({ age: 101, score: 85 })).toBe(false);
|
|
30
18
|
expect(schema.validate({ age: 25, score: 101 })).toBe(false);
|
|
31
19
|
});
|
|
32
|
-
it('should
|
|
33
|
-
const schema = index_1.Schema
|
|
34
|
-
age:
|
|
35
|
-
type: 'number',
|
|
36
|
-
validations: {
|
|
37
|
-
min: 18,
|
|
38
|
-
max: 100
|
|
39
|
-
}
|
|
40
|
-
}
|
|
20
|
+
it('should describe number validations as JSON Schema', () => {
|
|
21
|
+
const schema = new index_1.Schema({
|
|
22
|
+
age: index_1.Schema.number().min(18).max(100)
|
|
41
23
|
});
|
|
42
24
|
const description = schema.describe();
|
|
43
|
-
expect(description.age).toEqual({
|
|
25
|
+
expect(description.properties?.age).toEqual({
|
|
44
26
|
type: 'number',
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
max: 100
|
|
48
|
-
}
|
|
27
|
+
minimum: 18,
|
|
28
|
+
maximum: 100
|
|
49
29
|
});
|
|
50
30
|
});
|
|
51
31
|
});
|
|
52
32
|
describe('Schema Custom String Validations', () => {
|
|
53
33
|
it('should validate string email format', () => {
|
|
54
|
-
const schema = index_1.Schema
|
|
55
|
-
email:
|
|
56
|
-
type: 'string',
|
|
57
|
-
validations: {
|
|
58
|
-
email: true
|
|
59
|
-
}
|
|
60
|
-
}
|
|
34
|
+
const schema = new index_1.Schema({
|
|
35
|
+
email: index_1.Schema.email()
|
|
61
36
|
});
|
|
62
37
|
// Valid cases
|
|
63
38
|
expect(schema.validate({ email: 'user@example.com' })).toBe(true);
|
|
@@ -69,14 +44,8 @@ describe('Schema Custom String Validations', () => {
|
|
|
69
44
|
expect(schema.validate({ email: '@domain.com' })).toBe(false);
|
|
70
45
|
});
|
|
71
46
|
it('should validate string length constraints', () => {
|
|
72
|
-
const schema = index_1.Schema
|
|
73
|
-
username:
|
|
74
|
-
type: 'string',
|
|
75
|
-
validations: {
|
|
76
|
-
minLength: 3,
|
|
77
|
-
maxLength: 20
|
|
78
|
-
}
|
|
79
|
-
}
|
|
47
|
+
const schema = new index_1.Schema({
|
|
48
|
+
username: index_1.Schema.string().min(3).max(20)
|
|
80
49
|
});
|
|
81
50
|
// Valid cases
|
|
82
51
|
expect(schema.validate({ username: 'abc' })).toBe(true);
|
|
@@ -87,13 +56,8 @@ describe('Schema Custom String Validations', () => {
|
|
|
87
56
|
expect(schema.validate({ username: 'a'.repeat(21) })).toBe(false);
|
|
88
57
|
});
|
|
89
58
|
it('should validate string regex pattern', () => {
|
|
90
|
-
const schema = index_1.Schema
|
|
91
|
-
username:
|
|
92
|
-
type: 'string',
|
|
93
|
-
validations: {
|
|
94
|
-
regex: '^[a-zA-Z0-9_]+$'
|
|
95
|
-
}
|
|
96
|
-
}
|
|
59
|
+
const schema = new index_1.Schema({
|
|
60
|
+
username: index_1.Schema.string().regex(/^[a-zA-Z0-9_]+$/)
|
|
97
61
|
});
|
|
98
62
|
// Valid cases
|
|
99
63
|
expect(schema.validate({ username: 'john_doe123' })).toBe(true);
|
|
@@ -104,54 +68,31 @@ describe('Schema Custom String Validations', () => {
|
|
|
104
68
|
expect(schema.validate({ username: 'user@123' })).toBe(false);
|
|
105
69
|
expect(schema.validate({ username: 'user name' })).toBe(false);
|
|
106
70
|
});
|
|
107
|
-
it('should
|
|
108
|
-
const schema = index_1.Schema
|
|
109
|
-
email:
|
|
110
|
-
type: 'string',
|
|
111
|
-
validations: {
|
|
112
|
-
email: true,
|
|
113
|
-
minLength: 5,
|
|
114
|
-
maxLength: 100
|
|
115
|
-
}
|
|
116
|
-
}
|
|
71
|
+
it('should describe string length validations as JSON Schema', () => {
|
|
72
|
+
const schema = new index_1.Schema({
|
|
73
|
+
email: index_1.Schema.email().min(5).max(100)
|
|
117
74
|
});
|
|
118
75
|
const description = schema.describe();
|
|
119
|
-
expect(description.email).
|
|
76
|
+
expect(description.properties?.email).toMatchObject({
|
|
120
77
|
type: 'string',
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
maxLength: 100
|
|
125
|
-
}
|
|
78
|
+
format: 'email',
|
|
79
|
+
minLength: 5,
|
|
80
|
+
maxLength: 100
|
|
126
81
|
});
|
|
127
82
|
});
|
|
128
|
-
it('should
|
|
129
|
-
const schema = index_1.Schema
|
|
130
|
-
username:
|
|
131
|
-
type: 'string',
|
|
132
|
-
validations: {
|
|
133
|
-
regex: '^[a-zA-Z0-9_]+$'
|
|
134
|
-
}
|
|
135
|
-
}
|
|
83
|
+
it('should describe string regex validation as a pattern', () => {
|
|
84
|
+
const schema = new index_1.Schema({
|
|
85
|
+
username: index_1.Schema.string().regex(/^[a-zA-Z0-9_]+$/)
|
|
136
86
|
});
|
|
137
87
|
const description = schema.describe();
|
|
138
|
-
expect(description.username).
|
|
88
|
+
expect(description.properties?.username).toMatchObject({
|
|
139
89
|
type: 'string',
|
|
140
|
-
|
|
141
|
-
regex: '^[a-zA-Z0-9_]+$'
|
|
142
|
-
}
|
|
90
|
+
pattern: '^[a-zA-Z0-9_]+$'
|
|
143
91
|
});
|
|
144
92
|
});
|
|
145
93
|
it('should handle regex with other string validations', () => {
|
|
146
|
-
const schema = index_1.Schema
|
|
147
|
-
username:
|
|
148
|
-
type: 'string',
|
|
149
|
-
validations: {
|
|
150
|
-
regex: '^[a-zA-Z0-9_]+$',
|
|
151
|
-
minLength: 3,
|
|
152
|
-
maxLength: 20
|
|
153
|
-
}
|
|
154
|
-
}
|
|
94
|
+
const schema = new index_1.Schema({
|
|
95
|
+
username: index_1.Schema.string().regex(/^[a-zA-Z0-9_]+$/).min(3).max(20)
|
|
155
96
|
});
|
|
156
97
|
// Valid cases
|
|
157
98
|
expect(schema.validate({ username: 'john_doe123' })).toBe(true);
|
|
@@ -164,13 +105,8 @@ describe('Schema Custom String Validations', () => {
|
|
|
164
105
|
expect(schema.validate({ username: 'a'.repeat(21) })).toBe(false);
|
|
165
106
|
});
|
|
166
107
|
it('should handle regex patterns containing forward slashes', () => {
|
|
167
|
-
const schema = index_1.Schema
|
|
168
|
-
path:
|
|
169
|
-
type: 'string',
|
|
170
|
-
validations: {
|
|
171
|
-
regex: '^/api/v[0-9]+/users/[0-9]+$'
|
|
172
|
-
}
|
|
173
|
-
}
|
|
108
|
+
const schema = new index_1.Schema({
|
|
109
|
+
path: index_1.Schema.string().regex(/^\/api\/v[0-9]+\/users\/[0-9]+$/)
|
|
174
110
|
});
|
|
175
111
|
// Valid cases
|
|
176
112
|
expect(schema.validate({ path: '/api/v1/users/123' })).toBe(true);
|
|
@@ -180,23 +116,16 @@ describe('Schema Custom String Validations', () => {
|
|
|
180
116
|
expect(schema.validate({ path: 'api/v1/users/123' })).toBe(false);
|
|
181
117
|
expect(schema.validate({ path: '/api/v1/users' })).toBe(false);
|
|
182
118
|
expect(schema.validate({ path: '/api/v1/users/abc' })).toBe(false);
|
|
183
|
-
// Verify the description preserves the
|
|
119
|
+
// Verify the description preserves the pattern (regex source keeps escaped slashes)
|
|
184
120
|
const description = schema.describe();
|
|
185
|
-
expect(description.path).
|
|
121
|
+
expect(description.properties?.path).toMatchObject({
|
|
186
122
|
type: 'string',
|
|
187
|
-
|
|
188
|
-
regex: '^/api/v[0-9]+/users/[0-9]+$'
|
|
189
|
-
}
|
|
123
|
+
pattern: '^\\/api\\/v[0-9]+\\/users\\/[0-9]+$'
|
|
190
124
|
});
|
|
191
125
|
});
|
|
192
126
|
it('should handle round trip of regex patterns containing forward slashes', () => {
|
|
193
|
-
const schema = index_1.Schema
|
|
194
|
-
path:
|
|
195
|
-
type: 'string',
|
|
196
|
-
validations: {
|
|
197
|
-
regex: '^/api/v[0-9]+/users/[0-9]+$'
|
|
198
|
-
}
|
|
199
|
-
}
|
|
127
|
+
const schema = new index_1.Schema({
|
|
128
|
+
path: index_1.Schema.string().regex(/^\/api\/v[0-9]+\/users\/[0-9]+$/)
|
|
200
129
|
});
|
|
201
130
|
const description = schema.describe();
|
|
202
131
|
const cloneSchema = index_1.Schema.from(description);
|
|
@@ -208,35 +137,20 @@ describe('Schema Custom String Validations', () => {
|
|
|
208
137
|
expect(cloneSchema.validate({ path: 'api/v1/users/123' })).toBe(false);
|
|
209
138
|
expect(cloneSchema.validate({ path: '/api/v1/users' })).toBe(false);
|
|
210
139
|
expect(cloneSchema.validate({ path: '/api/v1/users/abc' })).toBe(false);
|
|
211
|
-
// Verify the description preserves the
|
|
140
|
+
// Verify the description preserves the pattern (regex source keeps escaped slashes)
|
|
212
141
|
const cloneDescription = cloneSchema.describe();
|
|
213
|
-
expect(cloneDescription).toMatchObject(
|
|
142
|
+
expect(cloneDescription.properties?.path).toMatchObject({
|
|
143
|
+
type: 'string',
|
|
144
|
+
pattern: '^\\/api\\/v[0-9]+\\/users\\/[0-9]+$'
|
|
145
|
+
});
|
|
214
146
|
});
|
|
215
147
|
});
|
|
216
148
|
describe('Schema Roundtrip', () => {
|
|
217
149
|
it('should maintain custom validations through describe/from cycle', () => {
|
|
218
|
-
const originalSchema = index_1.Schema
|
|
219
|
-
age:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
min: 18,
|
|
223
|
-
max: 100
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
email: {
|
|
227
|
-
type: 'string',
|
|
228
|
-
validations: {
|
|
229
|
-
email: true
|
|
230
|
-
}
|
|
231
|
-
},
|
|
232
|
-
username: {
|
|
233
|
-
type: 'string',
|
|
234
|
-
validations: {
|
|
235
|
-
regex: '^[a-zA-Z0-9_]+$',
|
|
236
|
-
minLength: 3,
|
|
237
|
-
maxLength: 20
|
|
238
|
-
}
|
|
239
|
-
}
|
|
150
|
+
const originalSchema = new index_1.Schema({
|
|
151
|
+
age: index_1.Schema.number().min(18).max(100),
|
|
152
|
+
email: index_1.Schema.email(),
|
|
153
|
+
username: index_1.Schema.string().regex(/^[a-zA-Z0-9_]+$/).min(3).max(20)
|
|
240
154
|
});
|
|
241
155
|
const description = originalSchema.describe();
|
|
242
156
|
const reconstructedSchema = index_1.Schema.from(description);
|
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 {};
|