@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.
@@ -0,0 +1,293 @@
1
+ import { Schema } from '../index'
2
+
3
+ describe('Schema Custom Number Validations', () => {
4
+ it('should validate number min/max constraints', () => {
5
+ const schema = Schema.from({
6
+ age: {
7
+ type: 'number',
8
+ validations: {
9
+ min: 18,
10
+ max: 100
11
+ }
12
+ },
13
+ score: {
14
+ type: 'number',
15
+ validations: {
16
+ min: 0,
17
+ max: 100
18
+ }
19
+ }
20
+ })
21
+
22
+ // Valid cases
23
+ expect(schema.validate({ age: 25, score: 85 })).toBe(true)
24
+ expect(schema.validate({ age: 18, score: 0 })).toBe(true)
25
+ expect(schema.validate({ age: 100, score: 100 })).toBe(true)
26
+
27
+ // Invalid cases
28
+ expect(schema.validate({ age: 17, score: 85 })).toBe(false)
29
+ expect(schema.validate({ age: 25, score: -1 })).toBe(false)
30
+ expect(schema.validate({ age: 101, score: 85 })).toBe(false)
31
+ expect(schema.validate({ age: 25, score: 101 })).toBe(false)
32
+ })
33
+
34
+ it('should correctly describe number validations', () => {
35
+ const schema = Schema.from({
36
+ age: {
37
+ type: 'number',
38
+ validations: {
39
+ min: 18,
40
+ max: 100
41
+ }
42
+ }
43
+ })
44
+
45
+ const description = schema.describe()
46
+ expect(description.age).toEqual({
47
+ type: 'number',
48
+ validations: {
49
+ min: 18,
50
+ max: 100
51
+ }
52
+ })
53
+ })
54
+ })
55
+
56
+ describe('Schema Custom String Validations', () => {
57
+ it('should validate string email format', () => {
58
+ const schema = Schema.from({
59
+ email: {
60
+ type: 'string',
61
+ validations: {
62
+ email: true
63
+ }
64
+ }
65
+ })
66
+
67
+ // Valid cases
68
+ expect(schema.validate({ email: 'user@example.com' })).toBe(true)
69
+ expect(schema.validate({ email: 'test.name@domain.co.uk' })).toBe(true)
70
+ expect(schema.validate({ email: 'user+tag@example.com' })).toBe(true)
71
+
72
+ // Invalid cases
73
+ expect(schema.validate({ email: 'not-an-email' })).toBe(false)
74
+ expect(schema.validate({ email: 'missing@domain' })).toBe(false)
75
+ expect(schema.validate({ email: '@domain.com' })).toBe(false)
76
+ })
77
+
78
+ it('should validate string length constraints', () => {
79
+ const schema = Schema.from({
80
+ username: {
81
+ type: 'string',
82
+ validations: {
83
+ minLength: 3,
84
+ maxLength: 20
85
+ }
86
+ }
87
+ })
88
+
89
+ // Valid cases
90
+ expect(schema.validate({ username: 'abc' })).toBe(true)
91
+ expect(schema.validate({ username: 'valid_username' })).toBe(true)
92
+ expect(schema.validate({ username: 'a'.repeat(20) })).toBe(true)
93
+
94
+ // Invalid cases
95
+ expect(schema.validate({ username: 'ab' })).toBe(false)
96
+ expect(schema.validate({ username: 'a'.repeat(21) })).toBe(false)
97
+ })
98
+
99
+ it('should validate string regex pattern', () => {
100
+ const schema = Schema.from({
101
+ username: {
102
+ type: 'string',
103
+ validations: {
104
+ regex: '^[a-zA-Z0-9_]+$'
105
+ }
106
+ }
107
+ })
108
+
109
+ // Valid cases
110
+ expect(schema.validate({ username: 'john_doe123' })).toBe(true)
111
+ expect(schema.validate({ username: 'User123' })).toBe(true)
112
+ expect(schema.validate({ username: '123456' })).toBe(true)
113
+
114
+ // Invalid cases
115
+ expect(schema.validate({ username: 'john-doe' })).toBe(false)
116
+ expect(schema.validate({ username: 'user@123' })).toBe(false)
117
+ expect(schema.validate({ username: 'user name' })).toBe(false)
118
+ })
119
+
120
+ it('should correctly describe string validations', () => {
121
+ const schema = Schema.from({
122
+ email: {
123
+ type: 'string',
124
+ validations: {
125
+ email: true,
126
+ minLength: 5,
127
+ maxLength: 100
128
+ }
129
+ }
130
+ })
131
+
132
+ const description = schema.describe()
133
+ expect(description.email).toEqual({
134
+ type: 'string',
135
+ validations: {
136
+ email: true,
137
+ minLength: 5,
138
+ maxLength: 100
139
+ }
140
+ })
141
+ })
142
+
143
+ it('should correctly describe string regex validation', () => {
144
+ const schema = Schema.from({
145
+ username: {
146
+ type: 'string',
147
+ validations: {
148
+ regex: '^[a-zA-Z0-9_]+$'
149
+ }
150
+ }
151
+ })
152
+
153
+ const description = schema.describe()
154
+ expect(description.username).toEqual({
155
+ type: 'string',
156
+ validations: {
157
+ regex: '^[a-zA-Z0-9_]+$'
158
+ }
159
+ })
160
+ })
161
+
162
+ it('should handle regex with other string validations', () => {
163
+ const schema = Schema.from({
164
+ username: {
165
+ type: 'string',
166
+ validations: {
167
+ regex: '^[a-zA-Z0-9_]+$',
168
+ minLength: 3,
169
+ maxLength: 20
170
+ }
171
+ }
172
+ })
173
+
174
+ // Valid cases
175
+ expect(schema.validate({ username: 'john_doe123' })).toBe(true)
176
+ expect(schema.validate({ username: 'User123' })).toBe(true)
177
+
178
+ // Invalid cases - regex
179
+ expect(schema.validate({ username: 'john-doe' })).toBe(false)
180
+ expect(schema.validate({ username: 'user@123' })).toBe(false)
181
+
182
+ // Invalid cases - length
183
+ expect(schema.validate({ username: 'ab' })).toBe(false)
184
+ expect(schema.validate({ username: 'a'.repeat(21) })).toBe(false)
185
+ })
186
+
187
+ it('should handle regex patterns containing forward slashes', () => {
188
+ const schema = Schema.from({
189
+ path: {
190
+ type: 'string',
191
+ validations: {
192
+ regex: '^/api/v[0-9]+/users/[0-9]+$'
193
+ }
194
+ }
195
+ })
196
+
197
+ // Valid cases
198
+ expect(schema.validate({ path: '/api/v1/users/123' })).toBe(true)
199
+ expect(schema.validate({ path: '/api/v2/users/456' })).toBe(true)
200
+ expect(schema.validate({ path: '/api/v10/users/789' })).toBe(true)
201
+
202
+ // Invalid cases
203
+ expect(schema.validate({ path: 'api/v1/users/123' })).toBe(false)
204
+ expect(schema.validate({ path: '/api/v1/users' })).toBe(false)
205
+ expect(schema.validate({ path: '/api/v1/users/abc' })).toBe(false)
206
+
207
+ // Verify the description preserves the slashes within the pattern
208
+ const description = schema.describe()
209
+ expect(description.path).toEqual({
210
+ type: 'string',
211
+ validations: {
212
+ regex: '^/api/v[0-9]+/users/[0-9]+$'
213
+ }
214
+ })
215
+ })
216
+
217
+ it('should handle round trip of regex patterns containing forward slashes', () => {
218
+ const schema = Schema.from({
219
+ path: {
220
+ type: 'string',
221
+ validations: {
222
+ regex: '^/api/v[0-9]+/users/[0-9]+$'
223
+ }
224
+ }
225
+ })
226
+
227
+ const description = schema.describe()
228
+
229
+ const cloneSchema = Schema.from(description)
230
+
231
+ // Valid cases
232
+ expect(cloneSchema.validate({ path: '/api/v1/users/123' })).toBe(true)
233
+ expect(cloneSchema.validate({ path: '/api/v2/users/456' })).toBe(true)
234
+ expect(cloneSchema.validate({ path: '/api/v10/users/789' })).toBe(true)
235
+
236
+ // Invalid cases
237
+ expect(cloneSchema.validate({ path: 'api/v1/users/123' })).toBe(false)
238
+ expect(cloneSchema.validate({ path: '/api/v1/users' })).toBe(false)
239
+ expect(cloneSchema.validate({ path: '/api/v1/users/abc' })).toBe(false)
240
+
241
+ // Verify the description preserves the slashes within the pattern
242
+ const cloneDescription = cloneSchema.describe()
243
+ expect(cloneDescription).toMatchObject(description)
244
+ })
245
+ })
246
+
247
+ describe('Schema Roundtrip', () => {
248
+ it('should maintain custom validations through describe/from cycle', () => {
249
+ const originalSchema = Schema.from({
250
+ age: {
251
+ type: 'number',
252
+ validations: {
253
+ min: 18,
254
+ max: 100
255
+ }
256
+ },
257
+ email: {
258
+ type: 'string',
259
+ validations: {
260
+ email: true
261
+ }
262
+ },
263
+ username: {
264
+ type: 'string',
265
+ validations: {
266
+ regex: '^[a-zA-Z0-9_]+$',
267
+ minLength: 3,
268
+ maxLength: 20
269
+ }
270
+ }
271
+ })
272
+
273
+ const description = originalSchema.describe()
274
+ const reconstructedSchema = Schema.from(description)
275
+
276
+ // Both schemas should validate the same data
277
+ const validData = {
278
+ age: 25,
279
+ email: 'user@example.com',
280
+ username: 'john_doe123'
281
+ }
282
+ const invalidData = {
283
+ age: 17,
284
+ email: 'not-an-email',
285
+ username: 'john-doe'
286
+ }
287
+
288
+ expect(originalSchema.validate(validData)).toBe(true)
289
+ expect(reconstructedSchema.validate(validData)).toBe(true)
290
+ expect(originalSchema.validate(invalidData)).toBe(false)
291
+ expect(reconstructedSchema.validate(invalidData)).toBe(false)
292
+ })
293
+ })
@@ -0,0 +1,85 @@
1
+ import { Schema } from '../index'
2
+
3
+ describe('Schema single date field', () => {
4
+ const schema = Schema.from({
5
+ createdAt: { type: 'date' }
6
+ })
7
+
8
+ it('should validate a valid date', () => {
9
+ const data = {
10
+ createdAt: new Date()
11
+ }
12
+ expect(schema.validate(data)).toBe(true)
13
+ })
14
+
15
+ it('should validate a valid date string', () => {
16
+ const data = {
17
+ createdAt: new Date('2024-03-20T12:00:00Z')
18
+ }
19
+ expect(schema.validate(data)).toBe(true)
20
+ })
21
+
22
+ it('should reject invalid date', () => {
23
+ const data = {
24
+ createdAt: 'invalid-date'
25
+ }
26
+ expect(schema.validate(data)).toBe(false)
27
+ })
28
+
29
+ it('should reject non-date value', () => {
30
+ const data = {
31
+ createdAt: 123
32
+ }
33
+ expect(schema.validate(data)).toBe(false)
34
+ })
35
+ })
36
+
37
+ describe('Schema array of dates', () => {
38
+ const schema = Schema.from({
39
+ timestamps: { type: 'array', items: { type: 'date' } }
40
+ })
41
+
42
+ it('should validate an array of valid dates', () => {
43
+ const data = {
44
+ timestamps: [new Date(), new Date()]
45
+ }
46
+ expect(schema.validate(data)).toBe(true)
47
+ })
48
+
49
+ it('should validate an array of valid date strings', () => {
50
+ const data = {
51
+ timestamps: [new Date('2024-03-20T12:00:00Z'), new Date('2024-03-21T12:00:00Z')]
52
+ }
53
+ expect(schema.validate(data)).toBe(true)
54
+ })
55
+
56
+ it('should reject array with invalid date', () => {
57
+ const data = {
58
+ timestamps: [new Date(), 'invalid-date']
59
+ }
60
+ expect(schema.validate(data)).toBe(false)
61
+ })
62
+
63
+ it('should reject non-array value', () => {
64
+ const data = {
65
+ timestamps: 'not-an-array'
66
+ }
67
+ expect(schema.validate(data)).toBe(false)
68
+ })
69
+ })
70
+
71
+ describe('Dates Schema description', () => {
72
+ it('should correctly describe date fields', () => {
73
+ const schema = new Schema({
74
+ createdAt: Schema.date(),
75
+ timestamps: Schema.array(Schema.date())
76
+ })
77
+
78
+ const description = schema.describe()
79
+ expect(description).toEqual({
80
+ createdAt: { type: 'date' },
81
+ timestamps: { type: 'array', items: { type: 'date' } }
82
+ })
83
+ })
84
+ })
85
+
@@ -0,0 +1,205 @@
1
+ import Schema, { type SchemaDescription } from '../index'
2
+
3
+ describe('Schema basic types', () => {
4
+ it('should validate a string', () => {
5
+ const schema = new Schema({
6
+ name: Schema.string(),
7
+ })
8
+
9
+ const result = schema.validate({
10
+ name: 'World',
11
+ })
12
+
13
+ expect(result).toBe(true)
14
+ })
15
+
16
+ it('should validate a string and number', () => {
17
+ const schema = new Schema({
18
+ name: Schema.string(),
19
+ age: Schema.number(),
20
+ })
21
+
22
+ const result = schema.validate({
23
+ name: 'World',
24
+ age: 20,
25
+ })
26
+
27
+ expect(result).toEqual(true)
28
+ })
29
+ })
30
+
31
+ describe('Schema description', () => {
32
+ it('should describe a string', () => {
33
+ const schema = new Schema({
34
+ name: Schema.string(),
35
+ })
36
+
37
+ const result = schema.describe()
38
+ expect(result).toEqual({
39
+ name: { type: 'string' },
40
+ })
41
+ })
42
+
43
+ it('should describe a string and number', () => {
44
+ const schema = new Schema({
45
+ name: Schema.string(),
46
+ age: Schema.number(),
47
+ })
48
+
49
+ const result = schema.describe()
50
+ expect(result).toEqual({
51
+ name: { type: 'string' },
52
+ age: { type: 'number' },
53
+ })
54
+ })
55
+ })
56
+
57
+ describe('Schema hydrate', () => {
58
+ it('should describe a string', () => {
59
+ const description: SchemaDescription = {
60
+ name: { type: 'string' },
61
+ age: { type: 'number' },
62
+ }
63
+ const schema = Schema.from(description)
64
+
65
+ const result = schema.describe()
66
+ expect(result).toEqual({
67
+ name: { type: 'string' },
68
+ age: { type: 'number' },
69
+ })
70
+ })
71
+ })
72
+
73
+ describe('Schema validation errors', () => {
74
+ it('should reject invalid string type', () => {
75
+ const schema = new Schema({
76
+ name: Schema.string(),
77
+ })
78
+
79
+ const result = schema.validate({
80
+ name: 123, // number instead of string
81
+ })
82
+
83
+ expect(result).toBe(false)
84
+ })
85
+
86
+ it('should reject invalid number type', () => {
87
+ const schema = new Schema({
88
+ age: Schema.number(),
89
+ })
90
+
91
+ const result = schema.validate({
92
+ age: 'not a number',
93
+ })
94
+
95
+ expect(result).toBe(false)
96
+ })
97
+ })
98
+
99
+ // describe('Schema optional fields', () => {
100
+ // it('should validate with missing optional field', () => {
101
+ // const schema = new Schema({
102
+ // name: Schema.string(),
103
+ // age: Schema.number().optional(),
104
+ // })
105
+
106
+ // const result = schema.validate({
107
+ // name: 'World',
108
+ // })
109
+
110
+ // expect(result).toBe(true)
111
+ // })
112
+ // })
113
+
114
+ describe('Schema arrays', () => {
115
+ it('should validate array of strings', () => {
116
+ const schema = new Schema({
117
+ tags: Schema.array(Schema.string()),
118
+ })
119
+
120
+ const result = schema.validate({
121
+ tags: ['tag1', 'tag2', 'tag3'],
122
+ })
123
+
124
+ expect(result).toBe(true)
125
+ })
126
+
127
+ it('should validate array of numbers', () => {
128
+ const schema = new Schema({
129
+ scores: Schema.array(Schema.number()),
130
+ })
131
+
132
+ const result = schema.validate({
133
+ scores: [1, 2, 3, 4, 5],
134
+ })
135
+
136
+ expect(result).toBe(true)
137
+ })
138
+
139
+ it('should reject invalid array types', () => {
140
+ const schema = new Schema({
141
+ tags: Schema.array(Schema.string()),
142
+ })
143
+
144
+ const result = schema.validate({
145
+ tags: ['tag1', 123, 'tag3'], // mixed types
146
+ })
147
+
148
+ expect(result).toBe(false)
149
+ })
150
+
151
+ it('should validate empty arrays', () => {
152
+ const schema = new Schema({
153
+ tags: Schema.array(Schema.string()),
154
+ })
155
+
156
+ const result = schema.validate({
157
+ tags: [],
158
+ })
159
+
160
+ expect(result).toBe(true)
161
+ })
162
+
163
+ it('should describe array schema', () => {
164
+ const schema = new Schema({
165
+ tags: Schema.array(Schema.string()),
166
+ scores: Schema.array(Schema.number()),
167
+ })
168
+
169
+ const result = schema.describe()
170
+ expect(result).toEqual({
171
+ tags: { type: 'array', items: { type: 'string' } },
172
+ scores: { type: 'array', items: { type: 'number' } },
173
+ })
174
+ })
175
+
176
+ it('should hydrate array schema from description', () => {
177
+ const description: SchemaDescription = {
178
+ tags: { type: 'array', items: { type: 'string' } },
179
+ scores: { type: 'array', items: { type: 'number' } },
180
+ }
181
+ const schema = Schema.from(description)
182
+
183
+ const result = schema.describe()
184
+ expect(result).toEqual({
185
+ tags: { type: 'array', items: { type: 'string' } },
186
+ scores: { type: 'array', items: { type: 'number' } },
187
+ })
188
+ })
189
+ })
190
+
191
+ describe('Schema custom validation', () => {
192
+ it('should validate with custom rules', () => {
193
+ const schema = new Schema({
194
+ age: Schema.number().min(0).max(120),
195
+ email: Schema.string().email(),
196
+ })
197
+
198
+ const result = schema.validate({
199
+ age: 25,
200
+ email: 'test@example.com',
201
+ })
202
+
203
+ expect(result).toBe(true)
204
+ })
205
+ })
@@ -0,0 +1,73 @@
1
+ import { Schema } from '../index'
2
+
3
+ describe('Schema Custom Validations', () => {
4
+ describe('Optional Fields', () => {
5
+ it('should handle optional string fields', () => {
6
+ const schema = Schema.from({
7
+ name: { type: 'string', optional: true },
8
+ age: { type: 'number' }
9
+ })
10
+
11
+ // Valid with optional field present
12
+ expect(schema.validate({ name: 'John', age: 30 })).toBe(true)
13
+
14
+ // Valid with optional field missing
15
+ expect(schema.validate({ age: 30 })).toBe(true)
16
+
17
+ // Invalid - missing required field
18
+ expect(schema.validate({ name: 'John' })).toBe(false)
19
+ })
20
+
21
+ it('should handle optional array fields', () => {
22
+ const schema = Schema.from({
23
+ tags: { type: 'array', items: { type: 'string' }, optional: true },
24
+ scores: { type: 'array', items: { type: 'number' } }
25
+ })
26
+
27
+ // Valid with optional array present
28
+ expect(schema.validate({ tags: ['tag1', 'tag2'], scores: [1, 2, 3] })).toBe(true)
29
+
30
+ // Valid with optional array missing
31
+ expect(schema.validate({ scores: [1, 2, 3] })).toBe(true)
32
+
33
+ // Invalid - missing required array
34
+ expect(schema.validate({ tags: ['tag1'] })).toBe(false)
35
+ })
36
+
37
+ it('should correctly describe optional fields', () => {
38
+ const schema = Schema.from({
39
+ name: { type: 'string', optional: true },
40
+ age: { type: 'number' },
41
+ tags: { type: 'array', items: { type: 'string' }, optional: true }
42
+ })
43
+
44
+ const description = schema.describe()
45
+
46
+ expect(description.name).toEqual({ type: 'string', optional: true })
47
+ expect(description.age).toEqual({ type: 'number' })
48
+ expect(description.tags).toEqual({ type: 'array', items: { type: 'string' }, optional: true })
49
+ })
50
+ })
51
+
52
+ describe('Schema Roundtrip', () => {
53
+ it('should maintain optional fields through describe/from cycle', () => {
54
+ const originalSchema = Schema.from({
55
+ name: { type: 'string', optional: true },
56
+ age: { type: 'number' },
57
+ tags: { type: 'array', items: { type: 'string' }, optional: true }
58
+ })
59
+
60
+ const description = originalSchema.describe()
61
+ const reconstructedSchema = Schema.from(description)
62
+
63
+ // Both schemas should validate the same data
64
+ const validData = { name: 'John', age: 30, tags: ['tag1'] }
65
+ const dataWithoutOptional = { age: 30 }
66
+
67
+ expect(originalSchema.validate(validData)).toBe(true)
68
+ expect(reconstructedSchema.validate(validData)).toBe(true)
69
+ expect(originalSchema.validate(dataWithoutOptional)).toBe(true)
70
+ expect(reconstructedSchema.validate(dataWithoutOptional)).toBe(true)
71
+ })
72
+ })
73
+ })