Files
drizzle-team--drizzle-orm/drizzle-zod/src/column.ts
T
2026-07-13 12:10:05 +08:00

320 lines
9.2 KiB
TypeScript

import type { Column, ColumnBaseConfig } from 'drizzle-orm';
import type {
MySqlBigInt53,
MySqlChar,
MySqlDouble,
MySqlFloat,
MySqlInt,
MySqlMediumInt,
MySqlReal,
MySqlSerial,
MySqlSmallInt,
MySqlText,
MySqlTinyInt,
MySqlVarChar,
MySqlYear,
} from 'drizzle-orm/mysql-core';
import type {
PgArray,
PgBigInt53,
PgBigSerial53,
PgBinaryVector,
PgChar,
PgDoublePrecision,
PgGeometry,
PgGeometryObject,
PgHalfVector,
PgInteger,
PgLineABC,
PgLineTuple,
PgPointObject,
PgPointTuple,
PgReal,
PgSerial,
PgSmallInt,
PgSmallSerial,
PgUUID,
PgVarchar,
PgVector,
} from 'drizzle-orm/pg-core';
import type {
SingleStoreBigInt53,
SingleStoreChar,
SingleStoreDouble,
SingleStoreFloat,
SingleStoreInt,
SingleStoreMediumInt,
SingleStoreReal,
SingleStoreSerial,
SingleStoreSmallInt,
SingleStoreText,
SingleStoreTinyInt,
SingleStoreVarChar,
SingleStoreYear,
} from 'drizzle-orm/singlestore-core';
import type { SQLiteInteger, SQLiteReal, SQLiteText } from 'drizzle-orm/sqlite-core';
import { z as zod } from 'zod/v4';
import { CONSTANTS } from './constants.ts';
import type { CreateSchemaFactoryOptions } from './schema.types.ts';
import { isColumnType, isWithEnum } from './utils.ts';
import type { Json } from './utils.ts';
export const literalSchema = zod.union([zod.string(), zod.number(), zod.boolean(), zod.null()]);
export const jsonSchema: zod.ZodType<Json> = zod.union([
literalSchema,
zod.record(zod.string(), zod.any()),
zod.array(zod.any()),
]);
export const bufferSchema: zod.ZodType<Buffer> = zod.custom<Buffer>((v) => v instanceof Buffer); // eslint-disable-line no-instanceof/no-instanceof
export function columnToSchema(
column: Column,
factory:
| CreateSchemaFactoryOptions<
Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined
>
| undefined,
): zod.ZodType {
const z: typeof zod = factory?.zodInstance ?? zod;
const coerce = factory?.coerce ?? {};
let schema!: zod.ZodType;
if (isWithEnum(column)) {
schema = column.enumValues.length ? z.enum(column.enumValues) : z.string();
}
if (!schema) {
// Handle specific types
if (isColumnType<PgGeometry<any> | PgPointTuple<any>>(column, ['PgGeometry', 'PgPointTuple'])) {
schema = z.tuple([z.number(), z.number()]);
} else if (
isColumnType<PgPointObject<any> | PgGeometryObject<any>>(column, ['PgGeometryObject', 'PgPointObject'])
) {
schema = z.object({ x: z.number(), y: z.number() });
} else if (isColumnType<PgHalfVector<any> | PgVector<any>>(column, ['PgHalfVector', 'PgVector'])) {
schema = z.array(z.number());
schema = column.dimensions ? (schema as zod.ZodArray<any>).length(column.dimensions) : schema;
} else if (isColumnType<PgLineTuple<any>>(column, ['PgLine'])) {
schema = z.tuple([z.number(), z.number(), z.number()]);
} else if (isColumnType<PgLineABC<any>>(column, ['PgLineABC'])) {
schema = z.object({
a: z.number(),
b: z.number(),
c: z.number(),
});
} // Handle other types
else if (isColumnType<PgArray<any, any>>(column, ['PgArray'])) {
schema = z.array(columnToSchema(column.baseColumn, factory));
schema = column.size ? (schema as zod.ZodArray<any>).length(column.size) : schema;
} else if (column.dataType === 'array') {
schema = z.array(z.any());
} else if (column.dataType === 'number') {
schema = numberColumnToSchema(column, z, coerce);
} else if (column.dataType === 'bigint') {
schema = bigintColumnToSchema(column, z, coerce);
} else if (column.dataType === 'boolean') {
schema = coerce === true || coerce.boolean ? z.coerce.boolean() : z.boolean();
} else if (column.dataType === 'date') {
schema = coerce === true || coerce.date ? z.coerce.date() : z.date();
} else if (column.dataType === 'string') {
schema = stringColumnToSchema(column, z, coerce);
} else if (column.dataType === 'json') {
schema = jsonSchema;
} else if (column.dataType === 'custom') {
schema = z.any();
} else if (column.dataType === 'buffer') {
schema = bufferSchema;
}
}
if (!schema) {
schema = z.any();
}
return schema;
}
function numberColumnToSchema(
column: Column,
z: typeof zod,
coerce: CreateSchemaFactoryOptions<
Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined
>['coerce'],
): zod.ZodType {
let unsigned = column.getSQLType().includes('unsigned');
let min!: number;
let max!: number;
let integer = false;
if (isColumnType<MySqlTinyInt<any> | SingleStoreTinyInt<any>>(column, ['MySqlTinyInt', 'SingleStoreTinyInt'])) {
min = unsigned ? 0 : CONSTANTS.INT8_MIN;
max = unsigned ? CONSTANTS.INT8_UNSIGNED_MAX : CONSTANTS.INT8_MAX;
integer = true;
} else if (
isColumnType<PgSmallInt<any> | PgSmallSerial<any> | MySqlSmallInt<any> | SingleStoreSmallInt<any>>(column, [
'PgSmallInt',
'PgSmallSerial',
'MySqlSmallInt',
'SingleStoreSmallInt',
])
) {
min = unsigned ? 0 : CONSTANTS.INT16_MIN;
max = unsigned ? CONSTANTS.INT16_UNSIGNED_MAX : CONSTANTS.INT16_MAX;
integer = true;
} else if (
isColumnType<
PgReal<any> | MySqlFloat<any> | MySqlMediumInt<any> | SingleStoreMediumInt<any> | SingleStoreFloat<any>
>(column, [
'PgReal',
'MySqlFloat',
'MySqlMediumInt',
'SingleStoreMediumInt',
'SingleStoreFloat',
])
) {
min = unsigned ? 0 : CONSTANTS.INT24_MIN;
max = unsigned ? CONSTANTS.INT24_UNSIGNED_MAX : CONSTANTS.INT24_MAX;
integer = isColumnType(column, ['MySqlMediumInt', 'SingleStoreMediumInt']);
} else if (
isColumnType<PgInteger<any> | PgSerial<any> | MySqlInt<any> | SingleStoreInt<any>>(column, [
'PgInteger',
'PgSerial',
'MySqlInt',
'SingleStoreInt',
])
) {
min = unsigned ? 0 : CONSTANTS.INT32_MIN;
max = unsigned ? CONSTANTS.INT32_UNSIGNED_MAX : CONSTANTS.INT32_MAX;
integer = true;
} else if (
isColumnType<
| PgDoublePrecision<any>
| MySqlReal<any>
| MySqlDouble<any>
| SingleStoreReal<any>
| SingleStoreDouble<any>
| SQLiteReal<any>
>(column, [
'PgDoublePrecision',
'MySqlReal',
'MySqlDouble',
'SingleStoreReal',
'SingleStoreDouble',
'SQLiteReal',
])
) {
min = unsigned ? 0 : CONSTANTS.INT48_MIN;
max = unsigned ? CONSTANTS.INT48_UNSIGNED_MAX : CONSTANTS.INT48_MAX;
} else if (
isColumnType<
| PgBigInt53<any>
| PgBigSerial53<any>
| MySqlBigInt53<any>
| MySqlSerial<any>
| SingleStoreBigInt53<any>
| SingleStoreSerial<any>
| SQLiteInteger<any>
>(
column,
[
'PgBigInt53',
'PgBigSerial53',
'MySqlBigInt53',
'MySqlSerial',
'SingleStoreBigInt53',
'SingleStoreSerial',
'SQLiteInteger',
],
)
) {
unsigned = unsigned || isColumnType(column, ['MySqlSerial', 'SingleStoreSerial']);
min = unsigned ? 0 : Number.MIN_SAFE_INTEGER;
max = Number.MAX_SAFE_INTEGER;
integer = true;
} else if (isColumnType<MySqlYear<any> | SingleStoreYear<any>>(column, ['MySqlYear', 'SingleStoreYear'])) {
min = 1901;
max = 2155;
integer = true;
} else {
min = Number.MIN_SAFE_INTEGER;
max = Number.MAX_SAFE_INTEGER;
}
let schema = coerce === true || coerce?.number
? integer ? z.coerce.number() : z.coerce.number().int()
: integer
? z.int()
: z.number();
schema = schema.gte(min).lte(max);
return schema;
}
function bigintColumnToSchema(
column: Column,
z: typeof zod,
coerce: CreateSchemaFactoryOptions<
Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined
>['coerce'],
): zod.ZodType {
const unsigned = column.getSQLType().includes('unsigned');
const min = unsigned ? 0n : CONSTANTS.INT64_MIN;
const max = unsigned ? CONSTANTS.INT64_UNSIGNED_MAX : CONSTANTS.INT64_MAX;
const schema = coerce === true || coerce?.bigint ? z.coerce.bigint() : z.bigint();
return schema.gte(min).lte(max);
}
function stringColumnToSchema(
column: Column,
z: typeof zod,
coerce: CreateSchemaFactoryOptions<
Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined
>['coerce'],
): zod.ZodType {
if (isColumnType<PgUUID<ColumnBaseConfig<'string', 'PgUUID'>>>(column, ['PgUUID'])) {
return z.uuid();
}
let max: number | undefined;
let regex: RegExp | undefined;
let fixed = false;
if (isColumnType<PgVarchar<any> | SQLiteText<any>>(column, ['PgVarchar', 'SQLiteText'])) {
max = column.length;
} else if (
isColumnType<MySqlVarChar<any> | SingleStoreVarChar<any>>(column, ['MySqlVarChar', 'SingleStoreVarChar'])
) {
max = column.length ?? CONSTANTS.INT16_UNSIGNED_MAX;
} else if (isColumnType<MySqlText<any> | SingleStoreText<any>>(column, ['MySqlText', 'SingleStoreText'])) {
if (column.textType === 'longtext') {
max = CONSTANTS.INT32_UNSIGNED_MAX;
} else if (column.textType === 'mediumtext') {
max = CONSTANTS.INT24_UNSIGNED_MAX;
} else if (column.textType === 'text') {
max = CONSTANTS.INT16_UNSIGNED_MAX;
} else {
max = CONSTANTS.INT8_UNSIGNED_MAX;
}
}
if (
isColumnType<PgChar<any> | MySqlChar<any> | SingleStoreChar<any>>(column, [
'PgChar',
'MySqlChar',
'SingleStoreChar',
])
) {
max = column.length;
fixed = true;
}
if (isColumnType<PgBinaryVector<any>>(column, ['PgBinaryVector'])) {
regex = /^[01]+$/;
max = column.dimensions;
}
let schema = coerce === true || coerce?.string ? z.coerce.string() : z.string();
schema = regex ? schema.regex(regex) : schema;
return max && fixed ? schema.length(max) : max ? schema.max(max) : schema;
}