@forgehive/schema 0.1.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/LICENSE +21 -0
- package/dist/index.d.ts +133 -0
- package/dist/index.js +285 -0
- package/dist/test/custom-validations.test.d.ts +1 -0
- package/dist/test/custom-validations.test.js +259 -0
- package/dist/test/dates.test.d.ts +1 -0
- package/dist/test/dates.test.js +74 -0
- package/dist/test/index.test.d.ts +1 -0
- package/dist/test/index.test.js +170 -0
- package/dist/test/optionals.test.d.ts +1 -0
- package/dist/test/optionals.test.js +60 -0
- package/dist/test/record.test.d.ts +1 -0
- package/dist/test/record.test.js +303 -0
- package/jest.config.js +11 -0
- package/package.json +20 -0
- package/src/index.ts +361 -0
- package/src/test/custom-validations.test.ts +293 -0
- package/src/test/dates.test.ts +85 -0
- package/src/test/index.test.ts +205 -0
- package/src/test/optionals.test.ts +73 -0
- package/src/test/record.test.ts +368 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const index_1 = __importDefault(require("../index"));
|
|
7
|
+
describe('Schema Record Types', () => {
|
|
8
|
+
describe('String Record', () => {
|
|
9
|
+
it('should validate a record with string values', () => {
|
|
10
|
+
const schema = new index_1.default({
|
|
11
|
+
data: index_1.default.stringRecord(),
|
|
12
|
+
});
|
|
13
|
+
const result = schema.validate({
|
|
14
|
+
data: {
|
|
15
|
+
firstName: 'John',
|
|
16
|
+
lastName: 'Doe',
|
|
17
|
+
occupation: 'Developer'
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
expect(result).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
it('should reject a record with non-string values', () => {
|
|
23
|
+
const schema = new index_1.default({
|
|
24
|
+
data: index_1.default.stringRecord(),
|
|
25
|
+
});
|
|
26
|
+
const result = schema.validate({
|
|
27
|
+
data: {
|
|
28
|
+
name: 'John',
|
|
29
|
+
age: 30, // Number is not allowed
|
|
30
|
+
isActive: true // Boolean is not allowed
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
expect(result).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
it('should create a schema from description with string record type', () => {
|
|
36
|
+
const description = {
|
|
37
|
+
data: {
|
|
38
|
+
type: 'stringRecord',
|
|
39
|
+
optional: false
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const schema = index_1.default.from(description);
|
|
43
|
+
const validData = {
|
|
44
|
+
data: {
|
|
45
|
+
firstName: 'John',
|
|
46
|
+
lastName: 'Doe'
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const result = schema.validate(validData);
|
|
50
|
+
expect(result).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe('Number Record', () => {
|
|
54
|
+
it('should validate a record with number values', () => {
|
|
55
|
+
const schema = new index_1.default({
|
|
56
|
+
data: index_1.default.numberRecord(),
|
|
57
|
+
});
|
|
58
|
+
const result = schema.validate({
|
|
59
|
+
data: {
|
|
60
|
+
age: 30,
|
|
61
|
+
experience: 5,
|
|
62
|
+
salary: 100000
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
expect(result).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
it('should reject a record with non-number values', () => {
|
|
68
|
+
const schema = new index_1.default({
|
|
69
|
+
data: index_1.default.numberRecord(),
|
|
70
|
+
});
|
|
71
|
+
const result = schema.validate({
|
|
72
|
+
data: {
|
|
73
|
+
age: 30,
|
|
74
|
+
name: 'John', // String is not allowed
|
|
75
|
+
isActive: true // Boolean is not allowed
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
expect(result).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
it('should create a schema from description with number record type', () => {
|
|
81
|
+
const description = {
|
|
82
|
+
data: {
|
|
83
|
+
type: 'numberRecord',
|
|
84
|
+
optional: false
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const schema = index_1.default.from(description);
|
|
88
|
+
const validData = {
|
|
89
|
+
data: {
|
|
90
|
+
age: 30,
|
|
91
|
+
experience: 5
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const result = schema.validate(validData);
|
|
95
|
+
expect(result).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('Boolean Record', () => {
|
|
99
|
+
it('should validate a record with boolean values', () => {
|
|
100
|
+
const schema = new index_1.default({
|
|
101
|
+
data: index_1.default.booleanRecord(),
|
|
102
|
+
});
|
|
103
|
+
const result = schema.validate({
|
|
104
|
+
data: {
|
|
105
|
+
isActive: true,
|
|
106
|
+
isAdmin: false,
|
|
107
|
+
hasAccess: true
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
expect(result).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
it('should reject a record with non-boolean values', () => {
|
|
113
|
+
const schema = new index_1.default({
|
|
114
|
+
data: index_1.default.booleanRecord(),
|
|
115
|
+
});
|
|
116
|
+
const result = schema.validate({
|
|
117
|
+
data: {
|
|
118
|
+
isActive: true,
|
|
119
|
+
name: 'John', // String is not allowed
|
|
120
|
+
age: 30 // Number is not allowed
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
expect(result).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
it('should create a schema from description with boolean record type', () => {
|
|
126
|
+
const description = {
|
|
127
|
+
data: {
|
|
128
|
+
type: 'booleanRecord',
|
|
129
|
+
optional: false
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const schema = index_1.default.from(description);
|
|
133
|
+
const validData = {
|
|
134
|
+
data: {
|
|
135
|
+
isActive: true,
|
|
136
|
+
isAdmin: false
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const result = schema.validate(validData);
|
|
140
|
+
expect(result).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe('Mixed Record', () => {
|
|
144
|
+
it('should validate a record with mixed values (string, number, boolean)', () => {
|
|
145
|
+
const schema = new index_1.default({
|
|
146
|
+
data: index_1.default.mixedRecord(),
|
|
147
|
+
});
|
|
148
|
+
const result = schema.validate({
|
|
149
|
+
data: {
|
|
150
|
+
name: 'John',
|
|
151
|
+
age: 30,
|
|
152
|
+
isActive: true
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
expect(result).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
it('should validate a record with only string values', () => {
|
|
158
|
+
const schema = new index_1.default({
|
|
159
|
+
data: index_1.default.mixedRecord(),
|
|
160
|
+
});
|
|
161
|
+
const result = schema.validate({
|
|
162
|
+
data: {
|
|
163
|
+
firstName: 'John',
|
|
164
|
+
lastName: 'Doe',
|
|
165
|
+
occupation: 'Developer'
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
expect(result).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
it('should validate a record with only number values', () => {
|
|
171
|
+
const schema = new index_1.default({
|
|
172
|
+
data: index_1.default.mixedRecord(),
|
|
173
|
+
});
|
|
174
|
+
const result = schema.validate({
|
|
175
|
+
data: {
|
|
176
|
+
age: 30,
|
|
177
|
+
experience: 5,
|
|
178
|
+
salary: 100000
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
expect(result).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
it('should validate a record with only boolean values', () => {
|
|
184
|
+
const schema = new index_1.default({
|
|
185
|
+
data: index_1.default.mixedRecord(),
|
|
186
|
+
});
|
|
187
|
+
const result = schema.validate({
|
|
188
|
+
data: {
|
|
189
|
+
isActive: true,
|
|
190
|
+
isAdmin: false,
|
|
191
|
+
hasAccess: true
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
expect(result).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
it('should reject a record with invalid value types', () => {
|
|
197
|
+
const schema = new index_1.default({
|
|
198
|
+
data: index_1.default.mixedRecord(),
|
|
199
|
+
});
|
|
200
|
+
const result = schema.validate({
|
|
201
|
+
data: {
|
|
202
|
+
name: 'John',
|
|
203
|
+
createdAt: new Date(), // Date is not allowed
|
|
204
|
+
items: [1, 2, 3] // Array is not allowed
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
expect(result).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
it('should create a schema from description with mixed record type', () => {
|
|
210
|
+
const description = {
|
|
211
|
+
data: {
|
|
212
|
+
type: 'mixedRecord',
|
|
213
|
+
optional: false
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
const schema = index_1.default.from(description);
|
|
217
|
+
const validData = {
|
|
218
|
+
data: {
|
|
219
|
+
name: 'John',
|
|
220
|
+
age: 30,
|
|
221
|
+
isActive: true
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
const result = schema.validate(validData);
|
|
225
|
+
expect(result).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
describe('Common Record Functionality', () => {
|
|
229
|
+
it('should reject a record with non-string keys', () => {
|
|
230
|
+
const schema = new index_1.default({
|
|
231
|
+
data: index_1.default.stringRecord(),
|
|
232
|
+
});
|
|
233
|
+
// TypeScript would catch this at compile time, but we're testing runtime behavior
|
|
234
|
+
const invalidData = {
|
|
235
|
+
data: {
|
|
236
|
+
name: 'John',
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
// Add a non-string key using Object.defineProperty
|
|
240
|
+
Object.defineProperty(invalidData.data, 123, {
|
|
241
|
+
value: 'test',
|
|
242
|
+
enumerable: true
|
|
243
|
+
});
|
|
244
|
+
// Note: Zod's record validation doesn't actually check for non-string keys at runtime
|
|
245
|
+
// This is a limitation of JavaScript/TypeScript, as all object keys are converted to strings
|
|
246
|
+
// So this test will actually pass, not fail
|
|
247
|
+
const result = schema.validate(invalidData);
|
|
248
|
+
expect(result).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
it('should create a schema from description with optional record type', () => {
|
|
251
|
+
const description = {
|
|
252
|
+
data: {
|
|
253
|
+
type: 'stringRecord',
|
|
254
|
+
optional: true
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
const schema = index_1.default.from(description);
|
|
258
|
+
// Test with record present
|
|
259
|
+
const validData1 = {
|
|
260
|
+
data: {
|
|
261
|
+
name: 'John'
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
// Test with record missing
|
|
265
|
+
const validData2 = {};
|
|
266
|
+
expect(schema.validate(validData1)).toBe(true);
|
|
267
|
+
expect(schema.validate(validData2)).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
it('should describe a schema with string record type', () => {
|
|
270
|
+
const schema = new index_1.default({
|
|
271
|
+
data: index_1.default.stringRecord(),
|
|
272
|
+
});
|
|
273
|
+
const description = schema.describe();
|
|
274
|
+
expect(description).toHaveProperty('data');
|
|
275
|
+
expect(description.data).toHaveProperty('type', 'stringRecord');
|
|
276
|
+
// The optional property is only included when it's true, not when it's false
|
|
277
|
+
});
|
|
278
|
+
it('should describe a schema with number record type', () => {
|
|
279
|
+
const schema = new index_1.default({
|
|
280
|
+
data: index_1.default.numberRecord(),
|
|
281
|
+
});
|
|
282
|
+
const description = schema.describe();
|
|
283
|
+
expect(description).toHaveProperty('data');
|
|
284
|
+
expect(description.data).toHaveProperty('type', 'numberRecord');
|
|
285
|
+
});
|
|
286
|
+
it('should describe a schema with boolean record type', () => {
|
|
287
|
+
const schema = new index_1.default({
|
|
288
|
+
data: index_1.default.booleanRecord(),
|
|
289
|
+
});
|
|
290
|
+
const description = schema.describe();
|
|
291
|
+
expect(description).toHaveProperty('data');
|
|
292
|
+
expect(description.data).toHaveProperty('type', 'booleanRecord');
|
|
293
|
+
});
|
|
294
|
+
it('should describe a schema with mixed record type', () => {
|
|
295
|
+
const schema = new index_1.default({
|
|
296
|
+
data: index_1.default.mixedRecord(),
|
|
297
|
+
});
|
|
298
|
+
const description = schema.describe();
|
|
299
|
+
expect(description).toHaveProperty('data');
|
|
300
|
+
expect(description.data).toHaveProperty('type', 'mixedRecord');
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
});
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
preset: 'ts-jest',
|
|
4
|
+
testEnvironment: 'node',
|
|
5
|
+
roots: ['<rootDir>/src'],
|
|
6
|
+
testMatch: ['**/src/test/**/*.ts', '**/?(*.)+(spec|test).ts'],
|
|
7
|
+
transform: {
|
|
8
|
+
'^.+\\.ts$': 'ts-jest'
|
|
9
|
+
},
|
|
10
|
+
moduleFileExtensions: ['ts', 'js', 'json', 'node']
|
|
11
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forgehive/schema",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@types/jest": "^29.5.14",
|
|
9
|
+
"jest": "^29.7.0",
|
|
10
|
+
"ts-jest": "^29.1.2"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"zod": "^3.24.2"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"test": "jest",
|
|
18
|
+
"test:watch": "jest --watch"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
// Export a type alias for Schema fields
|
|
4
|
+
export type SchemaType = z.ZodType<string | boolean | number | Date | string[] | boolean[] | number[] | Date[] | Record<string, string | number | boolean>> | z.ZodOptional<z.ZodType<string | boolean | number | Date | string[] | boolean[] | number[] | Date[] | Record<string, string | number | boolean>>>;
|
|
5
|
+
|
|
6
|
+
type AllowedBaseTypes = 'string' | 'boolean' | 'number' | 'date' | 'stringRecord' | 'numberRecord' | 'booleanRecord' | 'mixedRecord'
|
|
7
|
+
type ArrayTypes = z.ZodString | z.ZodBoolean | z.ZodNumber | z.ZodDate
|
|
8
|
+
|
|
9
|
+
type NumberValidations = {
|
|
10
|
+
min?: number
|
|
11
|
+
max?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type StringValidations = {
|
|
15
|
+
email?: boolean
|
|
16
|
+
minLength?: number
|
|
17
|
+
maxLength?: number
|
|
18
|
+
regex?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Export extended Zod types for use throughout the app
|
|
22
|
+
export type ShadowString = z.ZodString
|
|
23
|
+
export type ShadowBoolean = z.ZodBoolean
|
|
24
|
+
export type ShadowNumber = z.ZodNumber
|
|
25
|
+
export type ShadowDate = z.ZodDate
|
|
26
|
+
export type ShadowArray<T extends ArrayTypes> = z.ZodArray<T>
|
|
27
|
+
export type ShadowStringRecord = z.ZodRecord<z.ZodString, z.ZodString>
|
|
28
|
+
export type ShadowNumberRecord = z.ZodRecord<z.ZodString, z.ZodNumber>
|
|
29
|
+
export type ShadowBooleanRecord = z.ZodRecord<z.ZodString, z.ZodBoolean>
|
|
30
|
+
export type ShadowMixedRecord = z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean]>>
|
|
31
|
+
|
|
32
|
+
// Export inferred types for use throughout the app
|
|
33
|
+
export type InferShadowString = z.infer<ShadowString>
|
|
34
|
+
export type InferShadowBoolean = z.infer<ShadowBoolean>
|
|
35
|
+
export type InferShadowNumber = z.infer<ShadowNumber>
|
|
36
|
+
export type InferShadowDate = z.infer<ShadowDate>
|
|
37
|
+
export type InferShadowArray<T extends ArrayTypes> = z.infer<ShadowArray<T>>
|
|
38
|
+
export type InferShadowStringRecord = z.infer<ShadowStringRecord>
|
|
39
|
+
export type InferShadowNumberRecord = z.infer<ShadowNumberRecord>
|
|
40
|
+
export type InferShadowBooleanRecord = z.infer<ShadowBooleanRecord>
|
|
41
|
+
export type InferShadowMixedRecord = z.infer<ShadowMixedRecord>
|
|
42
|
+
|
|
43
|
+
// Export a type utility for inferring schema types
|
|
44
|
+
export type InferSchema<S extends Schema<Record<string, SchemaType>>> = z.infer<S['schema']>
|
|
45
|
+
|
|
46
|
+
type BaseSchemaDescription = {
|
|
47
|
+
type: AllowedBaseTypes
|
|
48
|
+
optional?: boolean
|
|
49
|
+
validations?: NumberValidations | StringValidations
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type ArraySchemaDescription = {
|
|
53
|
+
type: 'array'
|
|
54
|
+
items: {
|
|
55
|
+
type: AllowedBaseTypes
|
|
56
|
+
}
|
|
57
|
+
optional?: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type SchemaDescription = Record<string, BaseSchemaDescription | ArraySchemaDescription>
|
|
61
|
+
|
|
62
|
+
// Static methods for type definitions
|
|
63
|
+
export class Schema<T extends Record<string, z.ZodType<string | boolean | number | Date | string[] | boolean[] | number[] | Date[] | Record<string, string | number | boolean>> | z.ZodOptional<z.ZodType<string | boolean | number | Date | string[] | boolean[] | number[] | Date[] | Record<string, string | number | boolean>>>>> {
|
|
64
|
+
readonly schema: z.ZodObject<T>
|
|
65
|
+
|
|
66
|
+
constructor(fields: T) {
|
|
67
|
+
this.schema = z.object(fields)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Creates a string schema
|
|
72
|
+
* @returns A string schema
|
|
73
|
+
*/
|
|
74
|
+
static string(): ShadowString {
|
|
75
|
+
return z.string()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Creates a boolean schema
|
|
80
|
+
* @returns A boolean schema
|
|
81
|
+
*/
|
|
82
|
+
static boolean(): ShadowBoolean {
|
|
83
|
+
return z.boolean()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Creates a number schema
|
|
88
|
+
* @returns A number schema
|
|
89
|
+
*/
|
|
90
|
+
static number(): ShadowNumber {
|
|
91
|
+
return z.number()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Creates a date schema
|
|
96
|
+
* @returns A date schema
|
|
97
|
+
*/
|
|
98
|
+
static date(): ShadowDate {
|
|
99
|
+
return z.date()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Creates a record schema with string keys and string values
|
|
104
|
+
* @returns A record schema with string values
|
|
105
|
+
*/
|
|
106
|
+
static stringRecord(): ShadowStringRecord {
|
|
107
|
+
return z.record(z.string(), z.string())
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Creates a record schema with string keys and number values
|
|
112
|
+
* @returns A record schema with number values
|
|
113
|
+
*/
|
|
114
|
+
static numberRecord(): ShadowNumberRecord {
|
|
115
|
+
return z.record(z.string(), z.number())
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Creates a record schema with string keys and boolean values
|
|
120
|
+
* @returns A record schema with boolean values
|
|
121
|
+
*/
|
|
122
|
+
static booleanRecord(): ShadowBooleanRecord {
|
|
123
|
+
return z.record(z.string(), z.boolean())
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Creates a record schema with string keys and mixed values (string, number, or boolean)
|
|
128
|
+
* @returns A record schema with mixed values
|
|
129
|
+
*/
|
|
130
|
+
static mixedRecord(): ShadowMixedRecord {
|
|
131
|
+
return z.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Creates an array schema
|
|
136
|
+
* @param type The type of items in the array
|
|
137
|
+
* @returns An array schema
|
|
138
|
+
*/
|
|
139
|
+
static array<T extends ArrayTypes>(type: T): ShadowArray<T> {
|
|
140
|
+
return z.array(type) as ShadowArray<T>
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Infers the TypeScript type from a Schema instance
|
|
145
|
+
* @template S The Schema type
|
|
146
|
+
* @returns The inferred TypeScript type
|
|
147
|
+
*/
|
|
148
|
+
static infer<S extends Schema<Record<string, z.ZodTypeAny>>>(_schema: S): z.infer<S['schema']> {
|
|
149
|
+
// This is a type-level utility, the implementation is not used at runtime
|
|
150
|
+
return {} as z.infer<S['schema']>
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Creates a Schema instance from a description object
|
|
155
|
+
* @param description Object describing the schema structure with type information
|
|
156
|
+
* @returns A new Schema instance
|
|
157
|
+
*/
|
|
158
|
+
static from(description: SchemaDescription): Schema<Record<string, z.ZodType<string | boolean | number | Date | string[] | boolean[] | number[] | Date[] | Record<string, string | number | boolean>> | z.ZodOptional<z.ZodType<string | boolean | number | Date | string[] | boolean[] | number[] | Date[] | Record<string, string | number | boolean>>>>> {
|
|
159
|
+
const fields: Record<string, z.ZodType<string | number | boolean | Date | string[] | number[] | boolean[] | Date[] | Record<string, string | number | boolean>> | z.ZodOptional<z.ZodType<string | number | boolean | Date | string[] | number[] | boolean[] | Date[] | Record<string, string | number | boolean>>>> = {}
|
|
160
|
+
|
|
161
|
+
for (const [key, field] of Object.entries(description)) {
|
|
162
|
+
const fieldType = field.type
|
|
163
|
+
let fieldSchema: z.ZodType<string | boolean | number | Date | string[] | boolean[] | number[] | Date[] | Record<string, string | number | boolean>>
|
|
164
|
+
|
|
165
|
+
switch (fieldType) {
|
|
166
|
+
case 'string': {
|
|
167
|
+
let stringSchema = Schema.string()
|
|
168
|
+
if (field.validations) {
|
|
169
|
+
const validations = field.validations as StringValidations
|
|
170
|
+
if (validations.email) {
|
|
171
|
+
stringSchema = stringSchema.email()
|
|
172
|
+
}
|
|
173
|
+
if (validations.minLength !== undefined) {
|
|
174
|
+
stringSchema = stringSchema.min(validations.minLength)
|
|
175
|
+
}
|
|
176
|
+
if (validations.maxLength !== undefined) {
|
|
177
|
+
stringSchema = stringSchema.max(validations.maxLength)
|
|
178
|
+
}
|
|
179
|
+
if (validations.regex !== undefined) {
|
|
180
|
+
stringSchema = stringSchema.regex(new RegExp(validations.regex))
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
fieldSchema = stringSchema
|
|
184
|
+
break
|
|
185
|
+
}
|
|
186
|
+
case 'boolean':
|
|
187
|
+
fieldSchema = Schema.boolean()
|
|
188
|
+
break
|
|
189
|
+
case 'number': {
|
|
190
|
+
let numberSchema = Schema.number()
|
|
191
|
+
if (field.validations) {
|
|
192
|
+
const validations = field.validations as NumberValidations
|
|
193
|
+
if (validations.min !== undefined) {
|
|
194
|
+
numberSchema = numberSchema.min(validations.min)
|
|
195
|
+
}
|
|
196
|
+
if (validations.max !== undefined) {
|
|
197
|
+
numberSchema = numberSchema.max(validations.max)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
fieldSchema = numberSchema
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
case 'date':
|
|
204
|
+
fieldSchema = Schema.date()
|
|
205
|
+
break
|
|
206
|
+
case 'stringRecord':
|
|
207
|
+
fieldSchema = Schema.stringRecord()
|
|
208
|
+
break
|
|
209
|
+
case 'numberRecord':
|
|
210
|
+
fieldSchema = Schema.numberRecord()
|
|
211
|
+
break
|
|
212
|
+
case 'booleanRecord':
|
|
213
|
+
fieldSchema = Schema.booleanRecord()
|
|
214
|
+
break
|
|
215
|
+
case 'mixedRecord':
|
|
216
|
+
fieldSchema = Schema.mixedRecord()
|
|
217
|
+
break
|
|
218
|
+
case 'array': {
|
|
219
|
+
const arrayField = field as ArraySchemaDescription
|
|
220
|
+
switch (arrayField.items.type) {
|
|
221
|
+
case 'string':
|
|
222
|
+
fieldSchema = Schema.array(Schema.string())
|
|
223
|
+
break
|
|
224
|
+
case 'boolean':
|
|
225
|
+
fieldSchema = Schema.array(Schema.boolean())
|
|
226
|
+
break
|
|
227
|
+
case 'number':
|
|
228
|
+
fieldSchema = Schema.array(Schema.number())
|
|
229
|
+
break
|
|
230
|
+
case 'date':
|
|
231
|
+
fieldSchema = Schema.array(Schema.date())
|
|
232
|
+
break
|
|
233
|
+
default:
|
|
234
|
+
throw new Error(`Unsupported array item type: ${arrayField.items.type}`)
|
|
235
|
+
}
|
|
236
|
+
break
|
|
237
|
+
}
|
|
238
|
+
default:
|
|
239
|
+
throw new Error(`Unsupported type: ${fieldType}`)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
fields[key] = field.optional ? fieldSchema.optional() : fieldSchema
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return new Schema(fields)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Validates the provided data against the schema
|
|
250
|
+
* @param data The data to validate
|
|
251
|
+
* @returns A boolean indicating whether the data is valid
|
|
252
|
+
*/
|
|
253
|
+
validate(data: unknown): boolean {
|
|
254
|
+
const result = this.schema.safeParse(data)
|
|
255
|
+
|
|
256
|
+
return result.success
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Parses and validates the provided data against the schema
|
|
261
|
+
* @param data The data to parse and validate
|
|
262
|
+
* @returns The parsed and typed data
|
|
263
|
+
* @throws {z.ZodError} If the data is invalid
|
|
264
|
+
*/
|
|
265
|
+
parse(data: unknown): z.infer<z.ZodObject<T>> {
|
|
266
|
+
return this.schema.parse(data)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Safely parses and validates the provided data against the schema
|
|
271
|
+
* @param data The data to parse and validate
|
|
272
|
+
* @returns An object containing either the successfully parsed data or error information
|
|
273
|
+
*/
|
|
274
|
+
safeParse(data: unknown): z.SafeParseReturnType<z.infer<z.ZodObject<T>>, z.infer<z.ZodObject<T>>> {
|
|
275
|
+
return this.schema.safeParse(data)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Describes the schema structure and allowed types
|
|
280
|
+
* @returns An object describing the schema structure with type information
|
|
281
|
+
*/
|
|
282
|
+
describe(): SchemaDescription {
|
|
283
|
+
const shape = this.schema.shape
|
|
284
|
+
const description: SchemaDescription = {}
|
|
285
|
+
|
|
286
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
287
|
+
const isOptional = value instanceof z.ZodOptional
|
|
288
|
+
const baseValue = isOptional ? value.unwrap() : value
|
|
289
|
+
|
|
290
|
+
if (baseValue instanceof z.ZodString) {
|
|
291
|
+
const validations: StringValidations = {}
|
|
292
|
+
if (baseValue._def.checks) {
|
|
293
|
+
for (const check of baseValue._def.checks) {
|
|
294
|
+
if (check.kind === 'email') {
|
|
295
|
+
validations.email = true
|
|
296
|
+
} else if (check.kind === 'min') {
|
|
297
|
+
validations.minLength = check.value
|
|
298
|
+
} else if (check.kind === 'max') {
|
|
299
|
+
validations.maxLength = check.value
|
|
300
|
+
} else if (check.kind === 'regex') {
|
|
301
|
+
validations.regex = check.regex.toString().replace(/^\/|\/$/g, '').replace(/\\\//g, '/')
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
description[key] = {
|
|
306
|
+
type: 'string',
|
|
307
|
+
...(isOptional && { optional: true }),
|
|
308
|
+
...(Object.keys(validations).length > 0 && { validations })
|
|
309
|
+
}
|
|
310
|
+
} else if (baseValue instanceof z.ZodBoolean) {
|
|
311
|
+
description[key] = { type: 'boolean', ...(isOptional && { optional: true }) }
|
|
312
|
+
} else if (baseValue instanceof z.ZodNumber) {
|
|
313
|
+
const validations: NumberValidations = {}
|
|
314
|
+
if (baseValue._def.checks) {
|
|
315
|
+
for (const check of baseValue._def.checks) {
|
|
316
|
+
if (check.kind === 'min') {
|
|
317
|
+
validations.min = check.value
|
|
318
|
+
} else if (check.kind === 'max') {
|
|
319
|
+
validations.max = check.value
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
description[key] = {
|
|
324
|
+
type: 'number',
|
|
325
|
+
...(isOptional && { optional: true }),
|
|
326
|
+
...(Object.keys(validations).length > 0 && { validations })
|
|
327
|
+
}
|
|
328
|
+
} else if (baseValue instanceof z.ZodDate) {
|
|
329
|
+
description[key] = { type: 'date', ...(isOptional && { optional: true }) }
|
|
330
|
+
} else if (baseValue instanceof z.ZodArray) {
|
|
331
|
+
const element = baseValue.element
|
|
332
|
+
if (element instanceof z.ZodString) {
|
|
333
|
+
description[key] = { type: 'array', items: { type: 'string' }, ...(isOptional && { optional: true }) }
|
|
334
|
+
} else if (element instanceof z.ZodBoolean) {
|
|
335
|
+
description[key] = { type: 'array', items: { type: 'boolean' }, ...(isOptional && { optional: true }) }
|
|
336
|
+
} else if (element instanceof z.ZodNumber) {
|
|
337
|
+
description[key] = { type: 'array', items: { type: 'number' }, ...(isOptional && { optional: true }) }
|
|
338
|
+
} else if (element instanceof z.ZodDate) {
|
|
339
|
+
description[key] = { type: 'array', items: { type: 'date' }, ...(isOptional && { optional: true }) }
|
|
340
|
+
}
|
|
341
|
+
} else if (baseValue instanceof z.ZodRecord) {
|
|
342
|
+
// Check the value type of the record
|
|
343
|
+
const valueType = baseValue._def.valueType
|
|
344
|
+
if (valueType instanceof z.ZodString) {
|
|
345
|
+
description[key] = { type: 'stringRecord', ...(isOptional && { optional: true }) }
|
|
346
|
+
} else if (valueType instanceof z.ZodNumber) {
|
|
347
|
+
description[key] = { type: 'numberRecord', ...(isOptional && { optional: true }) }
|
|
348
|
+
} else if (valueType instanceof z.ZodBoolean) {
|
|
349
|
+
description[key] = { type: 'booleanRecord', ...(isOptional && { optional: true }) }
|
|
350
|
+
} else if (valueType instanceof z.ZodUnion) {
|
|
351
|
+
description[key] = { type: 'mixedRecord', ...(isOptional && { optional: true }) }
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return description
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export default Schema
|
|
361
|
+
|