@naturalcycles/js-lib
Version:
Standard library for universal (browser + Node.js) javascript
140 lines (118 loc) • 4.05 kB
text/typescript
import type { ZodString } from 'zod'
import { z } from 'zod'
import type { IANATimezone, IsoDate, UnixTimestamp, UnixTimestampMillis } from '../types.js'
type ZodBranded<T, B> = T & Record<'_zod', Record<'output', B>>
export type ZodBrandedString<B> = ZodBranded<z.ZodString, B>
export type ZodBrandedInt<B> = ZodBranded<z.ZodInt, B>
export type ZodBrandedNumber<B> = ZodBranded<z.ZodNumber, B>
export type ZodBrandedIsoDate = ZodBranded<z.ZodISODate, IsoDate>
const TS_2500 = 16725225600 // 2500-01-01
const TS_2000 = 946684800 // 2000-01-01
function unixTimestamp(): ZodBrandedInt<UnixTimestamp> {
return z
.number()
.int()
.min(0)
.max(TS_2500, 'Must be a UnixTimestamp number')
.describe('UnixTimestamp') as ZodBrandedInt<UnixTimestamp>
}
function unixTimestamp2000(): ZodBrandedInt<UnixTimestamp> {
return z
.number()
.int()
.min(TS_2000)
.max(TS_2500, 'Must be a UnixTimestamp number after 2000-01-01')
.describe('UnixTimestamp2000') as ZodBrandedInt<UnixTimestamp>
}
function unixTimestampMillis(): ZodBranded<z.ZodNumber, UnixTimestampMillis> {
return z
.number()
.int()
.min(0)
.max(TS_2500 * 1000, 'Must be a UnixTimestampMillis number')
.describe('UnixTimestampMillis') as ZodBrandedInt<UnixTimestampMillis>
}
function unixTimestampMillis2000(): ZodBrandedInt<UnixTimestampMillis> {
return z
.number()
.int()
.min(TS_2000 * 1000)
.max(TS_2500 * 1000, 'Must be a UnixTimestampMillis number after 2000-01-01')
.describe('UnixTimestampMillis2000') as ZodBrandedInt<UnixTimestampMillis>
}
function semVer(): z.ZodString {
return z
.string()
.regex(/^[0-9]+\.[0-9]+\.[0-9]+$/, 'Must be a SemVer string')
.describe('SemVer')
}
function isoDate(): ZodBrandedString<IsoDate> {
return z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/, { error: 'Must be a YYYY-MM-DD string' })
.describe('IsoDate') as ZodBrandedString<IsoDate>
}
const BASE62_REGEX = /^[a-zA-Z0-9]+$/
const BASE64_REGEX = /^[A-Za-z0-9+/]+={0,2}$/
const BASE64URL_REGEX = /^[\w\-/]+$/
function base62(): z.ZodString {
return z.string().regex(BASE62_REGEX, 'Must be a base62 string').describe('Base62String')
}
function base64(): z.ZodString {
return z.string().regex(BASE64_REGEX, 'Must be a base64 string').describe('Base64String')
}
function base64Url(): z.ZodString {
return z.string().regex(BASE64URL_REGEX, 'Must be a base64url string').describe('Base64UrlString')
}
const JWT_REGEX = /^[\w-]+\.[\w-]+\.[\w-]+$/
function jwt(): z.ZodString {
return z.string().regex(JWT_REGEX, 'Must be a JWT string').describe('JWTString')
}
/**
* "Slug" - a valid URL, filename, etc.
*/
function slug(): z.ZodString {
return z
.string()
.regex(/^[a-z0-9-]{1,255}$/, 'Must be a slug string')
.describe('Slug')
}
function ianaTimezone(): ZodBrandedString<IANATimezone> {
return (
z
// UTC is added to assist unit-testing, which uses UTC by default (not technically a valid Iana timezone identifier)
.enum([...Intl.supportedValuesOf('timeZone'), 'UTC'])
.describe('IANATimezone') as unknown as ZodBrandedString<IANATimezone>
)
}
const baseDBEntitySchema = z.object({
id: z.string(),
created: unixTimestamp2000(),
updated: unixTimestamp2000(),
})
// oxlint-disable-next-line @typescript-eslint/consistent-type-definitions
type BaseDBEntityZodShape = {
id: ZodString
created: ZodBrandedInt<UnixTimestamp>
updated: ZodBrandedInt<UnixTimestamp>
}
function dbEntity(): z.ZodObject<BaseDBEntityZodShape>
function dbEntity<T extends z.ZodRawShape>(shape: T): z.ZodObject<BaseDBEntityZodShape & T>
function dbEntity<T extends z.ZodRawShape>(shape?: T): z.ZodObject<BaseDBEntityZodShape & T> {
return baseDBEntitySchema.extend(shape ?? {}) as z.ZodObject<BaseDBEntityZodShape & T>
}
export const customZodSchemas = {
base62,
base64,
base64Url,
dbEntity,
ianaTimezone,
isoDate,
jwt,
slug,
semver: semVer,
unixTimestamp,
unixTimestamp2000,
unixTimestampMillis,
unixTimestampMillis2000,
}