@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.
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const index_1 = require("../index");
4
+ describe('Schema.parse()', () => {
5
+ it('returns typed data on success', () => {
6
+ const schema = new index_1.Schema({
7
+ name: index_1.Schema.string(),
8
+ age: index_1.Schema.number(),
9
+ });
10
+ const data = schema.parse({ name: 'John', age: 30 });
11
+ expect(data).toEqual({ name: 'John', age: 30 });
12
+ // Type-level: the result is { name: string; age: number }
13
+ const name = data.name;
14
+ const age = data.age;
15
+ expect(name).toBe('John');
16
+ expect(age).toBe(30);
17
+ });
18
+ it('strips unknown keys', () => {
19
+ const schema = new index_1.Schema({
20
+ name: index_1.Schema.string(),
21
+ });
22
+ const data = schema.parse({ name: 'John', extra: 'ignored' });
23
+ expect(data).toEqual({ name: 'John' });
24
+ expect(data).not.toHaveProperty('extra');
25
+ });
26
+ it('coerces nothing and throws ZodError on invalid data', () => {
27
+ const schema = new index_1.Schema({
28
+ name: index_1.Schema.string(),
29
+ age: index_1.Schema.number(),
30
+ });
31
+ expect(() => schema.parse({ name: 'John', age: 'thirty' })).toThrow();
32
+ });
33
+ it('throws a ZodError exposing issues with path and message', () => {
34
+ const schema = new index_1.Schema({
35
+ name: index_1.Schema.string(),
36
+ age: index_1.Schema.number().min(18),
37
+ });
38
+ try {
39
+ schema.parse({ name: 123, age: 10 });
40
+ throw new Error('expected parse to throw');
41
+ }
42
+ catch (err) {
43
+ const zodError = err;
44
+ expect(Array.isArray(zodError.issues)).toBe(true);
45
+ const paths = (zodError.issues ?? []).map((issue) => issue.path.join('.'));
46
+ expect(paths).toContain('name');
47
+ expect(paths).toContain('age');
48
+ }
49
+ });
50
+ it('parses nested objects and optionals', () => {
51
+ const schema = new index_1.Schema({
52
+ user: index_1.Schema.object({
53
+ name: index_1.Schema.string(),
54
+ nickname: index_1.Schema.string().optional(),
55
+ }),
56
+ });
57
+ expect(schema.parse({ user: { name: 'John' } })).toEqual({ user: { name: 'John' } });
58
+ expect(schema.parse({ user: { name: 'John', nickname: 'JJ' } })).toEqual({
59
+ user: { name: 'John', nickname: 'JJ' },
60
+ });
61
+ expect(() => schema.parse({ user: {} })).toThrow();
62
+ });
63
+ });
64
+ describe('Schema.safeParse()', () => {
65
+ it('returns success: true with data on valid input', () => {
66
+ const schema = new index_1.Schema({
67
+ name: index_1.Schema.string(),
68
+ age: index_1.Schema.number(),
69
+ });
70
+ const result = schema.safeParse({ name: 'John', age: 30 });
71
+ expect(result.success).toBe(true);
72
+ if (result.success) {
73
+ expect(result.data).toEqual({ name: 'John', age: 30 });
74
+ // Type-level: result.data is fully typed
75
+ const name = result.data.name;
76
+ expect(name).toBe('John');
77
+ }
78
+ });
79
+ it('returns success: false with an error on invalid input', () => {
80
+ const schema = new index_1.Schema({
81
+ name: index_1.Schema.string(),
82
+ age: index_1.Schema.number(),
83
+ });
84
+ const result = schema.safeParse({ name: 'John', age: 'thirty' });
85
+ expect(result.success).toBe(false);
86
+ if (!result.success) {
87
+ expect(result.error).toBeDefined();
88
+ expect(Array.isArray(result.error.issues)).toBe(true);
89
+ const paths = result.error.issues.map((issue) => issue.path.join('.'));
90
+ expect(paths).toContain('age');
91
+ }
92
+ });
93
+ it('does not throw on invalid input', () => {
94
+ const schema = new index_1.Schema({
95
+ age: index_1.Schema.number(),
96
+ });
97
+ expect(() => schema.safeParse({ age: 'nope' })).not.toThrow();
98
+ });
99
+ it('reports each failing field in the issues list', () => {
100
+ const schema = new index_1.Schema({
101
+ email: index_1.Schema.email(),
102
+ age: index_1.Schema.number().min(18),
103
+ username: index_1.Schema.string().min(3),
104
+ });
105
+ const result = schema.safeParse({ email: 'nope', age: 5, username: 'ab' });
106
+ expect(result.success).toBe(false);
107
+ if (!result.success) {
108
+ const paths = result.error.issues.map((issue) => issue.path.join('.'));
109
+ expect(paths).toEqual(expect.arrayContaining(['email', 'age', 'username']));
110
+ }
111
+ });
112
+ it('strips unknown keys on success', () => {
113
+ const schema = new index_1.Schema({
114
+ name: index_1.Schema.string(),
115
+ });
116
+ const result = schema.safeParse({ name: 'John', extra: true });
117
+ expect(result.success).toBe(true);
118
+ if (result.success) {
119
+ expect(result.data).toEqual({ name: 'John' });
120
+ }
121
+ });
122
+ it('infers optional fields as possibly undefined', () => {
123
+ const schema = new index_1.Schema({
124
+ name: index_1.Schema.string(),
125
+ age: index_1.Schema.number().optional(),
126
+ });
127
+ const result = schema.safeParse({ name: 'John' });
128
+ expect(result.success).toBe(true);
129
+ if (result.success) {
130
+ const age = result.data.age;
131
+ expect(age).toBeUndefined();
132
+ }
133
+ });
134
+ });
@@ -32,22 +32,17 @@ describe('Schema Record Types', () => {
32
32
  });
33
33
  expect(result).toBe(false);
34
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);
35
+ it('should round-trip a string record schema', () => {
36
+ const schema = index_1.default.from(new index_1.default({
37
+ data: index_1.default.stringRecord(),
38
+ }).describe());
43
39
  const validData = {
44
40
  data: {
45
41
  firstName: 'John',
46
42
  lastName: 'Doe'
47
43
  }
48
44
  };
49
- const result = schema.validate(validData);
50
- expect(result).toBe(true);
45
+ expect(schema.validate(validData)).toBe(true);
51
46
  });
52
47
  });
53
48
  describe('Number Record', () => {
@@ -77,22 +72,17 @@ describe('Schema Record Types', () => {
77
72
  });
78
73
  expect(result).toBe(false);
79
74
  });
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);
75
+ it('should round-trip a number record schema', () => {
76
+ const schema = index_1.default.from(new index_1.default({
77
+ data: index_1.default.numberRecord(),
78
+ }).describe());
88
79
  const validData = {
89
80
  data: {
90
81
  age: 30,
91
82
  experience: 5
92
83
  }
93
84
  };
94
- const result = schema.validate(validData);
95
- expect(result).toBe(true);
85
+ expect(schema.validate(validData)).toBe(true);
96
86
  });
97
87
  });
98
88
  describe('Boolean Record', () => {
@@ -122,22 +112,17 @@ describe('Schema Record Types', () => {
122
112
  });
123
113
  expect(result).toBe(false);
124
114
  });
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);
115
+ it('should round-trip a boolean record schema', () => {
116
+ const schema = index_1.default.from(new index_1.default({
117
+ data: index_1.default.booleanRecord(),
118
+ }).describe());
133
119
  const validData = {
134
120
  data: {
135
121
  isActive: true,
136
122
  isAdmin: false
137
123
  }
138
124
  };
139
- const result = schema.validate(validData);
140
- expect(result).toBe(true);
125
+ expect(schema.validate(validData)).toBe(true);
141
126
  });
142
127
  });
143
128
  describe('Mixed Record', () => {
@@ -206,14 +191,10 @@ describe('Schema Record Types', () => {
206
191
  });
207
192
  expect(result).toBe(false);
208
193
  });
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);
194
+ it('should round-trip a mixed record schema', () => {
195
+ const schema = index_1.default.from(new index_1.default({
196
+ data: index_1.default.mixedRecord(),
197
+ }).describe());
217
198
  const validData = {
218
199
  data: {
219
200
  name: 'John',
@@ -221,12 +202,11 @@ describe('Schema Record Types', () => {
221
202
  isActive: true
222
203
  }
223
204
  };
224
- const result = schema.validate(validData);
225
- expect(result).toBe(true);
205
+ expect(schema.validate(validData)).toBe(true);
226
206
  });
227
207
  });
228
208
  describe('Common Record Functionality', () => {
229
- it('should reject a record with non-string keys', () => {
209
+ it('should treat numeric keys as strings (JS limitation)', () => {
230
210
  const schema = new index_1.default({
231
211
  data: index_1.default.stringRecord(),
232
212
  });
@@ -247,14 +227,10 @@ describe('Schema Record Types', () => {
247
227
  const result = schema.validate(invalidData);
248
228
  expect(result).toBe(true);
249
229
  });
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);
230
+ it('should round-trip an optional record schema', () => {
231
+ const schema = index_1.default.from(new index_1.default({
232
+ data: index_1.default.stringRecord().optional()
233
+ }).describe());
258
234
  // Test with record present
259
235
  const validData1 = {
260
236
  data: {
@@ -266,38 +242,47 @@ describe('Schema Record Types', () => {
266
242
  expect(schema.validate(validData1)).toBe(true);
267
243
  expect(schema.validate(validData2)).toBe(true);
268
244
  });
269
- it('should describe a schema with string record type', () => {
245
+ it('should describe a string record as an object with string additionalProperties', () => {
270
246
  const schema = new index_1.default({
271
247
  data: index_1.default.stringRecord(),
272
248
  });
273
249
  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
250
+ expect(description.properties?.data).toMatchObject({
251
+ type: 'object',
252
+ additionalProperties: { type: 'string' }
253
+ });
277
254
  });
278
- it('should describe a schema with number record type', () => {
255
+ it('should describe a number record as an object with number additionalProperties', () => {
279
256
  const schema = new index_1.default({
280
257
  data: index_1.default.numberRecord(),
281
258
  });
282
259
  const description = schema.describe();
283
- expect(description).toHaveProperty('data');
284
- expect(description.data).toHaveProperty('type', 'numberRecord');
260
+ expect(description.properties?.data).toMatchObject({
261
+ type: 'object',
262
+ additionalProperties: { type: 'number' }
263
+ });
285
264
  });
286
- it('should describe a schema with boolean record type', () => {
265
+ it('should describe a boolean record as an object with boolean additionalProperties', () => {
287
266
  const schema = new index_1.default({
288
267
  data: index_1.default.booleanRecord(),
289
268
  });
290
269
  const description = schema.describe();
291
- expect(description).toHaveProperty('data');
292
- expect(description.data).toHaveProperty('type', 'booleanRecord');
270
+ expect(description.properties?.data).toMatchObject({
271
+ type: 'object',
272
+ additionalProperties: { type: 'boolean' }
273
+ });
293
274
  });
294
- it('should describe a schema with mixed record type', () => {
275
+ it('should describe a mixed record as an object with anyOf additionalProperties', () => {
295
276
  const schema = new index_1.default({
296
277
  data: index_1.default.mixedRecord(),
297
278
  });
298
279
  const description = schema.describe();
299
- expect(description).toHaveProperty('data');
300
- expect(description.data).toHaveProperty('type', 'mixedRecord');
280
+ expect(description.properties?.data).toMatchObject({
281
+ type: 'object',
282
+ additionalProperties: {
283
+ anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }]
284
+ }
285
+ });
301
286
  });
302
287
  });
303
288
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgehive/schema",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,7 +10,7 @@
10
10
  "ts-jest": "^29.1.2"
11
11
  },
12
12
  "dependencies": {
13
- "zod": "^3.24.2"
13
+ "zod": "^4.4.3"
14
14
  },
15
15
  "scripts": {
16
16
  "build": "tsc",