Skip to content

Commit

Permalink
Merge pull request #3737 from L-Mario564/fix-validators
Browse files Browse the repository at this point in the history
Fix `drizzle-zod` and `drizzle-typebox` mapping refinements incorrectly
  • Loading branch information
AndriiSherman authored Dec 19, 2024
2 parents fe986e6 + 6f8a6cc commit 6492b1d
Show file tree
Hide file tree
Showing 16 changed files with 329 additions and 93 deletions.
1 change: 0 additions & 1 deletion drizzle-typebox/src/column.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ export function mapEnumValues(values: string[]) {
return Object.fromEntries(values.map((value) => [value, value]));
}

/** @internal */
export function columnToSchema(column: Column, t: typeof typebox): TSchema {
let schema!: TSchema;

Expand Down
18 changes: 12 additions & 6 deletions drizzle-typebox/src/schema.types.internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,20 @@ export type BuildRefine<
: never;

type HandleRefinement<
TType extends 'select' | 'insert' | 'update',
TRefinement extends t.TSchema | ((schema: t.TSchema) => t.TSchema),
TColumn extends Column,
> = TRefinement extends (schema: t.TSchema) => t.TSchema
? TColumn['_']['notNull'] extends true ? ReturnType<TRefinement>
: t.TTuple<[ReturnType<TRefinement>, t.TNull]>
> = TRefinement extends (schema: any) => t.TSchema ? (TColumn['_']['notNull'] extends true ? ReturnType<TRefinement>
: t.TUnion<[ReturnType<TRefinement>, t.TNull]>) extends infer TSchema
? TType extends 'update' ? t.TOptional<Assume<TSchema, t.TSchema>> : TSchema
: t.TSchema
: TRefinement;

type IsRefinementDefined<TRefinements, TKey extends string> = TKey extends keyof TRefinements
? TRefinements[TKey] extends t.TSchema | ((schema: any) => any) ? true
: false
: false;

export type BuildSchema<
TType extends 'select' | 'insert' | 'update',
TColumns extends Record<string, any>,
Expand All @@ -57,9 +64,8 @@ export type BuildSchema<
{
[K in keyof TColumns]: TColumns[K] extends infer TColumn extends Column
? TRefinements extends object
? TRefinements[Assume<K, keyof TRefinements>] extends
infer TRefinement extends t.TSchema | ((schema: t.TSchema) => t.TSchema)
? HandleRefinement<TRefinement, TColumn>
? IsRefinementDefined<TRefinements, Assume<K, string>> extends true
? HandleRefinement<TType, TRefinements[Assume<K, keyof TRefinements>], TColumn>
: HandleColumn<TType, TColumn>
: HandleColumn<TType, TColumn>
: TColumns[K] extends infer TObject extends SelectedFieldsFlat<Column> | Table | View ? BuildSchema<
Expand Down
28 changes: 27 additions & 1 deletion drizzle-typebox/tests/mysql.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Type as t } from '@sinclair/typebox';
import { type Equal, sql } from 'drizzle-orm';
import { int, mysqlSchema, mysqlTable, mysqlView, serial, text } from 'drizzle-orm/mysql-core';
import { customType, int, mysqlSchema, mysqlTable, mysqlView, serial, text } from 'drizzle-orm/mysql-core';
import { test } from 'vitest';
import { jsonSchema } from '~/column.ts';
import { CONSTANTS } from '~/constants.ts';
Expand Down Expand Up @@ -207,6 +207,32 @@ test('refine table - select', (tc) => {
Expect<Equal<typeof result, typeof expected>>();
});

test('refine table - select with custom data type', (tc) => {
const customText = customType({ dataType: () => 'text' });
const table = mysqlTable('test', {
c1: int(),
c2: int().notNull(),
c3: int().notNull(),
c4: customText(),
});

const customTextSchema = t.String({ minLength: 1, maxLength: 100 });
const result = createSelectSchema(table, {
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
c3: t.Integer({ minimum: 1, maximum: 10 }),
c4: customTextSchema,
});
const expected = t.Object({
c1: t.Union([intSchema, t.Null()]),
c2: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }),
c3: t.Integer({ minimum: 1, maximum: 10 }),
c4: customTextSchema,
});

expectSchemaShape(tc, expected).from(result);
Expect<Equal<typeof result, typeof expected>>();
});

test('refine table - insert', (tc) => {
const table = mysqlTable('test', {
c1: int(),
Expand Down
38 changes: 37 additions & 1 deletion drizzle-typebox/tests/pg.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { Type as t } from '@sinclair/typebox';
import { type Equal, sql } from 'drizzle-orm';
import { integer, pgEnum, pgMaterializedView, pgSchema, pgTable, pgView, serial, text } from 'drizzle-orm/pg-core';
import {
customType,
integer,
pgEnum,
pgMaterializedView,
pgSchema,
pgTable,
pgView,
serial,
text,
} from 'drizzle-orm/pg-core';
import { test } from 'vitest';
import { jsonSchema } from '~/column.ts';
import { CONSTANTS } from '~/constants.ts';
Expand Down Expand Up @@ -233,6 +243,32 @@ test('refine table - select', (tc) => {
Expect<Equal<typeof result, typeof expected>>();
});

test('refine table - select with custom data type', (tc) => {
const customText = customType({ dataType: () => 'text' });
const table = pgTable('test', {
c1: integer(),
c2: integer().notNull(),
c3: integer().notNull(),
c4: customText(),
});

const customTextSchema = t.String({ minLength: 1, maxLength: 100 });
const result = createSelectSchema(table, {
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
c3: t.Integer({ minimum: 1, maximum: 10 }),
c4: customTextSchema,
});
const expected = t.Object({
c1: t.Union([integerSchema, t.Null()]),
c2: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }),
c3: t.Integer({ minimum: 1, maximum: 10 }),
c4: customTextSchema,
});

expectSchemaShape(tc, expected).from(result);
Expect<Equal<typeof result, typeof expected>>();
});

test('refine table - insert', (tc) => {
const table = pgTable('test', {
c1: integer(),
Expand Down
28 changes: 27 additions & 1 deletion drizzle-typebox/tests/sqlite.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Type as t } from '@sinclair/typebox';
import { type Equal, sql } from 'drizzle-orm';
import { int, sqliteTable, sqliteView, text } from 'drizzle-orm/sqlite-core';
import { customType, int, sqliteTable, sqliteView, text } from 'drizzle-orm/sqlite-core';
import { test } from 'vitest';
import { bufferSchema, jsonSchema } from '~/column.ts';
import { CONSTANTS } from '~/constants.ts';
Expand Down Expand Up @@ -186,6 +186,32 @@ test('refine table - select', (tc) => {
Expect<Equal<typeof result, typeof expected>>();
});

test('refine table - select with custom data type', (tc) => {
const customText = customType({ dataType: () => 'text' });
const table = sqliteTable('test', {
c1: int(),
c2: int().notNull(),
c3: int().notNull(),
c4: customText(),
});

const customTextSchema = t.String({ minLength: 1, maxLength: 100 });
const result = createSelectSchema(table, {
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
c3: t.Integer({ minimum: 1, maximum: 10 }),
c4: customTextSchema,
});
const expected = t.Object({
c1: t.Union([intSchema, t.Null()]),
c2: t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: 1000 }),
c3: t.Integer({ minimum: 1, maximum: 10 }),
c4: customTextSchema,
});

expectSchemaShape(tc, expected).from(result);
Expect<Equal<typeof result, typeof expected>>();
});

test('refine table - insert', (tc) => {
const table = sqliteTable('test', {
c1: int(),
Expand Down
1 change: 0 additions & 1 deletion drizzle-valibot/src/column.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ export function mapEnumValues(values: string[]) {
return Object.fromEntries(values.map((value) => [value, value]));
}

/** @internal */
export function columnToSchema(column: Column): v.GenericSchema {
let schema!: v.GenericSchema;

Expand Down
98 changes: 31 additions & 67 deletions drizzle-valibot/src/column.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,23 @@ export type ExtractAdditionalProperties<TColumn extends Column> = {
fixedLength: TColumn['_']['columnType'] extends 'PgChar' | 'MySqlChar' | 'PgHalfVector' | 'PgVector' | 'PgArray'
? true
: false;
arrayPipelines: [];
};

type RemovePipeIfNoElements<T extends v.SchemaWithPipe<[any, ...any[]]>> = T extends
infer TPiped extends { pipe: [any, ...any[]] } ? TPiped['pipe'][1] extends undefined ? T['pipe'][0] : TPiped
: never;
type GetLengthAction<T extends Record<string, any>, TType extends string | ArrayLike<unknown>> =
T['fixedLength'] extends true ? v.LengthAction<TType, number, undefined>
: v.MaxLengthAction<TType, number, undefined>;

type BuildArraySchema<
TWrapped extends v.GenericSchema,
TPipelines extends any[][],
> = TPipelines extends [infer TFirst extends any[], ...infer TRest extends any[][]]
? BuildArraySchema<RemovePipeIfNoElements<v.SchemaWithPipe<[v.ArraySchema<TWrapped, undefined>, ...TFirst]>>, TRest>
: TPipelines extends [infer TFirst extends any[]]
? BuildArraySchema<RemovePipeIfNoElements<v.SchemaWithPipe<[v.ArraySchema<TWrapped, undefined>, ...TFirst]>>, []>
: TWrapped;
type GetArraySchema<T extends Column> = v.ArraySchema<
GetValibotType<
T['_']['data'],
T['_']['dataType'],
T['_']['columnType'],
GetEnumValuesFromColumn<T>,
GetBaseColumn<T>,
ExtractAdditionalProperties<T>
>,
undefined
>;

export type GetValibotType<
TData,
Expand All @@ -53,51 +55,22 @@ export type GetValibotType<
TEnumValues extends [string, ...string[]] | undefined,
TBaseColumn extends Column | undefined,
TAdditionalProperties extends Record<string, any>,
> = TColumnType extends 'PgHalfVector' | 'PgVector' ? RemovePipeIfNoElements<
v.SchemaWithPipe<
RemoveNeverElements<[
v.ArraySchema<v.NumberSchema<undefined>, undefined>,
TAdditionalProperties['max'] extends number
? TAdditionalProperties['fixedLength'] extends true ? v.LengthAction<number[], number, undefined>
: v.MaxLengthAction<number[], number, undefined>
: never,
]>
> = TColumnType extends 'PgHalfVector' | 'PgVector' ? TAdditionalProperties['max'] extends number ? v.SchemaWithPipe<
[v.ArraySchema<v.NumberSchema<undefined>, undefined>, GetLengthAction<TAdditionalProperties, number[]>]
>
>
: v.ArraySchema<v.NumberSchema<undefined>, undefined>
: TColumnType extends 'PgUUID' ? v.SchemaWithPipe<[v.StringSchema<undefined>, v.UuidAction<string, undefined>]>
// PG array handling start
// Nesting `GetValibotType` within `v.ArraySchema` will cause infinite recursion
// The workaround is to accumulate all the array validations (done via `arrayPipelines` in `TAdditionalProperties`) and then build the schema afterwards
: TAdditionalProperties['arrayFinished'] extends true ? GetValibotType<
TData,
TDataType,
TColumnType,
TEnumValues,
TBaseColumn,
Omit<TAdditionalProperties, 'arrayFinished'>
> extends infer TSchema extends v.GenericSchema ? BuildArraySchema<TSchema, TAdditionalProperties['arrayPipelines']>
: never
: TBaseColumn extends Column ? GetValibotType<
TBaseColumn['_']['data'],
TBaseColumn['_']['dataType'],
TBaseColumn['_']['columnType'],
GetEnumValuesFromColumn<TBaseColumn>,
GetBaseColumn<TBaseColumn>,
Omit<ExtractAdditionalProperties<TBaseColumn>, 'arrayPipelines'> & {
arrayPipelines: [
RemoveNeverElements<[
TAdditionalProperties['max'] extends number
? TAdditionalProperties['fixedLength'] extends true
? v.LengthAction<Assume<TBaseColumn['_']['data'], any[]>[], number, undefined>
: v.MaxLengthAction<Assume<TBaseColumn['_']['data'], any[]>[], number, undefined>
: never,
]>,
...TAdditionalProperties['arrayPipelines'],
];
arrayFinished: GetBaseColumn<TBaseColumn> extends undefined ? true : false;
}
: TColumnType extends 'PgBinaryVector' ? v.SchemaWithPipe<
RemoveNeverElements<[
v.StringSchema<undefined>,
v.RegexAction<string, undefined>,
TAdditionalProperties['max'] extends number ? GetLengthAction<TAdditionalProperties, string> : never,
]>
>
// PG array handling end
: TBaseColumn extends Column ? TAdditionalProperties['max'] extends number ? v.SchemaWithPipe<
[GetArraySchema<TBaseColumn>, GetLengthAction<TAdditionalProperties, TBaseColumn['_']['data'][]>]
>
: GetArraySchema<TBaseColumn>
: ArrayHasAtLeastOneValue<TEnumValues> extends true
? v.EnumSchema<EnumValuesToEnum<Assume<TEnumValues, [string, ...string[]]>>, undefined>
: TData extends infer TTuple extends [any, ...any[]] ? v.TupleSchema<
Expand Down Expand Up @@ -147,19 +120,10 @@ export type GetValibotType<
v.MaxValueAction<bigint, bigint, undefined>,
]>
: TData extends boolean ? v.BooleanSchema<undefined>
: TData extends string ? RemovePipeIfNoElements<
v.SchemaWithPipe<
RemoveNeverElements<[
v.StringSchema<undefined>,
TColumnType extends 'PgBinaryVector' ? v.RegexAction<string, undefined>
: never,
TAdditionalProperties['max'] extends number
? TAdditionalProperties['fixedLength'] extends true ? v.LengthAction<string, number, undefined>
: v.MaxLengthAction<string, number, undefined>
: never,
]>
>
>
: TData extends string
? TAdditionalProperties['max'] extends number
? v.SchemaWithPipe<[v.StringSchema<undefined>, GetLengthAction<TAdditionalProperties, string>]>
: v.StringSchema<undefined>
: v.AnySchema;

type HandleSelectColumn<
Expand Down
3 changes: 1 addition & 2 deletions drizzle-valibot/src/schema.types.internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,7 @@ export type BuildSchema<
TType extends 'select' | 'insert' | 'update',
TColumns extends Record<string, any>,
TRefinements extends Record<string, any> | undefined,
> // @ts-ignore false-positive
= v.ObjectSchema<
> = v.ObjectSchema<
Simplify<
RemoveNever<
{
Expand Down
28 changes: 27 additions & 1 deletion drizzle-valibot/tests/mysql.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Equal, sql } from 'drizzle-orm';
import { int, mysqlSchema, mysqlTable, mysqlView, serial, text } from 'drizzle-orm/mysql-core';
import { customType, int, mysqlSchema, mysqlTable, mysqlView, serial, text } from 'drizzle-orm/mysql-core';
import * as v from 'valibot';
import { test } from 'vitest';
import { jsonSchema } from '~/column.ts';
Expand Down Expand Up @@ -210,6 +210,32 @@ test('refine table - select', (t) => {
Expect<Equal<typeof result, typeof expected>>();
});

test('refine table - select with custom data type', (t) => {
const customText = customType({ dataType: () => 'text' });
const table = mysqlTable('test', {
c1: int(),
c2: int().notNull(),
c3: int().notNull(),
c4: customText(),
});

const customTextSchema = v.pipe(v.string(), v.minLength(1), v.maxLength(100));
const result = createSelectSchema(table, {
c2: (schema) => v.pipe(schema, v.maxValue(1000)),
c3: v.pipe(v.string(), v.transform(Number)),
c4: customTextSchema,
});
const expected = v.object({
c1: v.nullable(intSchema),
c2: v.pipe(intSchema, v.maxValue(1000)),
c3: v.pipe(v.string(), v.transform(Number)),
c4: customTextSchema,
});

expectSchemaShape(t, expected).from(result);
Expect<Equal<typeof result, typeof expected>>();
});

test('refine table - insert', (t) => {
const table = mysqlTable('test', {
c1: int(),
Expand Down
Loading

0 comments on commit 6492b1d

Please sign in to comment.