From cd7bba358a1ef0bc56d2f5e9d145096b6746bbcb Mon Sep 17 00:00:00 2001 From: Sukairo-02 Date: Sun, 28 Apr 2024 02:56:15 +0300 Subject: [PATCH] Fixed type-level enum overwrite of arrays (fixes drizzle-team/drizzle-orm#1110), added related type tests, removed unnecessary or repeating checks --- drizzle-typebox/src/index.ts | 85 +++++------ drizzle-typebox/type-tests/mysql.ts | 181 +++++++++++++++++++++++ drizzle-typebox/type-tests/pg.ts | 66 +++++++++ drizzle-typebox/type-tests/sqlite.ts | 65 ++++++++ drizzle-typebox/type-tests/tsconfig.json | 11 ++ drizzle-typebox/type-tests/utils.ts | 5 + drizzle-valibot/src/index.ts | 86 +++++------ drizzle-valibot/type-tests/mysql.ts | 181 +++++++++++++++++++++++ drizzle-valibot/type-tests/pg.ts | 66 +++++++++ drizzle-valibot/type-tests/sqlite.ts | 66 +++++++++ drizzle-valibot/type-tests/tsconfig.json | 11 ++ drizzle-valibot/type-tests/utils.ts | 5 + drizzle-zod/src/index.ts | 62 ++++---- drizzle-zod/type-tests/mysql.ts | 175 ++++++++++++++++++++++ drizzle-zod/type-tests/pg.ts | 66 +++++++++ drizzle-zod/type-tests/sqlite.ts | 56 +++++++ drizzle-zod/type-tests/tsconfig.json | 11 ++ drizzle-zod/type-tests/utils.ts | 5 + 18 files changed, 1078 insertions(+), 125 deletions(-) create mode 100644 drizzle-typebox/type-tests/mysql.ts create mode 100644 drizzle-typebox/type-tests/pg.ts create mode 100644 drizzle-typebox/type-tests/sqlite.ts create mode 100644 drizzle-typebox/type-tests/tsconfig.json create mode 100644 drizzle-typebox/type-tests/utils.ts create mode 100644 drizzle-valibot/type-tests/mysql.ts create mode 100644 drizzle-valibot/type-tests/pg.ts create mode 100644 drizzle-valibot/type-tests/sqlite.ts create mode 100644 drizzle-valibot/type-tests/tsconfig.json create mode 100644 drizzle-valibot/type-tests/utils.ts create mode 100644 drizzle-zod/type-tests/mysql.ts create mode 100644 drizzle-zod/type-tests/pg.ts create mode 100644 drizzle-zod/type-tests/sqlite.ts create mode 100644 drizzle-zod/type-tests/tsconfig.json create mode 100644 drizzle-zod/type-tests/utils.ts diff --git a/drizzle-typebox/src/index.ts b/drizzle-typebox/src/index.ts index e70d72c43..b3eee916f 100644 --- a/drizzle-typebox/src/index.ts +++ b/drizzle-typebox/src/index.ts @@ -82,9 +82,6 @@ type MaybeOptional< type GetTypeboxType = TColumn['_']['dataType'] extends infer TDataType ? TDataType extends 'custom' ? TAny : TDataType extends 'json' ? Json - : TColumn extends { enumValues: [string, ...string[]] } - ? Equal extends true ? TString - : TUnion> : TDataType extends 'array' ? TArray< GetTypeboxType< Assume< @@ -93,6 +90,9 @@ type GetTypeboxType = TColumn['_']['dataType'] extends i >['baseColumn'] > > + : TColumn extends { enumValues: [string, ...string[]] } + ? Equal extends true ? TString + : TUnion> : TDataType extends 'bigint' ? TBigInt : TDataType extends 'number' ? TNumber : TDataType extends 'string' ? TString @@ -166,7 +166,7 @@ export function createInsertSchema< & string}' does not exist in table '${TTable['_']['name']}'` >; }, - // + // @ts-ignore - following error does not break types during usage in any way ): TObject< BuildInsertSchema< TTable, @@ -281,8 +281,7 @@ function isWithEnum( column: AnyColumn, ): column is typeof column & { enumValues: [string, ...string[]] } { return ( - 'enumValues' in column - && Array.isArray(column.enumValues) + Array.isArray(column.enumValues) && column.enumValues.length > 0 ); } @@ -293,47 +292,41 @@ function mapColumnToSchema(column: Column): TSchema { let type: TSchema | undefined; if (isWithEnum(column)) { - type = column.enumValues?.length - ? Type.Union(column.enumValues.map((value) => Type.Literal(value))) - : Type.String(); - } - - if (!type) { - if (column.dataType === 'custom') { - type = Type.Any(); - } else if (column.dataType === 'json') { - type = jsonSchema; - } else if (column.dataType === 'array') { - type = Type.Array( - mapColumnToSchema((column as PgArray).baseColumn), - ); - } else if (column.dataType === 'number') { - type = Type.Number(); - } else if (column.dataType === 'bigint') { - type = Type.BigInt(); - } else if (column.dataType === 'boolean') { - type = Type.Boolean(); - } else if (column.dataType === 'date') { - type = Type.Date(); - } else if (column.dataType === 'string') { - const sType = Type.String(); - - if ( - (is(column, PgChar) - || is(column, PgVarchar) - || is(column, MySqlVarChar) - || is(column, MySqlVarBinary) - || is(column, MySqlChar) - || is(column, SQLiteText)) - && typeof column.length === 'number' - ) { - sType.maxLength = column.length; - } - - type = sType; - } else if (is(column, PgUUID)) { - type = Type.RegEx(uuidPattern); + type = Type.Union(column.enumValues.map((value) => Type.Literal(value))); + } else if (column.dataType === 'custom') { + type = Type.Any(); + } else if (column.dataType === 'json') { + type = jsonSchema; + } else if (column.dataType === 'array') { + type = Type.Array( + mapColumnToSchema((column as PgArray).baseColumn), + ); + } else if (column.dataType === 'number') { + type = Type.Number(); + } else if (column.dataType === 'bigint') { + type = Type.BigInt(); + } else if (column.dataType === 'boolean') { + type = Type.Boolean(); + } else if (column.dataType === 'date') { + type = Type.Date(); + } else if (column.dataType === 'string') { + const sType = Type.String(); + + if ( + (is(column, PgChar) + || is(column, PgVarchar) + || is(column, MySqlVarChar) + || is(column, MySqlVarBinary) + || is(column, MySqlChar) + || is(column, SQLiteText)) + && typeof column.length === 'number' + ) { + sType.maxLength = column.length; } + + type = sType; + } else if (is(column, PgUUID)) { + type = Type.RegEx(uuidPattern); } if (!type) { diff --git a/drizzle-typebox/type-tests/mysql.ts b/drizzle-typebox/type-tests/mysql.ts new file mode 100644 index 000000000..eb6b1bdcb --- /dev/null +++ b/drizzle-typebox/type-tests/mysql.ts @@ -0,0 +1,181 @@ +import type { Static } from '@sinclair/typebox'; +import { + bigint, + binary, + boolean, + char, + customType, + date, + datetime, + decimal, + double, + float, + int, + json, + longtext, + mediumint, + mediumtext, + mysqlEnum, + mysqlTable, + real, + serial, + smallint, + text, + time, + timestamp, + tinyint, + tinytext, + varbinary, + varchar, + year, +} from 'drizzle-orm/mysql-core'; +import { createInsertSchema, createSelectSchema } from '../src'; +import { type Equal, Expect } from './utils'; + +const customInt = customType<{ data: number }>({ + dataType() { + return 'int'; + }, +}); + +const testTable = mysqlTable('test', { + bigint: bigint('bigint', { mode: 'bigint' }).notNull(), + bigintNumber: bigint('bigintNumber', { mode: 'number' }).notNull(), + binary: binary('binary').notNull(), + boolean: boolean('boolean').notNull(), + char: char('char', { length: 4 }).notNull(), + charEnum: char('char', { enum: ['a', 'b', 'c'] }).notNull(), + customInt: customInt('customInt').notNull(), + date: date('date').notNull(), + dateString: date('dateString', { mode: 'string' }).notNull(), + datetime: datetime('datetime').notNull(), + datetimeString: datetime('datetimeString', { mode: 'string' }).notNull(), + decimal: decimal('decimal').notNull(), + double: double('double').notNull(), + enum: mysqlEnum('enum', ['a', 'b', 'c']).notNull(), + float: float('float').notNull(), + int: int('int').notNull(), + json: json('json').notNull(), + mediumint: mediumint('mediumint').notNull(), + real: real('real').notNull(), + serial: serial('serial').notNull(), + smallint: smallint('smallint').notNull(), + text: text('text').notNull(), + textEnum: text('textEnum', { enum: ['a', 'b', 'c'] }).notNull(), + tinytext: tinytext('tinytext').notNull(), + tinytextEnum: tinytext('tinytextEnum', { enum: ['a', 'b', 'c'] }).notNull(), + mediumtext: mediumtext('mediumtext').notNull(), + mediumtextEnum: mediumtext('mediumtextEnum', { + enum: ['a', 'b', 'c'], + }).notNull(), + longtext: longtext('longtext').notNull(), + longtextEnum: longtext('longtextEnum', { enum: ['a', 'b', 'c'] }).notNull(), + time: time('time').notNull(), + timestamp: timestamp('timestamp').notNull(), + timestampString: timestamp('timestampString', { mode: 'string' }).notNull(), + tinyint: tinyint('tinyint').notNull(), + varbinary: varbinary('varbinary', { length: 200 }).notNull(), + varchar: varchar('varchar', { length: 200 }).notNull(), + varcharEnum: varchar('varcharEnum', { + length: 1, + enum: ['a', 'b', 'c'], + }).notNull(), + year: year('year').notNull(), + autoIncrement: int('autoIncrement').notNull().autoincrement(), +}); + +const insertSchema = createInsertSchema(testTable); +const selectSchema = createSelectSchema(testTable); + +type InsertType = Static; +type SelectType = Static; + +Expect< + Equal +>; + +Expect< + Equal +>; diff --git a/drizzle-typebox/type-tests/pg.ts b/drizzle-typebox/type-tests/pg.ts new file mode 100644 index 000000000..fd381624b --- /dev/null +++ b/drizzle-typebox/type-tests/pg.ts @@ -0,0 +1,66 @@ +import type { Static } from '@sinclair/typebox'; +import { char, date, integer, pgEnum, pgTable, serial, text, timestamp, varchar } from 'drizzle-orm/pg-core'; +import { createInsertSchema, createSelectSchema } from '../src'; +import { type Equal, Expect } from './utils'; + +export const roleEnum = pgEnum('role', ['admin', 'user']); + +const testTable = pgTable('users', { + intArr: integer('int_arr').array(), + strArr: text('str_arr').array(), + id: serial('id').primaryKey(), + name: text('name'), + email: text('email').notNull(), + birthdayString: date('birthday_string').notNull(), + birthdayDate: date('birthday_date', { mode: 'date' }).notNull(), + createdAt: timestamp('created_at').notNull().defaultNow(), + role: roleEnum('role').notNull(), + roleText: text('role1', { enum: ['admin', 'user'] }).notNull(), + roleText2: text('role2', { enum: ['admin', 'user'] }) + .notNull() + .default('user'), + profession: varchar('profession', { length: 20 }).notNull(), + initials: char('initials', { length: 2 }).notNull(), +}); + +const insertSchema = createInsertSchema(testTable); +const selectSchema = createSelectSchema(testTable); + +type InsertType = Static; +type SelectType = Static; + +Expect< + Equal +>; + +Expect< + Equal +>; diff --git a/drizzle-typebox/type-tests/sqlite.ts b/drizzle-typebox/type-tests/sqlite.ts new file mode 100644 index 000000000..1d172ce8f --- /dev/null +++ b/drizzle-typebox/type-tests/sqlite.ts @@ -0,0 +1,65 @@ +import { type Static, Type } from '@sinclair/typebox'; +import { blob, integer, numeric, real, sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import { createInsertSchema, createSelectSchema } from '../src'; +import { type Equal, Expect } from './utils'; + +const blobJsonSchema = Type.Object({ + foo: Type.String(), +}); + +const testTable = sqliteTable('users', { + id: integer('id').primaryKey(), + blobJson: blob('blob', { mode: 'json' }) + .$type>() + .notNull(), + blobBigInt: blob('blob', { mode: 'bigint' }).notNull(), + numeric: numeric('numeric').notNull(), + createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), + createdAtMs: integer('created_at_ms', { mode: 'timestamp_ms' }).notNull(), + boolean: integer('boolean', { mode: 'boolean' }).notNull(), + real: real('real').notNull(), + text: text('text', { length: 255 }), + role: text('role', { enum: ['admin', 'user'] }) + .notNull() + .default('user'), +}); + +const insertSchema = createInsertSchema(testTable); +const selectSchema = createSelectSchema(testTable); + +type InsertType = Static; +type SelectType = Static; + +Expect< + Equal +>; + +Expect< + Equal +>; diff --git a/drizzle-typebox/type-tests/tsconfig.json b/drizzle-typebox/type-tests/tsconfig.json new file mode 100644 index 000000000..b4e6c8007 --- /dev/null +++ b/drizzle-typebox/type-tests/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.build.json", + "compilerOptions": { + "composite": false, + "noEmit": true, + "rootDir": "..", + "outDir": "./.cache" + }, + "include": [".", "../src"], + "exclude": ["**/playground"] +} diff --git a/drizzle-typebox/type-tests/utils.ts b/drizzle-typebox/type-tests/utils.ts new file mode 100644 index 000000000..51b56d381 --- /dev/null +++ b/drizzle-typebox/type-tests/utils.ts @@ -0,0 +1,5 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function +export function Expect() {} + +export type Equal = (() => T extends X ? 1 : 2) extends (() => T extends Y ? 1 : 2) ? true + : false; diff --git a/drizzle-valibot/src/index.ts b/drizzle-valibot/src/index.ts index 0c84c5052..79ab5cdf2 100644 --- a/drizzle-valibot/src/index.ts +++ b/drizzle-valibot/src/index.ts @@ -80,11 +80,12 @@ type MaybeOptional< type GetValibotType = TColumn['_']['dataType'] extends infer TDataType ? TDataType extends 'custom' ? AnySchema : TDataType extends 'json' ? Json + : TDataType extends 'array' + ? TColumn['_']['baseColumn'] extends Column ? ArraySchema> + : never : TColumn extends { enumValues: [string, ...string[]] } ? Equal extends true ? StringSchema : PicklistSchema - : TDataType extends 'array' - ? TColumn['_']['baseColumn'] extends Column ? ArraySchema> : never : TDataType extends 'bigint' ? BigintSchema : TDataType extends 'number' ? NumberSchema : TDataType extends 'string' ? StringSchema @@ -156,8 +157,8 @@ export function createInsertSchema< & string}' does not exist in table '${TTable['_']['name']}'` >; }, - // -): ObjectSchema< +): // @ts-ignore - following error does not break types during usage in any way +ObjectSchema< BuildInsertSchema< TTable, Equal> extends true ? {} : TRefine @@ -271,8 +272,7 @@ function isWithEnum( column: AnyColumn, ): column is typeof column & { enumValues: [string, ...string[]] } { return ( - 'enumValues' in column - && Array.isArray(column.enumValues) + Array.isArray(column.enumValues) && column.enumValues.length > 0 ); } @@ -281,47 +281,41 @@ function mapColumnToSchema(column: Column): BaseSchema { let type: BaseSchema | undefined; if (isWithEnum(column)) { - type = column.enumValues?.length - ? picklist(column.enumValues) - : string(); - } - - if (!type) { - if (column.dataType === 'custom') { - type = any(); - } else if (column.dataType === 'json') { - type = jsonSchema; - } else if (column.dataType === 'array') { - type = array( - mapColumnToSchema((column as PgArray).baseColumn), - ); - } else if (column.dataType === 'number') { - type = number(); - } else if (column.dataType === 'bigint') { - type = bigint(); - } else if (column.dataType === 'boolean') { - type = boolean(); - } else if (column.dataType === 'date') { - type = date(); - } else if (column.dataType === 'string') { - let sType = string(); - - if ( - (is(column, PgChar) - || is(column, PgVarchar) - || is(column, MySqlVarChar) - || is(column, MySqlVarBinary) - || is(column, MySqlChar) - || is(column, SQLiteText)) - && typeof column.length === 'number' - ) { - sType = string([maxLength(column.length)]); - } - - type = sType; - } else if (is(column, PgUUID)) { - type = string([uuid()]); + type = picklist(column.enumValues); + } else if (column.dataType === 'custom') { + type = any(); + } else if (column.dataType === 'json') { + type = jsonSchema; + } else if (column.dataType === 'array') { + type = array( + mapColumnToSchema((column as PgArray).baseColumn), + ); + } else if (column.dataType === 'number') { + type = number(); + } else if (column.dataType === 'bigint') { + type = bigint(); + } else if (column.dataType === 'boolean') { + type = boolean(); + } else if (column.dataType === 'date') { + type = date(); + } else if (column.dataType === 'string') { + let sType = string(); + + if ( + (is(column, PgChar) + || is(column, PgVarchar) + || is(column, MySqlVarChar) + || is(column, MySqlVarBinary) + || is(column, MySqlChar) + || is(column, SQLiteText)) + && typeof column.length === 'number' + ) { + sType = string([maxLength(column.length)]); } + + type = sType; + } else if (is(column, PgUUID)) { + type = string([uuid()]); } if (!type) { diff --git a/drizzle-valibot/type-tests/mysql.ts b/drizzle-valibot/type-tests/mysql.ts new file mode 100644 index 000000000..0973a41bf --- /dev/null +++ b/drizzle-valibot/type-tests/mysql.ts @@ -0,0 +1,181 @@ +import { + bigint, + binary, + boolean, + char, + customType, + date, + datetime, + decimal, + double, + float, + int, + json, + longtext, + mediumint, + mediumtext, + mysqlEnum, + mysqlTable, + real, + serial, + smallint, + text, + time, + timestamp, + tinyint, + tinytext, + varbinary, + varchar, + year, +} from 'drizzle-orm/mysql-core'; +import type { Output } from 'valibot'; +import { createInsertSchema, createSelectSchema } from '../src'; +import { type Equal, Expect } from './utils'; + +const customInt = customType<{ data: number }>({ + dataType() { + return 'int'; + }, +}); + +const testTable = mysqlTable('test', { + bigint: bigint('bigint', { mode: 'bigint' }).notNull(), + bigintNumber: bigint('bigintNumber', { mode: 'number' }).notNull(), + binary: binary('binary').notNull(), + boolean: boolean('boolean').notNull(), + char: char('char', { length: 4 }).notNull(), + charEnum: char('char', { enum: ['a', 'b', 'c'] }).notNull(), + customInt: customInt('customInt').notNull(), + date: date('date').notNull(), + dateString: date('dateString', { mode: 'string' }).notNull(), + datetime: datetime('datetime').notNull(), + datetimeString: datetime('datetimeString', { mode: 'string' }).notNull(), + decimal: decimal('decimal').notNull(), + double: double('double').notNull(), + enum: mysqlEnum('enum', ['a', 'b', 'c']).notNull(), + float: float('float').notNull(), + int: int('int').notNull(), + json: json('json').notNull(), + mediumint: mediumint('mediumint').notNull(), + real: real('real').notNull(), + serial: serial('serial').notNull(), + smallint: smallint('smallint').notNull(), + text: text('text').notNull(), + textEnum: text('textEnum', { enum: ['a', 'b', 'c'] }).notNull(), + tinytext: tinytext('tinytext').notNull(), + tinytextEnum: tinytext('tinytextEnum', { enum: ['a', 'b', 'c'] }).notNull(), + mediumtext: mediumtext('mediumtext').notNull(), + mediumtextEnum: mediumtext('mediumtextEnum', { + enum: ['a', 'b', 'c'], + }).notNull(), + longtext: longtext('longtext').notNull(), + longtextEnum: longtext('longtextEnum', { enum: ['a', 'b', 'c'] }).notNull(), + time: time('time').notNull(), + timestamp: timestamp('timestamp').notNull(), + timestampString: timestamp('timestampString', { mode: 'string' }).notNull(), + tinyint: tinyint('tinyint').notNull(), + varbinary: varbinary('varbinary', { length: 200 }).notNull(), + varchar: varchar('varchar', { length: 200 }).notNull(), + varcharEnum: varchar('varcharEnum', { + length: 1, + enum: ['a', 'b', 'c'], + }).notNull(), + year: year('year').notNull(), + autoIncrement: int('autoIncrement').notNull().autoincrement(), +}); + +const insertSchema = createInsertSchema(testTable); +const selectSchema = createSelectSchema(testTable); + +type InsertType = Output; +type SelectType = Output; + +Expect< + Equal +>; + +Expect< + Equal +>; diff --git a/drizzle-valibot/type-tests/pg.ts b/drizzle-valibot/type-tests/pg.ts new file mode 100644 index 000000000..cbc418c7b --- /dev/null +++ b/drizzle-valibot/type-tests/pg.ts @@ -0,0 +1,66 @@ +import { char, date, integer, pgEnum, pgTable, serial, text, timestamp, varchar } from 'drizzle-orm/pg-core'; +import type { Output } from 'valibot'; +import { createInsertSchema, createSelectSchema } from '../src'; +import { type Equal, Expect } from './utils'; + +export const roleEnum = pgEnum('role', ['admin', 'user']); + +const testTable = pgTable('users', { + intArr: integer('int_arr').array(), + strArr: text('str_arr').array(), + id: serial('id').primaryKey(), + name: text('name'), + email: text('email').notNull(), + birthdayString: date('birthday_string').notNull(), + birthdayDate: date('birthday_date', { mode: 'date' }).notNull(), + createdAt: timestamp('created_at').notNull().defaultNow(), + role: roleEnum('role').notNull(), + roleText: text('role1', { enum: ['admin', 'user'] }).notNull(), + roleText2: text('role2', { enum: ['admin', 'user'] }) + .notNull() + .default('user'), + profession: varchar('profession', { length: 20 }).notNull(), + initials: char('initials', { length: 2 }).notNull(), +}); + +const insertSchema = createInsertSchema(testTable); +const selectSchema = createSelectSchema(testTable); + +type InsertType = Output; +type SelectType = Output; + +Expect< + Equal +>; + +Expect< + Equal +>; diff --git a/drizzle-valibot/type-tests/sqlite.ts b/drizzle-valibot/type-tests/sqlite.ts new file mode 100644 index 000000000..8e04c250d --- /dev/null +++ b/drizzle-valibot/type-tests/sqlite.ts @@ -0,0 +1,66 @@ +import { blob, integer, numeric, real, sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import type { Output } from 'valibot'; +import { object, string } from 'valibot'; +import { createInsertSchema, createSelectSchema } from '../src'; +import { type Equal, Expect } from './utils'; + +const blobJsonSchema = object({ + foo: string(), +}); + +const testTable = sqliteTable('users', { + id: integer('id').primaryKey(), + blobJson: blob('blob', { mode: 'json' }) + .$type>() + .notNull(), + blobBigInt: blob('blob', { mode: 'bigint' }).notNull(), + numeric: numeric('numeric').notNull(), + createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), + createdAtMs: integer('created_at_ms', { mode: 'timestamp_ms' }).notNull(), + boolean: integer('boolean', { mode: 'boolean' }).notNull(), + real: real('real').notNull(), + text: text('text', { length: 255 }), + role: text('role', { enum: ['admin', 'user'] }) + .notNull() + .default('user'), +}); + +const insertSchema = createInsertSchema(testTable); +const selectSchema = createSelectSchema(testTable); + +type InsertType = Output; +type SelectType = Output; + +Expect< + Equal +>; + +Expect< + Equal +>; diff --git a/drizzle-valibot/type-tests/tsconfig.json b/drizzle-valibot/type-tests/tsconfig.json new file mode 100644 index 000000000..b4e6c8007 --- /dev/null +++ b/drizzle-valibot/type-tests/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.build.json", + "compilerOptions": { + "composite": false, + "noEmit": true, + "rootDir": "..", + "outDir": "./.cache" + }, + "include": [".", "../src"], + "exclude": ["**/playground"] +} diff --git a/drizzle-valibot/type-tests/utils.ts b/drizzle-valibot/type-tests/utils.ts new file mode 100644 index 000000000..51b56d381 --- /dev/null +++ b/drizzle-valibot/type-tests/utils.ts @@ -0,0 +1,5 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function +export function Expect() {} + +export type Equal = (() => T extends X ? 1 : 2) extends (() => T extends Y ? 1 : 2) ? true + : false; diff --git a/drizzle-zod/src/index.ts b/drizzle-zod/src/index.ts index 3f6547e0b..1f925030b 100644 --- a/drizzle-zod/src/index.ts +++ b/drizzle-zod/src/index.ts @@ -43,9 +43,9 @@ type MaybeOptional< type GetZodType = TColumn['_']['dataType'] extends infer TDataType ? TDataType extends 'custom' ? z.ZodAny : TDataType extends 'json' ? z.ZodType + : TDataType extends 'array' ? z.ZodArray['baseColumn']>> : TColumn extends { enumValues: [string, ...string[]] } ? Equal extends true ? z.ZodString : z.ZodEnum - : TDataType extends 'array' ? z.ZodArray['baseColumn']>> : TDataType extends 'bigint' ? z.ZodBigInt : TDataType extends 'number' ? z.ZodNumber : TDataType extends 'string' ? z.ZodString @@ -189,7 +189,7 @@ export function createSelectSchema< } function isWithEnum(column: Column): column is typeof column & { enumValues: [string, ...string[]] } { - return 'enumValues' in column && Array.isArray(column.enumValues) && column.enumValues.length > 0; + return Array.isArray(column.enumValues) && column.enumValues.length > 0; } function mapColumnToSchema(column: Column): z.ZodTypeAny { @@ -197,38 +197,34 @@ function mapColumnToSchema(column: Column): z.ZodTypeAny { if (isWithEnum(column)) { type = column.enumValues.length ? z.enum(column.enumValues) : z.string(); - } - - if (!type) { - if (is(column, PgUUID)) { - type = z.string().uuid(); - } else if (column.dataType === 'custom') { - type = z.any(); - } else if (column.dataType === 'json') { - type = jsonSchema; - } else if (column.dataType === 'array') { - type = z.array(mapColumnToSchema((column as PgArray).baseColumn)); - } else if (column.dataType === 'number') { - type = z.number(); - } else if (column.dataType === 'bigint') { - type = z.bigint(); - } else if (column.dataType === 'boolean') { - type = z.boolean(); - } else if (column.dataType === 'date') { - type = z.date(); - } else if (column.dataType === 'string') { - let sType = z.string(); - - if ( - (is(column, PgChar) || is(column, PgVarchar) || is(column, MySqlVarChar) - || is(column, MySqlVarBinary) || is(column, MySqlChar) || is(column, SQLiteText)) - && (typeof column.length === 'number') - ) { - sType = sType.max(column.length); - } - - type = sType; + } else if (is(column, PgUUID)) { + type = z.string().uuid(); + } else if (column.dataType === 'custom') { + type = z.any(); + } else if (column.dataType === 'json') { + type = jsonSchema; + } else if (column.dataType === 'array') { + type = z.array(mapColumnToSchema((column as PgArray).baseColumn)); + } else if (column.dataType === 'number') { + type = z.number(); + } else if (column.dataType === 'bigint') { + type = z.bigint(); + } else if (column.dataType === 'boolean') { + type = z.boolean(); + } else if (column.dataType === 'date') { + type = z.date(); + } else if (column.dataType === 'string') { + let sType = z.string(); + + if ( + (is(column, PgChar) || is(column, PgVarchar) || is(column, MySqlVarChar) + || is(column, MySqlVarBinary) || is(column, MySqlChar) || is(column, SQLiteText)) + && (typeof column.length === 'number') + ) { + sType = sType.max(column.length); } + + type = sType; } if (!type) { diff --git a/drizzle-zod/type-tests/mysql.ts b/drizzle-zod/type-tests/mysql.ts new file mode 100644 index 000000000..58fee4646 --- /dev/null +++ b/drizzle-zod/type-tests/mysql.ts @@ -0,0 +1,175 @@ +import { + bigint, + binary, + boolean, + char, + customType, + date, + datetime, + decimal, + double, + float, + int, + longtext, + mediumint, + mediumtext, + mysqlEnum, + mysqlTable, + real, + serial, + smallint, + text, + time, + timestamp, + tinyint, + tinytext, + varbinary, + varchar, + year, +} from 'drizzle-orm/mysql-core'; +import type { output } from 'zod'; +import { createInsertSchema, createSelectSchema } from '../src'; +import { type Equal, Expect } from './utils'; + +const customInt = customType<{ data: number }>({ + dataType() { + return 'int'; + }, +}); + +const testTable = mysqlTable('test', { + bigint: bigint('bigint', { mode: 'bigint' }).notNull(), + bigintNumber: bigint('bigintNumber', { mode: 'number' }).notNull(), + binary: binary('binary').notNull(), + boolean: boolean('boolean').notNull(), + char: char('char', { length: 4 }).notNull(), + charEnum: char('char', { enum: ['a', 'b', 'c'] }).notNull(), + customInt: customInt('customInt').notNull(), + date: date('date').notNull(), + dateString: date('dateString', { mode: 'string' }).notNull(), + datetime: datetime('datetime').notNull(), + datetimeString: datetime('datetimeString', { mode: 'string' }).notNull(), + decimal: decimal('decimal').notNull(), + double: double('double').notNull(), + enum: mysqlEnum('enum', ['a', 'b', 'c']).notNull(), + float: float('float').notNull(), + int: int('int').notNull(), + // JSON's are ommitted from these tests - their schema type is impossible to compare using current type comparison utils + // json: json('json').notNull(), + mediumint: mediumint('mediumint').notNull(), + real: real('real').notNull(), + serial: serial('serial').notNull(), + smallint: smallint('smallint').notNull(), + text: text('text').notNull(), + textEnum: text('textEnum', { enum: ['a', 'b', 'c'] }).notNull(), + tinytext: tinytext('tinytext').notNull(), + tinytextEnum: tinytext('tinytextEnum', { enum: ['a', 'b', 'c'] }).notNull(), + mediumtext: mediumtext('mediumtext').notNull(), + mediumtextEnum: mediumtext('mediumtextEnum', { + enum: ['a', 'b', 'c'], + }).notNull(), + longtext: longtext('longtext').notNull(), + longtextEnum: longtext('longtextEnum', { enum: ['a', 'b', 'c'] }).notNull(), + time: time('time').notNull(), + timestamp: timestamp('timestamp').notNull(), + timestampString: timestamp('timestampString', { mode: 'string' }).notNull(), + tinyint: tinyint('tinyint').notNull(), + varbinary: varbinary('varbinary', { length: 200 }).notNull(), + varchar: varchar('varchar', { length: 200 }).notNull(), + varcharEnum: varchar('varcharEnum', { + length: 1, + enum: ['a', 'b', 'c'], + }).notNull(), + year: year('year').notNull(), + autoIncrement: int('autoIncrement').notNull().autoincrement(), +}); + +const insertSchema = createInsertSchema(testTable); +const selectSchema = createSelectSchema(testTable); + +type InsertType = output; +type SelectType = output; + +Expect< + Equal +>; + +Expect< + Equal +>; diff --git a/drizzle-zod/type-tests/pg.ts b/drizzle-zod/type-tests/pg.ts new file mode 100644 index 000000000..323f1a2d5 --- /dev/null +++ b/drizzle-zod/type-tests/pg.ts @@ -0,0 +1,66 @@ +import { char, date, integer, pgEnum, pgTable, serial, text, timestamp, varchar } from 'drizzle-orm/pg-core'; +import type { output } from 'zod'; +import { createInsertSchema, createSelectSchema } from '../src'; +import { type Equal, Expect } from './utils'; + +export const roleEnum = pgEnum('role', ['admin', 'user']); + +const testTable = pgTable('users', { + intArr: integer('int_arr').array(), + strArr: text('str_arr').array(), + id: serial('id').primaryKey(), + name: text('name'), + email: text('email').notNull(), + birthdayString: date('birthday_string').notNull(), + birthdayDate: date('birthday_date', { mode: 'date' }).notNull(), + createdAt: timestamp('created_at').notNull().defaultNow(), + role: roleEnum('role').notNull(), + roleText: text('role1', { enum: ['admin', 'user'] }).notNull(), + roleText2: text('role2', { enum: ['admin', 'user'] }) + .notNull() + .default('user'), + profession: varchar('profession', { length: 20 }).notNull(), + initials: char('initials', { length: 2 }).notNull(), +}); + +const insertSchema = createInsertSchema(testTable); +const selectSchema = createSelectSchema(testTable); + +type InsertType = output; +type SelectType = output; + +Expect< + Equal +>; + +Expect< + Equal +>; diff --git a/drizzle-zod/type-tests/sqlite.ts b/drizzle-zod/type-tests/sqlite.ts new file mode 100644 index 000000000..deefb3ee8 --- /dev/null +++ b/drizzle-zod/type-tests/sqlite.ts @@ -0,0 +1,56 @@ +import { blob, integer, numeric, real, sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import type { output } from 'zod'; +import { createInsertSchema, createSelectSchema } from '../src'; +import { type Equal, Expect } from './utils'; + +const testTable = sqliteTable('users', { + id: integer('id').primaryKey(), + // JSON's are ommitted from these tests - their schema type is impossible to compare using current type comparison utils + // blobJson: blob('blob', { mode: 'json' }) + // .$type>() + // .notNull(), + blobBigInt: blob('blob', { mode: 'bigint' }).notNull(), + numeric: numeric('numeric').notNull(), + createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), + createdAtMs: integer('created_at_ms', { mode: 'timestamp_ms' }).notNull(), + boolean: integer('boolean', { mode: 'boolean' }).notNull(), + real: real('real').notNull(), + text: text('text', { length: 255 }), + role: text('role', { enum: ['admin', 'user'] }) + .notNull() + .default('user'), +}); + +const insertSchema = createInsertSchema(testTable); +const selectSchema = createSelectSchema(testTable); + +type InsertType = output; +type SelectType = output; + +Expect< + Equal +>; + +Expect< + Equal +>; diff --git a/drizzle-zod/type-tests/tsconfig.json b/drizzle-zod/type-tests/tsconfig.json new file mode 100644 index 000000000..b4e6c8007 --- /dev/null +++ b/drizzle-zod/type-tests/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.build.json", + "compilerOptions": { + "composite": false, + "noEmit": true, + "rootDir": "..", + "outDir": "./.cache" + }, + "include": [".", "../src"], + "exclude": ["**/playground"] +} diff --git a/drizzle-zod/type-tests/utils.ts b/drizzle-zod/type-tests/utils.ts new file mode 100644 index 000000000..51b56d381 --- /dev/null +++ b/drizzle-zod/type-tests/utils.ts @@ -0,0 +1,5 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function +export function Expect() {} + +export type Equal = (() => T extends X ? 1 : 2) extends (() => T extends Y ? 1 : 2) ? true + : false;