hanzio 1.1.0 → 2.0.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.
Files changed (172) hide show
  1. package/README.md +832 -219
  2. package/dist/api-wrapper/builders.d.ts +15 -3
  3. package/dist/api-wrapper/create-client.d.ts +1 -1
  4. package/dist/api-wrapper/describe.d.ts +16 -0
  5. package/dist/api-wrapper/endpoint.d.ts +18 -0
  6. package/dist/api-wrapper/errors.d.ts +40 -2
  7. package/dist/api-wrapper/headers.d.ts +2 -1
  8. package/dist/api-wrapper/index.d.ts +5 -0
  9. package/dist/api-wrapper/otel.d.ts +44 -0
  10. package/dist/api-wrapper/paginate.d.ts +12 -0
  11. package/dist/api-wrapper/redaction.d.ts +2 -0
  12. package/dist/api-wrapper/shared.d.ts +16 -2
  13. package/dist/api-wrapper/testing.d.ts +23 -0
  14. package/dist/api-wrapper/transport.d.ts +12 -10
  15. package/dist/api-wrapper/types.d.ts +104 -18
  16. package/dist/array/index.d.ts +50 -4
  17. package/dist/cache/createCache.d.ts +31 -0
  18. package/dist/cache/index.d.ts +45 -2
  19. package/dist/color/contrast.d.ts +40 -0
  20. package/dist/color/index.d.ts +2 -0
  21. package/dist/{math → color}/metricsSeriesColors.d.ts +14 -7
  22. package/dist/cool-console-log/index.d.ts +34 -5
  23. package/dist/crypto/encoding.d.ts +18 -0
  24. package/dist/crypto/hash.d.ts +33 -0
  25. package/dist/crypto/index.d.ts +4 -0
  26. package/dist/crypto/sealer.d.ts +25 -0
  27. package/dist/crypto/webhook.d.ts +20 -0
  28. package/dist/date/index.d.ts +46 -2
  29. package/dist/emitter/index.d.ts +21 -0
  30. package/dist/error/index.d.ts +12 -0
  31. package/dist/format/index.d.ts +102 -0
  32. package/dist/function/index.d.ts +60 -0
  33. package/dist/guard/index.d.ts +20 -0
  34. package/dist/i18n/index.d.ts +37 -0
  35. package/dist/index-0zvtcbzy.js +224 -0
  36. package/dist/index-181e6vfw.js +29 -0
  37. package/dist/index-8r2vdzjg.js +87 -0
  38. package/dist/index-hy7kbb99.js +157 -0
  39. package/dist/{src/secrets/loaders.js → index-j1ybktbr.js} +608 -405
  40. package/dist/index-k9yx6bne.js +89 -0
  41. package/dist/index-kj1phgrx.js +169 -0
  42. package/dist/index-mnkcfyx8.js +78 -0
  43. package/dist/{p-queue/index.js → index-pwt0dz63.js} +7 -11
  44. package/dist/index-rp5pqghz.js +157 -0
  45. package/dist/index-srt06xd2.js +15 -0
  46. package/dist/index-wjyb9bsd.js +263 -0
  47. package/dist/index.d.ts +7 -7
  48. package/dist/jwt/index.d.ts +50 -2
  49. package/dist/math/index.d.ts +2 -2
  50. package/dist/math/linearRegressionTrend.d.ts +14 -1
  51. package/dist/math/scale.d.ts +8 -0
  52. package/dist/object/index.d.ts +19 -0
  53. package/dist/p-queue/index.d.ts +1 -1
  54. package/dist/promise/index.d.ts +97 -2
  55. package/dist/secrets/core.d.ts +47 -0
  56. package/dist/secrets/deadline.d.ts +6 -0
  57. package/dist/secrets/environment.d.ts +9 -0
  58. package/dist/secrets/errors.d.ts +11 -0
  59. package/dist/secrets/index.d.ts +13 -25
  60. package/dist/secrets/infisical-cli.d.ts +6 -0
  61. package/dist/secrets/infisical-config.d.ts +17 -0
  62. package/dist/secrets/infisical-session.d.ts +4 -0
  63. package/dist/secrets/infisical.d.ts +8 -13
  64. package/dist/secrets/loaders.d.ts +5 -12
  65. package/dist/secrets/vite.d.ts +7 -3
  66. package/dist/secrets/worker-loader.d.ts +9 -0
  67. package/dist/secrets/worker.d.ts +3 -0
  68. package/dist/sitemap/index.d.ts +48 -6
  69. package/dist/src/api-wrapper/index.js +60 -1100
  70. package/dist/src/api-wrapper/testing.js +20 -0
  71. package/dist/{cool-console-log → src/cool-console-log}/index.js +80 -33
  72. package/dist/src/crypto/index.js +195 -0
  73. package/dist/src/format/index.js +211 -0
  74. package/dist/src/i18n/index.js +36 -0
  75. package/dist/src/index.js +863 -2809
  76. package/dist/src/jwt/index.js +138 -92
  77. package/dist/src/p-queue/index.js +9 -437
  78. package/dist/src/secrets/index.js +261 -1377
  79. package/dist/src/secrets/vite.js +6 -17
  80. package/dist/src/secrets/worker.js +12 -0
  81. package/dist/src/sitemap/index.js +183 -511
  82. package/dist/src/storage/index.js +119 -0
  83. package/dist/src/zod/index.js +347 -174
  84. package/dist/state/index.d.ts +21 -0
  85. package/dist/storage/index.d.ts +58 -0
  86. package/dist/string/index.d.ts +75 -2
  87. package/dist/typedswitch/index.d.ts +37 -26
  88. package/dist/types/index.d.ts +11 -0
  89. package/dist/url/index.d.ts +50 -1
  90. package/dist/zod/index.d.ts +32 -3
  91. package/dist/zod/introspect.d.ts +19 -0
  92. package/dist/zod/jsonToZodSchemaMapper.d.ts +3 -1
  93. package/dist/zod/zodMinMaxFilter.d.ts +22 -3
  94. package/dist/zod/zodToTypeString.d.ts +8 -3
  95. package/package.json +87 -7
  96. package/src/api-wrapper/actions-impl.ts +11 -72
  97. package/src/api-wrapper/builders.ts +84 -52
  98. package/src/api-wrapper/create-client.ts +246 -96
  99. package/src/api-wrapper/describe.ts +157 -0
  100. package/src/api-wrapper/endpoint.ts +26 -0
  101. package/src/api-wrapper/error-reporting.ts +7 -1
  102. package/src/api-wrapper/errors.ts +117 -2
  103. package/src/api-wrapper/headers.ts +19 -1
  104. package/src/api-wrapper/index.ts +5 -0
  105. package/src/api-wrapper/otel.ts +139 -0
  106. package/src/api-wrapper/paginate.ts +28 -0
  107. package/src/api-wrapper/redaction.ts +43 -0
  108. package/src/api-wrapper/shared.ts +21 -2
  109. package/src/api-wrapper/testing.ts +48 -0
  110. package/src/api-wrapper/transport.ts +68 -74
  111. package/src/api-wrapper/types.ts +186 -18
  112. package/src/array/index.ts +195 -42
  113. package/src/cache/createCache.ts +157 -0
  114. package/src/cache/index.ts +235 -83
  115. package/src/color/contrast.ts +163 -0
  116. package/src/color/index.ts +22 -0
  117. package/src/{math → color}/metricsSeriesColors.ts +47 -49
  118. package/src/cool-console-log/index.ts +128 -41
  119. package/src/crypto/encoding.ts +83 -0
  120. package/src/crypto/hash.ts +170 -0
  121. package/src/crypto/index.ts +26 -0
  122. package/src/crypto/sealer.ts +168 -0
  123. package/src/crypto/webhook.ts +86 -0
  124. package/src/date/index.ts +109 -34
  125. package/src/emitter/index.ts +82 -0
  126. package/src/error/index.ts +63 -0
  127. package/src/format/index.ts +389 -0
  128. package/src/function/index.ts +201 -0
  129. package/src/guard/index.ts +80 -0
  130. package/src/i18n/index.ts +125 -0
  131. package/src/index.ts +11 -7
  132. package/src/jwt/index.ts +199 -125
  133. package/src/math/index.ts +5 -10
  134. package/src/math/linearRegressionTrend.ts +33 -8
  135. package/src/math/scale.ts +17 -0
  136. package/src/object/index.ts +99 -0
  137. package/src/p-queue/index.ts +2 -0
  138. package/src/promise/index.ts +304 -9
  139. package/src/secrets/core.ts +348 -0
  140. package/src/secrets/deadline.ts +69 -0
  141. package/src/secrets/environment.ts +69 -0
  142. package/src/secrets/errors.ts +29 -0
  143. package/src/secrets/index.ts +79 -210
  144. package/src/secrets/infisical-cli.ts +99 -0
  145. package/src/secrets/infisical-config.ts +70 -0
  146. package/src/secrets/infisical-session.ts +42 -0
  147. package/src/secrets/infisical.ts +214 -66
  148. package/src/secrets/loaders.ts +24 -48
  149. package/src/secrets/vite.ts +44 -15
  150. package/src/secrets/worker-loader.ts +56 -0
  151. package/src/secrets/worker.ts +4 -0
  152. package/src/sitemap/index.ts +293 -130
  153. package/src/state/index.ts +133 -81
  154. package/src/storage/index.ts +187 -0
  155. package/src/string/index.ts +261 -23
  156. package/src/typedswitch/index.ts +134 -103
  157. package/src/types/index.ts +14 -0
  158. package/src/url/index.ts +146 -1
  159. package/src/zod/index.ts +105 -9
  160. package/src/zod/introspect.ts +103 -0
  161. package/src/zod/jsonToZodSchemaMapper.ts +44 -21
  162. package/src/zod/zodMinMaxFilter.ts +37 -13
  163. package/src/zod/zodToTypeString.ts +184 -181
  164. package/dist/api-wrapper/index.js +0 -250
  165. package/dist/gzip/index.d.ts +0 -2
  166. package/dist/gzip/index.js +0 -16
  167. package/dist/index.js +0 -1823
  168. package/dist/jwt/index.js +0 -19
  169. package/dist/sitemap/index.js +0 -552
  170. package/dist/src/gzip/index.js +0 -16
  171. package/dist/src/secrets/infisical.js +0 -1194
  172. package/dist/zod/index.js +0 -210
package/src/url/index.ts CHANGED
@@ -1,5 +1,150 @@
1
- export function getDomainFaviconUrl(domain: string, size = 128): string {
1
+ export type UrlQueryValue = string | number | boolean | null | undefined
2
+ export type UrlQueryParams = Record<
3
+ string,
4
+ UrlQueryValue | readonly UrlQueryValue[]
5
+ >
6
+
7
+ /**
8
+ * Serializes query params. Arrays repeat the key (`a=1&a=2`); `null` and
9
+ * `undefined` are skipped. Returns the string without a leading `?`.
10
+ */
11
+ export function buildQueryString(
12
+ params: UrlQueryParams,
13
+ options: { encode?: boolean } = {}
14
+ ): string {
15
+ const pairs: [string, string][] = []
16
+ for (const [key, value] of Object.entries(params)) {
17
+ const values = Array.isArray(value) ? value : [value]
18
+ for (const item of values) {
19
+ if (item != null) pairs.push([key, String(item)])
20
+ }
21
+ }
22
+
23
+ if (options.encode === false) {
24
+ return pairs.map(([key, value]) => `${key}=${value}`).join('&')
25
+ }
26
+ return new URLSearchParams(pairs).toString()
27
+ }
28
+
29
+ /**
30
+ * Replaces `:name` placeholders with encoded values. Throws when a placeholder
31
+ * has no value. Only `:` followed by a letter or `_` counts, so ports like
32
+ * `:8080` are left alone.
33
+ */
34
+ export function replacePathParams(
35
+ path: string,
36
+ params: Record<string, string | number | boolean | undefined> = {}
37
+ ): string {
38
+ return path.replace(/:([A-Za-z_]\w*)/g, (_, key: string) => {
39
+ const value = params[key]
40
+ if (value === undefined) {
41
+ throw new Error(`Missing required path parameter: ${key}`)
42
+ }
43
+ return encodeURIComponent(String(value))
44
+ })
45
+ }
46
+
47
+ /** Joins base and path with exactly one `/`, fills `:params`, appends query. */
48
+ export function buildUrl(
49
+ base: string,
50
+ path = '',
51
+ options: {
52
+ params?: Record<string, string | number | boolean | undefined>
53
+ query?: UrlQueryParams
54
+ } = {}
55
+ ): string {
56
+ const joined = path
57
+ ? `${trimTrailingSlash(base)}/${path.replace(/^\/+/, '')}`
58
+ : base
59
+ const url = replacePathParams(joined, options.params)
60
+ const query = options.query ? buildQueryString(options.query) : ''
61
+ if (!query) return url
62
+ return `${url}${url.includes('?') ? '&' : '?'}${query}`
63
+ }
64
+
65
+ /**
66
+ * Normalizes a domain or URL to its origin: adds `https://` when no scheme is
67
+ * given and strips any path, query or trailing slash.
68
+ */
69
+ export function normalizeDomain(input: string): string {
70
+ const trimmed = input.trim()
71
+ const withScheme = /^[a-z][a-z\d+.-]*:\/\//i.test(trimmed)
72
+ ? trimmed
73
+ : `https://${trimmed}`
74
+ return new URL(withScheme).origin
75
+ }
76
+
77
+ /** Accepts a bare domain (`example.com`) or a full URL. */
78
+ export function getDomainFaviconUrl(domainOrUrl: string, size = 128): string {
79
+ let domain = domainOrUrl
80
+ try {
81
+ if (/^[a-z][a-z\d+.-]*:\/\//i.test(domainOrUrl)) {
82
+ domain = new URL(domainOrUrl).hostname
83
+ }
84
+ } catch {
85
+ // Not a URL; use the input as-is.
86
+ }
2
87
  return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(
3
88
  domain
4
89
  )}&sz=${size}`
5
90
  }
91
+
92
+ /** Removes trailing slashes: `'https://x.com/a/'` → `'https://x.com/a'`. */
93
+ export function trimTrailingSlash(url: string): string {
94
+ return url.replace(/\/+$/, '')
95
+ }
96
+
97
+ /** Prefixes `/` unless the path already starts with one. */
98
+ export function ensureLeadingSlash(path: string): string {
99
+ return path.startsWith('/') ? path : `/${path}`
100
+ }
101
+
102
+ type MailtoRecipients = string | readonly string[]
103
+
104
+ /**
105
+ * Builds a `mailto:` link. Spaces become `%20` (not `+`, which mail clients
106
+ * show literally) and body line breaks become `%0D%0A`.
107
+ */
108
+ export function buildMailto(options: {
109
+ to?: MailtoRecipients
110
+ cc?: MailtoRecipients
111
+ bcc?: MailtoRecipients
112
+ subject?: string
113
+ body?: string
114
+ }): string {
115
+ const { to, cc, bcc, subject, body } = options
116
+ const headers = Object.entries({
117
+ cc: cc && recipients(cc),
118
+ bcc: bcc && recipients(bcc),
119
+ subject: subject && encodeURIComponent(subject),
120
+ body: body && encodeURIComponent(body.replace(/\r?\n/g, '\r\n'))
121
+ })
122
+ .filter(([, value]) => value)
123
+ .map(([key, value]) => `${key}=${value}`)
124
+ .join('&')
125
+ return `mailto:${to ? recipients(to) : ''}${headers ? `?${headers}` : ''}`
126
+ }
127
+
128
+ function recipients(value: MailtoRecipients): string {
129
+ return (typeof value === 'string' ? [value] : value)
130
+ .map((address) => encodeURIComponent(address.trim()).replace(/%40/g, '@'))
131
+ .join(',')
132
+ }
133
+
134
+ /**
135
+ * Milliseconds to wait from a `Retry-After` header: delta-seconds (fractions
136
+ * allowed) or an HTTP date relative to `now`. Past dates and negative values
137
+ * give `0`; a missing or invalid value gives `undefined`.
138
+ */
139
+ export function parseRetryAfter(
140
+ value: string | null | undefined,
141
+ now = Date.now()
142
+ ): number | undefined {
143
+ const trimmed = value?.trim()
144
+ if (!trimmed) return undefined
145
+ if (/^-?\d*\.?\d+$/.test(trimmed)) {
146
+ return Math.max(0, Number(trimmed) * 1000)
147
+ }
148
+ const date = Date.parse(trimmed)
149
+ return Number.isNaN(date) ? undefined : Math.max(0, date - now)
150
+ }
package/src/zod/index.ts CHANGED
@@ -1,8 +1,21 @@
1
1
  import { z } from 'zod'
2
+ import { toArray } from '../array'
3
+ import { err, ok, type Result } from '../promise'
4
+ import type { JsonObject, JsonValue } from '../string'
5
+
6
+ export {
7
+ getEnumValues,
8
+ getObjectShape,
9
+ unwrapSchema,
10
+ zodDefaults
11
+ } from './introspect'
2
12
 
3
13
  export { jsonToZodSchemaMapper } from './jsonToZodSchemaMapper'
4
14
  export {
15
+ createMinMaxFilter,
16
+ type MinMaxFilter,
5
17
  toGteLteFilter,
18
+ toGtLtFilter,
6
19
  type ZodMinMaxFilter,
7
20
  zodMinMaxFilter
8
21
  } from './zodMinMaxFilter'
@@ -10,8 +23,96 @@ export { zodToExample, zodToTypeString } from './zodToTypeString'
10
23
 
11
24
  export type GenericId<TypeName extends string> = string & z.$brand<TypeName>
12
25
 
13
- export function createZId<TypeName extends string>(typeName: TypeName) {
14
- return z.string().min(1, `${typeName} ID is required`).brand<TypeName>()
26
+ export function createZId<TypeName extends string>(
27
+ typeName: TypeName,
28
+ { prefix = '' }: { prefix?: string } = {}
29
+ ) {
30
+ return z
31
+ .string()
32
+ .min(1, `${typeName} ID is required`)
33
+ .startsWith(prefix, `${typeName} ID must start with "${prefix}"`)
34
+ .brand<TypeName>()
35
+ }
36
+
37
+ /** Branded string id: `Id<'User'>` is not assignable to `Id<'Organization'>`. */
38
+ export type Id<TypeName extends string> = GenericId<TypeName>
39
+
40
+ /** Brands a string without checking it (fixtures, trusted sources). */
41
+ export function asId<TypeName extends string>(value: string): Id<TypeName> {
42
+ return value as Id<TypeName>
43
+ }
44
+
45
+ /** Validates a non-empty string and brands it; throws a `ZodError` otherwise. */
46
+ export function parseId<TypeName extends string>(
47
+ typeName: TypeName,
48
+ value: unknown
49
+ ): Id<TypeName> {
50
+ return createZId(typeName).parse(value) as Id<TypeName>
51
+ }
52
+
53
+ export function safeParseId<TypeName extends string>(
54
+ typeName: TypeName,
55
+ value: unknown
56
+ ): Result<Id<TypeName>, z.ZodError> {
57
+ // `.brand()` output stays deferred for a generic TypeName
58
+ return toResult(createZId(typeName).safeParse(value)) as Result<
59
+ Id<TypeName>,
60
+ z.ZodError
61
+ >
62
+ }
63
+
64
+ /**
65
+ * Binds the id helpers to a project's union of entity names once:
66
+ * `export const { zId, asId, parseId, safeParseId } = createIdHelpers<'User' | 'Organization'>()`
67
+ */
68
+ export function createIdHelpers<TypeNames extends string>() {
69
+ return {
70
+ zId: <T extends TypeNames>(typeName: T) => createZId(typeName),
71
+ asId: asId as <T extends TypeNames>(value: string) => Id<T>,
72
+ parseId: parseId as <T extends TypeNames>(
73
+ typeName: T,
74
+ value: unknown
75
+ ) => Id<T>,
76
+ safeParseId: safeParseId as <T extends TypeNames>(
77
+ typeName: T,
78
+ value: unknown
79
+ ) => Result<Id<T>, z.ZodError>
80
+ }
81
+ }
82
+
83
+ /** Any JSON value (no `undefined`, `NaN` or `Infinity`). */
84
+ export const zJsonValue: z.ZodType<JsonValue> = z.lazy(() =>
85
+ z.union([
86
+ z.string(),
87
+ z.number(),
88
+ z.boolean(),
89
+ z.null(),
90
+ z.array(zJsonValue),
91
+ zJsonObject
92
+ ])
93
+ )
94
+
95
+ export const zJsonObject: z.ZodType<JsonObject> = z.record(
96
+ z.string(),
97
+ zJsonValue
98
+ )
99
+
100
+ /** `JSON.parse` + `schema.safeParse` without throwing. */
101
+ export function parseJson<S extends z.ZodType>(
102
+ text: string,
103
+ schema: S
104
+ ): Result<z.output<S>, SyntaxError | z.ZodError> {
105
+ let value: unknown
106
+ try {
107
+ value = JSON.parse(text)
108
+ } catch (error) {
109
+ return { ok: false, data: null, error: error as SyntaxError }
110
+ }
111
+ return toResult(schema.safeParse(value))
112
+ }
113
+
114
+ function toResult<T>(result: z.ZodSafeParseResult<T>): Result<T, z.ZodError> {
115
+ return result.success ? ok(result.data) : err(result.error)
15
116
  }
16
117
 
17
118
  export function zSingleOrArray<T extends z.ZodType>(
@@ -20,10 +121,5 @@ export function zSingleOrArray<T extends z.ZodType>(
20
121
  return z.union([z.array(schema), schema])
21
122
  }
22
123
 
23
- export function normalizeSingleOrArray<T>(
24
- value: T | readonly T[] | null | undefined
25
- ): T[] {
26
- if (value == null) return []
27
- if (Array.isArray(value)) return [...(value as readonly T[])]
28
- return [value as T]
29
- }
124
+ /** @deprecated Use `toArray` from `hanzio`. */
125
+ export const normalizeSingleOrArray = toArray
@@ -0,0 +1,103 @@
1
+ import type { z } from 'zod'
2
+ import { type Def, defOf, type Schema, walk } from './zodToTypeString'
3
+
4
+ // Pipes are read from their input side, except preprocess (`in` is a transform)
5
+ function pipeInput(def: z.core.$ZodPipeDef): Schema {
6
+ return defOf(def.in).type === 'transform' ? def.out : def.in
7
+ }
8
+
9
+ function unwrapOnce(def: Def): Schema | undefined {
10
+ switch (def.type) {
11
+ case 'optional':
12
+ case 'nullable':
13
+ case 'default':
14
+ case 'prefault':
15
+ case 'nonoptional':
16
+ case 'catch':
17
+ case 'readonly':
18
+ return def.innerType
19
+ case 'lazy':
20
+ return def.getter()
21
+ case 'pipe':
22
+ return pipeInput(def)
23
+ default:
24
+ return undefined
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Strips optional/nullable/default/prefault/nonoptional/catch/readonly/lazy
30
+ * wrappers and pipes (input side) until the schema that carries the shape.
31
+ */
32
+ export function unwrapSchema(schema: Schema): Schema {
33
+ const inner = unwrapOnce(defOf(schema))
34
+ return inner ? unwrapSchema(inner) : schema
35
+ }
36
+
37
+ /** Values of a (possibly wrapped) `z.enum`, stringified; `undefined` for other schemas. */
38
+ export function getEnumValues(schema: Schema): string[] | undefined {
39
+ const def = defOf(unwrapSchema(schema))
40
+ return def.type === 'enum'
41
+ ? Object.values(def.entries).map(String)
42
+ : undefined
43
+ }
44
+
45
+ /** Shape of a (possibly wrapped) `z.object`; `undefined` for other schemas. */
46
+ export function getObjectShape(
47
+ schema: Schema
48
+ ): Readonly<Record<string, Schema>> | undefined {
49
+ const def = defOf(unwrapSchema(schema))
50
+ return def.type === 'object' ? def.shape : undefined
51
+ }
52
+
53
+ /**
54
+ * Initial form state for a schema: `.default()` values, `undefined` for
55
+ * optional, `null` for nullable, first enum/literal/union option, `''`, `0`,
56
+ * `false`, `[]`, and objects filled per key. Other types give `undefined`.
57
+ * The result is typed as the schema output but is NOT validated (`''` fails
58
+ * `.min(1)`, dates stay `undefined`).
59
+ */
60
+ export function zodDefaults<S extends Schema>(schema: S): z.output<S> {
61
+ return defaultFor(schema) as z.output<S>
62
+ }
63
+
64
+ function defaultFor(schema: Schema): unknown {
65
+ return walk(schema, undefined, (def): unknown => {
66
+ switch (def.type) {
67
+ case 'default':
68
+ case 'prefault':
69
+ return def.defaultValue
70
+ case 'optional':
71
+ return undefined
72
+ case 'nullable':
73
+ return null
74
+ case 'object': {
75
+ const result: Record<string, unknown> = {}
76
+ for (const [key, child] of Object.entries(def.shape)) {
77
+ result[key] = defaultFor(child)
78
+ }
79
+ return result
80
+ }
81
+ case 'array':
82
+ return []
83
+ case 'string':
84
+ return ''
85
+ case 'number':
86
+ return 0
87
+ case 'bigint':
88
+ return 0n
89
+ case 'boolean':
90
+ return false
91
+ case 'enum':
92
+ return Object.values(def.entries)[0]
93
+ case 'literal':
94
+ return def.values[0]
95
+ case 'union':
96
+ return def.options[0] && defaultFor(def.options[0])
97
+ default: {
98
+ const inner = unwrapOnce(def)
99
+ return inner && defaultFor(inner)
100
+ }
101
+ }
102
+ })
103
+ }
@@ -1,7 +1,18 @@
1
1
  import { z } from 'zod'
2
+ import { isRecord } from '../guard'
3
+
4
+ type ObjectConfig<V> =
5
+ V extends Record<string, unknown>
6
+ ? PropertyConfig<V, Record<string, any>>
7
+ : never
8
+
9
+ // Nested objects (and arrays of objects) take a config for the nested JSON value
10
+ type NestedConfig<V> = V extends readonly (infer E)[]
11
+ ? ObjectConfig<E>
12
+ : ObjectConfig<V>
2
13
 
3
14
  type PropertyConfig<T, U> = {
4
- [K in keyof T]?: (jsonObject: U) => T[K]
15
+ [K in keyof T]?: ((jsonObject: U) => T[K]) | NestedConfig<NonNullable<T[K]>>
5
16
  }
6
17
 
7
18
  export function jsonToZodSchemaMapper<
@@ -12,33 +23,45 @@ export function jsonToZodSchemaMapper<
12
23
  zodSchema: T,
13
24
  config: PropertyConfig<z.infer<T>, U>
14
25
  ): z.infer<T> {
15
- const result: Partial<z.infer<T>> = {}
16
-
17
- const shape = zodSchema instanceof z.ZodObject ? zodSchema.shape : {}
26
+ return zodSchema.parse(mapObject(jsonObject, zodSchema, config))
27
+ }
18
28
 
19
- for (const key of Object.keys(shape)) {
20
- const fieldConfig = config[key as keyof z.infer<T>]
21
- let value: any = jsonObject[key as keyof U]
29
+ function mapObject(
30
+ jsonObject: Record<string, any>,
31
+ schema: z.core.$ZodType,
32
+ config: Record<string, unknown>
33
+ ): Record<string, unknown> {
34
+ const shape = schema instanceof z.ZodObject ? schema.shape : {}
35
+ const result: Record<string, unknown> = {}
22
36
 
23
- if (fieldConfig) {
24
- value = fieldConfig(jsonObject)
37
+ for (const [key, fieldSchema] of Object.entries(shape)) {
38
+ const fieldConfig = config[key]
39
+ if (typeof fieldConfig === 'function') {
40
+ result[key] = fieldConfig(jsonObject)
41
+ continue
25
42
  }
26
43
 
27
- if (
28
- shape[key] instanceof z.ZodObject &&
29
- value &&
30
- typeof value === 'object'
31
- ) {
32
- // @ts-expect-error recursive mapper narrows key by shape iteration
33
- result[key] = jsonToZodSchemaMapper(
34
- value,
35
- shape[key] as z.ZodObject,
36
- (config[key as keyof z.infer<T>] || {}) as PropertyConfig<any, any>
44
+ const nested = (fieldConfig ?? {}) as Record<string, unknown>
45
+ const value = jsonObject[key]
46
+ const inner = unwrap(fieldSchema)
47
+ const element = inner instanceof z.ZodArray && unwrap(inner.element)
48
+
49
+ if (inner instanceof z.ZodObject && isRecord(value)) {
50
+ result[key] = mapObject(value, inner, nested)
51
+ } else if (element instanceof z.ZodObject && Array.isArray(value)) {
52
+ result[key] = value.map((item) =>
53
+ isRecord(item) ? mapObject(item, element, nested) : item
37
54
  )
38
55
  } else {
39
- result[key as keyof z.infer<T>] = value
56
+ result[key] = value
40
57
  }
41
58
  }
42
59
 
43
- return zodSchema.parse(result)
60
+ return result
61
+ }
62
+
63
+ function unwrap(schema: z.core.$ZodType): z.core.$ZodType {
64
+ return schema instanceof z.ZodOptional || schema instanceof z.ZodNullable
65
+ ? unwrap(schema.unwrap())
66
+ : schema
44
67
  }
@@ -1,19 +1,24 @@
1
1
  import { z } from 'zod'
2
2
 
3
- export const zodMinMaxFilter = z.object({
4
- min: z.number().optional(),
5
- max: z.number().optional()
6
- })
3
+ /**
4
+ * Creates a `{ min?, max? }` filter schema for any comparable value schema,
5
+ * e.g. `createMinMaxFilter(z.coerce.date())`.
6
+ */
7
+ export function createMinMaxFilter<T extends z.ZodType>(schema: T) {
8
+ return z.object({ min: schema.optional(), max: schema.optional() })
9
+ }
10
+
11
+ export const zodMinMaxFilter = createMinMaxFilter(z.number())
7
12
 
8
13
  export type ZodMinMaxFilter = z.infer<typeof zodMinMaxFilter>
9
14
 
10
- /**
11
- * Maps a min/max filter to a generic `gte`/`lte` range object (e.g. ORM-friendly).
12
- * Returns `undefined` if both bounds are undefined.
13
- */
14
- export function toGteLteFilter(
15
- filter: ZodMinMaxFilter | undefined
16
- ): { gte?: number; lte?: number } | undefined {
15
+ export type MinMaxFilter<T> = { min?: T; max?: T }
16
+
17
+ function toRange<T>(
18
+ filter: MinMaxFilter<T> | undefined,
19
+ lower: string,
20
+ upper: string
21
+ ): Record<string, T> | undefined {
17
22
  if (!filter) return undefined
18
23
 
19
24
  const hasMin = filter.min !== undefined
@@ -22,7 +27,26 @@ export function toGteLteFilter(
22
27
  if (!hasMin && !hasMax) return undefined
23
28
 
24
29
  return {
25
- ...(hasMin && { gte: filter.min }),
26
- ...(hasMax && { lte: filter.max })
30
+ ...(hasMin && { [lower]: filter.min as T }),
31
+ ...(hasMax && { [upper]: filter.max as T })
27
32
  }
28
33
  }
34
+
35
+ /**
36
+ * Maps a min/max filter to a generic `gte`/`lte` range object (e.g. ORM-friendly).
37
+ * Returns `undefined` if both bounds are undefined.
38
+ */
39
+ export function toGteLteFilter<T>(
40
+ filter: MinMaxFilter<T> | undefined
41
+ ): { gte?: T; lte?: T } | undefined {
42
+ return toRange(filter, 'gte', 'lte')
43
+ }
44
+
45
+ /**
46
+ * Exclusive variant of `toGteLteFilter`, mapping min/max to `gt`/`lt`.
47
+ */
48
+ export function toGtLtFilter<T>(
49
+ filter: MinMaxFilter<T> | undefined
50
+ ): { gt?: T; lt?: T } | undefined {
51
+ return toRange(filter, 'gt', 'lt')
52
+ }