Skip to content

Commit 3dff816

Browse files
Merge pull request #3852 from L-Mario564/zod-coerce
Add type coercion support to `drizzle-zod`
2 parents 79df8c1 + 6201629 commit 3dff816

File tree

8 files changed

+246
-18
lines changed

8 files changed

+246
-18
lines changed

drizzle-zod/src/column.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@ import type {
5454
} from 'drizzle-orm/singlestore-core';
5555
import type { SQLiteInteger, SQLiteReal, SQLiteText } from 'drizzle-orm/sqlite-core';
5656
import { z } from 'zod';
57-
import type { z as zod } from 'zod';
57+
import { z as zod } from 'zod';
5858
import { CONSTANTS } from './constants.ts';
59+
import type { CreateSchemaFactoryOptions } from './schema.types.ts';
5960
import { isColumnType, isWithEnum } from './utils.ts';
6061
import type { Json } from './utils.ts';
6162

@@ -65,7 +66,9 @@ export const jsonSchema: z.ZodType<Json> = z.lazy(() =>
6566
);
6667
export const bufferSchema: z.ZodType<Buffer> = z.custom<Buffer>((v) => v instanceof Buffer); // eslint-disable-line no-instanceof/no-instanceof
6768

68-
export function columnToSchema(column: Column, z: typeof zod): z.ZodTypeAny {
69+
export function columnToSchema(column: Column, factory: CreateSchemaFactoryOptions | undefined): z.ZodTypeAny {
70+
const z = factory?.zodInstance ?? zod;
71+
const coerce = factory?.coerce ?? {};
6972
let schema!: z.ZodTypeAny;
7073

7174
if (isWithEnum(column)) {
@@ -98,15 +101,15 @@ export function columnToSchema(column: Column, z: typeof zod): z.ZodTypeAny {
98101
} else if (column.dataType === 'array') {
99102
schema = z.array(z.any());
100103
} else if (column.dataType === 'number') {
101-
schema = numberColumnToSchema(column, z);
104+
schema = numberColumnToSchema(column, z, coerce);
102105
} else if (column.dataType === 'bigint') {
103-
schema = bigintColumnToSchema(column, z);
106+
schema = bigintColumnToSchema(column, z, coerce);
104107
} else if (column.dataType === 'boolean') {
105-
schema = z.boolean();
108+
schema = coerce === true || coerce.boolean ? z.coerce.boolean() : z.boolean();
106109
} else if (column.dataType === 'date') {
107-
schema = z.date();
110+
schema = coerce === true || coerce.date ? z.coerce.date() : z.date();
108111
} else if (column.dataType === 'string') {
109-
schema = stringColumnToSchema(column, z);
112+
schema = stringColumnToSchema(column, z, coerce);
110113
} else if (column.dataType === 'json') {
111114
schema = jsonSchema;
112115
} else if (column.dataType === 'custom') {
@@ -123,7 +126,11 @@ export function columnToSchema(column: Column, z: typeof zod): z.ZodTypeAny {
123126
return schema;
124127
}
125128

126-
function numberColumnToSchema(column: Column, z: typeof zod): z.ZodTypeAny {
129+
function numberColumnToSchema(
130+
column: Column,
131+
z: typeof zod,
132+
coerce: CreateSchemaFactoryOptions['coerce'],
133+
): z.ZodTypeAny {
127134
let unsigned = column.getSQLType().includes('unsigned');
128135
let min!: number;
129136
let max!: number;
@@ -223,19 +230,29 @@ function numberColumnToSchema(column: Column, z: typeof zod): z.ZodTypeAny {
223230
max = Number.MAX_SAFE_INTEGER;
224231
}
225232

226-
const schema = z.number().min(min).max(max);
233+
let schema = coerce === true || coerce?.number ? z.coerce.number() : z.number();
234+
schema = schema.min(min).max(max);
227235
return integer ? schema.int() : schema;
228236
}
229237

230-
function bigintColumnToSchema(column: Column, z: typeof zod): z.ZodTypeAny {
238+
function bigintColumnToSchema(
239+
column: Column,
240+
z: typeof zod,
241+
coerce: CreateSchemaFactoryOptions['coerce'],
242+
): z.ZodTypeAny {
231243
const unsigned = column.getSQLType().includes('unsigned');
232244
const min = unsigned ? 0n : CONSTANTS.INT64_MIN;
233245
const max = unsigned ? CONSTANTS.INT64_UNSIGNED_MAX : CONSTANTS.INT64_MAX;
234246

235-
return z.bigint().min(min).max(max);
247+
const schema = coerce === true || coerce?.bigint ? z.coerce.bigint() : z.bigint();
248+
return schema.min(min).max(max);
236249
}
237250

238-
function stringColumnToSchema(column: Column, z: typeof zod): z.ZodTypeAny {
251+
function stringColumnToSchema(
252+
column: Column,
253+
z: typeof zod,
254+
coerce: CreateSchemaFactoryOptions['coerce'],
255+
): z.ZodTypeAny {
239256
if (isColumnType<PgUUID<ColumnBaseConfig<'string', 'PgUUID'>>>(column, ['PgUUID'])) {
240257
return z.string().uuid();
241258
}
@@ -278,7 +295,7 @@ function stringColumnToSchema(column: Column, z: typeof zod): z.ZodTypeAny {
278295
max = column.dimensions;
279296
}
280297

281-
let schema = z.string();
298+
let schema = coerce === true || coerce?.string ? z.coerce.string() : z.string();
282299
schema = regex ? schema.regex(regex) : schema;
283300
return max && fixed ? schema.length(max) : max ? schema.max(max) : schema;
284301
}

drizzle-zod/src/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ function handleColumns(
3838
}
3939

4040
const column = is(selected, Column) ? selected : undefined;
41-
const schema = column ? columnToSchema(column, factory?.zodInstance ?? z) : z.any();
41+
const schema = column ? columnToSchema(column, factory) : z.any();
4242
const refined = typeof refinement === 'function' ? refinement(schema) : schema;
4343

4444
if (conditions.never(column)) {

drizzle-zod/src/schema.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,5 @@ export interface CreateUpdateSchema {
4949

5050
export interface CreateSchemaFactoryOptions {
5151
zodInstance?: any;
52+
coerce?: Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true;
5253
}

drizzle-zod/tests/mysql.test.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { test } from 'vitest';
44
import { z } from 'zod';
55
import { jsonSchema } from '~/column.ts';
66
import { CONSTANTS } from '~/constants.ts';
7-
import { createInsertSchema, createSelectSchema, createUpdateSchema } from '../src';
7+
import { createInsertSchema, createSchemaFactory, createSelectSchema, createUpdateSchema } from '../src';
88
import { Expect, expectSchemaShape } from './utils.ts';
99

1010
const intSchema = z.number().min(CONSTANTS.INT32_MIN).max(CONSTANTS.INT32_MAX).int();
@@ -454,6 +454,59 @@ test('all data types', (t) => {
454454
Expect<Equal<typeof result, typeof expected>>();
455455
});
456456

457+
test('type coercion - all', (t) => {
458+
const table = mysqlTable('test', ({
459+
bigint,
460+
boolean,
461+
timestamp,
462+
int,
463+
text,
464+
}) => ({
465+
bigint: bigint({ mode: 'bigint' }).notNull(),
466+
boolean: boolean().notNull(),
467+
timestamp: timestamp().notNull(),
468+
int: int().notNull(),
469+
text: text().notNull(),
470+
}));
471+
472+
const { createSelectSchema } = createSchemaFactory({
473+
coerce: true,
474+
});
475+
const result = createSelectSchema(table);
476+
const expected = z.object({
477+
bigint: z.coerce.bigint().min(CONSTANTS.INT64_MIN).max(CONSTANTS.INT64_MAX),
478+
boolean: z.coerce.boolean(),
479+
timestamp: z.coerce.date(),
480+
int: z.coerce.number().min(CONSTANTS.INT32_MIN).max(CONSTANTS.INT32_MAX).int(),
481+
text: z.coerce.string().max(CONSTANTS.INT16_UNSIGNED_MAX),
482+
});
483+
expectSchemaShape(t, expected).from(result);
484+
Expect<Equal<typeof result, typeof expected>>();
485+
});
486+
487+
test('type coercion - mixed', (t) => {
488+
const table = mysqlTable('test', ({
489+
timestamp,
490+
int,
491+
}) => ({
492+
timestamp: timestamp().notNull(),
493+
int: int().notNull(),
494+
}));
495+
496+
const { createSelectSchema } = createSchemaFactory({
497+
coerce: {
498+
date: true,
499+
},
500+
});
501+
const result = createSelectSchema(table);
502+
const expected = z.object({
503+
timestamp: z.coerce.date(),
504+
int: z.number().min(CONSTANTS.INT32_MIN).max(CONSTANTS.INT32_MAX).int(),
505+
});
506+
expectSchemaShape(t, expected).from(result);
507+
Expect<Equal<typeof result, typeof expected>>();
508+
});
509+
457510
/* Disallow unknown keys in table refinement - select */ {
458511
const table = mysqlTable('test', { id: int() });
459512
// @ts-expect-error

drizzle-zod/tests/pg.test.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { test } from 'vitest';
1414
import { z } from 'zod';
1515
import { jsonSchema } from '~/column.ts';
1616
import { CONSTANTS } from '~/constants.ts';
17-
import { createInsertSchema, createSelectSchema, createUpdateSchema } from '../src';
17+
import { createInsertSchema, createSchemaFactory, createSelectSchema, createUpdateSchema } from '../src';
1818
import { Expect, expectEnumValues, expectSchemaShape } from './utils.ts';
1919

2020
const integerSchema = z.number().min(CONSTANTS.INT32_MIN).max(CONSTANTS.INT32_MAX).int();
@@ -500,6 +500,59 @@ test('all data types', (t) => {
500500
Expect<Equal<typeof result, typeof expected>>();
501501
});
502502

503+
test('type coercion - all', (t) => {
504+
const table = pgTable('test', ({
505+
bigint,
506+
boolean,
507+
timestamp,
508+
integer,
509+
text,
510+
}) => ({
511+
bigint: bigint({ mode: 'bigint' }).notNull(),
512+
boolean: boolean().notNull(),
513+
timestamp: timestamp().notNull(),
514+
integer: integer().notNull(),
515+
text: text().notNull(),
516+
}));
517+
518+
const { createSelectSchema } = createSchemaFactory({
519+
coerce: true,
520+
});
521+
const result = createSelectSchema(table);
522+
const expected = z.object({
523+
bigint: z.coerce.bigint().min(CONSTANTS.INT64_MIN).max(CONSTANTS.INT64_MAX),
524+
boolean: z.coerce.boolean(),
525+
timestamp: z.coerce.date(),
526+
integer: z.coerce.number().min(CONSTANTS.INT32_MIN).max(CONSTANTS.INT32_MAX).int(),
527+
text: z.coerce.string(),
528+
});
529+
expectSchemaShape(t, expected).from(result);
530+
Expect<Equal<typeof result, typeof expected>>();
531+
});
532+
533+
test('type coercion - mixed', (t) => {
534+
const table = pgTable('test', ({
535+
timestamp,
536+
integer,
537+
}) => ({
538+
timestamp: timestamp().notNull(),
539+
integer: integer().notNull(),
540+
}));
541+
542+
const { createSelectSchema } = createSchemaFactory({
543+
coerce: {
544+
date: true,
545+
},
546+
});
547+
const result = createSelectSchema(table);
548+
const expected = z.object({
549+
timestamp: z.coerce.date(),
550+
integer: z.number().min(CONSTANTS.INT32_MIN).max(CONSTANTS.INT32_MAX).int(),
551+
});
552+
expectSchemaShape(t, expected).from(result);
553+
Expect<Equal<typeof result, typeof expected>>();
554+
});
555+
503556
/* Disallow unknown keys in table refinement - select */ {
504557
const table = pgTable('test', { id: integer() });
505558
// @ts-expect-error

drizzle-zod/tests/singlestore.test.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { test } from 'vitest';
44
import { z } from 'zod';
55
import { jsonSchema } from '~/column.ts';
66
import { CONSTANTS } from '~/constants.ts';
7-
import { createInsertSchema, createSelectSchema, createUpdateSchema } from '../src';
7+
import { createInsertSchema, createSchemaFactory, createSelectSchema, createUpdateSchema } from '../src';
88
import { Expect, expectSchemaShape } from './utils.ts';
99

1010
const intSchema = z.number().min(CONSTANTS.INT32_MIN).max(CONSTANTS.INT32_MAX).int();
@@ -456,6 +456,59 @@ test('all data types', (t) => {
456456
Expect<Equal<typeof result, typeof expected>>();
457457
});
458458

459+
test('type coercion - all', (t) => {
460+
const table = singlestoreTable('test', ({
461+
bigint,
462+
boolean,
463+
timestamp,
464+
int,
465+
text,
466+
}) => ({
467+
bigint: bigint({ mode: 'bigint' }).notNull(),
468+
boolean: boolean().notNull(),
469+
timestamp: timestamp().notNull(),
470+
int: int().notNull(),
471+
text: text().notNull(),
472+
}));
473+
474+
const { createSelectSchema } = createSchemaFactory({
475+
coerce: true,
476+
});
477+
const result = createSelectSchema(table);
478+
const expected = z.object({
479+
bigint: z.coerce.bigint().min(CONSTANTS.INT64_MIN).max(CONSTANTS.INT64_MAX),
480+
boolean: z.coerce.boolean(),
481+
timestamp: z.coerce.date(),
482+
int: z.coerce.number().min(CONSTANTS.INT32_MIN).max(CONSTANTS.INT32_MAX).int(),
483+
text: z.coerce.string().max(CONSTANTS.INT16_UNSIGNED_MAX),
484+
});
485+
expectSchemaShape(t, expected).from(result);
486+
Expect<Equal<typeof result, typeof expected>>();
487+
});
488+
489+
test('type coercion - mixed', (t) => {
490+
const table = singlestoreTable('test', ({
491+
timestamp,
492+
int,
493+
}) => ({
494+
timestamp: timestamp().notNull(),
495+
int: int().notNull(),
496+
}));
497+
498+
const { createSelectSchema } = createSchemaFactory({
499+
coerce: {
500+
date: true,
501+
},
502+
});
503+
const result = createSelectSchema(table);
504+
const expected = z.object({
505+
timestamp: z.coerce.date(),
506+
int: z.number().min(CONSTANTS.INT32_MIN).max(CONSTANTS.INT32_MAX).int(),
507+
});
508+
expectSchemaShape(t, expected).from(result);
509+
Expect<Equal<typeof result, typeof expected>>();
510+
});
511+
459512
/* Disallow unknown keys in table refinement - select */ {
460513
const table = singlestoreTable('test', { id: int() });
461514
// @ts-expect-error

drizzle-zod/tests/sqlite.test.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { test } from 'vitest';
44
import { z } from 'zod';
55
import { bufferSchema, jsonSchema } from '~/column.ts';
66
import { CONSTANTS } from '~/constants.ts';
7-
import { createInsertSchema, createSelectSchema, createUpdateSchema } from '../src';
7+
import { createInsertSchema, createSchemaFactory, createSelectSchema, createUpdateSchema } from '../src';
88
import { Expect, expectSchemaShape } from './utils.ts';
99

1010
const intSchema = z.number().min(Number.MIN_SAFE_INTEGER).max(Number.MAX_SAFE_INTEGER).int();
@@ -350,6 +350,56 @@ test('all data types', (t) => {
350350
Expect<Equal<typeof result, typeof expected>>();
351351
});
352352

353+
test('type coercion - all', (t) => {
354+
const table = sqliteTable('test', ({
355+
blob,
356+
integer,
357+
text,
358+
}) => ({
359+
blob: blob({ mode: 'bigint' }).notNull(),
360+
integer1: integer({ mode: 'boolean' }).notNull(),
361+
integer2: integer({ mode: 'timestamp' }).notNull(),
362+
integer3: integer().notNull(),
363+
text: text().notNull(),
364+
}));
365+
366+
const { createSelectSchema } = createSchemaFactory({
367+
coerce: true,
368+
});
369+
const result = createSelectSchema(table);
370+
const expected = z.object({
371+
blob: z.coerce.bigint().min(CONSTANTS.INT64_MIN).max(CONSTANTS.INT64_MAX),
372+
integer1: z.coerce.boolean(),
373+
integer2: z.coerce.date(),
374+
integer3: z.coerce.number().min(Number.MIN_SAFE_INTEGER).max(Number.MAX_SAFE_INTEGER).int(),
375+
text: z.coerce.string(),
376+
});
377+
expectSchemaShape(t, expected).from(result);
378+
Expect<Equal<typeof result, typeof expected>>();
379+
});
380+
381+
test('type coercion - mixed', (t) => {
382+
const table = sqliteTable('test', ({
383+
integer,
384+
}) => ({
385+
integer1: integer({ mode: 'timestamp' }).notNull(),
386+
integer2: integer().notNull(),
387+
}));
388+
389+
const { createSelectSchema } = createSchemaFactory({
390+
coerce: {
391+
date: true,
392+
},
393+
});
394+
const result = createSelectSchema(table);
395+
const expected = z.object({
396+
integer1: z.coerce.date(),
397+
integer2: z.number().min(Number.MIN_SAFE_INTEGER).max(Number.MAX_SAFE_INTEGER).int(),
398+
});
399+
expectSchemaShape(t, expected).from(result);
400+
Expect<Equal<typeof result, typeof expected>>();
401+
});
402+
353403
/* Disallow unknown keys in table refinement - select */ {
354404
const table = sqliteTable('test', { id: int() });
355405
// @ts-expect-error

drizzle-zod/tests/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export function expectSchemaShape<T extends z.ZodObject<z.ZodRawShape>>(t: TaskC
99
for (const key of Object.keys(actual.shape)) {
1010
expect(actual.shape[key]!._def.typeName).toStrictEqual(expected.shape[key]?._def.typeName);
1111
expect(actual.shape[key]!._def?.checks).toEqual(expected.shape[key]?._def?.checks);
12+
expect(actual.shape[key]!._def?.coerce).toEqual(expected.shape[key]?._def?.coerce);
1213
if (actual.shape[key]?._def.typeName === 'ZodOptional') {
1314
expect(actual.shape[key]!._def.innerType._def.typeName).toStrictEqual(
1415
actual.shape[key]!._def.innerType._def.typeName,

0 commit comments

Comments
 (0)