Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix drizzle-zod and drizzle-typebox mapping refinements incorrectly #3737

Merged
merged 7 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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