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.
- package/README.md +832 -219
- package/dist/api-wrapper/builders.d.ts +15 -3
- package/dist/api-wrapper/create-client.d.ts +1 -1
- package/dist/api-wrapper/describe.d.ts +16 -0
- package/dist/api-wrapper/endpoint.d.ts +18 -0
- package/dist/api-wrapper/errors.d.ts +40 -2
- package/dist/api-wrapper/headers.d.ts +2 -1
- package/dist/api-wrapper/index.d.ts +5 -0
- package/dist/api-wrapper/otel.d.ts +44 -0
- package/dist/api-wrapper/paginate.d.ts +12 -0
- package/dist/api-wrapper/redaction.d.ts +2 -0
- package/dist/api-wrapper/shared.d.ts +16 -2
- package/dist/api-wrapper/testing.d.ts +23 -0
- package/dist/api-wrapper/transport.d.ts +12 -10
- package/dist/api-wrapper/types.d.ts +104 -18
- package/dist/array/index.d.ts +50 -4
- package/dist/cache/createCache.d.ts +31 -0
- package/dist/cache/index.d.ts +45 -2
- package/dist/color/contrast.d.ts +40 -0
- package/dist/color/index.d.ts +2 -0
- package/dist/{math → color}/metricsSeriesColors.d.ts +14 -7
- package/dist/cool-console-log/index.d.ts +34 -5
- package/dist/crypto/encoding.d.ts +18 -0
- package/dist/crypto/hash.d.ts +33 -0
- package/dist/crypto/index.d.ts +4 -0
- package/dist/crypto/sealer.d.ts +25 -0
- package/dist/crypto/webhook.d.ts +20 -0
- package/dist/date/index.d.ts +46 -2
- package/dist/emitter/index.d.ts +21 -0
- package/dist/error/index.d.ts +12 -0
- package/dist/format/index.d.ts +102 -0
- package/dist/function/index.d.ts +60 -0
- package/dist/guard/index.d.ts +20 -0
- package/dist/i18n/index.d.ts +37 -0
- package/dist/index-0zvtcbzy.js +224 -0
- package/dist/index-181e6vfw.js +29 -0
- package/dist/index-8r2vdzjg.js +87 -0
- package/dist/index-hy7kbb99.js +157 -0
- package/dist/{src/secrets/loaders.js → index-j1ybktbr.js} +608 -405
- package/dist/index-k9yx6bne.js +89 -0
- package/dist/index-kj1phgrx.js +169 -0
- package/dist/index-mnkcfyx8.js +78 -0
- package/dist/{p-queue/index.js → index-pwt0dz63.js} +7 -11
- package/dist/index-rp5pqghz.js +157 -0
- package/dist/index-srt06xd2.js +15 -0
- package/dist/index-wjyb9bsd.js +263 -0
- package/dist/index.d.ts +7 -7
- package/dist/jwt/index.d.ts +50 -2
- package/dist/math/index.d.ts +2 -2
- package/dist/math/linearRegressionTrend.d.ts +14 -1
- package/dist/math/scale.d.ts +8 -0
- package/dist/object/index.d.ts +19 -0
- package/dist/p-queue/index.d.ts +1 -1
- package/dist/promise/index.d.ts +97 -2
- package/dist/secrets/core.d.ts +47 -0
- package/dist/secrets/deadline.d.ts +6 -0
- package/dist/secrets/environment.d.ts +9 -0
- package/dist/secrets/errors.d.ts +11 -0
- package/dist/secrets/index.d.ts +13 -25
- package/dist/secrets/infisical-cli.d.ts +6 -0
- package/dist/secrets/infisical-config.d.ts +17 -0
- package/dist/secrets/infisical-session.d.ts +4 -0
- package/dist/secrets/infisical.d.ts +8 -13
- package/dist/secrets/loaders.d.ts +5 -12
- package/dist/secrets/vite.d.ts +7 -3
- package/dist/secrets/worker-loader.d.ts +9 -0
- package/dist/secrets/worker.d.ts +3 -0
- package/dist/sitemap/index.d.ts +48 -6
- package/dist/src/api-wrapper/index.js +60 -1100
- package/dist/src/api-wrapper/testing.js +20 -0
- package/dist/{cool-console-log → src/cool-console-log}/index.js +80 -33
- package/dist/src/crypto/index.js +195 -0
- package/dist/src/format/index.js +211 -0
- package/dist/src/i18n/index.js +36 -0
- package/dist/src/index.js +863 -2809
- package/dist/src/jwt/index.js +138 -92
- package/dist/src/p-queue/index.js +9 -437
- package/dist/src/secrets/index.js +261 -1377
- package/dist/src/secrets/vite.js +6 -17
- package/dist/src/secrets/worker.js +12 -0
- package/dist/src/sitemap/index.js +183 -511
- package/dist/src/storage/index.js +119 -0
- package/dist/src/zod/index.js +347 -174
- package/dist/state/index.d.ts +21 -0
- package/dist/storage/index.d.ts +58 -0
- package/dist/string/index.d.ts +75 -2
- package/dist/typedswitch/index.d.ts +37 -26
- package/dist/types/index.d.ts +11 -0
- package/dist/url/index.d.ts +50 -1
- package/dist/zod/index.d.ts +32 -3
- package/dist/zod/introspect.d.ts +19 -0
- package/dist/zod/jsonToZodSchemaMapper.d.ts +3 -1
- package/dist/zod/zodMinMaxFilter.d.ts +22 -3
- package/dist/zod/zodToTypeString.d.ts +8 -3
- package/package.json +87 -7
- package/src/api-wrapper/actions-impl.ts +11 -72
- package/src/api-wrapper/builders.ts +84 -52
- package/src/api-wrapper/create-client.ts +246 -96
- package/src/api-wrapper/describe.ts +157 -0
- package/src/api-wrapper/endpoint.ts +26 -0
- package/src/api-wrapper/error-reporting.ts +7 -1
- package/src/api-wrapper/errors.ts +117 -2
- package/src/api-wrapper/headers.ts +19 -1
- package/src/api-wrapper/index.ts +5 -0
- package/src/api-wrapper/otel.ts +139 -0
- package/src/api-wrapper/paginate.ts +28 -0
- package/src/api-wrapper/redaction.ts +43 -0
- package/src/api-wrapper/shared.ts +21 -2
- package/src/api-wrapper/testing.ts +48 -0
- package/src/api-wrapper/transport.ts +68 -74
- package/src/api-wrapper/types.ts +186 -18
- package/src/array/index.ts +195 -42
- package/src/cache/createCache.ts +157 -0
- package/src/cache/index.ts +235 -83
- package/src/color/contrast.ts +163 -0
- package/src/color/index.ts +22 -0
- package/src/{math → color}/metricsSeriesColors.ts +47 -49
- package/src/cool-console-log/index.ts +128 -41
- package/src/crypto/encoding.ts +83 -0
- package/src/crypto/hash.ts +170 -0
- package/src/crypto/index.ts +26 -0
- package/src/crypto/sealer.ts +168 -0
- package/src/crypto/webhook.ts +86 -0
- package/src/date/index.ts +109 -34
- package/src/emitter/index.ts +82 -0
- package/src/error/index.ts +63 -0
- package/src/format/index.ts +389 -0
- package/src/function/index.ts +201 -0
- package/src/guard/index.ts +80 -0
- package/src/i18n/index.ts +125 -0
- package/src/index.ts +11 -7
- package/src/jwt/index.ts +199 -125
- package/src/math/index.ts +5 -10
- package/src/math/linearRegressionTrend.ts +33 -8
- package/src/math/scale.ts +17 -0
- package/src/object/index.ts +99 -0
- package/src/p-queue/index.ts +2 -0
- package/src/promise/index.ts +304 -9
- package/src/secrets/core.ts +348 -0
- package/src/secrets/deadline.ts +69 -0
- package/src/secrets/environment.ts +69 -0
- package/src/secrets/errors.ts +29 -0
- package/src/secrets/index.ts +79 -210
- package/src/secrets/infisical-cli.ts +99 -0
- package/src/secrets/infisical-config.ts +70 -0
- package/src/secrets/infisical-session.ts +42 -0
- package/src/secrets/infisical.ts +214 -66
- package/src/secrets/loaders.ts +24 -48
- package/src/secrets/vite.ts +44 -15
- package/src/secrets/worker-loader.ts +56 -0
- package/src/secrets/worker.ts +4 -0
- package/src/sitemap/index.ts +293 -130
- package/src/state/index.ts +133 -81
- package/src/storage/index.ts +187 -0
- package/src/string/index.ts +261 -23
- package/src/typedswitch/index.ts +134 -103
- package/src/types/index.ts +14 -0
- package/src/url/index.ts +146 -1
- package/src/zod/index.ts +105 -9
- package/src/zod/introspect.ts +103 -0
- package/src/zod/jsonToZodSchemaMapper.ts +44 -21
- package/src/zod/zodMinMaxFilter.ts +37 -13
- package/src/zod/zodToTypeString.ts +184 -181
- package/dist/api-wrapper/index.js +0 -250
- package/dist/gzip/index.d.ts +0 -2
- package/dist/gzip/index.js +0 -16
- package/dist/index.js +0 -1823
- package/dist/jwt/index.js +0 -19
- package/dist/sitemap/index.js +0 -552
- package/dist/src/gzip/index.js +0 -16
- package/dist/src/secrets/infisical.js +0 -1194
- package/dist/zod/index.js +0 -210
package/src/url/index.ts
CHANGED
|
@@ -1,5 +1,150 @@
|
|
|
1
|
-
export
|
|
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>(
|
|
14
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
const shape = zodSchema instanceof z.ZodObject ? zodSchema.shape : {}
|
|
26
|
+
return zodSchema.parse(mapObject(jsonObject, zodSchema, config))
|
|
27
|
+
}
|
|
18
28
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
result[key] =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
(
|
|
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
|
|
56
|
+
result[key] = value
|
|
40
57
|
}
|
|
41
58
|
}
|
|
42
59
|
|
|
43
|
-
return
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
):
|
|
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 && {
|
|
26
|
-
...(hasMax && {
|
|
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
|
+
}
|