@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/src/index.ts CHANGED
@@ -1,109 +1,121 @@
1
1
  import { z } from 'zod'
2
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>>>>> {
3
+ // `z` is re-exported as an escape hatch for zod types the helpers don't cover
4
+ // (enums, unions, refinements, ...). Prefer the `Schema.*` helpers for fields:
5
+ // they keep call sites independent of zod, so the underlying library can be
6
+ // swapped without touching consumers.
7
+ export { z }
8
+
9
+ /**
10
+ * A single field within a schema. Any zod type is allowed, which enables
11
+ * nested objects, arrays, records, unions and the full range of zod validations.
12
+ */
13
+ export type SchemaType = z.ZodType
14
+
15
+ /**
16
+ * The serialized form of a Schema. `describe()` produces standard JSON Schema
17
+ * (draft 2020-12) and `from()` consumes it.
18
+ */
19
+ export type SchemaDescription = z.core.JSONSchema.BaseSchema
20
+
21
+ /**
22
+ * Infers the TypeScript type produced by a Schema instance.
23
+ */
24
+ export type InferSchema<S extends Schema<z.ZodRawShape>> = z.infer<S['schema']>
25
+
26
+ /**
27
+ * A thin wrapper around a zod object schema. Fields are created with the static
28
+ * `Schema.*` helpers so call sites stay independent of the underlying validation
29
+ * library; the wrapper owns validation plus JSON Schema serialization
30
+ * (`describe`) and rehydration (`from`).
31
+ */
32
+ export class Schema<T extends z.ZodRawShape = z.ZodRawShape> {
64
33
  readonly schema: z.ZodObject<T>
65
34
 
66
35
  constructor(fields: T) {
67
- this.schema = z.object(fields)
36
+ this.schema = z.object(fields) as z.ZodObject<T>
68
37
  }
69
38
 
70
39
  /**
71
40
  * Creates a string schema
72
41
  * @returns A string schema
73
42
  */
74
- static string(): ShadowString {
43
+ static string(): z.ZodString {
75
44
  return z.string()
76
45
  }
77
46
 
47
+ /**
48
+ * Creates a number schema
49
+ * @returns A number schema
50
+ */
51
+ static number(): z.ZodNumber {
52
+ return z.number()
53
+ }
54
+
78
55
  /**
79
56
  * Creates a boolean schema
80
57
  * @returns A boolean schema
81
58
  */
82
- static boolean(): ShadowBoolean {
59
+ static boolean(): z.ZodBoolean {
83
60
  return z.boolean()
84
61
  }
85
62
 
86
63
  /**
87
- * Creates a number schema
88
- * @returns A number schema
64
+ * Creates an ISO 8601 date-time schema. The runtime value is a string
65
+ * (e.g. "2020-01-01T00:00:00Z"), which is natively representable in JSON Schema.
66
+ * @returns An ISO date-time schema
89
67
  */
90
- static number(): ShadowNumber {
91
- return z.number()
68
+ static date(): z.ZodISODateTime {
69
+ return z.iso.datetime()
70
+ }
71
+
72
+ /**
73
+ * Creates an email string schema (serializes to JSON Schema `format: "email"`).
74
+ * @returns An email schema
75
+ */
76
+ static email(): z.ZodEmail {
77
+ return z.email()
92
78
  }
93
79
 
94
80
  /**
95
- * Creates a date schema
96
- * @returns A date schema
81
+ * Creates a UUID string schema (serializes to JSON Schema `format: "uuid"`).
82
+ * @returns A UUID schema
97
83
  */
98
- static date(): ShadowDate {
99
- return z.date()
84
+ static uuid(): z.ZodUUID {
85
+ return z.uuid()
86
+ }
87
+
88
+ /**
89
+ * Creates a URL string schema (serializes to JSON Schema `format: "uri"`).
90
+ * @returns A URL schema
91
+ */
92
+ static url(): z.ZodURL {
93
+ return z.url()
94
+ }
95
+
96
+ /**
97
+ * Creates an array schema
98
+ * @param type The type of items in the array
99
+ * @returns An array schema
100
+ */
101
+ static array<E extends z.ZodType>(type: E): z.ZodArray<E> {
102
+ return z.array(type)
103
+ }
104
+
105
+ /**
106
+ * Creates a nested object schema
107
+ * @param fields The fields of the object
108
+ * @returns An object schema
109
+ */
110
+ static object<S extends z.ZodRawShape>(fields: S): z.ZodObject<S> {
111
+ return z.object(fields) as z.ZodObject<S>
100
112
  }
101
113
 
102
114
  /**
103
115
  * Creates a record schema with string keys and string values
104
116
  * @returns A record schema with string values
105
117
  */
106
- static stringRecord(): ShadowStringRecord {
118
+ static stringRecord(): z.ZodRecord<z.ZodString, z.ZodString> {
107
119
  return z.record(z.string(), z.string())
108
120
  }
109
121
 
@@ -111,7 +123,7 @@ export class Schema<T extends Record<string, z.ZodType<string | boolean | number
111
123
  * Creates a record schema with string keys and number values
112
124
  * @returns A record schema with number values
113
125
  */
114
- static numberRecord(): ShadowNumberRecord {
126
+ static numberRecord(): z.ZodRecord<z.ZodString, z.ZodNumber> {
115
127
  return z.record(z.string(), z.number())
116
128
  }
117
129
 
@@ -119,7 +131,7 @@ export class Schema<T extends Record<string, z.ZodType<string | boolean | number
119
131
  * Creates a record schema with string keys and boolean values
120
132
  * @returns A record schema with boolean values
121
133
  */
122
- static booleanRecord(): ShadowBooleanRecord {
134
+ static booleanRecord(): z.ZodRecord<z.ZodString, z.ZodBoolean> {
123
135
  return z.record(z.string(), z.boolean())
124
136
  }
125
137
 
@@ -127,122 +139,38 @@ export class Schema<T extends Record<string, z.ZodType<string | boolean | number
127
139
  * Creates a record schema with string keys and mixed values (string, number, or boolean)
128
140
  * @returns A record schema with mixed values
129
141
  */
130
- static mixedRecord(): ShadowMixedRecord {
142
+ static mixedRecord(): z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean]>> {
131
143
  return z.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
132
144
  }
133
145
 
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
146
  /**
144
147
  * Infers the TypeScript type from a Schema instance
145
148
  * @template S The Schema type
146
149
  * @returns The inferred TypeScript type
147
150
  */
148
- static infer<S extends Schema<Record<string, z.ZodTypeAny>>>(_schema: S): z.infer<S['schema']> {
151
+ static infer<S extends Schema<z.ZodRawShape>>(_schema: S): z.infer<S['schema']> {
149
152
  // This is a type-level utility, the implementation is not used at runtime
150
153
  return {} as z.infer<S['schema']>
151
154
  }
152
155
 
153
156
  /**
154
- * Creates a Schema instance from a description object
155
- * @param description Object describing the schema structure with type information
157
+ * Creates a Schema instance from a JSON Schema description.
158
+ *
159
+ * Note: this relies on zod's `fromJSONSchema`, which zod considers
160
+ * semi-experimental. Round-trips of schemas produced by `describe()` are
161
+ * covered by the package tests.
162
+ *
163
+ * @param description A JSON Schema object describing an object schema
156
164
  * @returns A new Schema instance
157
165
  */
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>>>> = {}
166
+ static from(description: SchemaDescription): Schema<z.ZodRawShape> {
167
+ const zodSchema = z.fromJSONSchema(description as z.core.JSONSchema.JSONSchema)
160
168
 
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
169
+ if (!(zodSchema instanceof z.ZodObject)) {
170
+ throw new Error('Schema.from expects a JSON Schema that describes an object')
243
171
  }
244
172
 
245
- return new Schema(fields)
173
+ return new Schema(zodSchema.shape)
246
174
  }
247
175
 
248
176
  /**
@@ -271,89 +199,16 @@ export class Schema<T extends Record<string, z.ZodType<string | boolean | number
271
199
  * @param data The data to parse and validate
272
200
  * @returns An object containing either the successfully parsed data or error information
273
201
  */
274
- safeParse(data: unknown): z.SafeParseReturnType<z.infer<z.ZodObject<T>>, z.infer<z.ZodObject<T>>> {
202
+ safeParse(data: unknown): z.ZodSafeParseResult<z.infer<z.ZodObject<T>>> {
275
203
  return this.schema.safeParse(data)
276
204
  }
277
205
 
278
206
  /**
279
- * Describes the schema structure and allowed types
280
- * @returns An object describing the schema structure with type information
207
+ * Serializes the schema to JSON Schema (draft 2020-12).
208
+ * @returns A JSON Schema object describing the schema structure
281
209
  */
282
210
  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
211
+ return z.toJSONSchema(this.schema, { target: 'draft-2020-12' })
357
212
  }
358
213
 
359
214
  /**
@@ -366,4 +221,3 @@ export class Schema<T extends Record<string, z.ZodType<string | boolean | number
366
221
  }
367
222
 
368
223
  export default Schema
369
-