diff --git a/changelogs/drizzle-kit/0.30.0.md b/changelogs/drizzle-kit/0.30.0.md new file mode 100644 index 000000000..7accf9c9c --- /dev/null +++ b/changelogs/drizzle-kit/0.30.0.md @@ -0,0 +1,7 @@ +Starting from this update, the PostgreSQL dialect will align with the behavior of all other dialects. It will no longer include `IF NOT EXISTS`, `$DO`, or similar statements, which could cause incorrect DDL statements to not fail when an object already exists in the database and should actually fail. + +This change marks our first step toward several major upgrades we are preparing: + +- An updated and improved migration workflow featuring commutative migrations, a revised folder structure, and enhanced collaboration capabilities for migrations. +- Better support for Xata migrations. +- Compatibility with CockroachDB (achieving full compatibility will only require removing serial fields from the migration folder). \ No newline at end of file diff --git a/changelogs/drizzle-orm/0.38.0.md b/changelogs/drizzle-orm/0.38.0.md new file mode 100644 index 000000000..863fe5182 --- /dev/null +++ b/changelogs/drizzle-orm/0.38.0.md @@ -0,0 +1,75 @@ +# Types breaking changes + +A few internal types were changed and extra generic types for length of column types were added in this release. It won't affect anyone, unless you are using those internal types for some custom wrappers, logic, etc. Here is a list of all types that were changed, so if you are relying on those, please review them before upgrading + +- `MySqlCharBuilderInitial` +- `MySqlVarCharBuilderInitial` +- `PgCharBuilderInitial` +- `PgArrayBuilder` +- `PgArray` +- `PgVarcharBuilderInitial` +- `PgBinaryVectorBuilderInitial` +- `PgBinaryVectorBuilder` +- `PgBinaryVector` +- `PgHalfVectorBuilderInitial` +- `PgHalfVectorBuilder` +- `PgHalfVector` +- `PgVectorBuilderInitial` +- `PgVectorBuilder` +- `PgVector` +- `SQLiteTextBuilderInitial` + +# New Features + +- Added new function `getViewSelectedFields` +- Added `$inferSelect` function to views +- Added `InferSelectViewModel` type for views +- Added `isView` function + +# Validator packages updates + +- `drizzle-zod` has been completely rewritten. You can find detailed information about it [here](https://github.com/drizzle-team/drizzle-orm/blob/main/changelogs/drizzle-zod/0.6.0.md) +- `drizzle-valibot` has been completely rewritten. You can find detailed information about it [here](https://github.com/drizzle-team/drizzle-orm/blob/main/changelogs/drizzle-valibot/0.3.0.md) +- `drizzle-typebox` has been completely rewritten. You can find detailed information about it [here](https://github.com/drizzle-team/drizzle-orm/blob/main/changelogs/drizzle-typebox/0.2.0.md) + +Thanks to @L-Mario564 for making more updates than we expected to be shipped in this release. We'll copy his message from a PR regarding improvements made in this release: + +- Output for all packages are now unminified, makes exploring the compiled code easier when published to npm. +- Smaller footprint. Previously, we imported the column types at runtime for each dialect, meaning that for example, if you're just using Postgres then you'd likely only have drizzle-orm and drizzle-orm/pg-core in the build output of your app; however, these packages imported all dialects which could lead to mysql-core and sqlite-core being bundled as well even if they're unused in your app. This is now fixed. +- Slight performance gain. To determine the column data type we used the is function which performs a few checks to ensure the column data type matches. This was slow, as these checks would pile up every quickly when comparing all data types for many fields in a table/view. The easier and faster alternative is to simply go off of the column's columnType property. +- Some changes had to be made at the type level in the ORM package for better compatibility with drizzle-valibot. + +And a set of new features + +- `createSelectSchema` function now also accepts views and enums. +- New function: `createUpdateSchema`, for use in updating queries. +- New function: `createSchemaFactory`, to provide more advanced options and to avoid bloating the parameters of the other schema functions + +# Bug fixes + +- [[FEATURE]: publish packages un-minified](https://github.com/drizzle-team/drizzle-orm/issues/2247) +- [Don't allow unknown keys in drizzle-zod refinement](https://github.com/drizzle-team/drizzle-orm/issues/573) +- [[BUG]:drizzle-zod not working with pgSchema](https://github.com/drizzle-team/drizzle-orm/issues/1458) +- [Add createUpdateSchema to drizzle-zod](https://github.com/drizzle-team/drizzle-orm/issues/503) +- [[BUG]:drizzle-zod produces wrong type](https://github.com/drizzle-team/drizzle-orm/issues/1110) +- [[BUG]:Drizzle-zod:Boolean and Serial types from Schema are defined as enum when using CreateInsertSchema and CreateSelectSchema](https://github.com/drizzle-team/drizzle-orm/issues/1327) +- [[BUG]: Drizzle typebox enum array wrong schema and type](https://github.com/drizzle-team/drizzle-orm/issues/1345) +- [[BUG]:drizzle-zod not working with pgSchema](https://github.com/drizzle-team/drizzle-orm/issues/1458) +- [[BUG]: drizzle-zod not parsing arrays correctly](https://github.com/drizzle-team/drizzle-orm/issues/1609) +- [[BUG]: Drizzle typebox not supporting array](https://github.com/drizzle-team/drizzle-orm/issues/1810) +- [[FEATURE]: Export factory functions from drizzle-zod to allow usage with extended Zod classes](https://github.com/drizzle-team/drizzle-orm/issues/2245) +- [[FEATURE]: Add support for new pipe syntax for drizzle-valibot](https://github.com/drizzle-team/drizzle-orm/issues/2358) +- [[BUG]: drizzle-zod's createInsertSchema() can't handle column of type vector](https://github.com/drizzle-team/drizzle-orm/issues/2424) +- [[BUG]: drizzle-typebox fails to map geometry column to type-box schema](https://github.com/drizzle-team/drizzle-orm/issues/2516) +- [[BUG]: drizzle-valibot does not provide types for returned schemas](https://github.com/drizzle-team/drizzle-orm/issues/2521) +- [[BUG]: Drizzle-typebox types SQLite real field to string](https://github.com/drizzle-team/drizzle-orm/issues/2524) +- [[BUG]: drizzle-zod: documented usage generates type error with exactOptionalPropertyTypes](https://github.com/drizzle-team/drizzle-orm/issues/2550) +- [[BUG]: drizzle-zod does not respect/count db type range](https://github.com/drizzle-team/drizzle-orm/issues/2737) +- [[BUG]: drizzle-zod not overriding optional](https://github.com/drizzle-team/drizzle-orm/issues/2755) +- [[BUG]:drizzle-zod doesn't accept custom id value](https://github.com/drizzle-team/drizzle-orm/issues/2957) +- [[FEATURE]: Support for Database Views in Drizzle Zod](https://github.com/drizzle-team/drizzle-orm/issues/3398) +- [[BUG]: drizzle-valibot return type any](https://github.com/drizzle-team/drizzle-orm/issues/3621) +- [[BUG]: drizzle-zod Type generation results in undefined types](https://github.com/drizzle-team/drizzle-orm/issues/3645) +- [[BUG]: GeneratedAlwaysAs](https://github.com/drizzle-team/drizzle-orm/issues/3511) +- [[FEATURE]: $inferSelect on a view](https://github.com/drizzle-team/drizzle-orm/issues/2610) +- [[BUG]:Can't infer props from view in schema](https://github.com/drizzle-team/drizzle-orm/issues/3392) diff --git a/changelogs/drizzle-typebox/0.2.0.md b/changelogs/drizzle-typebox/0.2.0.md new file mode 100644 index 000000000..aad630f7a --- /dev/null +++ b/changelogs/drizzle-typebox/0.2.0.md @@ -0,0 +1,87 @@ +This version fully updates `drizzle-typebox` integration and makes sure it's compatible with newer typebox versions + +# Breaking Changes + +> You must also have Drizzle ORM v0.38.0 or greater and Typebox v0.34.8 or greater installed. + +- When refining a field, if a schema is provided instead of a callback function, it will ignore the field's nullability and optional status. +- Some data types have more specific schemas for improved validation + +# Improvements + +Thanks to @L-Mario564 for making more updates than we expected to be shipped in this release. We'll copy his message from a PR regarding improvements made in this release: + +- Output for all packages are now unminified, makes exploring the compiled code easier when published to npm. +- Smaller footprint. Previously, we imported the column types at runtime for each dialect, meaning that for example, if you're just using Postgres then you'd likely only have drizzle-orm and drizzle-orm/pg-core in the build output of your app; however, these packages imported all dialects which could lead to mysql-core and sqlite-core being bundled as well even if they're unused in your app. This is now fixed. +- Slight performance gain. To determine the column data type we used the is function which performs a few checks to ensure the column data type matches. This was slow, as these checks would pile up every quickly when comparing all data types for many fields in a table/view. The easier and faster alternative is to simply go off of the column's columnType property. + +# New features + +- `createSelectSchema` function now also accepts views and enums. + +```ts +import { pgEnum } from 'drizzle-orm/pg-core'; +import { createSelectSchema } from 'drizzle-typebox'; +import { Value } from '@sinclair/typebox/value'; + +const roles = pgEnum('roles', ['admin', 'basic']); +const rolesSchema = createSelectSchema(roles); +const parsed: 'admin' | 'basic' = Value.Parse(rolesSchema, ...); + +const usersView = pgView('users_view').as((qb) => qb.select().from(users).where(gt(users.age, 18))); +const usersViewSchema = createSelectSchema(usersView); +const parsed: { id: number; name: string; age: number } = Value.Parse(usersViewSchema, ...); +``` + +- New function: `createUpdateSchema`, for use in updating queries. + +```ts copy +import { pgTable, text, integer } from 'drizzle-orm/pg-core'; +import { createUpdateSchema } from 'drizzle-typebox'; +import { Value } from '@sinclair/typebox/value'; + +const users = pgTable('users', { + id: integer().generatedAlwaysAsIdentity().primaryKey(), + name: text().notNull(), + age: integer().notNull() +}); + +const userUpdateSchema = createUpdateSchema(users); + +const user = { id: 5, name: 'John' }; +const parsed: { name?: string | undefined, age?: number | undefined } = Value.Parse(userUpdateSchema, user); // Error: `id` is a generated column, it can't be updated + +const user = { age: 35 }; +const parsed: { name?: string | undefined, age?: number | undefined } = Value.Parse(userUpdateSchema, user); // Will parse successfully +await db.update(users).set(parsed).where(eq(users.name, 'Jane')); +``` + +- New function: `createSchemaFactory`, to provide more advanced options and to avoid bloating the parameters of the other schema functions + +```ts copy +import { pgTable, text, integer } from 'drizzle-orm/pg-core'; +import { createSchemaFactory } from 'drizzle-typebox'; +import { t } from 'elysia'; // Extended Typebox instance + +const users = pgTable('users', { + id: integer().generatedAlwaysAsIdentity().primaryKey(), + name: text().notNull(), + age: integer().notNull() +}); + +const { createInsertSchema } = createSchemaFactory({ typeboxInstance: t }); + +const userInsertSchema = createInsertSchema(users, { + // We can now use the extended instance + name: (schema) => t.Number({ ...schema }, { error: '`name` must be a string' }) +}); +``` + +- Full support for PG arrays + +```ts +pg.dataType().array(...); + +// Schema +Type.Array(baseDataTypeSchema, { minItems: size, maxItems: size }); +``` \ No newline at end of file diff --git a/changelogs/drizzle-valibot/0.3.0.md b/changelogs/drizzle-valibot/0.3.0.md new file mode 100644 index 000000000..de8ec63b4 --- /dev/null +++ b/changelogs/drizzle-valibot/0.3.0.md @@ -0,0 +1,67 @@ +This version fully updates `drizzle-valibot` integration and makes sure it's compatible with newer valibot versions + +# Breaking Changes + +> You must also have Drizzle ORM v0.38.0 or greater and Valibot v1.0.0-beta.7 or greater installed. + +- When refining a field, if a schema is provided instead of a callback function, it will ignore the field's nullability and optional status. +- Some data types have more specific schemas for improved validation + +# Improvements + +Thanks to @L-Mario564 for making more updates than we expected to be shipped in this release. We'll copy his message from a PR regarding improvements made in this release: + +- Output for all packages are now unminified, makes exploring the compiled code easier when published to npm. +- Smaller footprint. Previously, we imported the column types at runtime for each dialect, meaning that for example, if you're just using Postgres then you'd likely only have drizzle-orm and drizzle-orm/pg-core in the build output of your app; however, these packages imported all dialects which could lead to mysql-core and sqlite-core being bundled as well even if they're unused in your app. This is now fixed. +- Slight performance gain. To determine the column data type we used the is function which performs a few checks to ensure the column data type matches. This was slow, as these checks would pile up every quickly when comparing all data types for many fields in a table/view. The easier and faster alternative is to simply go off of the column's columnType property. +- Some changes had to be made at the type level in the ORM package for better compatibility with drizzle-valibot. + +# New features + +- `createSelectSchema` function now also accepts views and enums. + +```ts copy +import { pgEnum } from 'drizzle-orm/pg-core'; +import { createSelectSchema } from 'drizzle-valibot'; +import { parse } from 'valibot'; + +const roles = pgEnum('roles', ['admin', 'basic']); +const rolesSchema = createSelectSchema(roles); +const parsed: 'admin' | 'basic' = parse(rolesSchema, ...); + +const usersView = pgView('users_view').as((qb) => qb.select().from(users).where(gt(users.age, 18))); +const usersViewSchema = createSelectSchema(usersView); +const parsed: { id: number; name: string; age: number } = parse(usersViewSchema, ...); +``` + +- New function: `createUpdateSchema`, for use in updating queries. + +```ts copy +import { pgTable, text, integer } from 'drizzle-orm/pg-core'; +import { createUpdateSchema } from 'drizzle-valibot'; +import { parse } from 'valibot'; + +const users = pgTable('users', { + id: integer().generatedAlwaysAsIdentity().primaryKey(), + name: text().notNull(), + age: integer().notNull() +}); + +const userUpdateSchema = createUpdateSchema(users); + +const user = { id: 5, name: 'John' }; +const parsed: { name?: string | undefined, age?: number | undefined } = parse(userUpdateSchema, user); // Error: `id` is a generated column, it can't be updated + +const user = { age: 35 }; +const parsed: { name?: string | undefined, age?: number | undefined } = parse(userUpdateSchema, user); // Will parse successfully +await db.update(users).set(parsed).where(eq(users.name, 'Jane')); +``` + +- Full support for PG arrays + +```ts +pg.dataType().array(...); + +// Schema +z.array(baseDataTypeSchema).length(size); +``` \ No newline at end of file diff --git a/changelogs/drizzle-zod/0.6.0.md b/changelogs/drizzle-zod/0.6.0.md new file mode 100644 index 000000000..31ffd4fdb --- /dev/null +++ b/changelogs/drizzle-zod/0.6.0.md @@ -0,0 +1,85 @@ +This version fully updates `drizzle-zod` integration and makes sure it's compatible with newer zod versions + +# Breaking Changes + +> You must also have Drizzle ORM v0.38.0 or greater and Zod v3.0.0 or greater installed. + +- When refining a field, if a schema is provided instead of a callback function, it will ignore the field's nullability and optional status. +- Some data types have more specific schemas for improved validation + +# Improvements + +Thanks to @L-Mario564 for making more updates than we expected to be shipped in this release. We'll copy his message from a PR regarding improvements made in this release: + +- Output for all packages are now unminified, makes exploring the compiled code easier when published to npm. +- Smaller footprint. Previously, we imported the column types at runtime for each dialect, meaning that for example, if you're just using Postgres then you'd likely only have drizzle-orm and drizzle-orm/pg-core in the build output of your app; however, these packages imported all dialects which could lead to mysql-core and sqlite-core being bundled as well even if they're unused in your app. This is now fixed. +- Slight performance gain. To determine the column data type we used the is function which performs a few checks to ensure the column data type matches. This was slow, as these checks would pile up every quickly when comparing all data types for many fields in a table/view. The easier and faster alternative is to simply go off of the column's columnType property. + +# New features + +- `createSelectSchema` function now also accepts views and enums. + +```ts copy +import { pgEnum } from 'drizzle-orm/pg-core'; +import { createSelectSchema } from 'drizzle-zod'; + +const roles = pgEnum('roles', ['admin', 'basic']); +const rolesSchema = createSelectSchema(roles); +const parsed: 'admin' | 'basic' = rolesSchema.parse(...); + +const usersView = pgView('users_view').as((qb) => qb.select().from(users).where(gt(users.age, 18))); +const usersViewSchema = createSelectSchema(usersView); +const parsed: { id: number; name: string; age: number } = usersViewSchema.parse(...); +``` + +- New function: `createUpdateSchema`, for use in updating queries. + +```ts copy +import { pgTable, text, integer } from 'drizzle-orm/pg-core'; +import { createUpdateSchema } from 'drizzle-zod'; + +const users = pgTable('users', { + id: integer().generatedAlwaysAsIdentity().primaryKey(), + name: text().notNull(), + age: integer().notNull() +}); + +const userUpdateSchema = createUpdateSchema(users); + +const user = { id: 5, name: 'John' }; +const parsed: { name?: string | undefined, age?: number | undefined } = userUpdateSchema.parse(user); // Error: `id` is a generated column, it can't be updated + +const user = { age: 35 }; +const parsed: { name?: string | undefined, age?: number | undefined } = userUpdateSchema.parse(user); // Will parse successfully +await db.update(users).set(parsed).where(eq(users.name, 'Jane')); +``` + +- New function: `createSchemaFactory`, to provide more advanced options and to avoid bloating the parameters of the other schema functions + +```ts copy +import { pgTable, text, integer } from 'drizzle-orm/pg-core'; +import { createSchemaFactory } from 'drizzle-zod'; +import { z } from '@hono/zod-openapi'; // Extended Zod instance + +const users = pgTable('users', { + id: integer().generatedAlwaysAsIdentity().primaryKey(), + name: text().notNull(), + age: integer().notNull() +}); + +const { createInsertSchema } = createSchemaFactory({ zodInstance: z }); + +const userInsertSchema = createInsertSchema(users, { + // We can now use the extended instance + name: (schema) => schema.openapi({ example: 'John' }) +}); +``` + +- Full support for PG arrays + +```ts +pg.dataType().array(...); + +// Schema +z.array(baseDataTypeSchema).length(size); +``` \ No newline at end of file diff --git a/drizzle-kit/package.json b/drizzle-kit/package.json index 4d86c9beb..7d3debd1f 100644 --- a/drizzle-kit/package.json +++ b/drizzle-kit/package.json @@ -1,6 +1,6 @@ { "name": "drizzle-kit", - "version": "0.29.1", + "version": "0.30.0", "homepage": "https://orm.drizzle.team", "keywords": [ "drizzle", diff --git a/drizzle-kit/src/introspect-mysql.ts b/drizzle-kit/src/introspect-mysql.ts index ebf30f70d..005a2af42 100644 --- a/drizzle-kit/src/introspect-mysql.ts +++ b/drizzle-kit/src/introspect-mysql.ts @@ -16,9 +16,6 @@ import { import { indexName } from './serializer/mysqlSerializer'; import { unescapeSingleQuotes } from './utils'; -// time precision to fsp -// {mode: "string"} for timestamp by default - const mysqlImportsList = new Set([ 'mysqlTable', 'mysqlEnum', @@ -263,8 +260,7 @@ export const schemaToTypeScript = ( || Object.keys(table.checkConstraint).length > 0 ) { statement += ',\n'; - statement += '(table) => {\n'; - statement += '\treturn {\n'; + statement += '(table) => ['; statement += createTableIndexes( table.name, Object.values(table.indexes), @@ -283,8 +279,7 @@ export const schemaToTypeScript = ( Object.values(table.checkConstraint), withCasing, ); - statement += '\t}\n'; - statement += '}'; + statement += '\n]'; } statement += ');'; @@ -932,7 +927,7 @@ const createTableIndexes = ( const indexGeneratedName = indexName(tableName, it.columns); const escapedIndexName = indexGeneratedName === it.name ? '' : `"${it.name}"`; - statement += `\t\t${idxKey}: `; + statement += `\n\t`; statement += it.isUnique ? 'uniqueIndex(' : 'index('; statement += `${escapedIndexName})`; statement += `.on(${ @@ -940,7 +935,6 @@ const createTableIndexes = ( .map((it) => `table.${casing(it)}`) .join(', ') }),`; - statement += `\n`; }); return statement; @@ -955,7 +949,7 @@ const createTableUniques = ( unqs.forEach((it) => { const idxKey = casing(it.name); - statement += `\t\t${idxKey}: `; + statement += `\n\t`; statement += 'unique('; statement += `"${it.name}")`; statement += `.on(${ @@ -963,7 +957,6 @@ const createTableUniques = ( .map((it) => `table.${casing(it)}`) .join(', ') }),`; - statement += `\n`; }); return statement; @@ -976,13 +969,11 @@ const createTableChecks = ( let statement = ''; checks.forEach((it) => { - const checkKey = casing(it.name); - - statement += `\t\t${checkKey}: `; + statement += `\n\t`; statement += 'check('; statement += `"${it.name}", `; statement += `sql\`${it.value.replace(/`/g, '\\`')}\`)`; - statement += `,\n`; + statement += `,`; }); return statement; @@ -997,7 +988,7 @@ const createTablePKs = ( pks.forEach((it) => { let idxKey = casing(it.name); - statement += `\t\t${idxKey}: `; + statement += `\n\t`; statement += 'primaryKey({ columns: ['; statement += `${ it.columns @@ -1007,7 +998,6 @@ const createTablePKs = ( .join(', ') }]${it.name ? `, name: "${it.name}"` : ''}}`; statement += '),'; - statement += `\n`; }); return statement; @@ -1022,7 +1012,8 @@ const createTableFKs = ( fks.forEach((it) => { const isSelf = it.tableTo === it.tableFrom; const tableTo = isSelf ? 'table' : `${casing(it.tableTo)}`; - statement += `\t\t${casing(it.name)}: foreignKey({\n`; + statement += `\n\t`; + statement += `foreignKey({\n`; statement += `\t\t\tcolumns: [${ it.columnsFrom .map((i) => `table.${casing(i)}`) @@ -1044,7 +1035,7 @@ const createTableFKs = ( ? `.onDelete("${it.onDelete}")` : ''; - statement += `,\n`; + statement += `,`; }); return statement; diff --git a/drizzle-kit/src/introspect-pg.ts b/drizzle-kit/src/introspect-pg.ts index 9c9383ebe..4bb65ee0c 100644 --- a/drizzle-kit/src/introspect-pg.ts +++ b/drizzle-kit/src/introspect-pg.ts @@ -537,8 +537,7 @@ export const schemaToTypeScript = (schema: PgSchemaInternal, casing: Casing) => || Object.keys(table.checkConstraints).length > 0 ) { statement += ', '; - statement += '(table) => {\n'; - statement += '\treturn {\n'; + statement += '(table) => ['; statement += createTableIndexes(table.name, Object.values(table.indexes), casing); statement += createTableFKs(Object.values(table.foreignKeys), schemas, casing); statement += createTablePKs( @@ -558,8 +557,7 @@ export const schemaToTypeScript = (schema: PgSchemaInternal, casing: Casing) => Object.values(table.checkConstraints), casing, ); - statement += '\t}\n'; - statement += '}'; + statement += '\n]'; } statement += ');'; @@ -1216,7 +1214,7 @@ const createTableIndexes = (tableName: string, idxs: Index[], casing: Casing): s ); const escapedIndexName = indexGeneratedName === it.name ? '' : `"${it.name}"`; - statement += `\t\t${idxKey}: `; + statement += `\n\t`; statement += it.isUnique ? 'uniqueIndex(' : 'index('; statement += `${escapedIndexName})`; statement += `${it.concurrently ? `.concurrently()` : ''}`; @@ -1252,7 +1250,7 @@ const createTableIndexes = (tableName: string, idxs: Index[], casing: Casing): s } statement += it.with && Object.keys(it.with).length > 0 ? `.with(${reverseLogic(it.with)})` : ''; - statement += `,\n`; + statement += `,`; }); return statement; @@ -1262,9 +1260,7 @@ const createTablePKs = (pks: PrimaryKey[], casing: Casing): string => { let statement = ''; pks.forEach((it) => { - let idxKey = withCasing(it.name, casing); - - statement += `\t\t${idxKey}: `; + statement += `\n\t`; statement += 'primaryKey({ columns: ['; statement += `${ it.columns @@ -1274,7 +1270,7 @@ const createTablePKs = (pks: PrimaryKey[], casing: Casing): string => { .join(', ') }]${it.name ? `, name: "${it.name}"` : ''}}`; statement += ')'; - statement += `,\n`; + statement += `,`; }); return statement; @@ -1297,13 +1293,13 @@ const createTablePolicies = ( return rolesNameToTsKey[v] ? withCasing(rolesNameToTsKey[v], casing) : `"${v}"`; }); - statement += `\t\t${idxKey}: `; + statement += `\n\t`; statement += 'pgPolicy('; statement += `"${it.name}", { `; statement += `as: "${it.as?.toLowerCase()}", for: "${it.for?.toLowerCase()}", to: [${mappedItTo?.join(', ')}]${ it.using ? `, using: sql\`${it.using}\`` : '' }${it.withCheck ? `, withCheck: sql\`${it.withCheck}\` ` : ''}`; - statement += ` }),\n`; + statement += ` }),`; }); return statement; @@ -1316,14 +1312,12 @@ const createTableUniques = ( let statement = ''; unqs.forEach((it) => { - const idxKey = withCasing(it.name, casing); - - statement += `\t\t${idxKey}: `; + statement += `\n\t`; statement += 'unique('; statement += `"${it.name}")`; statement += `.on(${it.columns.map((it) => `table.${withCasing(it, casing)}`).join(', ')})`; statement += it.nullsNotDistinct ? `.nullsNotDistinct()` : ''; - statement += `,\n`; + statement += `,`; }); return statement; @@ -1336,12 +1330,11 @@ const createTableChecks = ( let statement = ''; checkConstraints.forEach((it) => { - const checkKey = withCasing(it.name, casing); - statement += `\t\t${checkKey}: `; + statement += `\n\t`; statement += 'check('; statement += `"${it.name}", `; statement += `sql\`${it.value}\`)`; - statement += `,\n`; + statement += `,`; }); return statement; @@ -1356,7 +1349,8 @@ const createTableFKs = (fks: ForeignKey[], schemas: Record, casi const isSelf = it.tableTo === it.tableFrom; const tableTo = isSelf ? 'table' : `${withCasing(paramName, casing)}`; - statement += `\t\t${withCasing(it.name, casing)}: foreignKey({\n`; + statement += `\n\t`; + statement += `foreignKey({\n`; statement += `\t\t\tcolumns: [${it.columnsFrom.map((i) => `table.${withCasing(i, casing)}`).join(', ')}],\n`; statement += `\t\t\tforeignColumns: [${ it.columnsTo.map((i) => `${tableTo}.${withCasing(i, casing)}`).join(', ') @@ -1368,7 +1362,7 @@ const createTableFKs = (fks: ForeignKey[], schemas: Record, casi statement += it.onDelete && it.onDelete !== 'no action' ? `.onDelete("${it.onDelete}")` : ''; - statement += `,\n`; + statement += `,`; }); return statement; diff --git a/drizzle-kit/src/introspect-singlestore.ts b/drizzle-kit/src/introspect-singlestore.ts index 8f93cdfda..09c2feec0 100644 --- a/drizzle-kit/src/introspect-singlestore.ts +++ b/drizzle-kit/src/introspect-singlestore.ts @@ -249,8 +249,7 @@ export const schemaToTypeScript = ( || Object.keys(table.uniqueConstraints).length > 0 ) { statement += ',\n'; - statement += '(table) => {\n'; - statement += '\treturn {\n'; + statement += '(table) => ['; statement += createTableIndexes( table.name, Object.values(table.indexes), @@ -264,8 +263,7 @@ export const schemaToTypeScript = ( Object.values(table.uniqueConstraints), withCasing, ); - statement += '\t}\n'; - statement += '}'; + statement += '\n]'; } statement += ');'; @@ -855,7 +853,7 @@ const createTableIndexes = ( const indexGeneratedName = indexName(tableName, it.columns); const escapedIndexName = indexGeneratedName === it.name ? '' : `"${it.name}"`; - statement += `\t\t${idxKey}: `; + statement += `\n\t`; statement += it.isUnique ? 'uniqueIndex(' : 'index('; statement += `${escapedIndexName})`; statement += `.on(${ @@ -863,7 +861,6 @@ const createTableIndexes = ( .map((it) => `table.${casing(it)}`) .join(', ') }),`; - statement += `\n`; }); return statement; @@ -876,9 +873,7 @@ const createTableUniques = ( let statement = ''; unqs.forEach((it) => { - const idxKey = casing(it.name); - - statement += `\t\t${idxKey}: `; + statement += `\n\t`; statement += 'unique('; statement += `"${it.name}")`; statement += `.on(${ @@ -886,7 +881,6 @@ const createTableUniques = ( .map((it) => `table.${casing(it)}`) .join(', ') }),`; - statement += `\n`; }); return statement; @@ -901,7 +895,7 @@ const createTablePKs = ( pks.forEach((it) => { let idxKey = casing(it.name); - statement += `\t\t${idxKey}: `; + statement += `\n\t`; statement += 'primaryKey({ columns: ['; statement += `${ it.columns @@ -911,7 +905,6 @@ const createTablePKs = ( .join(', ') }]${it.name ? `, name: "${it.name}"` : ''}}`; statement += '),'; - statement += `\n`; }); return statement; diff --git a/drizzle-kit/src/introspect-sqlite.ts b/drizzle-kit/src/introspect-sqlite.ts index 464a32aa3..d3aac6f04 100644 --- a/drizzle-kit/src/introspect-sqlite.ts +++ b/drizzle-kit/src/introspect-sqlite.ts @@ -162,8 +162,7 @@ export const schemaToTypeScript = ( || Object.keys(table.checkConstraints).length > 0 ) { statement += ',\n'; - statement += '(table) => {\n'; - statement += '\treturn {\n'; + statement += '(table) => ['; statement += createTableIndexes( table.name, Object.values(table.indexes), @@ -182,8 +181,7 @@ export const schemaToTypeScript = ( Object.values(table.checkConstraints), casing, ); - statement += '\t}\n'; - statement += '}'; + statement += '\n]'; } statement += ');'; @@ -429,7 +427,7 @@ const createTableIndexes = ( const indexGeneratedName = indexName(tableName, it.columns); const escapedIndexName = indexGeneratedName === it.name ? '' : `"${it.name}"`; - statement += `\t\t${idxKey}: `; + statement += `\n\t`; statement += it.isUnique ? 'uniqueIndex(' : 'index('; statement += `${escapedIndexName})`; statement += `.on(${ @@ -437,7 +435,6 @@ const createTableIndexes = ( .map((it) => `table.${withCasing(it, casing)}`) .join(', ') }),`; - statement += `\n`; }); return statement; @@ -452,7 +449,7 @@ const createTableUniques = ( unqs.forEach((it) => { const idxKey = withCasing(it.name, casing); - statement += `\t\t${idxKey}: `; + statement += `\n\t`; statement += 'unique('; statement += `"${it.name}")`; statement += `.on(${ @@ -460,7 +457,6 @@ const createTableUniques = ( .map((it) => `table.${withCasing(it, casing)}`) .join(', ') }),`; - statement += `\n`; }); return statement; @@ -472,13 +468,11 @@ const createTableChecks = ( let statement = ''; checks.forEach((it) => { - const checkKey = withCasing(it.name, casing); - - statement += `\t\t${checkKey}: `; + statement += `\n\t`; statement += 'check('; statement += `"${it.name}", `; statement += `sql\`${it.value}\`)`; - statement += `,\n`; + statement += `,`; }); return statement; @@ -488,7 +482,7 @@ const createTablePKs = (pks: PrimaryKey[], casing: Casing): string => { let statement = ''; pks.forEach((it, i) => { - statement += `\t\tpk${i}: `; + statement += `\n\t`; statement += 'primaryKey({ columns: ['; statement += `${ it.columns @@ -498,7 +492,6 @@ const createTablePKs = (pks: PrimaryKey[], casing: Casing): string => { .join(', ') }]${it.name ? `, name: "${it.name}"` : ''}}`; statement += ')'; - statement += `\n`; }); return statement; @@ -510,7 +503,8 @@ const createTableFKs = (fks: ForeignKey[], casing: Casing): string => { fks.forEach((it) => { const isSelf = it.tableTo === it.tableFrom; const tableTo = isSelf ? 'table' : `${withCasing(it.tableTo, casing)}`; - statement += `\t\t${withCasing(it.name, casing)}: foreignKey(() => ({\n`; + statement += `\n\t`; + statement += `foreignKey(() => ({\n`; statement += `\t\t\tcolumns: [${ it.columnsFrom .map((i) => `table.${withCasing(i, casing)}`) @@ -532,7 +526,7 @@ const createTableFKs = (fks: ForeignKey[], casing: Casing): string => { ? `.onDelete("${it.onDelete}")` : ''; - statement += `,\n`; + statement += `,`; }); return statement; diff --git a/drizzle-kit/src/sqlgenerator.ts b/drizzle-kit/src/sqlgenerator.ts index a35c001fd..26adaf531 100644 --- a/drizzle-kit/src/sqlgenerator.ts +++ b/drizzle-kit/src/sqlgenerator.ts @@ -389,7 +389,7 @@ class PgCreateTableConvertor extends Convertor { let statement = ''; const name = schema ? `"${schema}"."${tableName}"` : `"${tableName}"`; - statement += `CREATE TABLE IF NOT EXISTS ${name} (\n`; + statement += `CREATE TABLE ${name} (\n`; for (let i = 0; i < columns.length; i++) { const column = columns[i]; @@ -1672,7 +1672,7 @@ class PgAlterTableDropColumnConvertor extends Convertor { ? `"${schema}"."${tableName}"` : `"${tableName}"`; - return `ALTER TABLE ${tableNameWithSchema} DROP COLUMN IF EXISTS "${columnName}";`; + return `ALTER TABLE ${tableNameWithSchema} DROP COLUMN "${columnName}";`; } } @@ -2330,7 +2330,7 @@ export class LibSQLModifyColumn extends Convertor { for (const table of Object.values(json2.tables)) { for (const index of Object.values(table.indexes)) { const unsquashed = SQLiteSquasher.unsquashIdx(index); - sqlStatements.push(`DROP INDEX IF EXISTS "${unsquashed.name}";`); + sqlStatements.push(`DROP INDEX "${unsquashed.name}";`); indexes.push({ ...unsquashed, tableName: table.name }); } } @@ -3283,14 +3283,9 @@ class PgCreateForeignKeyConvertor extends Convertor { : `"${tableTo}"`; const alterStatement = - `ALTER TABLE ${tableNameWithSchema} ADD CONSTRAINT "${name}" FOREIGN KEY (${fromColumnsString}) REFERENCES ${tableToNameWithSchema}(${toColumnsString})${onDeleteStatement}${onUpdateStatement}`; + `ALTER TABLE ${tableNameWithSchema} ADD CONSTRAINT "${name}" FOREIGN KEY (${fromColumnsString}) REFERENCES ${tableToNameWithSchema}(${toColumnsString})${onDeleteStatement}${onUpdateStatement};`; - let sql = 'DO $$ BEGIN\n'; - sql += ' ' + alterStatement + ';\n'; - sql += 'EXCEPTION\n'; - sql += ' WHEN duplicate_object THEN null;\n'; - sql += 'END $$;\n'; - return sql; + return alterStatement; } } @@ -3387,13 +3382,9 @@ class PgAlterForeignKeyConvertor extends Convertor { : `"${newFk.tableFrom}"`; const alterStatement = - `ALTER TABLE ${tableFromNameWithSchema} ADD CONSTRAINT "${newFk.name}" FOREIGN KEY (${fromColumnsString}) REFERENCES ${tableToNameWithSchema}(${toColumnsString})${onDeleteStatement}${onUpdateStatement}`; + `ALTER TABLE ${tableFromNameWithSchema} ADD CONSTRAINT "${newFk.name}" FOREIGN KEY (${fromColumnsString}) REFERENCES ${tableToNameWithSchema}(${toColumnsString})${onDeleteStatement}${onUpdateStatement};`; - sql += 'DO $$ BEGIN\n'; - sql += ' ' + alterStatement + ';\n'; - sql += 'EXCEPTION\n'; - sql += ' WHEN duplicate_object THEN null;\n'; - sql += 'END $$;\n'; + sql += alterStatement; return sql; } } @@ -3474,7 +3465,7 @@ class CreatePgIndexConvertor extends Convertor { return `CREATE ${indexPart}${ concurrently ? ' CONCURRENTLY' : '' - } IF NOT EXISTS "${name}" ON ${tableNameWithSchema} USING ${method} (${value})${ + } "${name}" ON ${tableNameWithSchema} USING ${method} (${value})${ Object.keys(withMap!).length !== 0 ? ` WITH (${reverseLogic(withMap!)})` : '' @@ -3567,7 +3558,7 @@ class PgDropIndexConvertor extends Convertor { convert(statement: JsonDropIndexStatement): string { const { name } = PgSquasher.unsquashIdx(statement.data); - return `DROP INDEX IF EXISTS "${name}";`; + return `DROP INDEX "${name}";`; } } @@ -3663,7 +3654,7 @@ export class SqliteDropIndexConvertor extends Convertor { convert(statement: JsonDropIndexStatement): string { const { name } = PgSquasher.unsquashIdx(statement.data); - return `DROP INDEX IF EXISTS \`${name}\`;`; + return `DROP INDEX \`${name}\`;`; } } diff --git a/drizzle-kit/tests/indexes/pg.test.ts b/drizzle-kit/tests/indexes/pg.test.ts index b9ff36020..57f77c103 100644 --- a/drizzle-kit/tests/indexes/pg.test.ts +++ b/drizzle-kit/tests/indexes/pg.test.ts @@ -64,7 +64,7 @@ const pgSuite: DialectSuite = { }); expect(sqlStatements.length).toBe(1); expect(sqlStatements[0]).toBe( - `CREATE INDEX IF NOT EXISTS "vector_embedding_idx" ON "users" USING hnsw ("name" vector_ip_ops) WITH (m=16,ef_construction=64);`, + `CREATE INDEX "vector_embedding_idx" ON "users" USING hnsw ("name" vector_ip_ops) WITH (m=16,ef_construction=64);`, ); }, @@ -123,15 +123,15 @@ const pgSuite: DialectSuite = { ); expect(sqlStatements).toStrictEqual([ - 'DROP INDEX IF EXISTS "indx";', - 'DROP INDEX IF EXISTS "indx1";', - 'DROP INDEX IF EXISTS "indx2";', - 'DROP INDEX IF EXISTS "indx3";', - 'CREATE INDEX IF NOT EXISTS "indx4" ON "users" USING btree (lower(id)) WHERE true;', - 'CREATE INDEX IF NOT EXISTS "indx" ON "users" USING btree ("name" DESC NULLS LAST);', - 'CREATE INDEX IF NOT EXISTS "indx1" ON "users" USING btree ("name" DESC NULLS LAST) WHERE false;', - 'CREATE INDEX IF NOT EXISTS "indx2" ON "users" USING btree ("name" test) WHERE true;', - 'CREATE INDEX IF NOT EXISTS "indx3" ON "users" USING btree (lower("id")) WHERE true;', + 'DROP INDEX "indx";', + 'DROP INDEX "indx1";', + 'DROP INDEX "indx2";', + 'DROP INDEX "indx3";', + 'CREATE INDEX "indx4" ON "users" USING btree (lower(id)) WHERE true;', + 'CREATE INDEX "indx" ON "users" USING btree ("name" DESC NULLS LAST);', + 'CREATE INDEX "indx1" ON "users" USING btree ("name" DESC NULLS LAST) WHERE false;', + 'CREATE INDEX "indx2" ON "users" USING btree ("name" test) WHERE true;', + 'CREATE INDEX "indx3" ON "users" USING btree (lower("id")) WHERE true;', ]); }, @@ -234,10 +234,10 @@ const pgSuite: DialectSuite = { }); expect(sqlStatements.length).toBe(2); expect(sqlStatements[0]).toBe( - `CREATE INDEX IF NOT EXISTS "users_name_id_index" ON "users" USING btree ("name" DESC NULLS LAST,"id") WITH (fillfactor=70) WHERE select 1;`, + `CREATE INDEX "users_name_id_index" ON "users" USING btree ("name" DESC NULLS LAST,"id") WITH (fillfactor=70) WHERE select 1;`, ); expect(sqlStatements[1]).toBe( - `CREATE INDEX IF NOT EXISTS "indx1" ON "users" USING hash ("name" DESC NULLS LAST,"name") WITH (fillfactor=70);`, + `CREATE INDEX "indx1" ON "users" USING hash ("name" DESC NULLS LAST,"name") WITH (fillfactor=70);`, ); }, }; diff --git a/drizzle-kit/tests/libsql-statements.test.ts b/drizzle-kit/tests/libsql-statements.test.ts index a7cbc0602..636496c45 100644 --- a/drizzle-kit/tests/libsql-statements.test.ts +++ b/drizzle-kit/tests/libsql-statements.test.ts @@ -917,7 +917,7 @@ test('set not null with index', async (t) => { expect(sqlStatements.length).toBe(3); expect(sqlStatements[0]).toBe( - `DROP INDEX IF EXISTS "users_name_index";`, + `DROP INDEX "users_name_index";`, ); expect(sqlStatements[1]).toBe( `ALTER TABLE \`users\` ALTER COLUMN "name" TO "name" text NOT NULL;`, @@ -972,10 +972,10 @@ test('drop not null with two indexes', async (t) => { expect(sqlStatements.length).toBe(5); expect(sqlStatements[0]).toBe( - `DROP INDEX IF EXISTS "users_name_unique";`, + `DROP INDEX "users_name_unique";`, ); expect(sqlStatements[1]).toBe( - `DROP INDEX IF EXISTS "users_age_index";`, + `DROP INDEX "users_age_index";`, ); expect(sqlStatements[2]).toBe( `ALTER TABLE \`users\` ALTER COLUMN "name" TO "name" text;`, diff --git a/drizzle-kit/tests/pg-checks.test.ts b/drizzle-kit/tests/pg-checks.test.ts index 50a01a6c1..8033aacef 100644 --- a/drizzle-kit/tests/pg-checks.test.ts +++ b/drizzle-kit/tests/pg-checks.test.ts @@ -44,7 +44,7 @@ test('create table with check', async (t) => { } as JsonCreateTableStatement); expect(sqlStatements.length).toBe(1); - expect(sqlStatements[0]).toBe(`CREATE TABLE IF NOT EXISTS "users" ( + expect(sqlStatements[0]).toBe(`CREATE TABLE "users" ( \t"id" serial PRIMARY KEY NOT NULL, \t"age" integer, \tCONSTRAINT "some_check_name" CHECK ("users"."age" > 21) diff --git a/drizzle-kit/tests/pg-identity.test.ts b/drizzle-kit/tests/pg-identity.test.ts index 9f6ce8ba7..efb481da3 100644 --- a/drizzle-kit/tests/pg-identity.test.ts +++ b/drizzle-kit/tests/pg-identity.test.ts @@ -54,7 +54,7 @@ test('create table: identity always/by default - no params', async () => { }, ]); expect(sqlStatements).toStrictEqual([ - 'CREATE TABLE IF NOT EXISTS "users" (\n\t"id" integer GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1)\n);\n', + 'CREATE TABLE "users" (\n\t"id" integer GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1)\n);\n', ]); }); @@ -95,7 +95,7 @@ test('create table: identity always/by default - few params', async () => { }, ]); expect(sqlStatements).toStrictEqual([ - 'CREATE TABLE IF NOT EXISTS "users" (\n\t"id" integer GENERATED BY DEFAULT AS IDENTITY (sequence name "custom_seq" INCREMENT BY 4 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1)\n);\n', + 'CREATE TABLE "users" (\n\t"id" integer GENERATED BY DEFAULT AS IDENTITY (sequence name "custom_seq" INCREMENT BY 4 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1)\n);\n', ]); }); @@ -140,7 +140,7 @@ test('create table: identity always/by default - all params', async () => { }, ]); expect(sqlStatements).toStrictEqual([ - 'CREATE TABLE IF NOT EXISTS "users" (\n\t"id" integer GENERATED BY DEFAULT AS IDENTITY (sequence name "custom_seq" INCREMENT BY 4 MINVALUE 3 MAXVALUE 1000 START WITH 3 CACHE 200)\n);\n', + 'CREATE TABLE "users" (\n\t"id" integer GENERATED BY DEFAULT AS IDENTITY (sequence name "custom_seq" INCREMENT BY 4 MINVALUE 3 MAXVALUE 1000 START WITH 3 CACHE 200)\n);\n', ]); }); diff --git a/drizzle-kit/tests/pg-tables.test.ts b/drizzle-kit/tests/pg-tables.test.ts index 6ea6e472a..4ca01f1fe 100644 --- a/drizzle-kit/tests/pg-tables.test.ts +++ b/drizzle-kit/tests/pg-tables.test.ts @@ -261,7 +261,7 @@ test('add table #8: geometry types', async () => { expect(statements.length).toBe(1); expect(sqlStatements).toStrictEqual([ - `CREATE TABLE IF NOT EXISTS "users" (\n\t"geom" geometry(point) NOT NULL,\n\t"geom1" geometry(point) NOT NULL\n);\n`, + `CREATE TABLE "users" (\n\t"geom" geometry(point) NOT NULL,\n\t"geom1" geometry(point) NOT NULL\n);\n`, ]); }); @@ -360,7 +360,7 @@ test('add table #8: column with pgvector', async () => { const { sqlStatements } = await diffTestSchemas(from, to, []); expect(sqlStatements[0]).toBe( - `CREATE TABLE IF NOT EXISTS "users2" (\n\t"id" serial PRIMARY KEY NOT NULL,\n\t"name" vector(3)\n); + `CREATE TABLE "users2" (\n\t"id" serial PRIMARY KEY NOT NULL,\n\t"name" vector(3)\n); `, ); }); @@ -671,8 +671,8 @@ test('create table with tsvector', async () => { const { statements, sqlStatements } = await diffTestSchemas(from, to, []); expect(sqlStatements).toStrictEqual([ - 'CREATE TABLE IF NOT EXISTS "posts" (\n\t"id" serial PRIMARY KEY NOT NULL,\n\t"title" text NOT NULL,\n\t"description" text NOT NULL\n);\n', - `CREATE INDEX IF NOT EXISTS "title_search_index" ON "posts" USING gin (to_tsvector('english', "title"));`, + 'CREATE TABLE "posts" (\n\t"id" serial PRIMARY KEY NOT NULL,\n\t"title" text NOT NULL,\n\t"description" text NOT NULL\n);\n', + `CREATE INDEX "title_search_index" ON "posts" USING gin (to_tsvector('english', "title"));`, ]); }); @@ -693,7 +693,7 @@ test('composite primary key', async () => { const { sqlStatements } = await diffTestSchemas(from, to, []); expect(sqlStatements).toStrictEqual([ - 'CREATE TABLE IF NOT EXISTS "works_to_creators" (\n\t"work_id" integer NOT NULL,\n\t"creator_id" integer NOT NULL,\n\t"classification" text NOT NULL,\n\tCONSTRAINT "works_to_creators_work_id_creator_id_classification_pk" PRIMARY KEY("work_id","creator_id","classification")\n);\n', + 'CREATE TABLE "works_to_creators" (\n\t"work_id" integer NOT NULL,\n\t"creator_id" integer NOT NULL,\n\t"classification" text NOT NULL,\n\tCONSTRAINT "works_to_creators_work_id_creator_id_classification_pk" PRIMARY KEY("work_id","creator_id","classification")\n);\n', ]); }); @@ -772,7 +772,7 @@ test('add index with op', async () => { const { sqlStatements } = await diffTestSchemas(from, to, []); expect(sqlStatements).toStrictEqual([ - 'CREATE INDEX IF NOT EXISTS "users_name_index" ON "users" USING gin ("name" gin_trgm_ops);', + 'CREATE INDEX "users_name_index" ON "users" USING gin ("name" gin_trgm_ops);', ]); }); @@ -829,7 +829,7 @@ test('optional db aliases (snake case)', async () => { const { sqlStatements } = await diffTestSchemas(from, to, [], false, 'snake_case'); - const st1 = `CREATE TABLE IF NOT EXISTS "t1" ( + const st1 = `CREATE TABLE "t1" ( "t1_id1" integer PRIMARY KEY NOT NULL, "t1_col2" integer NOT NULL, "t1_col3" integer NOT NULL, @@ -841,35 +841,27 @@ test('optional db aliases (snake case)', async () => { ); `; - const st2 = `CREATE TABLE IF NOT EXISTS "t2" ( + const st2 = `CREATE TABLE "t2" ( "t2_id" serial PRIMARY KEY NOT NULL ); `; - const st3 = `CREATE TABLE IF NOT EXISTS "t3" ( + const st3 = `CREATE TABLE "t3" ( "t3_id1" integer, "t3_id2" integer, CONSTRAINT "t3_t3_id1_t3_id2_pk" PRIMARY KEY("t3_id1","t3_id2") ); `; - const st4 = `DO $$ BEGIN - ALTER TABLE "t1" ADD CONSTRAINT "t1_t2_ref_t2_t2_id_fk" FOREIGN KEY ("t2_ref") REFERENCES "public"."t2"("t2_id") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; -`; + const st4 = + `ALTER TABLE "t1" ADD CONSTRAINT "t1_t2_ref_t2_t2_id_fk" FOREIGN KEY ("t2_ref") REFERENCES "public"."t2"("t2_id") ON DELETE no action ON UPDATE no action;`; - const st5 = `DO $$ BEGIN - ALTER TABLE "t1" ADD CONSTRAINT "t1_t1_col2_t1_col3_t3_t3_id1_t3_id2_fk" FOREIGN KEY ("t1_col2","t1_col3") REFERENCES "public"."t3"("t3_id1","t3_id2") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; -`; + const st5 = + `ALTER TABLE "t1" ADD CONSTRAINT "t1_t1_col2_t1_col3_t3_t3_id1_t3_id2_fk" FOREIGN KEY ("t1_col2","t1_col3") REFERENCES "public"."t3"("t3_id1","t3_id2") ON DELETE no action ON UPDATE no action;`; - const st6 = `CREATE UNIQUE INDEX IF NOT EXISTS "t1_uni_idx" ON "t1" USING btree ("t1_uni_idx");`; + const st6 = `CREATE UNIQUE INDEX "t1_uni_idx" ON "t1" USING btree ("t1_uni_idx");`; - const st7 = `CREATE INDEX IF NOT EXISTS "t1_idx" ON "t1" USING btree ("t1_idx") WHERE "t1"."t1_idx" > 0;`; + const st7 = `CREATE INDEX "t1_idx" ON "t1" USING btree ("t1_idx") WHERE "t1"."t1_idx" > 0;`; expect(sqlStatements).toStrictEqual([st1, st2, st3, st4, st5, st6, st7]); }); @@ -927,7 +919,7 @@ test('optional db aliases (camel case)', async () => { const { sqlStatements } = await diffTestSchemas(from, to, [], false, 'camelCase'); - const st1 = `CREATE TABLE IF NOT EXISTS "t1" ( + const st1 = `CREATE TABLE "t1" ( "t1Id1" integer PRIMARY KEY NOT NULL, "t1Col2" integer NOT NULL, "t1Col3" integer NOT NULL, @@ -939,35 +931,27 @@ test('optional db aliases (camel case)', async () => { ); `; - const st2 = `CREATE TABLE IF NOT EXISTS "t2" ( + const st2 = `CREATE TABLE "t2" ( "t2Id" serial PRIMARY KEY NOT NULL ); `; - const st3 = `CREATE TABLE IF NOT EXISTS "t3" ( + const st3 = `CREATE TABLE "t3" ( "t3Id1" integer, "t3Id2" integer, CONSTRAINT "t3_t3Id1_t3Id2_pk" PRIMARY KEY("t3Id1","t3Id2") ); `; - const st4 = `DO $$ BEGIN - ALTER TABLE "t1" ADD CONSTRAINT "t1_t2Ref_t2_t2Id_fk" FOREIGN KEY ("t2Ref") REFERENCES "public"."t2"("t2Id") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; -`; + const st4 = + `ALTER TABLE "t1" ADD CONSTRAINT "t1_t2Ref_t2_t2Id_fk" FOREIGN KEY ("t2Ref") REFERENCES "public"."t2"("t2Id") ON DELETE no action ON UPDATE no action;`; - const st5 = `DO $$ BEGIN - ALTER TABLE "t1" ADD CONSTRAINT "t1_t1Col2_t1Col3_t3_t3Id1_t3Id2_fk" FOREIGN KEY ("t1Col2","t1Col3") REFERENCES "public"."t3"("t3Id1","t3Id2") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; -`; + const st5 = + `ALTER TABLE "t1" ADD CONSTRAINT "t1_t1Col2_t1Col3_t3_t3Id1_t3Id2_fk" FOREIGN KEY ("t1Col2","t1Col3") REFERENCES "public"."t3"("t3Id1","t3Id2") ON DELETE no action ON UPDATE no action;`; - const st6 = `CREATE UNIQUE INDEX IF NOT EXISTS "t1UniIdx" ON "t1" USING btree ("t1UniIdx");`; + const st6 = `CREATE UNIQUE INDEX "t1UniIdx" ON "t1" USING btree ("t1UniIdx");`; - const st7 = `CREATE INDEX IF NOT EXISTS "t1Idx" ON "t1" USING btree ("t1Idx") WHERE "t1"."t1Idx" > 0;`; + const st7 = `CREATE INDEX "t1Idx" ON "t1" USING btree ("t1Idx") WHERE "t1"."t1Idx" > 0;`; expect(sqlStatements).toStrictEqual([st1, st2, st3, st4, st5, st6, st7]); }); diff --git a/drizzle-kit/tests/pg-views.test.ts b/drizzle-kit/tests/pg-views.test.ts index 002004c47..4f24cd776 100644 --- a/drizzle-kit/tests/pg-views.test.ts +++ b/drizzle-kit/tests/pg-views.test.ts @@ -45,7 +45,7 @@ test('create table and view #1', async () => { }); expect(sqlStatements.length).toBe(2); - expect(sqlStatements[0]).toBe(`CREATE TABLE IF NOT EXISTS "users" ( + expect(sqlStatements[0]).toBe(`CREATE TABLE "users" ( \t"id" integer PRIMARY KEY NOT NULL );\n`); expect(sqlStatements[1]).toBe(`CREATE VIEW "public"."some_view" AS (select "id" from "users");`); @@ -93,7 +93,7 @@ test('create table and view #2', async () => { }); expect(sqlStatements.length).toBe(2); - expect(sqlStatements[0]).toBe(`CREATE TABLE IF NOT EXISTS "users" ( + expect(sqlStatements[0]).toBe(`CREATE TABLE "users" ( \t"id" integer PRIMARY KEY NOT NULL );\n`); expect(sqlStatements[1]).toBe(`CREATE VIEW "public"."some_view" AS (SELECT * FROM "users");`); @@ -169,7 +169,7 @@ test('create table and view #3', async () => { }); expect(sqlStatements.length).toBe(3); - expect(sqlStatements[0]).toBe(`CREATE TABLE IF NOT EXISTS "users" ( + expect(sqlStatements[0]).toBe(`CREATE TABLE "users" ( \t"id" integer PRIMARY KEY NOT NULL );\n`); expect(sqlStatements[1]).toBe( @@ -258,7 +258,7 @@ test('create table and view #4', async () => { expect(sqlStatements.length).toBe(4); expect(sqlStatements[0]).toBe(`CREATE SCHEMA "new_schema";\n`); - expect(sqlStatements[1]).toBe(`CREATE TABLE IF NOT EXISTS "new_schema"."users" ( + expect(sqlStatements[1]).toBe(`CREATE TABLE "new_schema"."users" ( \t"id" integer PRIMARY KEY NOT NULL );\n`); expect(sqlStatements[2]).toBe( @@ -328,7 +328,7 @@ test('create table and view #6', async () => { }); expect(sqlStatements.length).toBe(2); - expect(sqlStatements[0]).toBe(`CREATE TABLE IF NOT EXISTS "users" ( + expect(sqlStatements[0]).toBe(`CREATE TABLE "users" ( \t"id" integer PRIMARY KEY NOT NULL );\n`); expect(sqlStatements[1]).toBe( @@ -398,7 +398,7 @@ test('create table and materialized view #1', async () => { }); expect(sqlStatements.length).toBe(2); - expect(sqlStatements[0]).toBe(`CREATE TABLE IF NOT EXISTS "users" ( + expect(sqlStatements[0]).toBe(`CREATE TABLE "users" ( \t"id" integer PRIMARY KEY NOT NULL );\n`); expect(sqlStatements[1]).toBe(`CREATE MATERIALIZED VIEW "public"."some_view" AS (select "id" from "users");`); @@ -446,7 +446,7 @@ test('create table and materialized view #2', async () => { }); expect(sqlStatements.length).toBe(2); - expect(sqlStatements[0]).toBe(`CREATE TABLE IF NOT EXISTS "users" ( + expect(sqlStatements[0]).toBe(`CREATE TABLE "users" ( \t"id" integer PRIMARY KEY NOT NULL );\n`); expect(sqlStatements[1]).toBe(`CREATE MATERIALIZED VIEW "public"."some_view" AS (SELECT * FROM "users");`); @@ -544,7 +544,7 @@ test('create table and materialized view #3', async () => { }); expect(sqlStatements.length).toBe(3); - expect(sqlStatements[0]).toBe(`CREATE TABLE IF NOT EXISTS "users" ( + expect(sqlStatements[0]).toBe(`CREATE TABLE "users" ( \t"id" integer PRIMARY KEY NOT NULL );\n`); expect(sqlStatements[1]).toBe( @@ -617,7 +617,7 @@ test('create table and materialized view #5', async () => { }); expect(sqlStatements.length).toBe(2); - expect(sqlStatements[0]).toBe(`CREATE TABLE IF NOT EXISTS "users" ( + expect(sqlStatements[0]).toBe(`CREATE TABLE "users" ( \t"id" integer PRIMARY KEY NOT NULL );\n`); expect(sqlStatements[1]).toBe( diff --git a/drizzle-kit/tests/push/libsql.test.ts b/drizzle-kit/tests/push/libsql.test.ts index 460809d9e..2ae2e3811 100644 --- a/drizzle-kit/tests/push/libsql.test.ts +++ b/drizzle-kit/tests/push/libsql.test.ts @@ -185,7 +185,7 @@ test('added, dropped index', async (t) => { expect(sqlStatements.length).toBe(2); expect(sqlStatements[0]).toBe( - `DROP INDEX IF EXISTS \`customers_address_unique\`;`, + `DROP INDEX \`customers_address_unique\`;`, ); expect(sqlStatements[1]).toBe( `CREATE UNIQUE INDEX \`customers_is_confirmed_unique\` ON \`customers\` (\`is_confirmed\`);`, @@ -963,7 +963,7 @@ test('set not null with index', async (t) => { expect(sqlStatements.length).toBe(3); expect(sqlStatements[0]).toBe( - `DROP INDEX IF EXISTS "users_name_index";`, + `DROP INDEX "users_name_index";`, ); expect(sqlStatements[1]).toBe( `ALTER TABLE \`users\` ALTER COLUMN "name" TO "name" text NOT NULL;`, @@ -1035,10 +1035,10 @@ test('drop not null with two indexes', async (t) => { expect(sqlStatements.length).toBe(5); expect(sqlStatements[0]).toBe( - `DROP INDEX IF EXISTS "users_name_unique";`, + `DROP INDEX "users_name_unique";`, ); expect(sqlStatements[1]).toBe( - `DROP INDEX IF EXISTS "users_age_index";`, + `DROP INDEX "users_age_index";`, ); expect(sqlStatements[2]).toBe( `ALTER TABLE \`users\` ALTER COLUMN "name" TO "name" text;`, diff --git a/drizzle-kit/tests/push/pg.test.ts b/drizzle-kit/tests/push/pg.test.ts index 44ec786b6..a7bed413d 100644 --- a/drizzle-kit/tests/push/pg.test.ts +++ b/drizzle-kit/tests/push/pg.test.ts @@ -318,10 +318,10 @@ const pgSuite: DialectSuite = { }); expect(sqlStatements.length).toBe(2); expect(sqlStatements[0]).toBe( - `CREATE INDEX IF NOT EXISTS "users_name_id_index" ON "users" USING btree ("name" DESC NULLS LAST,"id") WITH (fillfactor=70) WHERE select 1;`, + `CREATE INDEX "users_name_id_index" ON "users" USING btree ("name" DESC NULLS LAST,"id") WITH (fillfactor=70) WHERE select 1;`, ); expect(sqlStatements[1]).toBe( - `CREATE INDEX IF NOT EXISTS "indx1" ON "users" USING hash ("name" DESC NULLS LAST,"name") WITH (fillfactor=70);`, + `CREATE INDEX "indx1" ON "users" USING hash ("name" DESC NULLS LAST,"name") WITH (fillfactor=70);`, ); }, @@ -547,7 +547,7 @@ const pgSuite: DialectSuite = { }, ]); expect(sqlStatements).toStrictEqual([ - 'CREATE TABLE IF NOT EXISTS "users" (\n\t"id" integer,\n\t"id2" integer,\n\t"name" text,\n\t"gen_name" text GENERATED ALWAYS AS ("users"."name" || \'hello\') STORED\n);\n', + 'CREATE TABLE "users" (\n\t"id" integer,\n\t"id2" integer,\n\t"name" text,\n\t"gen_name" text GENERATED ALWAYS AS ("users"."name" || \'hello\') STORED\n);\n', ]); }, @@ -616,20 +616,20 @@ const pgSuite: DialectSuite = { const { statements, sqlStatements } = await diffTestSchemasPush(client, schema1, schema2, [], false, ['public']); expect(sqlStatements).toStrictEqual([ - 'DROP INDEX IF EXISTS "changeName";', - 'DROP INDEX IF EXISTS "addColumn";', - 'DROP INDEX IF EXISTS "changeExpression";', - 'DROP INDEX IF EXISTS "changeUsing";', - 'DROP INDEX IF EXISTS "changeWith";', - 'DROP INDEX IF EXISTS "removeColumn";', - 'DROP INDEX IF EXISTS "removeExpression";', - 'CREATE INDEX IF NOT EXISTS "newName" ON "users" USING btree ("name" DESC NULLS LAST,name) WITH (fillfactor=70);', - 'CREATE INDEX IF NOT EXISTS "addColumn" ON "users" USING btree ("name" DESC NULLS LAST,"id") WITH (fillfactor=70);', - 'CREATE INDEX IF NOT EXISTS "changeExpression" ON "users" USING btree ("id" DESC NULLS LAST,name desc);', - 'CREATE INDEX IF NOT EXISTS "changeUsing" ON "users" USING hash ("name");', - 'CREATE INDEX IF NOT EXISTS "changeWith" ON "users" USING btree ("name") WITH (fillfactor=90);', - 'CREATE INDEX IF NOT EXISTS "removeColumn" ON "users" USING btree ("name");', - 'CREATE INDEX CONCURRENTLY IF NOT EXISTS "removeExpression" ON "users" USING btree ("name" DESC NULLS LAST);', + 'DROP INDEX "changeName";', + 'DROP INDEX "addColumn";', + 'DROP INDEX "changeExpression";', + 'DROP INDEX "changeUsing";', + 'DROP INDEX "changeWith";', + 'DROP INDEX "removeColumn";', + 'DROP INDEX "removeExpression";', + 'CREATE INDEX "newName" ON "users" USING btree ("name" DESC NULLS LAST,name) WITH (fillfactor=70);', + 'CREATE INDEX "addColumn" ON "users" USING btree ("name" DESC NULLS LAST,"id") WITH (fillfactor=70);', + 'CREATE INDEX "changeExpression" ON "users" USING btree ("id" DESC NULLS LAST,name desc);', + 'CREATE INDEX "changeUsing" ON "users" USING hash ("name");', + 'CREATE INDEX "changeWith" ON "users" USING btree ("name") WITH (fillfactor=90);', + 'CREATE INDEX "removeColumn" ON "users" USING btree ("name");', + 'CREATE INDEX CONCURRENTLY "removeExpression" ON "users" USING btree ("name" DESC NULLS LAST);', ]); }, @@ -667,7 +667,7 @@ const pgSuite: DialectSuite = { }); expect(sqlStatements.length).toBe(1); - expect(sqlStatements[0]).toBe(`DROP INDEX IF EXISTS "users_name_id_index";`); + expect(sqlStatements[0]).toBe(`DROP INDEX "users_name_id_index";`); }, async indexesToBeNotTriggered() { @@ -958,7 +958,7 @@ const pgSuite: DialectSuite = { }, ]); expect(sqlStatements).toStrictEqual([ - 'CREATE TABLE IF NOT EXISTS "table" (\n\t"col1" integer NOT NULL,\n\t"col2" integer NOT NULL,\n\tCONSTRAINT "table_col1_col2_pk" PRIMARY KEY("col1","col2")\n);\n', + 'CREATE TABLE "table" (\n\t"col1" integer NOT NULL,\n\t"col2" integer NOT NULL,\n\tCONSTRAINT "table_col1_col2_pk" PRIMARY KEY("col1","col2")\n);\n', ]); }, @@ -1304,7 +1304,7 @@ test('create table: identity always/by default - no params', async () => { }, ]); expect(sqlStatements).toStrictEqual([ - 'CREATE TABLE IF NOT EXISTS "users" (\n\t"id" integer GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),\n\t"id1" bigint GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id1_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),\n\t"id2" smallint GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id2_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 32767 START WITH 1 CACHE 1)\n);\n', + 'CREATE TABLE "users" (\n\t"id" integer GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),\n\t"id1" bigint GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id1_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),\n\t"id2" smallint GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id2_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 32767 START WITH 1 CACHE 1)\n);\n', ]); for (const st of sqlStatements) { @@ -1367,7 +1367,7 @@ test('create table: identity always/by default - few params', async () => { }, ]); expect(sqlStatements).toStrictEqual([ - 'CREATE TABLE IF NOT EXISTS "users" (\n\t"id" integer GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id_seq" INCREMENT BY 4 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),\n\t"id1" bigint GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id1_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 17000 START WITH 120 CACHE 1),\n\t"id2" smallint GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id2_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 32767 START WITH 1 CACHE 1 CYCLE)\n);\n', + 'CREATE TABLE "users" (\n\t"id" integer GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id_seq" INCREMENT BY 4 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),\n\t"id1" bigint GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id1_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 17000 START WITH 120 CACHE 1),\n\t"id2" smallint GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id2_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 32767 START WITH 1 CACHE 1 CYCLE)\n);\n', ]); for (const st of sqlStatements) { @@ -1436,7 +1436,7 @@ test('create table: identity always/by default - all params', async () => { }, ]); expect(sqlStatements).toStrictEqual([ - 'CREATE TABLE IF NOT EXISTS "users" (\n\t"id" integer GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id_seq" INCREMENT BY 4 MINVALUE 100 MAXVALUE 2147483647 START WITH 100 CACHE 1),\n\t"id1" bigint GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id1_seq" INCREMENT BY 3 MINVALUE 1 MAXVALUE 17000 START WITH 120 CACHE 100 CYCLE),\n\t"id2" smallint GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id2_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 32767 START WITH 1 CACHE 1 CYCLE)\n);\n', + 'CREATE TABLE "users" (\n\t"id" integer GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id_seq" INCREMENT BY 4 MINVALUE 100 MAXVALUE 2147483647 START WITH 100 CACHE 1),\n\t"id1" bigint GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id1_seq" INCREMENT BY 3 MINVALUE 1 MAXVALUE 17000 START WITH 120 CACHE 100 CYCLE),\n\t"id2" smallint GENERATED BY DEFAULT AS IDENTITY (sequence name "users_id2_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 32767 START WITH 1 CACHE 1 CYCLE)\n);\n', ]); for (const st of sqlStatements) { @@ -2258,7 +2258,7 @@ test('Column with same name as enum', async () => { }, ]); expect(sqlStatements).toStrictEqual([ - 'CREATE TABLE IF NOT EXISTS "table2" (\n\t"id" serial PRIMARY KEY NOT NULL,\n\t"status" "status" DEFAULT \'inactive\'\n);\n', + 'CREATE TABLE "table2" (\n\t"id" serial PRIMARY KEY NOT NULL,\n\t"status" "status" DEFAULT \'inactive\'\n);\n', 'ALTER TABLE "table1" ADD COLUMN "status" "status" DEFAULT \'inactive\';', ]); }); @@ -3591,7 +3591,7 @@ test('create table with a policy', async (t) => { ); expect(sqlStatements).toStrictEqual([ - 'CREATE TABLE IF NOT EXISTS "users2" (\n\t"id" integer PRIMARY KEY NOT NULL\n);\n', + 'CREATE TABLE "users2" (\n\t"id" integer PRIMARY KEY NOT NULL\n);\n', 'ALTER TABLE "users2" ENABLE ROW LEVEL SECURITY;', 'CREATE POLICY "test" ON "users2" AS PERMISSIVE FOR ALL TO public;', ]); diff --git a/drizzle-kit/tests/push/sqlite.test.ts b/drizzle-kit/tests/push/sqlite.test.ts index dd1d88fe3..e2c85233a 100644 --- a/drizzle-kit/tests/push/sqlite.test.ts +++ b/drizzle-kit/tests/push/sqlite.test.ts @@ -185,7 +185,7 @@ test('dropped, added unique index', async (t) => { expect(sqlStatements.length).toBe(2); expect(sqlStatements[0]).toBe( - `DROP INDEX IF EXISTS \`customers_address_unique\`;`, + `DROP INDEX \`customers_address_unique\`;`, ); expect(sqlStatements[1]).toBe( `CREATE UNIQUE INDEX \`customers_is_confirmed_unique\` ON \`customers\` (\`is_confirmed\`);`, diff --git a/drizzle-kit/tests/rls/pg-policy.test.ts b/drizzle-kit/tests/rls/pg-policy.test.ts index b42385e3e..3d5dcbd14 100644 --- a/drizzle-kit/tests/rls/pg-policy.test.ts +++ b/drizzle-kit/tests/rls/pg-policy.test.ts @@ -587,7 +587,7 @@ test('create table with a policy', async (t) => { const { statements, sqlStatements } = await diffTestSchemas(schema1, schema2, []); expect(sqlStatements).toStrictEqual([ - 'CREATE TABLE IF NOT EXISTS "users2" (\n\t"id" integer PRIMARY KEY NOT NULL\n);\n', + 'CREATE TABLE "users2" (\n\t"id" integer PRIMARY KEY NOT NULL\n);\n', 'ALTER TABLE "users2" ENABLE ROW LEVEL SECURITY;', 'CREATE POLICY "test" ON "users2" AS PERMISSIVE FOR ALL TO public;', ]); @@ -720,12 +720,10 @@ test('create table with rls enabled', async (t) => { const { statements, sqlStatements } = await diffTestSchemas(schema1, schema2, []); expect(sqlStatements).toStrictEqual([ - `CREATE TABLE IF NOT EXISTS "users" (\n\t"id" integer PRIMARY KEY NOT NULL\n); + `CREATE TABLE "users" (\n\t"id" integer PRIMARY KEY NOT NULL\n); `, 'ALTER TABLE "users" ENABLE ROW LEVEL SECURITY;', ]); - - console.log(statements); }); test('enable rls force', async (t) => { diff --git a/drizzle-orm/package.json b/drizzle-orm/package.json index 3fa8e4bc3..c7528fdbe 100644 --- a/drizzle-orm/package.json +++ b/drizzle-orm/package.json @@ -1,6 +1,6 @@ { "name": "drizzle-orm", - "version": "0.37.0", + "version": "0.38.0", "description": "Drizzle ORM package for SQL databases", "type": "module", "scripts": { diff --git a/drizzle-orm/src/column-builder.ts b/drizzle-orm/src/column-builder.ts index 207f28026..6d6dfeeba 100644 --- a/drizzle-orm/src/column-builder.ts +++ b/drizzle-orm/src/column-builder.ts @@ -313,11 +313,49 @@ export type BuildColumn< TTableName extends string, TBuilder extends ColumnBuilderBase, TDialect extends Dialect, -> = TDialect extends 'pg' ? PgColumn> - : TDialect extends 'mysql' ? MySqlColumn> - : TDialect extends 'singlestore' ? SingleStoreColumn> - : TDialect extends 'sqlite' ? SQLiteColumn> - : TDialect extends 'common' ? Column> +> = TDialect extends 'pg' ? PgColumn< + MakeColumnConfig, + {}, + Simplify | 'brand' | 'dialect'>> + > + : TDialect extends 'mysql' ? MySqlColumn< + MakeColumnConfig, + {}, + Simplify< + Omit< + TBuilder['_'], + | keyof MakeColumnConfig + | 'brand' + | 'dialect' + | 'primaryKeyHasDefault' + | 'mysqlColumnBuilderBrand' + > + > + > + : TDialect extends 'sqlite' ? SQLiteColumn< + MakeColumnConfig, + {}, + Simplify | 'brand' | 'dialect'>> + > + : TDialect extends 'common' ? Column< + MakeColumnConfig, + {}, + Simplify | 'brand' | 'dialect'>> + > + : TDialect extends 'singlestore' ? SingleStoreColumn< + MakeColumnConfig, + {}, + Simplify< + Omit< + TBuilder['_'], + | keyof MakeColumnConfig + | 'brand' + | 'dialect' + | 'primaryKeyHasDefault' + | 'singlestoreColumnBuilderBrand' + > + > + > : never; export type BuildIndexColumn< diff --git a/drizzle-orm/src/mysql-core/columns/all.ts b/drizzle-orm/src/mysql-core/columns/all.ts index 428b3c330..44c03eff0 100644 --- a/drizzle-orm/src/mysql-core/columns/all.ts +++ b/drizzle-orm/src/mysql-core/columns/all.ts @@ -15,7 +15,7 @@ import { mediumint } from './mediumint.ts'; import { real } from './real.ts'; import { serial } from './serial.ts'; import { smallint } from './smallint.ts'; -import { text } from './text.ts'; +import { longtext, mediumtext, text, tinytext } from './text.ts'; import { time } from './time.ts'; import { timestamp } from './timestamp.ts'; import { tinyint } from './tinyint.ts'; @@ -49,6 +49,9 @@ export function getMySqlColumnBuilders() { varbinary, varchar, year, + longtext, + mediumtext, + tinytext, }; } diff --git a/drizzle-orm/src/mysql-core/columns/char.ts b/drizzle-orm/src/mysql-core/columns/char.ts index 88492288e..55743a5d4 100644 --- a/drizzle-orm/src/mysql-core/columns/char.ts +++ b/drizzle-orm/src/mysql-core/columns/char.ts @@ -5,22 +5,30 @@ import type { AnyMySqlTable } from '~/mysql-core/table.ts'; import { getColumnNameAndConfig, type Writable } from '~/utils.ts'; import { MySqlColumn, MySqlColumnBuilder } from './common.ts'; -export type MySqlCharBuilderInitial = MySqlCharBuilder<{ +export type MySqlCharBuilderInitial< + TName extends string, + TEnum extends [string, ...string[]], + TLength extends number | undefined, +> = MySqlCharBuilder<{ name: TName; dataType: 'string'; columnType: 'MySqlChar'; data: TEnum[number]; driverParam: number | string; enumValues: TEnum; + length: TLength; }>; -export class MySqlCharBuilder> extends MySqlColumnBuilder< +export class MySqlCharBuilder< + T extends ColumnBuilderBaseConfig<'string', 'MySqlChar'> & { length?: number | undefined }, +> extends MySqlColumnBuilder< T, - MySqlCharConfig + MySqlCharConfig, + { length: T['length'] } > { static override readonly [entityKind]: string = 'MySqlCharBuilder'; - constructor(name: T['name'], config: MySqlCharConfig) { + constructor(name: T['name'], config: MySqlCharConfig) { super(name, 'string', 'MySqlChar'); this.config.length = config.length; this.config.enum = config.enum; @@ -29,20 +37,20 @@ export class MySqlCharBuilder( table: AnyMySqlTable<{ name: TTableName }>, - ): MySqlChar & { enumValues: T['enumValues'] }> { - return new MySqlChar & { enumValues: T['enumValues'] }>( + ): MySqlChar & { length: T['length']; enumValues: T['enumValues'] }> { + return new MySqlChar & { length: T['length']; enumValues: T['enumValues'] }>( table, this.config as ColumnBuilderRuntimeConfig, ); } } -export class MySqlChar> - extends MySqlColumn> +export class MySqlChar & { length?: number | undefined }> + extends MySqlColumn, { length: T['length'] }> { static override readonly [entityKind]: string = 'MySqlChar'; - readonly length: number | undefined = this.config.length; + readonly length: T['length'] = this.config.length; override readonly enumValues = this.config.enum; getSQLType(): string { @@ -52,19 +60,25 @@ export class MySqlChar> export interface MySqlCharConfig< TEnum extends readonly string[] | string[] | undefined = readonly string[] | string[] | undefined, + TLength extends number | undefined = number | undefined, > { - length?: number; enum?: TEnum; + length?: TLength; } -export function char(): MySqlCharBuilderInitial<'', [string, ...string[]]>; -export function char>( - config?: MySqlCharConfig>, -): MySqlCharBuilderInitial<'', Writable>; -export function char>( +export function char(): MySqlCharBuilderInitial<'', [string, ...string[]], undefined>; +export function char, L extends number | undefined>( + config?: MySqlCharConfig, L>, +): MySqlCharBuilderInitial<'', Writable, L>; +export function char< + TName extends string, + U extends string, + T extends Readonly<[U, ...U[]]>, + L extends number | undefined, +>( name: TName, - config?: MySqlCharConfig>, -): MySqlCharBuilderInitial>; + config?: MySqlCharConfig, L>, +): MySqlCharBuilderInitial, L>; export function char(a?: string | MySqlCharConfig, b: MySqlCharConfig = {}): any { const { name, config } = getColumnNameAndConfig(a, b); return new MySqlCharBuilder(name, config as any); diff --git a/drizzle-orm/src/mysql-core/columns/common.ts b/drizzle-orm/src/mysql-core/columns/common.ts index 2f1073e53..289c420ae 100644 --- a/drizzle-orm/src/mysql-core/columns/common.ts +++ b/drizzle-orm/src/mysql-core/columns/common.ts @@ -101,8 +101,9 @@ export abstract class MySqlColumnBuilder< // To understand how to use `MySqlColumn` and `AnyMySqlColumn`, see `Column` and `AnyColumn` documentation. export abstract class MySqlColumn< T extends ColumnBaseConfig = ColumnBaseConfig, - TRuntimeConfig extends object = object, -> extends Column { + TRuntimeConfig extends object = {}, + TTypeConfig extends object = {}, +> extends Column { static override readonly [entityKind]: string = 'MySqlColumn'; constructor( diff --git a/drizzle-orm/src/mysql-core/columns/text.ts b/drizzle-orm/src/mysql-core/columns/text.ts index 0604ef141..6106fd45b 100644 --- a/drizzle-orm/src/mysql-core/columns/text.ts +++ b/drizzle-orm/src/mysql-core/columns/text.ts @@ -41,7 +41,7 @@ export class MySqlText> { static override readonly [entityKind]: string = 'MySqlText'; - private textType: MySqlTextColumnType = this.config.textType; + readonly textType: MySqlTextColumnType = this.config.textType; override readonly enumValues = this.config.enumValues; diff --git a/drizzle-orm/src/mysql-core/columns/varchar.ts b/drizzle-orm/src/mysql-core/columns/varchar.ts index 6a335fef7..0a0bde857 100644 --- a/drizzle-orm/src/mysql-core/columns/varchar.ts +++ b/drizzle-orm/src/mysql-core/columns/varchar.ts @@ -5,7 +5,11 @@ import type { AnyMySqlTable } from '~/mysql-core/table.ts'; import { getColumnNameAndConfig, type Writable } from '~/utils.ts'; import { MySqlColumn, MySqlColumnBuilder } from './common.ts'; -export type MySqlVarCharBuilderInitial = MySqlVarCharBuilder< +export type MySqlVarCharBuilderInitial< + TName extends string, + TEnum extends [string, ...string[]], + TLength extends number | undefined, +> = MySqlVarCharBuilder< { name: TName; dataType: 'string'; @@ -13,16 +17,17 @@ export type MySqlVarCharBuilderInitial; -export class MySqlVarCharBuilder> - extends MySqlColumnBuilder> -{ +export class MySqlVarCharBuilder< + T extends ColumnBuilderBaseConfig<'string', 'MySqlVarChar'> & { length?: number | undefined }, +> extends MySqlColumnBuilder> { static override readonly [entityKind]: string = 'MySqlVarCharBuilder'; /** @internal */ - constructor(name: T['name'], config: MySqlVarCharConfig) { + constructor(name: T['name'], config: MySqlVarCharConfig) { super(name, 'string', 'MySqlVarChar'); this.config.length = config.length; this.config.enum = config.enum; @@ -31,16 +36,16 @@ export class MySqlVarCharBuilder( table: AnyMySqlTable<{ name: TTableName }>, - ): MySqlVarChar & { enumValues: T['enumValues'] }> { - return new MySqlVarChar & { enumValues: T['enumValues'] }>( + ): MySqlVarChar & { length: T['length']; enumValues: T['enumValues'] }> { + return new MySqlVarChar & { length: T['length']; enumValues: T['enumValues'] }>( table, this.config as ColumnBuilderRuntimeConfig, ); } } -export class MySqlVarChar> - extends MySqlColumn> +export class MySqlVarChar & { length?: number | undefined }> + extends MySqlColumn, { length: T['length'] }> { static override readonly [entityKind]: string = 'MySqlVarChar'; @@ -55,18 +60,24 @@ export class MySqlVarChar> export interface MySqlVarCharConfig< TEnum extends string[] | readonly string[] | undefined = string[] | readonly string[] | undefined, + TLength extends number | undefined = number | undefined, > { - length: number; enum?: TEnum; + length?: TLength; } -export function varchar>( - config: MySqlVarCharConfig>, -): MySqlVarCharBuilderInitial<'', Writable>; -export function varchar>( +export function varchar, L extends number | undefined>( + config: MySqlVarCharConfig, L>, +): MySqlVarCharBuilderInitial<'', Writable, L>; +export function varchar< + TName extends string, + U extends string, + T extends Readonly<[U, ...U[]]>, + L extends number | undefined, +>( name: TName, - config: MySqlVarCharConfig>, -): MySqlVarCharBuilderInitial>; + config: MySqlVarCharConfig, L>, +): MySqlVarCharBuilderInitial, L>; export function varchar(a?: string | MySqlVarCharConfig, b?: MySqlVarCharConfig): any { const { name, config } = getColumnNameAndConfig(a, b); return new MySqlVarCharBuilder(name, config as any); diff --git a/drizzle-orm/src/mysql-core/table.ts b/drizzle-orm/src/mysql-core/table.ts index e09278dc5..c3d3e581a 100644 --- a/drizzle-orm/src/mysql-core/table.ts +++ b/drizzle-orm/src/mysql-core/table.ts @@ -9,13 +9,16 @@ import type { AnyIndexBuilder } from './indexes.ts'; import type { PrimaryKeyBuilder } from './primary-keys.ts'; import type { UniqueConstraintBuilder } from './unique-constraint.ts'; -export type MySqlTableExtraConfig = Record< - string, +export type MySqlTableExtraConfigValue = | AnyIndexBuilder | CheckBuilder | ForeignKeyBuilder | PrimaryKeyBuilder - | UniqueConstraintBuilder + | UniqueConstraintBuilder; + +export type MySqlTableExtraConfig = Record< + string, + MySqlTableExtraConfigValue >; export type TableConfig = TableConfigBase; @@ -62,7 +65,11 @@ export function mysqlTableWithSchema< >( name: TTableName, columns: TColumnsMap | ((columnTypes: MySqlColumnBuilders) => TColumnsMap), - extraConfig: ((self: BuildColumns) => MySqlTableExtraConfig) | undefined, + extraConfig: + | (( + self: BuildColumns, + ) => MySqlTableExtraConfig | MySqlTableExtraConfigValue[]) + | undefined, schema: TSchemaName, baseName = name, ): MySqlTableWithColumns<{ @@ -109,13 +116,87 @@ export function mysqlTableWithSchema< } export interface MySqlTableFn { + /** + * @deprecated The third parameter of mysqlTable is changing and will only accept an array instead of an object + * + * @example + * Deprecated version: + * ```ts + * export const users = mysqlTable("users", { + * id: int(), + * }, (t) => ({ + * idx: index('custom_name').on(t.id) + * })); + * ``` + * + * New API: + * ```ts + * export const users = mysqlTable("users", { + * id: int(), + * }, (t) => [ + * index('custom_name').on(t.id) + * ]); + * ``` + */ + < + TTableName extends string, + TColumnsMap extends Record, + >( + name: TTableName, + columns: TColumnsMap, + extraConfig: (self: BuildColumns) => MySqlTableExtraConfig, + ): MySqlTableWithColumns<{ + name: TTableName; + schema: TSchemaName; + columns: BuildColumns; + dialect: 'mysql'; + }>; + + /** + * @deprecated The third parameter of mysqlTable is changing and will only accept an array instead of an object + * + * @example + * Deprecated version: + * ```ts + * export const users = mysqlTable("users", { + * id: int(), + * }, (t) => ({ + * idx: index('custom_name').on(t.id) + * })); + * ``` + * + * New API: + * ```ts + * export const users = mysqlTable("users", { + * id: int(), + * }, (t) => [ + * index('custom_name').on(t.id) + * ]); + * ``` + */ + < + TTableName extends string, + TColumnsMap extends Record, + >( + name: TTableName, + columns: (columnTypes: MySqlColumnBuilders) => TColumnsMap, + extraConfig: (self: BuildColumns) => MySqlTableExtraConfig, + ): MySqlTableWithColumns<{ + name: TTableName; + schema: TSchemaName; + columns: BuildColumns; + dialect: 'mysql'; + }>; + < TTableName extends string, TColumnsMap extends Record, >( name: TTableName, columns: TColumnsMap, - extraConfig?: (self: BuildColumns) => MySqlTableExtraConfig, + extraConfig?: ( + self: BuildColumns, + ) => MySqlTableExtraConfigValue[], ): MySqlTableWithColumns<{ name: TTableName; schema: TSchemaName; @@ -129,7 +210,7 @@ export interface MySqlTableFn( name: TTableName, columns: (columnTypes: MySqlColumnBuilders) => TColumnsMap, - extraConfig?: (self: BuildColumns) => MySqlTableExtraConfig, + extraConfig?: (self: BuildColumns) => MySqlTableExtraConfigValue[], ): MySqlTableWithColumns<{ name: TTableName; schema: TSchemaName; diff --git a/drizzle-orm/src/mysql-core/utils.ts b/drizzle-orm/src/mysql-core/utils.ts index f09f65f3e..2cdc68620 100644 --- a/drizzle-orm/src/mysql-core/utils.ts +++ b/drizzle-orm/src/mysql-core/utils.ts @@ -29,7 +29,8 @@ export function getTableConfig(table: MySqlTable) { if (extraConfigBuilder !== undefined) { const extraConfig = extraConfigBuilder(table[MySqlTable.Symbol.Columns]); - for (const builder of Object.values(extraConfig)) { + const extraValues = Array.isArray(extraConfig) ? extraConfig.flat(1) as any[] : Object.values(extraConfig); + for (const builder of Object.values(extraValues)) { if (is(builder, IndexBuilder)) { indexes.push(builder.build(table)); } else if (is(builder, CheckBuilder)) { diff --git a/drizzle-orm/src/mysql-core/view.ts b/drizzle-orm/src/mysql-core/view.ts index 6054e022c..42d9b3af6 100644 --- a/drizzle-orm/src/mysql-core/view.ts +++ b/drizzle-orm/src/mysql-core/view.ts @@ -7,7 +7,6 @@ import type { ColumnsSelection, SQL } from '~/sql/sql.ts'; import { getTableColumns } from '~/utils.ts'; import type { MySqlColumn, MySqlColumnBuilderBase } from './columns/index.ts'; import { QueryBuilder } from './query-builders/query-builder.ts'; -import type { SelectedFields } from './query-builders/select.types.ts'; import { mysqlTable } from './table.ts'; import { MySqlViewBase } from './view-base.ts'; import { MySqlViewConfig } from './view-common.ts'; @@ -58,7 +57,7 @@ export class ViewBuilderCore extends ViewBuilderCore<{ name: TName }> { static override readonly [entityKind]: string = 'MySqlViewBuilder'; - as( + as( qb: TypedQueryBuilder | ((qb: QueryBuilder) => TypedQueryBuilder), ): MySqlViewWithSelection> { if (typeof qb === 'function') { @@ -160,7 +159,7 @@ export class MySqlView< config: { name: TName; schema: string | undefined; - selectedFields: SelectedFields; + selectedFields: ColumnsSelection; query: SQL | undefined; }; }) { diff --git a/drizzle-orm/src/pg-core/columns/char.ts b/drizzle-orm/src/pg-core/columns/char.ts index 2cc304221..e362e2f42 100644 --- a/drizzle-orm/src/pg-core/columns/char.ts +++ b/drizzle-orm/src/pg-core/columns/char.ts @@ -5,22 +5,30 @@ import type { AnyPgTable } from '~/pg-core/table.ts'; import { getColumnNameAndConfig, type Writable } from '~/utils.ts'; import { PgColumn, PgColumnBuilder } from './common.ts'; -export type PgCharBuilderInitial = PgCharBuilder<{ +export type PgCharBuilderInitial< + TName extends string, + TEnum extends [string, ...string[]], + TLength extends number | undefined, +> = PgCharBuilder<{ name: TName; dataType: 'string'; columnType: 'PgChar'; data: TEnum[number]; enumValues: TEnum; driverParam: string; + length: TLength; }>; -export class PgCharBuilder> extends PgColumnBuilder< - T, - { length: number | undefined; enumValues: T['enumValues'] } -> { +export class PgCharBuilder & { length?: number | undefined }> + extends PgColumnBuilder< + T, + { length: T['length']; enumValues: T['enumValues'] }, + { length: T['length'] } + > +{ static override readonly [entityKind]: string = 'PgCharBuilder'; - constructor(name: T['name'], config: PgCharConfig) { + constructor(name: T['name'], config: PgCharConfig) { super(name, 'string', 'PgChar'); this.config.length = config.length; this.config.enumValues = config.enum; @@ -29,13 +37,16 @@ export class PgCharBuilder /** @internal */ override build( table: AnyPgTable<{ name: TTableName }>, - ): PgChar> { - return new PgChar>(table, this.config as ColumnBuilderRuntimeConfig); + ): PgChar & { length: T['length'] }> { + return new PgChar & { length: T['length'] }>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); } } -export class PgChar> - extends PgColumn +export class PgChar & { length?: number | undefined }> + extends PgColumn { static override readonly [entityKind]: string = 'PgChar'; @@ -49,19 +60,25 @@ export class PgChar> export interface PgCharConfig< TEnum extends readonly string[] | string[] | undefined = readonly string[] | string[] | undefined, + TLength extends number | undefined = number | undefined, > { - length?: number; enum?: TEnum; + length?: TLength; } -export function char(): PgCharBuilderInitial<'', [string, ...string[]]>; -export function char>( - config?: PgCharConfig>, -): PgCharBuilderInitial<'', Writable>; -export function char>( +export function char(): PgCharBuilderInitial<'', [string, ...string[]], undefined>; +export function char, L extends number | undefined>( + config?: PgCharConfig, L>, +): PgCharBuilderInitial<'', Writable, L>; +export function char< + TName extends string, + U extends string, + T extends Readonly<[U, ...U[]]>, + L extends number | undefined, +>( name: TName, - config?: PgCharConfig>, -): PgCharBuilderInitial>; + config?: PgCharConfig, L>, +): PgCharBuilderInitial, L>; export function char(a?: string | PgCharConfig, b: PgCharConfig = {}): any { const { name, config } = getColumnNameAndConfig(a, b); return new PgCharBuilder(name, config as any); diff --git a/drizzle-orm/src/pg-core/columns/common.ts b/drizzle-orm/src/pg-core/columns/common.ts index d9384b344..aa1d6c1f2 100644 --- a/drizzle-orm/src/pg-core/columns/common.ts +++ b/drizzle-orm/src/pg-core/columns/common.ts @@ -11,7 +11,7 @@ import { ColumnBuilder } from '~/column-builder.ts'; import type { ColumnBaseConfig } from '~/column.ts'; import { Column } from '~/column.ts'; import { entityKind, is } from '~/entity.ts'; -import type { Update } from '~/utils.ts'; +import type { Simplify, Update } from '~/utils.ts'; import type { ForeignKey, UpdateDeleteAction } from '~/pg-core/foreign-keys.ts'; import { ForeignKeyBuilder } from '~/pg-core/foreign-keys.ts'; @@ -47,7 +47,7 @@ export abstract class PgColumnBuilder< static override readonly [entityKind]: string = 'PgColumnBuilder'; - array(size?: number): PgArrayBuilder< + array(size?: TSize): PgArrayBuilder< & { name: T['name']; dataType: 'array'; @@ -55,12 +55,14 @@ export abstract class PgColumnBuilder< data: T['data'][]; driverParam: T['driverParam'][] | string; enumValues: T['enumValues']; + size: TSize; + baseBuilder: T; } & (T extends { notNull: true } ? { notNull: true } : {}) & (T extends { hasDefault: true } ? { hasDefault: true } : {}), T > { - return new PgArrayBuilder(this.config.name, this as PgColumnBuilder, size); + return new PgArrayBuilder(this.config.name, this as PgColumnBuilder, size as any); } references( @@ -250,17 +252,33 @@ export type AnyPgColumn, TPartial>> >; +export type PgArrayColumnBuilderBaseConfig = ColumnBuilderBaseConfig<'array', 'PgArray'> & { + size: number | undefined; + baseBuilder: ColumnBuilderBaseConfig; +}; + export class PgArrayBuilder< - T extends ColumnBuilderBaseConfig<'array', 'PgArray'>, - TBase extends ColumnBuilderBaseConfig, + T extends PgArrayColumnBuilderBaseConfig, + TBase extends ColumnBuilderBaseConfig | PgArrayColumnBuilderBaseConfig, > extends PgColumnBuilder< T, { - baseBuilder: PgColumnBuilder; - size: number | undefined; + baseBuilder: TBase extends PgArrayColumnBuilderBaseConfig ? PgArrayBuilder< + TBase, + TBase extends { baseBuilder: infer TBaseBuilder extends ColumnBuilderBaseConfig } ? TBaseBuilder + : never + > + : PgColumnBuilder>>>; + size: T['size']; }, { - baseBuilder: PgColumnBuilder; + baseBuilder: TBase extends PgArrayColumnBuilderBaseConfig ? PgArrayBuilder< + TBase, + TBase extends { baseBuilder: infer TBaseBuilder extends ColumnBuilderBaseConfig } ? TBaseBuilder + : never + > + : PgColumnBuilder>>>; + size: T['size']; } > { static override readonly [entityKind] = 'PgArrayBuilder'; @@ -268,7 +286,7 @@ export class PgArrayBuilder< constructor( name: string, baseBuilder: PgArrayBuilder['config']['baseBuilder'], - size: number | undefined, + size: T['size'], ) { super(name, 'array', 'PgArray'); this.config.baseBuilder = baseBuilder; @@ -278,9 +296,9 @@ export class PgArrayBuilder< /** @internal */ override build( table: AnyPgTable<{ name: TTableName }>, - ): PgArray, TBase> { + ): PgArray & { size: T['size']; baseBuilder: T['baseBuilder'] }, TBase> { const baseColumn = this.config.baseBuilder.build(table); - return new PgArray, TBase>( + return new PgArray & { size: T['size']; baseBuilder: T['baseBuilder'] }, TBase>( table as AnyPgTable<{ name: MakeColumnConfig['tableName'] }>, this.config as ColumnBuilderRuntimeConfig, baseColumn, @@ -289,10 +307,13 @@ export class PgArrayBuilder< } export class PgArray< - T extends ColumnBaseConfig<'array', 'PgArray'>, + T extends ColumnBaseConfig<'array', 'PgArray'> & { + size: number | undefined; + baseBuilder: ColumnBuilderBaseConfig; + }, TBase extends ColumnBuilderBaseConfig, -> extends PgColumn { - readonly size: number | undefined; +> extends PgColumn { + readonly size: T['size']; static override readonly [entityKind]: string = 'PgArray'; diff --git a/drizzle-orm/src/pg-core/columns/varchar.ts b/drizzle-orm/src/pg-core/columns/varchar.ts index 192262bef..6c2042ae4 100644 --- a/drizzle-orm/src/pg-core/columns/varchar.ts +++ b/drizzle-orm/src/pg-core/columns/varchar.ts @@ -5,22 +5,30 @@ import type { AnyPgTable } from '~/pg-core/table.ts'; import { getColumnNameAndConfig, type Writable } from '~/utils.ts'; import { PgColumn, PgColumnBuilder } from './common.ts'; -export type PgVarcharBuilderInitial = PgVarcharBuilder<{ +export type PgVarcharBuilderInitial< + TName extends string, + TEnum extends [string, ...string[]], + TLength extends number | undefined, +> = PgVarcharBuilder<{ name: TName; dataType: 'string'; columnType: 'PgVarchar'; data: TEnum[number]; driverParam: string; enumValues: TEnum; + length: TLength; }>; -export class PgVarcharBuilder> extends PgColumnBuilder< +export class PgVarcharBuilder< + T extends ColumnBuilderBaseConfig<'string', 'PgVarchar'> & { length?: number | undefined }, +> extends PgColumnBuilder< T, - { length: number | undefined; enumValues: T['enumValues'] } + { length: T['length']; enumValues: T['enumValues'] }, + { length: T['length'] } > { static override readonly [entityKind]: string = 'PgVarcharBuilder'; - constructor(name: T['name'], config: PgVarcharConfig) { + constructor(name: T['name'], config: PgVarcharConfig) { super(name, 'string', 'PgVarchar'); this.config.length = config.length; this.config.enumValues = config.enum; @@ -29,13 +37,16 @@ export class PgVarcharBuilder( table: AnyPgTable<{ name: TTableName }>, - ): PgVarchar> { - return new PgVarchar>(table, this.config as ColumnBuilderRuntimeConfig); + ): PgVarchar & { length: T['length'] }> { + return new PgVarchar & { length: T['length'] }>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); } } -export class PgVarchar> - extends PgColumn +export class PgVarchar & { length?: number | undefined }> + extends PgColumn { static override readonly [entityKind]: string = 'PgVarchar'; @@ -49,19 +60,29 @@ export class PgVarchar> export interface PgVarcharConfig< TEnum extends readonly string[] | string[] | undefined = readonly string[] | string[] | undefined, + TLength extends number | undefined = number | undefined, > { - length?: number; enum?: TEnum; + length?: TLength; } -export function varchar(): PgVarcharBuilderInitial<'', [string, ...string[]]>; -export function varchar>( - config?: PgVarcharConfig>, -): PgVarcharBuilderInitial<'', Writable>; -export function varchar>( +export function varchar(): PgVarcharBuilderInitial<'', [string, ...string[]], undefined>; +export function varchar< + U extends string, + T extends Readonly<[U, ...U[]]>, + L extends number | undefined, +>( + config?: PgVarcharConfig, L>, +): PgVarcharBuilderInitial<'', Writable, L>; +export function varchar< + TName extends string, + U extends string, + T extends Readonly<[U, ...U[]]>, + L extends number | undefined, +>( name: TName, - config?: PgVarcharConfig>, -): PgVarcharBuilderInitial>; + config?: PgVarcharConfig, L>, +): PgVarcharBuilderInitial, L>; export function varchar(a?: string | PgVarcharConfig, b: PgVarcharConfig = {}): any { const { name, config } = getColumnNameAndConfig(a, b); return new PgVarcharBuilder(name, config as any); diff --git a/drizzle-orm/src/pg-core/columns/vector_extension/bit.ts b/drizzle-orm/src/pg-core/columns/vector_extension/bit.ts index f1d692821..a3bdd6bdf 100644 --- a/drizzle-orm/src/pg-core/columns/vector_extension/bit.ts +++ b/drizzle-orm/src/pg-core/columns/vector_extension/bit.ts @@ -5,24 +5,25 @@ import type { AnyPgTable } from '~/pg-core/table.ts'; import { getColumnNameAndConfig } from '~/utils.ts'; import { PgColumn, PgColumnBuilder } from '../common.ts'; -export type PgBinaryVectorBuilderInitial = PgBinaryVectorBuilder<{ +export type PgBinaryVectorBuilderInitial = PgBinaryVectorBuilder<{ name: TName; dataType: 'string'; columnType: 'PgBinaryVector'; data: string; driverParam: string; enumValues: undefined; + dimensions: TDimensions; }>; -export class PgBinaryVectorBuilder> - extends PgColumnBuilder< - T, - { dimensions: number | undefined } - > -{ +export class PgBinaryVectorBuilder< + T extends ColumnBuilderBaseConfig<'string', 'PgBinaryVector'> & { dimensions: number }, +> extends PgColumnBuilder< + T, + { dimensions: T['dimensions'] } +> { static override readonly [entityKind]: string = 'PgBinaryVectorBuilder'; - constructor(name: string, config: PgBinaryVectorConfig) { + constructor(name: string, config: PgBinaryVectorConfig) { super(name, 'string', 'PgBinaryVector'); this.config.dimensions = config.dimensions; } @@ -30,16 +31,16 @@ export class PgBinaryVectorBuilder( table: AnyPgTable<{ name: TTableName }>, - ): PgBinaryVector> { - return new PgBinaryVector>( + ): PgBinaryVector & { dimensions: T['dimensions'] }> { + return new PgBinaryVector & { dimensions: T['dimensions'] }>( table, this.config as ColumnBuilderRuntimeConfig, ); } } -export class PgBinaryVector> - extends PgColumn +export class PgBinaryVector & { dimensions: number }> + extends PgColumn { static override readonly [entityKind]: string = 'PgBinaryVector'; @@ -50,17 +51,17 @@ export class PgBinaryVector { + dimensions: TDimensions; } -export function bit( - config: PgBinaryVectorConfig, -): PgBinaryVectorBuilderInitial<''>; -export function bit( +export function bit( + config: PgBinaryVectorConfig, +): PgBinaryVectorBuilderInitial<'', D>; +export function bit( name: TName, - config: PgBinaryVectorConfig, -): PgBinaryVectorBuilderInitial; + config: PgBinaryVectorConfig, +): PgBinaryVectorBuilderInitial; export function bit(a: string | PgBinaryVectorConfig, b?: PgBinaryVectorConfig) { const { name, config } = getColumnNameAndConfig(a, b); return new PgBinaryVectorBuilder(name, config); diff --git a/drizzle-orm/src/pg-core/columns/vector_extension/halfvec.ts b/drizzle-orm/src/pg-core/columns/vector_extension/halfvec.ts index 1c89e7b60..5f52f5f16 100644 --- a/drizzle-orm/src/pg-core/columns/vector_extension/halfvec.ts +++ b/drizzle-orm/src/pg-core/columns/vector_extension/halfvec.ts @@ -5,22 +5,26 @@ import type { AnyPgTable } from '~/pg-core/table.ts'; import { getColumnNameAndConfig } from '~/utils.ts'; import { PgColumn, PgColumnBuilder } from '../common.ts'; -export type PgHalfVectorBuilderInitial = PgHalfVectorBuilder<{ +export type PgHalfVectorBuilderInitial = PgHalfVectorBuilder<{ name: TName; dataType: 'array'; columnType: 'PgHalfVector'; data: number[]; driverParam: string; enumValues: undefined; + dimensions: TDimensions; }>; -export class PgHalfVectorBuilder> extends PgColumnBuilder< - T, - { dimensions: number | undefined } -> { +export class PgHalfVectorBuilder & { dimensions: number }> + extends PgColumnBuilder< + T, + { dimensions: T['dimensions'] }, + { dimensions: T['dimensions'] } + > +{ static override readonly [entityKind]: string = 'PgHalfVectorBuilder'; - constructor(name: string, config: PgHalfVectorConfig) { + constructor(name: string, config: PgHalfVectorConfig) { super(name, 'array', 'PgHalfVector'); this.config.dimensions = config.dimensions; } @@ -28,20 +32,20 @@ export class PgHalfVectorBuilder( table: AnyPgTable<{ name: TTableName }>, - ): PgHalfVector> { - return new PgHalfVector>( + ): PgHalfVector & { dimensions: T['dimensions'] }> { + return new PgHalfVector & { dimensions: T['dimensions'] }>( table, this.config as ColumnBuilderRuntimeConfig, ); } } -export class PgHalfVector> - extends PgColumn +export class PgHalfVector & { dimensions: number }> + extends PgColumn { static override readonly [entityKind]: string = 'PgHalfVector'; - readonly dimensions = this.config.dimensions; + readonly dimensions: T['dimensions'] = this.config.dimensions; getSQLType(): string { return `halfvec(${this.dimensions})`; @@ -59,17 +63,17 @@ export class PgHalfVector> } } -export interface PgHalfVectorConfig { - dimensions: number; +export interface PgHalfVectorConfig { + dimensions: TDimensions; } -export function halfvec( - config: PgHalfVectorConfig, -): PgHalfVectorBuilderInitial<''>; -export function halfvec( +export function halfvec( + config: PgHalfVectorConfig, +): PgHalfVectorBuilderInitial<'', D>; +export function halfvec( name: TName, config: PgHalfVectorConfig, -): PgHalfVectorBuilderInitial; +): PgHalfVectorBuilderInitial; export function halfvec(a: string | PgHalfVectorConfig, b?: PgHalfVectorConfig) { const { name, config } = getColumnNameAndConfig(a, b); return new PgHalfVectorBuilder(name, config); diff --git a/drizzle-orm/src/pg-core/columns/vector_extension/vector.ts b/drizzle-orm/src/pg-core/columns/vector_extension/vector.ts index 84fb54964..f36a9b1ca 100644 --- a/drizzle-orm/src/pg-core/columns/vector_extension/vector.ts +++ b/drizzle-orm/src/pg-core/columns/vector_extension/vector.ts @@ -5,22 +5,26 @@ import type { AnyPgTable } from '~/pg-core/table.ts'; import { getColumnNameAndConfig } from '~/utils.ts'; import { PgColumn, PgColumnBuilder } from '../common.ts'; -export type PgVectorBuilderInitial = PgVectorBuilder<{ +export type PgVectorBuilderInitial = PgVectorBuilder<{ name: TName; dataType: 'array'; columnType: 'PgVector'; data: number[]; driverParam: string; enumValues: undefined; + dimensions: TDimensions; }>; -export class PgVectorBuilder> extends PgColumnBuilder< - T, - { dimensions: number | undefined } -> { +export class PgVectorBuilder & { dimensions: number }> + extends PgColumnBuilder< + T, + { dimensions: T['dimensions'] }, + { dimensions: T['dimensions'] } + > +{ static override readonly [entityKind]: string = 'PgVectorBuilder'; - constructor(name: string, config: PgVectorConfig) { + constructor(name: string, config: PgVectorConfig) { super(name, 'array', 'PgVector'); this.config.dimensions = config.dimensions; } @@ -28,17 +32,20 @@ export class PgVectorBuilder( table: AnyPgTable<{ name: TTableName }>, - ): PgVector> { - return new PgVector>(table, this.config as ColumnBuilderRuntimeConfig); + ): PgVector & { dimensions: T['dimensions'] }> { + return new PgVector & { dimensions: T['dimensions'] }>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); } } -export class PgVector> - extends PgColumn +export class PgVector & { dimensions: number | undefined }> + extends PgColumn { static override readonly [entityKind]: string = 'PgVector'; - readonly dimensions = this.config.dimensions; + readonly dimensions: T['dimensions'] = this.config.dimensions; getSQLType(): string { return `vector(${this.dimensions})`; @@ -56,17 +63,17 @@ export class PgVector> } } -export interface PgVectorConfig { - dimensions: number; +export interface PgVectorConfig { + dimensions: TDimensions; } -export function vector( - config: PgVectorConfig, -): PgVectorBuilderInitial<''>; -export function vector( +export function vector( + config: PgVectorConfig, +): PgVectorBuilderInitial<'', D>; +export function vector( name: TName, - config: PgVectorConfig, -): PgVectorBuilderInitial; + config: PgVectorConfig, +): PgVectorBuilderInitial; export function vector(a: string | PgVectorConfig, b?: PgVectorConfig) { const { name, config } = getColumnNameAndConfig(a, b); return new PgVectorBuilder(name, config); diff --git a/drizzle-orm/src/pg-core/table.ts b/drizzle-orm/src/pg-core/table.ts index c3c34c577..7f12e634d 100644 --- a/drizzle-orm/src/pg-core/table.ts +++ b/drizzle-orm/src/pg-core/table.ts @@ -135,7 +135,26 @@ export function pgTableWithSchema< export interface PgTableFn { /** - * @deprecated This overload is deprecated. Use the other method overload instead. + * @deprecated The third parameter of pgTable is changing and will only accept an array instead of an object + * + * @example + * Deprecated version: + * ```ts + * export const users = pgTable("users", { + * id: integer(), + * }, (t) => ({ + * idx: index('custom_name').on(t.id) + * })); + * ``` + * + * New API: + * ```ts + * export const users = pgTable("users", { + * id: integer(), + * }, (t) => [ + * index('custom_name').on(t.id) + * ]); + * ``` */ < TTableName extends string, @@ -154,7 +173,26 @@ export interface PgTableFn { }>; /** - * @deprecated This overload is deprecated. Use the other method overload instead. + * @deprecated The third parameter of pgTable is changing and will only accept an array instead of an object + * + * @example + * Deprecated version: + * ```ts + * export const users = pgTable("users", { + * id: integer(), + * }, (t) => ({ + * idx: index('custom_name').on(t.id) + * })); + * ``` + * + * New API: + * ```ts + * export const users = pgTable("users", { + * id: integer(), + * }, (t) => [ + * index('custom_name').on(t.id) + * ]); + * ``` */ < TTableName extends string, diff --git a/drizzle-orm/src/pg-core/view.ts b/drizzle-orm/src/pg-core/view.ts index 2f88d7e17..f8e916a31 100644 --- a/drizzle-orm/src/pg-core/view.ts +++ b/drizzle-orm/src/pg-core/view.ts @@ -8,7 +8,6 @@ import { getTableColumns } from '~/utils.ts'; import type { RequireAtLeastOne } from '~/utils.ts'; import type { PgColumn, PgColumnBuilderBase } from './columns/common.ts'; import { QueryBuilder } from './query-builders/query-builder.ts'; -import type { SelectedFields } from './query-builders/select.types.ts'; import { pgTable } from './table.ts'; import { PgViewBase } from './view-base.ts'; import { PgViewConfig } from './view-common.ts'; @@ -45,7 +44,7 @@ export class DefaultViewBuilderCore extends DefaultViewBuilderCore<{ name: TName }> { static override readonly [entityKind]: string = 'PgViewBuilder'; - as( + as( qb: TypedQueryBuilder | ((qb: QueryBuilder) => TypedQueryBuilder), ): PgViewWithSelection> { if (typeof qb === 'function') { @@ -198,7 +197,7 @@ export class MaterializedViewBuilder { static override readonly [entityKind]: string = 'PgMaterializedViewBuilder'; - as( + as( qb: TypedQueryBuilder | ((qb: QueryBuilder) => TypedQueryBuilder), ): PgMaterializedViewWithSelection> { if (typeof qb === 'function') { @@ -317,7 +316,7 @@ export class PgView< config: { name: TName; schema: string | undefined; - selectedFields: SelectedFields; + selectedFields: ColumnsSelection; query: SQL | undefined; }; }) { @@ -362,7 +361,7 @@ export class PgMaterializedView< config: { name: TName; schema: string | undefined; - selectedFields: SelectedFields; + selectedFields: ColumnsSelection; query: SQL | undefined; }; }) { diff --git a/drizzle-orm/src/postgres-js/driver.ts b/drizzle-orm/src/postgres-js/driver.ts index b6043f187..69d5a126d 100644 --- a/drizzle-orm/src/postgres-js/driver.ts +++ b/drizzle-orm/src/postgres-js/driver.ts @@ -119,6 +119,11 @@ export namespace drizzle { ): PostgresJsDatabase & { $client: '$client is not available on drizzle.mock()'; } { - return construct({} as any, config) as any; + return construct({ + options: { + parsers: {}, + serializers: {}, + }, + } as any, config) as any; } } diff --git a/drizzle-orm/src/query-builders/select.types.ts b/drizzle-orm/src/query-builders/select.types.ts index 07579662f..e7975af65 100644 --- a/drizzle-orm/src/query-builders/select.types.ts +++ b/drizzle-orm/src/query-builders/select.types.ts @@ -93,6 +93,7 @@ export type AddAliasToSelection< : { [Key in keyof TSelection]: TSelection[Key] extends Column ? ChangeColumnTableName + : TSelection[Key] extends Table ? AddAliasToSelection : TSelection[Key] extends SQL | SQL.Aliased ? TSelection[Key] : TSelection[Key] extends ColumnsSelection ? MapColumnsToTableAlias : never; @@ -120,6 +121,7 @@ export type BuildSubquerySelection< [Key in keyof TSelection]: TSelection[Key] extends SQL ? DrizzleTypeError<'You cannot reference this field without assigning it an alias first - use `.as()`'> : TSelection[Key] extends SQL.Aliased ? TSelection[Key] + : TSelection[Key] extends Table ? BuildSubquerySelection : TSelection[Key] extends Column ? ApplyNullabilityToColumn : TSelection[Key] extends ColumnsSelection ? BuildSubquerySelection diff --git a/drizzle-orm/src/singlestore-core/columns/bigint.ts b/drizzle-orm/src/singlestore-core/columns/bigint.ts index 1e6b64c49..fed436993 100644 --- a/drizzle-orm/src/singlestore-core/columns/bigint.ts +++ b/drizzle-orm/src/singlestore-core/columns/bigint.ts @@ -12,7 +12,6 @@ export type SingleStoreBigInt53BuilderInitial = SingleStor data: number; driverParam: number | string; enumValues: undefined; - generated: undefined; }>; export class SingleStoreBigInt53Builder> diff --git a/drizzle-orm/src/singlestore-core/columns/common.ts b/drizzle-orm/src/singlestore-core/columns/common.ts index c0dc7fb67..7f2a521e5 100644 --- a/drizzle-orm/src/singlestore-core/columns/common.ts +++ b/drizzle-orm/src/singlestore-core/columns/common.ts @@ -64,8 +64,9 @@ export abstract class SingleStoreColumnBuilder< // To understand how to use `SingleStoreColumn` and `AnySingleStoreColumn`, see `Column` and `AnyColumn` documentation. export abstract class SingleStoreColumn< T extends ColumnBaseConfig = ColumnBaseConfig, - TRuntimeConfig extends object = object, -> extends Column { + TRuntimeConfig extends object = {}, + TTypeConfig extends object = {}, +> extends Column { static override readonly [entityKind]: string = 'SingleStoreColumn'; constructor( diff --git a/drizzle-orm/src/singlestore-core/table.ts b/drizzle-orm/src/singlestore-core/table.ts index 4cc8973ee..ffad22d74 100644 --- a/drizzle-orm/src/singlestore-core/table.ts +++ b/drizzle-orm/src/singlestore-core/table.ts @@ -7,11 +7,14 @@ import type { AnyIndexBuilder } from './indexes.ts'; import type { PrimaryKeyBuilder } from './primary-keys.ts'; import type { UniqueConstraintBuilder } from './unique-constraint.ts'; -export type SingleStoreTableExtraConfig = Record< - string, +export type SingleStoreTableExtraConfigValue = | AnyIndexBuilder | PrimaryKeyBuilder - | UniqueConstraintBuilder + | UniqueConstraintBuilder; + +export type SingleStoreTableExtraConfig = Record< + string, + SingleStoreTableExtraConfigValue >; export type TableConfig = TableConfigBase; @@ -51,7 +54,9 @@ export function singlestoreTableWithSchema< name: TTableName, columns: TColumnsMap | ((columnTypes: SingleStoreColumnBuilders) => TColumnsMap), extraConfig: - | ((self: BuildColumns) => SingleStoreTableExtraConfig) + | (( + self: BuildColumns, + ) => SingleStoreTableExtraConfig | SingleStoreTableExtraConfigValue[]) | undefined, schema: TSchemaName, baseName = name, @@ -98,6 +103,28 @@ export function singlestoreTableWithSchema< } export interface SingleStoreTableFn { + /** + * @deprecated The third parameter of singlestoreTable is changing and will only accept an array instead of an object + * + * @example + * Deprecated version: + * ```ts + * export const users = singlestoreTable("users", { + * id: int(), + * }, (t) => ({ + * idx: index('custom_name').on(t.id) + * })); + * ``` + * + * New API: + * ```ts + * export const users = singlestoreTable("users", { + * id: int(), + * }, (t) => [ + * index('custom_name').on(t.id) + * ]); + * ``` + */ < TTableName extends string, TColumnsMap extends Record, @@ -112,6 +139,28 @@ export interface SingleStoreTableFn; + /** + * @deprecated The third parameter of singlestoreTable is changing and will only accept an array instead of an object + * + * @example + * Deprecated version: + * ```ts + * export const users = singlestoreTable("users", { + * id: int(), + * }, (t) => ({ + * idx: index('custom_name').on(t.id) + * })); + * ``` + * + * New API: + * ```ts + * export const users = singlestoreTable("users", { + * id: int(), + * }, (t) => [ + * index('custom_name').on(t.id) + * ]); + * ``` + */ < TTableName extends string, TColumnsMap extends Record, @@ -125,6 +174,36 @@ export interface SingleStoreTableFn; dialect: 'singlestore'; }>; + + < + TTableName extends string, + TColumnsMap extends Record, + >( + name: TTableName, + columns: TColumnsMap, + extraConfig?: ( + self: BuildColumns, + ) => SingleStoreTableExtraConfigValue[], + ): SingleStoreTableWithColumns<{ + name: TTableName; + schema: TSchemaName; + columns: BuildColumns; + dialect: 'singlestore'; + }>; + + < + TTableName extends string, + TColumnsMap extends Record, + >( + name: TTableName, + columns: (columnTypes: SingleStoreColumnBuilders) => TColumnsMap, + extraConfig?: (self: BuildColumns) => SingleStoreTableExtraConfigValue[], + ): SingleStoreTableWithColumns<{ + name: TTableName; + schema: TSchemaName; + columns: BuildColumns; + dialect: 'singlestore'; + }>; } export const singlestoreTable: SingleStoreTableFn = (name, columns, extraConfig) => { diff --git a/drizzle-orm/src/singlestore-core/utils.ts b/drizzle-orm/src/singlestore-core/utils.ts index 634b4e261..9ec8b7b0d 100644 --- a/drizzle-orm/src/singlestore-core/utils.ts +++ b/drizzle-orm/src/singlestore-core/utils.ts @@ -22,7 +22,8 @@ export function getTableConfig(table: SingleStoreTable) { if (extraConfigBuilder !== undefined) { const extraConfig = extraConfigBuilder(table[SingleStoreTable.Symbol.Columns]); - for (const builder of Object.values(extraConfig)) { + const extraValues = Array.isArray(extraConfig) ? extraConfig.flat(1) as any[] : Object.values(extraConfig); + for (const builder of Object.values(extraValues)) { if (is(builder, IndexBuilder)) { indexes.push(builder.build(table)); } else if (is(builder, UniqueConstraintBuilder)) { diff --git a/drizzle-orm/src/sql/sql.ts b/drizzle-orm/src/sql/sql.ts index 234370a85..ba7586fe8 100644 --- a/drizzle-orm/src/sql/sql.ts +++ b/drizzle-orm/src/sql/sql.ts @@ -1,9 +1,10 @@ import type { CasingCache } from '~/casing.ts'; import { entityKind, is } from '~/entity.ts'; -import type { SelectedFields } from '~/operations.ts'; import { isPgEnum } from '~/pg-core/columns/enum.ts'; +import type { SelectResult } from '~/query-builders/select.types.ts'; import { Subquery } from '~/subquery.ts'; import { tracer } from '~/tracing.ts'; +import type { Assume, Equal } from '~/utils.ts'; import { ViewBaseConfig } from '~/view-common.ts'; import type { AnyColumn } from '../column.ts'; import { Column } from '../column.ts'; @@ -617,6 +618,8 @@ export function fillPlaceholders(params: unknown[], values: Record; +const IsDrizzleView = Symbol.for('drizzle:IsDrizzleView'); + export abstract class View< TName extends string = string, TExisting extends boolean = boolean, @@ -637,17 +640,22 @@ export abstract class View< name: TName; originalName: TName; schema: string | undefined; - selectedFields: SelectedFields; + selectedFields: ColumnsSelection; isExisting: TExisting; query: TExisting extends true ? undefined : SQL; isAlias: boolean; }; + /** @internal */ + [IsDrizzleView] = true; + + declare readonly $inferSelect: InferSelectViewModel, TExisting, TSelection>>; + constructor( { name, schema, selectedFields, query }: { name: TName; schema: string | undefined; - selectedFields: SelectedFields; + selectedFields: ColumnsSelection; query: SQL | undefined; }, ) { @@ -667,6 +675,18 @@ export abstract class View< } } +export function isView(view: unknown): view is View { + return typeof view === 'object' && view !== null && IsDrizzleView in view; +} + +export type InferSelectViewModel = + Equal extends true ? { [x: string]: unknown } + : SelectResult< + TView['_']['selectedFields'], + 'single', + Record + >; + // Defined separately from the Column class to resolve circular dependency Column.prototype.getSQL = function() { return new SQL([this]); diff --git a/drizzle-orm/src/sqlite-core/columns/common.ts b/drizzle-orm/src/sqlite-core/columns/common.ts index 953e5692d..8eacb8c93 100644 --- a/drizzle-orm/src/sqlite-core/columns/common.ts +++ b/drizzle-orm/src/sqlite-core/columns/common.ts @@ -102,8 +102,9 @@ export abstract class SQLiteColumnBuilder< // To understand how to use `SQLiteColumn` and `AnySQLiteColumn`, see `Column` and `AnyColumn` documentation. export abstract class SQLiteColumn< T extends ColumnBaseConfig = ColumnBaseConfig, - TRuntimeConfig extends object = object, -> extends Column { + TRuntimeConfig extends object = {}, + TTypeConfig extends object = {}, +> extends Column { static override readonly [entityKind]: string = 'SQLiteColumn'; constructor( diff --git a/drizzle-orm/src/sqlite-core/columns/text.ts b/drizzle-orm/src/sqlite-core/columns/text.ts index 241eb860d..0696b76f8 100644 --- a/drizzle-orm/src/sqlite-core/columns/text.ts +++ b/drizzle-orm/src/sqlite-core/columns/text.ts @@ -5,22 +5,30 @@ import type { AnySQLiteTable } from '~/sqlite-core/table.ts'; import { type Equal, getColumnNameAndConfig, type Writable } from '~/utils.ts'; import { SQLiteColumn, SQLiteColumnBuilder } from './common.ts'; -export type SQLiteTextBuilderInitial = SQLiteTextBuilder<{ +export type SQLiteTextBuilderInitial< + TName extends string, + TEnum extends [string, ...string[]], + TLength extends number | undefined, +> = SQLiteTextBuilder<{ name: TName; dataType: 'string'; columnType: 'SQLiteText'; data: TEnum[number]; driverParam: string; enumValues: TEnum; + length: TLength; }>; -export class SQLiteTextBuilder> extends SQLiteColumnBuilder< +export class SQLiteTextBuilder< + T extends ColumnBuilderBaseConfig<'string', 'SQLiteText'> & { length?: number | undefined }, +> extends SQLiteColumnBuilder< T, - { length: number | undefined; enumValues: T['enumValues'] } + { length: T['length']; enumValues: T['enumValues'] }, + { length: T['length'] } > { static override readonly [entityKind]: string = 'SQLiteTextBuilder'; - constructor(name: T['name'], config: SQLiteTextConfig<'text', T['enumValues']>) { + constructor(name: T['name'], config: SQLiteTextConfig<'text', T['enumValues'], T['length']>) { super(name, 'string', 'SQLiteText'); this.config.enumValues = config.enum; this.config.length = config.length; @@ -29,19 +37,22 @@ export class SQLiteTextBuilder( table: AnySQLiteTable<{ name: TTableName }>, - ): SQLiteText> { - return new SQLiteText>(table, this.config as ColumnBuilderRuntimeConfig); + ): SQLiteText & { length: T['length'] }> { + return new SQLiteText & { length: T['length'] }>( + table, + this.config as ColumnBuilderRuntimeConfig, + ); } } -export class SQLiteText> - extends SQLiteColumn +export class SQLiteText & { length?: number | undefined }> + extends SQLiteColumn { static override readonly [entityKind]: string = 'SQLiteText'; override readonly enumValues = this.config.enumValues; - readonly length: number | undefined = this.config.length; + readonly length: T['length'] = this.config.length; constructor( table: AnySQLiteTable<{ name: T['tableName'] }>, @@ -106,34 +117,37 @@ export class SQLiteTextJson export type SQLiteTextConfig< TMode extends 'text' | 'json' = 'text' | 'json', TEnum extends readonly string[] | string[] | undefined = readonly string[] | string[] | undefined, + TLength extends number | undefined = number | undefined, > = TMode extends 'text' ? { mode?: TMode; - length?: number; + length?: TLength; enum?: TEnum; } : { mode?: TMode; }; -export function text(): SQLiteTextBuilderInitial<'', [string, ...string[]]>; +export function text(): SQLiteTextBuilderInitial<'', [string, ...string[]], undefined>; export function text< U extends string, T extends Readonly<[U, ...U[]]>, + L extends number | undefined, TMode extends 'text' | 'json' = 'text' | 'json', >( - config?: SQLiteTextConfig>, + config?: SQLiteTextConfig, L>, ): Equal extends true ? SQLiteTextJsonBuilderInitial<''> - : SQLiteTextBuilderInitial<'', Writable>; + : SQLiteTextBuilderInitial<'', Writable, L>; export function text< TName extends string, U extends string, T extends Readonly<[U, ...U[]]>, + L extends number | undefined, TMode extends 'text' | 'json' = 'text' | 'json', >( name: TName, - config?: SQLiteTextConfig>, + config?: SQLiteTextConfig, L>, ): Equal extends true ? SQLiteTextJsonBuilderInitial - : SQLiteTextBuilderInitial>; + : SQLiteTextBuilderInitial, L>; export function text(a?: string | SQLiteTextConfig, b: SQLiteTextConfig = {}): any { const { name, config } = getColumnNameAndConfig(a, b); if (config.mode === 'json') { diff --git a/drizzle-orm/src/sqlite-core/table.ts b/drizzle-orm/src/sqlite-core/table.ts index d7c5a060b..5a68f9cb1 100644 --- a/drizzle-orm/src/sqlite-core/table.ts +++ b/drizzle-orm/src/sqlite-core/table.ts @@ -9,13 +9,16 @@ import type { IndexBuilder } from './indexes.ts'; import type { PrimaryKeyBuilder } from './primary-keys.ts'; import type { UniqueConstraintBuilder } from './unique-constraint.ts'; -export type SQLiteTableExtraConfig = Record< - string, +export type SQLiteTableExtraConfigValue = | IndexBuilder | CheckBuilder | ForeignKeyBuilder | PrimaryKeyBuilder - | UniqueConstraintBuilder + | UniqueConstraintBuilder; + +export type SQLiteTableExtraConfig = Record< + string, + SQLiteTableExtraConfigValue >; export type TableConfig = TableConfigBase>; @@ -54,6 +57,28 @@ export type SQLiteTableWithColumns = }; export interface SQLiteTableFn { + /** + * @deprecated The third parameter of sqliteTable is changing and will only accept an array instead of an object + * + * @example + * Deprecated version: + * ```ts + * export const users = sqliteTable("users", { + * id: int(), + * }, (t) => ({ + * idx: index('custom_name').on(t.id) + * })); + * ``` + * + * New API: + * ```ts + * export const users = sqliteTable("users", { + * id: int(), + * }, (t) => [ + * index('custom_name').on(t.id) + * ]); + * ``` + */ < TTableName extends string, TColumnsMap extends Record, @@ -68,6 +93,28 @@ export interface SQLiteTableFn { dialect: 'sqlite'; }>; + /** + * @deprecated The third parameter of sqliteTable is changing and will only accept an array instead of an object + * + * @example + * Deprecated version: + * ```ts + * export const users = sqliteTable("users", { + * id: int(), + * }, (t) => ({ + * idx: index('custom_name').on(t.id) + * })); + * ``` + * + * New API: + * ```ts + * export const users = sqliteTable("users", { + * id: int(), + * }, (t) => [ + * index('custom_name').on(t.id) + * ]); + * ``` + */ < TTableName extends string, TColumnsMap extends Record, @@ -81,6 +128,36 @@ export interface SQLiteTableFn { columns: BuildColumns; dialect: 'sqlite'; }>; + + < + TTableName extends string, + TColumnsMap extends Record, + >( + name: TTableName, + columns: TColumnsMap, + extraConfig?: ( + self: BuildColumns, + ) => SQLiteTableExtraConfigValue[], + ): SQLiteTableWithColumns<{ + name: TTableName; + schema: TSchema; + columns: BuildColumns; + dialect: 'sqlite'; + }>; + + < + TTableName extends string, + TColumnsMap extends Record, + >( + name: TTableName, + columns: (columnTypes: SQLiteColumnBuilders) => TColumnsMap, + extraConfig?: (self: BuildColumns) => SQLiteTableExtraConfigValue[], + ): SQLiteTableWithColumns<{ + name: TTableName; + schema: TSchema; + columns: BuildColumns; + dialect: 'sqlite'; + }>; } function sqliteTableBase< @@ -90,7 +167,11 @@ function sqliteTableBase< >( name: TTableName, columns: TColumnsMap | ((columnTypes: SQLiteColumnBuilders) => TColumnsMap), - extraConfig?: (self: BuildColumns) => SQLiteTableExtraConfig, + extraConfig: + | (( + self: BuildColumns, + ) => SQLiteTableExtraConfig | SQLiteTableExtraConfigValue[]) + | undefined, schema?: TSchema, baseName = name, ): SQLiteTableWithColumns<{ diff --git a/drizzle-orm/src/sqlite-core/utils.ts b/drizzle-orm/src/sqlite-core/utils.ts index 33ae2c248..7d21483b0 100644 --- a/drizzle-orm/src/sqlite-core/utils.ts +++ b/drizzle-orm/src/sqlite-core/utils.ts @@ -26,7 +26,8 @@ export function getTableConfig(table: TTable) { if (extraConfigBuilder !== undefined) { const extraConfig = extraConfigBuilder(table[SQLiteTable.Symbol.Columns]); - for (const builder of Object.values(extraConfig)) { + const extraValues = Array.isArray(extraConfig) ? extraConfig.flat(1) as any[] : Object.values(extraConfig); + for (const builder of Object.values(extraValues)) { if (is(builder, IndexBuilder)) { indexes.push(builder.build(table)); } else if (is(builder, CheckBuilder)) { diff --git a/drizzle-orm/src/sqlite-core/view.ts b/drizzle-orm/src/sqlite-core/view.ts index 03ef08025..1caf211ce 100644 --- a/drizzle-orm/src/sqlite-core/view.ts +++ b/drizzle-orm/src/sqlite-core/view.ts @@ -7,7 +7,6 @@ import type { ColumnsSelection, SQL } from '~/sql/sql.ts'; import { getTableColumns } from '~/utils.ts'; import type { SQLiteColumn, SQLiteColumnBuilderBase } from './columns/common.ts'; import { QueryBuilder } from './query-builders/query-builder.ts'; -import type { SelectedFields } from './query-builders/select.types.ts'; import { sqliteTable } from './table.ts'; import { SQLiteViewBase } from './view-base.ts'; @@ -38,7 +37,7 @@ export class ViewBuilderCore< export class ViewBuilder extends ViewBuilderCore<{ name: TName }> { static override readonly [entityKind]: string = 'SQLiteViewBuilder'; - as( + as( qb: TypedQueryBuilder | ((qb: QueryBuilder) => TypedQueryBuilder), ): SQLiteViewWithSelection> { if (typeof qb === 'function') { @@ -135,7 +134,7 @@ export class SQLiteView< config: { name: TName; schema: string | undefined; - selectedFields: SelectedFields; + selectedFields: ColumnsSelection; query: SQL | undefined; }; }) { diff --git a/drizzle-orm/src/utils.ts b/drizzle-orm/src/utils.ts index 58d0c3d91..51d30e97c 100644 --- a/drizzle-orm/src/utils.ts +++ b/drizzle-orm/src/utils.ts @@ -188,6 +188,10 @@ export function getTableColumns(table: T): T['_']['columns'] { return table[Table.Symbol.Columns]; } +export function getViewSelectedFields(view: T): T['_']['selectedFields'] { + return view[ViewBaseConfig].selectedFields; +} + /** @internal */ export function getTableLikeName(table: TableLike): string | undefined { return is(table, Subquery) diff --git a/drizzle-orm/type-tests/mysql/select.ts b/drizzle-orm/type-tests/mysql/select.ts index 0a6af743b..bad55aa59 100644 --- a/drizzle-orm/type-tests/mysql/select.ts +++ b/drizzle-orm/type-tests/mysql/select.ts @@ -22,11 +22,19 @@ import { or, } from '~/expressions.ts'; import { alias } from '~/mysql-core/alias.ts'; -import { param, sql } from '~/sql/sql.ts'; +import { type InferSelectViewModel, param, sql } from '~/sql/sql.ts'; import type { Equal } from 'type-tests/utils.ts'; import { Expect } from 'type-tests/utils.ts'; -import { type MySqlSelect, type MySqlSelectQueryBuilder, QueryBuilder } from '~/mysql-core/index.ts'; +import { + int, + type MySqlSelect, + type MySqlSelectQueryBuilder, + mysqlTable, + mysqlView, + QueryBuilder, + text, +} from '~/mysql-core/index.ts'; import { db } from './db.ts'; import { cities, classes, newYorkers, users } from './tables.ts'; @@ -604,3 +612,40 @@ await db // @ts-expect-error method was already called .for('update'); } + +{ + const table1 = mysqlTable('table1', { + id: int().primaryKey(), + name: text().notNull(), + }); + const table2 = mysqlTable('table2', { + id: int().primaryKey(), + age: int().notNull(), + }); + const table3 = mysqlTable('table3', { + id: int().primaryKey(), + phone: text().notNull(), + }); + const view = mysqlView('view').as((qb) => + qb.select({ + table: table1, + column: table2.age, + nested: { + column: table3.phone, + }, + }).from(table1).innerJoin(table2, sql``).leftJoin(table3, sql``) + ); + const result = await db.select().from(view); + + Expect< + Equal + >; + Expect>; + Expect[]>>; +} diff --git a/drizzle-orm/type-tests/mysql/tables.ts b/drizzle-orm/type-tests/mysql/tables.ts index 473976d1a..24ce2582b 100644 --- a/drizzle-orm/type-tests/mysql/tables.ts +++ b/drizzle-orm/type-tests/mysql/tables.ts @@ -91,57 +91,69 @@ export const cities = mysqlTable('cities_table', { Expect< Equal< { - id: MySqlColumn<{ - name: 'id'; - tableName: 'cities_table'; - dataType: 'number'; - columnType: 'MySqlSerial'; - data: number; - driverParam: number; - notNull: true; - hasDefault: true; - isPrimaryKey: true; - enumValues: undefined; - baseColumn: never; - generated: undefined; - identity: undefined; - isAutoincrement: true; - hasRuntimeDefault: false; - }, object>; - name: MySqlColumn<{ - name: 'name_db'; - tableName: 'cities_table'; - dataType: 'string'; - columnType: 'MySqlText'; - data: string; - driverParam: string; - notNull: true; - hasDefault: false; - isPrimaryKey: false; - enumValues: [string, ...string[]]; - baseColumn: never; - generated: undefined; - identity: undefined; - isAutoincrement: false; - hasRuntimeDefault: false; - }, object>; - population: MySqlColumn<{ - name: 'population'; - tableName: 'cities_table'; - dataType: 'number'; - columnType: 'MySqlInt'; - data: number; - driverParam: string | number; - notNull: false; - hasDefault: true; - isPrimaryKey: false; - enumValues: undefined; - baseColumn: never; - generated: undefined; - identity: undefined; - isAutoincrement: false; - hasRuntimeDefault: false; - }, object>; + id: MySqlColumn< + { + name: 'id'; + tableName: 'cities_table'; + dataType: 'number'; + columnType: 'MySqlSerial'; + data: number; + driverParam: number; + notNull: true; + hasDefault: true; + isPrimaryKey: true; + enumValues: undefined; + baseColumn: never; + generated: undefined; + identity: undefined; + isAutoincrement: true; + hasRuntimeDefault: false; + }, + {}, + {} + >; + name: MySqlColumn< + { + name: 'name_db'; + tableName: 'cities_table'; + dataType: 'string'; + columnType: 'MySqlText'; + data: string; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + enumValues: [string, ...string[]]; + baseColumn: never; + generated: undefined; + identity: undefined; + isAutoincrement: false; + hasRuntimeDefault: false; + }, + {}, + {} + >; + population: MySqlColumn< + { + name: 'population'; + tableName: 'cities_table'; + dataType: 'number'; + columnType: 'MySqlInt'; + data: number; + driverParam: string | number; + notNull: false; + hasDefault: true; + isPrimaryKey: false; + enumValues: undefined; + baseColumn: never; + generated: undefined; + identity: undefined; + isAutoincrement: false; + hasRuntimeDefault: false; + }, + {}, + {} + >; }, typeof cities._.columns > diff --git a/drizzle-orm/type-tests/pg/array.ts b/drizzle-orm/type-tests/pg/array.ts index 586acb1c7..d7a34cb2b 100644 --- a/drizzle-orm/type-tests/pg/array.ts +++ b/drizzle-orm/type-tests/pg/array.ts @@ -25,7 +25,9 @@ import { integer, pgTable } from '~/pg-core/index.ts'; isPrimaryKey: false; isAutoincrement: false; hasRuntimeDefault: false; - } + }, + {}, + {} >, typeof table['a']['_']['baseColumn'] > diff --git a/drizzle-orm/type-tests/pg/select.ts b/drizzle-orm/type-tests/pg/select.ts index 0fde90a71..4ab3a86b3 100644 --- a/drizzle-orm/type-tests/pg/select.ts +++ b/drizzle-orm/type-tests/pg/select.ts @@ -31,13 +31,15 @@ import { alias } from '~/pg-core/alias.ts'; import { boolean, integer, + pgMaterializedView, type PgSelect, type PgSelectQueryBuilder, pgTable, + pgView, QueryBuilder, text, } from '~/pg-core/index.ts'; -import { type SQL, sql } from '~/sql/sql.ts'; +import { type InferSelectViewModel, type SQL, sql } from '~/sql/sql.ts'; import { db } from './db.ts'; import { cities, classes, newYorkers, newYorkers2, users } from './tables.ts'; @@ -1156,3 +1158,77 @@ await db ), ); } + +{ + const table1 = pgTable('table1', { + id: integer().primaryKey(), + name: text().notNull(), + }); + const table2 = pgTable('table2', { + id: integer().primaryKey(), + age: integer().notNull(), + }); + const table3 = pgTable('table3', { + id: integer().primaryKey(), + phone: text().notNull(), + }); + const view = pgView('view').as((qb) => + qb.select({ + table: table1, + column: table2.age, + nested: { + column: table3.phone, + }, + }).from(table1).innerJoin(table2, sql``).leftJoin(table3, sql``) + ); + const result = await db.select().from(view); + + Expect< + Equal + >; + Expect>; + Expect[]>>; +} + +{ + const table1 = pgTable('table1', { + id: integer().primaryKey(), + name: text().notNull(), + }); + const table2 = pgTable('table2', { + id: integer().primaryKey(), + age: integer().notNull(), + }); + const table3 = pgTable('table3', { + id: integer().primaryKey(), + phone: text().notNull(), + }); + const view = pgMaterializedView('view').as((qb) => + qb.select({ + table: table1, + column: table2.age, + nested: { + column: table3.phone, + }, + }).from(table1).innerJoin(table2, sql``).leftJoin(table3, sql``) + ); + const result = await db.select().from(view); + + Expect< + Equal + >; + Expect>; + Expect[]>>; +} diff --git a/drizzle-orm/type-tests/singlestore/tables.ts b/drizzle-orm/type-tests/singlestore/tables.ts index 43a1b05dc..73d9c6993 100644 --- a/drizzle-orm/type-tests/singlestore/tables.ts +++ b/drizzle-orm/type-tests/singlestore/tables.ts @@ -81,57 +81,69 @@ export const cities = singlestoreTable('cities_table', { Expect< Equal< { - id: SingleStoreColumn<{ - name: 'id'; - tableName: 'cities_table'; - dataType: 'number'; - columnType: 'SingleStoreSerial'; - data: number; - driverParam: number; - notNull: true; - hasDefault: true; - isPrimaryKey: true; - isAutoincrement: true; - hasRuntimeDefault: false; - enumValues: undefined; - baseColumn: never; - identity: undefined; - generated: undefined; - }, object>; - name: SingleStoreColumn<{ - name: 'name_db'; - tableName: 'cities_table'; - dataType: 'string'; - columnType: 'SingleStoreText'; - data: string; - driverParam: string; - notNull: true; - hasDefault: false; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: [string, ...string[]]; - baseColumn: never; - identity: undefined; - generated: undefined; - }, object>; - population: SingleStoreColumn<{ - name: 'population'; - tableName: 'cities_table'; - dataType: 'number'; - columnType: 'SingleStoreInt'; - data: number; - driverParam: string | number; - notNull: false; - hasDefault: true; - isPrimaryKey: false; - isAutoincrement: false; - hasRuntimeDefault: false; - enumValues: undefined; - baseColumn: never; - identity: undefined; - generated: undefined; - }, object>; + id: SingleStoreColumn< + { + name: 'id'; + tableName: 'cities_table'; + dataType: 'number'; + columnType: 'SingleStoreSerial'; + data: number; + driverParam: number; + notNull: true; + hasDefault: true; + isPrimaryKey: true; + isAutoincrement: true; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + identity: undefined; + generated: undefined; + }, + {}, + {} + >; + name: SingleStoreColumn< + { + name: 'name_db'; + tableName: 'cities_table'; + dataType: 'string'; + columnType: 'SingleStoreText'; + data: string; + driverParam: string; + notNull: true; + hasDefault: false; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: [string, ...string[]]; + baseColumn: never; + identity: undefined; + generated: undefined; + }, + {}, + {} + >; + population: SingleStoreColumn< + { + name: 'population'; + tableName: 'cities_table'; + dataType: 'number'; + columnType: 'SingleStoreInt'; + data: number; + driverParam: string | number; + notNull: false; + hasDefault: true; + isPrimaryKey: false; + isAutoincrement: false; + hasRuntimeDefault: false; + enumValues: undefined; + baseColumn: never; + identity: undefined; + generated: undefined; + }, + {}, + {} + >; }, typeof cities._.columns > diff --git a/drizzle-orm/type-tests/sqlite/select.ts b/drizzle-orm/type-tests/sqlite/select.ts index 196dbc4de..92bb6055a 100644 --- a/drizzle-orm/type-tests/sqlite/select.ts +++ b/drizzle-orm/type-tests/sqlite/select.ts @@ -21,12 +21,15 @@ import { notLike, or, } from '~/expressions.ts'; -import { param, sql } from '~/sql/sql.ts'; +import { type InferSelectViewModel, param, sql } from '~/sql/sql.ts'; import { alias } from '~/sqlite-core/alias.ts'; import type { Equal } from 'type-tests/utils.ts'; import { Expect } from 'type-tests/utils.ts'; +import { integer, text } from '~/sqlite-core/index.ts'; import type { SQLiteSelect, SQLiteSelectQueryBuilder } from '~/sqlite-core/query-builders/select.types.ts'; +import { sqliteTable } from '~/sqlite-core/table.ts'; +import { sqliteView } from '~/sqlite-core/view.ts'; import { db } from './db.ts'; import { cities, classes, newYorkers, users } from './tables.ts'; @@ -579,3 +582,40 @@ Expect< // @ts-expect-error method was already called .offset(10); } + +{ + const table1 = sqliteTable('table1', { + id: integer().primaryKey(), + name: text().notNull(), + }); + const table2 = sqliteTable('table2', { + id: integer().primaryKey(), + age: integer().notNull(), + }); + const table3 = sqliteTable('table3', { + id: integer().primaryKey(), + phone: text().notNull(), + }); + const view = sqliteView('view').as((qb) => + qb.select({ + table: table1, + column: table2.age, + nested: { + column: table3.phone, + }, + }).from(table1).innerJoin(table2, sql``).leftJoin(table3, sql``) + ); + const result = await db.select().from(view); + + Expect< + Equal + >; + Expect>; + Expect[]>>; +} diff --git a/drizzle-typebox/README.md b/drizzle-typebox/README.md index 912a67f7f..72d85e684 100644 --- a/drizzle-typebox/README.md +++ b/drizzle-typebox/README.md @@ -1,10 +1,10 @@ `drizzle-typebox` is a plugin for [Drizzle ORM](https://github.com/drizzle-team/drizzle-orm) that allows you to generate [@sinclair/typebox](https://github.com/sinclairzx81/typebox) schemas from Drizzle ORM schemas. -| Database | Insert schema | Select schema | -| :--------- | :-----------: | :-----------: | -| PostgreSQL | ✅ | ✅ | -| MySQL | ✅ | ✅ | -| SQLite | ✅ | ✅ | +**Features** + +- Create a select schema for tables, views and enums. +- Create insert and update schemas for tables. +- Supports all dialects: PostgreSQL, MySQL and SQLite. # Usage @@ -25,6 +25,9 @@ const users = pgTable('users', { // Schema for inserting a user - can be used to validate API requests const insertUserSchema = createInsertSchema(users); +// Schema for updating a user - can be used to validate API requests +const updateUserSchema = createUpdateSchema(users); + // Schema for selecting a user - can be used to validate API responses const selectUserSchema = createSelectSchema(users); @@ -35,7 +38,7 @@ const insertUserSchema = createInsertSchema(users, { // Refining the fields - useful if you want to change the fields before they become nullable/optional in the final schema const insertUserSchema = createInsertSchema(users, { - id: (schema) => Type.Number({ minimum: 0 }), + id: (schema) => Type.Number({ ...schema, minimum: 0 }), role: Type.String(), }); diff --git a/drizzle-typebox/package.json b/drizzle-typebox/package.json index 5e812f4fe..a6c34fc69 100644 --- a/drizzle-typebox/package.json +++ b/drizzle-typebox/package.json @@ -1,6 +1,6 @@ { "name": "drizzle-typebox", - "version": "0.1.1", + "version": "0.2.0", "description": "Generate Typebox schemas from Drizzle ORM schemas", "type": "module", "scripts": { @@ -55,13 +55,12 @@ "author": "Drizzle Team", "license": "Apache-2.0", "peerDependencies": { - "@sinclair/typebox": ">=0.17.6", - "drizzle-orm": ">=0.23.13" + "@sinclair/typebox": ">=0.34.8", + "drizzle-orm": ">=0.36.0" }, "devDependencies": { - "@rollup/plugin-terser": "^0.4.1", "@rollup/plugin-typescript": "^11.1.0", - "@sinclair/typebox": "^0.29.6", + "@sinclair/typebox": "^0.34.8", "@types/node": "^18.15.10", "cpy": "^10.1.0", "drizzle-orm": "link:../drizzle-orm/dist", diff --git a/drizzle-typebox/rollup.config.ts b/drizzle-typebox/rollup.config.ts index 2ed2d33d3..a29fdd38a 100644 --- a/drizzle-typebox/rollup.config.ts +++ b/drizzle-typebox/rollup.config.ts @@ -1,4 +1,3 @@ -import terser from '@rollup/plugin-terser'; import typescript from '@rollup/plugin-typescript'; import { defineConfig } from 'rollup'; @@ -23,13 +22,12 @@ export default defineConfig([ ], external: [ /^drizzle-orm\/?/, - 'zod', + '@sinclair/typebox', ], plugins: [ typescript({ tsconfig: 'tsconfig.build.json', }), - terser(), ], }, ]); diff --git a/drizzle-typebox/scripts/build.ts b/drizzle-typebox/scripts/build.ts index 1910feac6..07330ffd0 100644 --- a/drizzle-typebox/scripts/build.ts +++ b/drizzle-typebox/scripts/build.ts @@ -13,3 +13,4 @@ await cpy('dist/**/*.d.ts', 'dist', { rename: (basename) => basename.replace(/\.d\.ts$/, '.d.cts'), }); await fs.copy('package.json', 'dist/package.json'); +await $`scripts/fix-imports.ts`; diff --git a/drizzle-typebox/scripts/fix-imports.ts b/drizzle-typebox/scripts/fix-imports.ts new file mode 100755 index 000000000..a90057c5b --- /dev/null +++ b/drizzle-typebox/scripts/fix-imports.ts @@ -0,0 +1,136 @@ +#!/usr/bin/env -S pnpm tsx +import 'zx/globals'; + +import path from 'node:path'; +import { parse, print, visit } from 'recast'; +import parser from 'recast/parsers/typescript'; + +function resolvePathAlias(importPath: string, file: string) { + if (importPath.startsWith('~/')) { + const relativePath = path.relative(path.dirname(file), path.resolve('dist.new', importPath.slice(2))); + importPath = relativePath.startsWith('.') ? relativePath : './' + relativePath; + } + + return importPath; +} + +function fixImportPath(importPath: string, file: string, ext: string) { + importPath = resolvePathAlias(importPath, file); + + if (!/\..*\.(js|ts)$/.test(importPath)) { + return importPath; + } + + return importPath.replace(/\.(js|ts)$/, ext); +} + +const cjsFiles = await glob('dist/**/*.{cjs,d.cts}'); + +await Promise.all(cjsFiles.map(async (file) => { + const code = parse(await fs.readFile(file, 'utf8'), { parser }); + + visit(code, { + visitImportDeclaration(path) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.cjs'); + this.traverse(path); + }, + visitExportAllDeclaration(path) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.cjs'); + this.traverse(path); + }, + visitExportNamedDeclaration(path) { + if (path.value.source) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.cjs'); + } + this.traverse(path); + }, + visitCallExpression(path) { + if (path.value.callee.type === 'Identifier' && path.value.callee.name === 'require') { + path.value.arguments[0].value = fixImportPath(path.value.arguments[0].value, file, '.cjs'); + } + this.traverse(path); + }, + visitTSImportType(path) { + path.value.argument.value = resolvePathAlias(path.value.argument.value, file); + this.traverse(path); + }, + visitAwaitExpression(path) { + if (print(path.value).code.startsWith(`await import("./`)) { + path.value.argument.arguments[0].value = fixImportPath(path.value.argument.arguments[0].value, file, '.cjs'); + } + this.traverse(path); + }, + }); + + await fs.writeFile(file, print(code).code); +})); + +let esmFiles = await glob('dist/**/*.{js,d.ts}'); + +await Promise.all(esmFiles.map(async (file) => { + const code = parse(await fs.readFile(file, 'utf8'), { parser }); + + visit(code, { + visitImportDeclaration(path) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.js'); + this.traverse(path); + }, + visitExportAllDeclaration(path) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.js'); + this.traverse(path); + }, + visitExportNamedDeclaration(path) { + if (path.value.source) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.js'); + } + this.traverse(path); + }, + visitTSImportType(path) { + path.value.argument.value = fixImportPath(path.value.argument.value, file, '.js'); + this.traverse(path); + }, + visitAwaitExpression(path) { + if (print(path.value).code.startsWith(`await import("./`)) { + path.value.argument.arguments[0].value = fixImportPath(path.value.argument.arguments[0].value, file, '.js'); + } + this.traverse(path); + }, + }); + + await fs.writeFile(file, print(code).code); +})); + +esmFiles = await glob('dist/**/*.{mjs,d.mts}'); + +await Promise.all(esmFiles.map(async (file) => { + const code = parse(await fs.readFile(file, 'utf8'), { parser }); + + visit(code, { + visitImportDeclaration(path) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.mjs'); + this.traverse(path); + }, + visitExportAllDeclaration(path) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.mjs'); + this.traverse(path); + }, + visitExportNamedDeclaration(path) { + if (path.value.source) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.mjs'); + } + this.traverse(path); + }, + visitTSImportType(path) { + path.value.argument.value = fixImportPath(path.value.argument.value, file, '.mjs'); + this.traverse(path); + }, + visitAwaitExpression(path) { + if (print(path.value).code.startsWith(`await import("./`)) { + path.value.argument.arguments[0].value = fixImportPath(path.value.argument.arguments[0].value, file, '.mjs'); + } + this.traverse(path); + }, + }); + + await fs.writeFile(file, print(code).code); +})); diff --git a/drizzle-typebox/src/column.ts b/drizzle-typebox/src/column.ts new file mode 100644 index 000000000..80e6ff39d --- /dev/null +++ b/drizzle-typebox/src/column.ts @@ -0,0 +1,259 @@ +import { Kind, Type as t, TypeRegistry } from '@sinclair/typebox'; +import type { StringOptions, TSchema, Type as typebox } from '@sinclair/typebox'; +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 { SQLiteInteger, SQLiteReal, SQLiteText } from 'drizzle-orm/sqlite-core'; +import { CONSTANTS } from './constants.ts'; +import { isColumnType, isWithEnum } from './utils.ts'; +import type { BufferSchema, JsonSchema } from './utils.ts'; + +export const literalSchema = t.Union([t.String(), t.Number(), t.Boolean(), t.Null()]); +export const jsonSchema: JsonSchema = t.Recursive((self) => + t.Union([literalSchema, t.Array(self), t.Record(t.String(), self)]) +) as any; +TypeRegistry.Set('Buffer', (_, value) => value instanceof Buffer); // eslint-disable-line no-instanceof/no-instanceof +export const bufferSchema: BufferSchema = { [Kind]: 'Buffer', type: 'buffer' } as any; + +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; + + if (isWithEnum(column)) { + schema = column.enumValues.length ? t.Enum(mapEnumValues(column.enumValues)) : t.String(); + } + + if (!schema) { + // Handle specific types + if (isColumnType | PgPointTuple>(column, ['PgGeometry', 'PgPointTuple'])) { + schema = t.Tuple([t.Number(), t.Number()]); + } else if ( + isColumnType | PgGeometryObject>(column, ['PgGeometryObject', 'PgPointObject']) + ) { + schema = t.Object({ x: t.Number(), y: t.Number() }); + } else if (isColumnType | PgVector>(column, ['PgHalfVector', 'PgVector'])) { + schema = t.Array( + t.Number(), + column.dimensions + ? { + minItems: column.dimensions, + maxItems: column.dimensions, + } + : undefined, + ); + } else if (isColumnType>(column, ['PgLine'])) { + schema = t.Tuple([t.Number(), t.Number(), t.Number()]); + } else if (isColumnType>(column, ['PgLineABC'])) { + schema = t.Object({ + a: t.Number(), + b: t.Number(), + c: t.Number(), + }); + } // Handle other types + else if (isColumnType>(column, ['PgArray'])) { + schema = t.Array( + columnToSchema(column.baseColumn, t), + column.size + ? { + minItems: column.size, + maxItems: column.size, + } + : undefined, + ); + } else if (column.dataType === 'array') { + schema = t.Array(t.Any()); + } else if (column.dataType === 'number') { + schema = numberColumnToSchema(column, t); + } else if (column.dataType === 'bigint') { + schema = bigintColumnToSchema(column, t); + } else if (column.dataType === 'boolean') { + schema = t.Boolean(); + } else if (column.dataType === 'date') { + schema = t.Date(); + } else if (column.dataType === 'string') { + schema = stringColumnToSchema(column, t); + } else if (column.dataType === 'json') { + schema = jsonSchema; + } else if (column.dataType === 'custom') { + schema = t.Any(); + } else if (column.dataType === 'buffer') { + schema = bufferSchema; + } + } + + if (!schema) { + schema = t.Any(); + } + + return schema; +} + +function numberColumnToSchema(column: Column, t: typeof typebox): TSchema { + let unsigned = column.getSQLType().includes('unsigned'); + let min!: number; + let max!: number; + let integer = false; + + if (isColumnType>(column, ['MySqlTinyInt'])) { + min = unsigned ? 0 : CONSTANTS.INT8_MIN; + max = unsigned ? CONSTANTS.INT8_UNSIGNED_MAX : CONSTANTS.INT8_MAX; + integer = true; + } else if ( + isColumnType | PgSmallSerial | MySqlSmallInt>(column, [ + 'PgSmallInt', + 'PgSmallSerial', + 'MySqlSmallInt', + ]) + ) { + min = unsigned ? 0 : CONSTANTS.INT16_MIN; + max = unsigned ? CONSTANTS.INT16_UNSIGNED_MAX : CONSTANTS.INT16_MAX; + integer = true; + } else if ( + isColumnType | MySqlFloat | MySqlMediumInt>(column, [ + 'PgReal', + 'MySqlFloat', + 'MySqlMediumInt', + ]) + ) { + min = unsigned ? 0 : CONSTANTS.INT24_MIN; + max = unsigned ? CONSTANTS.INT24_UNSIGNED_MAX : CONSTANTS.INT24_MAX; + integer = isColumnType(column, ['MySqlMediumInt']); + } else if ( + isColumnType | PgSerial | MySqlInt>(column, ['PgInteger', 'PgSerial', 'MySqlInt']) + ) { + min = unsigned ? 0 : CONSTANTS.INT32_MIN; + max = unsigned ? CONSTANTS.INT32_UNSIGNED_MAX : CONSTANTS.INT32_MAX; + integer = true; + } else if ( + isColumnType | MySqlReal | MySqlDouble | SQLiteReal>(column, [ + 'PgDoublePrecision', + 'MySqlReal', + 'MySqlDouble', + 'SQLiteReal', + ]) + ) { + min = unsigned ? 0 : CONSTANTS.INT48_MIN; + max = unsigned ? CONSTANTS.INT48_UNSIGNED_MAX : CONSTANTS.INT48_MAX; + } else if ( + isColumnType | PgBigSerial53 | MySqlBigInt53 | MySqlSerial | SQLiteInteger>( + column, + ['PgBigInt53', 'PgBigSerial53', 'MySqlBigInt53', 'MySqlSerial', 'SQLiteInteger'], + ) + ) { + unsigned = unsigned || isColumnType(column, ['MySqlSerial']); + min = unsigned ? 0 : Number.MIN_SAFE_INTEGER; + max = Number.MAX_SAFE_INTEGER; + integer = true; + } else if (isColumnType>(column, ['MySqlYear'])) { + min = 1901; + max = 2155; + integer = true; + } else { + min = Number.MIN_SAFE_INTEGER; + max = Number.MAX_SAFE_INTEGER; + } + + const key = integer ? 'Integer' : 'Number'; + return t[key]({ + minimum: min, + maximum: max, + }); +} + +function bigintColumnToSchema(column: Column, t: typeof typebox): TSchema { + const unsigned = column.getSQLType().includes('unsigned'); + const min = unsigned ? 0n : CONSTANTS.INT64_MIN; + const max = unsigned ? CONSTANTS.INT64_UNSIGNED_MAX : CONSTANTS.INT64_MAX; + + return t.BigInt({ + minimum: min, + maximum: max, + }); +} + +function stringColumnToSchema(column: Column, t: typeof typebox): TSchema { + if (isColumnType>>(column, ['PgUUID'])) { + return t.String({ format: 'uuid' }); + } else if ( + isColumnType & { dimensions: number }>>(column, [ + 'PgBinaryVector', + ]) + ) { + return t.RegExp(/^[01]+$/, column.dimensions ? { maxLength: column.dimensions } : undefined); + } + + let max: number | undefined; + let fixed = false; + + if (isColumnType | SQLiteText>(column, ['PgVarchar', 'SQLiteText'])) { + max = column.length; + } else if (isColumnType>(column, ['MySqlVarChar'])) { + max = column.length ?? CONSTANTS.INT16_UNSIGNED_MAX; + } else if (isColumnType>(column, ['MySqlText'])) { + 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 | MySqlChar>(column, ['PgChar', 'MySqlChar'])) { + max = column.length; + fixed = true; + } + + const options: Partial = {}; + + if (max !== undefined && fixed) { + options.minLength = max; + options.maxLength = max; + } else if (max !== undefined) { + options.maxLength = max; + } + + return t.String(Object.keys(options).length > 0 ? options : undefined); +} diff --git a/drizzle-typebox/src/column.types.ts b/drizzle-typebox/src/column.types.ts new file mode 100644 index 000000000..7010f234e --- /dev/null +++ b/drizzle-typebox/src/column.types.ts @@ -0,0 +1,100 @@ +import type * as t from '@sinclair/typebox'; +import type { Assume, Column } from 'drizzle-orm'; +import type { ArrayHasAtLeastOneValue, BufferSchema, ColumnIsGeneratedAlwaysAs, IsNever, JsonSchema } from './utils.ts'; + +export type GetEnumValuesFromColumn = TColumn['_'] extends { enumValues: [string, ...string[]] } + ? TColumn['_']['enumValues'] + : undefined; + +export type GetBaseColumn = TColumn['_'] extends { baseColumn: Column | never | undefined } + ? IsNever extends false ? TColumn['_']['baseColumn'] + : undefined + : undefined; + +export type EnumValuesToEnum = { [K in TEnumValues[number]]: K }; + +export type GetTypeboxType< + TData, + TDataType extends string, + TColumnType extends string, + TEnumValues extends [string, ...string[]] | undefined, + TBaseColumn extends Column | undefined, +> = TColumnType extends + | 'MySqlTinyInt' + | 'PgSmallInt' + | 'PgSmallSerial' + | 'MySqlSmallInt' + | 'MySqlMediumInt' + | 'PgInteger' + | 'PgSerial' + | 'MySqlInt' + | 'PgBigInt53' + | 'PgBigSerial53' + | 'MySqlBigInt53' + | 'MySqlSerial' + | 'SQLiteInteger' + | 'MySqlYear' ? t.TInteger + : TColumnType extends 'PgBinaryVector' ? t.TRegExp + : TBaseColumn extends Column ? t.TArray< + GetTypeboxType< + TBaseColumn['_']['data'], + TBaseColumn['_']['dataType'], + TBaseColumn['_']['columnType'], + GetEnumValuesFromColumn, + GetBaseColumn + > + > + : ArrayHasAtLeastOneValue extends true + ? t.TEnum>> + : TData extends infer TTuple extends [any, ...any[]] ? t.TTuple< + Assume<{ [K in keyof TTuple]: GetTypeboxType }, [any, ...any[]]> + > + : TData extends Date ? t.TDate + : TData extends Buffer ? BufferSchema + : TDataType extends 'array' + ? t.TArray[number], string, string, undefined, undefined>> + : TData extends infer TDict extends Record + ? t.TObject<{ [K in keyof TDict]: GetTypeboxType }> + : TDataType extends 'json' ? JsonSchema + : TData extends number ? t.TNumber + : TData extends bigint ? t.TBigInt + : TData extends boolean ? t.TBoolean + : TData extends string ? t.TString + : t.TAny; + +type HandleSelectColumn< + TSchema extends t.TSchema, + TColumn extends Column, +> = TColumn['_']['notNull'] extends true ? TSchema + : t.Union<[TSchema, t.TNull]>; + +type HandleInsertColumn< + TSchema extends t.TSchema, + TColumn extends Column, +> = ColumnIsGeneratedAlwaysAs extends true ? never + : TColumn['_']['notNull'] extends true ? TColumn['_']['hasDefault'] extends true ? t.TOptional + : TSchema + : t.TOptional>; + +type HandleUpdateColumn< + TSchema extends t.TSchema, + TColumn extends Column, +> = ColumnIsGeneratedAlwaysAs extends true ? never + : TColumn['_']['notNull'] extends true ? t.TOptional + : t.TOptional>; + +export type HandleColumn< + TType extends 'select' | 'insert' | 'update', + TColumn extends Column, +> = GetTypeboxType< + TColumn['_']['data'], + TColumn['_']['dataType'], + TColumn['_']['columnType'], + GetEnumValuesFromColumn, + GetBaseColumn +> extends infer TSchema extends t.TSchema ? TSchema extends t.TAny ? t.TAny + : TType extends 'select' ? HandleSelectColumn + : TType extends 'insert' ? HandleInsertColumn + : TType extends 'update' ? HandleUpdateColumn + : TSchema + : t.TAny; diff --git a/drizzle-typebox/src/constants.ts b/drizzle-typebox/src/constants.ts new file mode 100644 index 000000000..99f5d7a42 --- /dev/null +++ b/drizzle-typebox/src/constants.ts @@ -0,0 +1,20 @@ +export const CONSTANTS = { + INT8_MIN: -128, + INT8_MAX: 127, + INT8_UNSIGNED_MAX: 255, + INT16_MIN: -32768, + INT16_MAX: 32767, + INT16_UNSIGNED_MAX: 65535, + INT24_MIN: -8388608, + INT24_MAX: 8388607, + INT24_UNSIGNED_MAX: 16777215, + INT32_MIN: -2147483648, + INT32_MAX: 2147483647, + INT32_UNSIGNED_MAX: 4294967295, + INT48_MIN: -140737488355328, + INT48_MAX: 140737488355327, + INT48_UNSIGNED_MAX: 281474976710655, + INT64_MIN: -9223372036854775808n, + INT64_MAX: 9223372036854775807n, + INT64_UNSIGNED_MAX: 18446744073709551615n, +}; diff --git a/drizzle-typebox/src/index.ts b/drizzle-typebox/src/index.ts index e70d72c43..0a6499e5b 100644 --- a/drizzle-typebox/src/index.ts +++ b/drizzle-typebox/src/index.ts @@ -1,344 +1,2 @@ -import type { - TAny, - TArray, - TBigInt, - TBoolean, - TDate, - TLiteral, - TNull, - TNumber, - TObject, - TOptional, - TSchema, - TString, - TUnion, -} from '@sinclair/typebox'; -import { Type } from '@sinclair/typebox'; -import { - type AnyColumn, - type Assume, - type Column, - type DrizzleTypeError, - type Equal, - getTableColumns, - is, - type Simplify, - type Table, -} from 'drizzle-orm'; -import { MySqlChar, MySqlVarBinary, MySqlVarChar } from 'drizzle-orm/mysql-core'; -import { type PgArray, PgChar, PgUUID, PgVarchar } from 'drizzle-orm/pg-core'; -import { SQLiteText } from 'drizzle-orm/sqlite-core'; - -type TUnionLiterals = T extends readonly [ - infer U extends string, - ...infer Rest extends string[], -] ? [TLiteral, ...TUnionLiterals] - : []; - -const literalSchema = Type.Union([ - Type.String(), - Type.Number(), - Type.Boolean(), - Type.Null(), -]); - -type Json = typeof jsonSchema; - -export const jsonSchema = Type.Union([ - literalSchema, - Type.Array(Type.Any()), - Type.Record(Type.String(), Type.Any()), -]); - -type TNullable = TUnion<[TType, TNull]>; - -type MapInsertColumnToTypebox< - TColumn extends Column, - TType extends TSchema, -> = TColumn['_']['notNull'] extends false ? TOptional> - : TColumn['_']['hasDefault'] extends true ? TOptional - : TType; - -type MapSelectColumnToTypebox< - TColumn extends Column, - TType extends TSchema, -> = TColumn['_']['notNull'] extends false ? TNullable : TType; - -type MapColumnToTypebox< - TColumn extends Column, - TType extends TSchema, - TMode extends 'insert' | 'select', -> = TMode extends 'insert' ? MapInsertColumnToTypebox - : MapSelectColumnToTypebox; - -type MaybeOptional< - TColumn extends Column, - TType extends TSchema, - TMode extends 'insert' | 'select', - TNoOptional extends boolean, -> = TNoOptional extends true ? TType - : MapColumnToTypebox; - -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< - TColumn['_'], - { baseColumn: Column } - >['baseColumn'] - > - > - : TDataType extends 'bigint' ? TBigInt - : TDataType extends 'number' ? TNumber - : TDataType extends 'string' ? TString - : TDataType extends 'boolean' ? TBoolean - : TDataType extends 'date' ? TDate - : TAny - : never; - -type ValueOrUpdater = T | ((arg: TUpdaterArg) => T); - -type UnwrapValueOrUpdater = T extends ValueOrUpdater ? U - : never; - -export type Refine = { - [K in keyof TTable['_']['columns']]?: ValueOrUpdater< - TSchema, - TMode extends 'select' ? BuildSelectSchema - : BuildInsertSchema - >; -}; - -export type BuildInsertSchema< - TTable extends Table, - TRefine extends Refine | {}, - TNoOptional extends boolean = false, -> = TTable['_']['columns'] extends infer TColumns extends Record< - string, - Column -> ? { - [K in keyof TColumns & string]: MaybeOptional< - TColumns[K], - K extends keyof TRefine ? Assume, TSchema> - : GetTypeboxType, - 'insert', - TNoOptional - >; - } - : never; - -export type BuildSelectSchema< - TTable extends Table, - TRefine extends Refine, - TNoOptional extends boolean = false, -> = Simplify< - { - [K in keyof TTable['_']['columns']]: MaybeOptional< - TTable['_']['columns'][K], - K extends keyof TRefine ? Assume, TSchema> - : GetTypeboxType, - 'select', - TNoOptional - >; - } ->; - -export const Nullable = (schema: T) => Type.Union([schema, Type.Null()]); - -export function createInsertSchema< - TTable extends Table, - TRefine extends Refine = Refine, ->( - table: TTable, - /** - * @param refine Refine schema fields - */ - refine?: { - [K in keyof TRefine]: K extends keyof TTable['_']['columns'] ? TRefine[K] - : DrizzleTypeError< - `Column '${ - & K - & string}' does not exist in table '${TTable['_']['name']}'` - >; - }, - // -): TObject< - BuildInsertSchema< - TTable, - Equal> extends true ? {} : TRefine - > -> { - const columns = getTableColumns(table); - const columnEntries = Object.entries(columns); - - let schemaEntries = Object.fromEntries( - columnEntries.map(([name, column]) => { - return [name, mapColumnToSchema(column)]; - }), - ); - - if (refine) { - schemaEntries = Object.assign( - schemaEntries, - Object.fromEntries( - Object.entries(refine).map(([name, refineColumn]) => { - return [ - name, - typeof refineColumn === 'function' - ? refineColumn( - schemaEntries as BuildInsertSchema< - TTable, - {}, - true - >, - ) - : refineColumn, - ]; - }), - ), - ); - } - - for (const [name, column] of columnEntries) { - if (!column.notNull) { - schemaEntries[name] = Type.Optional(Nullable(schemaEntries[name]!)); - } else if (column.hasDefault) { - schemaEntries[name] = Type.Optional(schemaEntries[name]!); - } - } - - return Type.Object(schemaEntries) as any; -} - -export function createSelectSchema< - TTable extends Table, - TRefine extends Refine = Refine, ->( - table: TTable, - /** - * @param refine Refine schema fields - */ - refine?: { - [K in keyof TRefine]: K extends keyof TTable['_']['columns'] ? TRefine[K] - : DrizzleTypeError< - `Column '${ - & K - & string}' does not exist in table '${TTable['_']['name']}'` - >; - }, -): TObject< - BuildSelectSchema< - TTable, - Equal> extends true ? {} : TRefine - > -> { - const columns = getTableColumns(table); - const columnEntries = Object.entries(columns); - - let schemaEntries = Object.fromEntries( - columnEntries.map(([name, column]) => { - return [name, mapColumnToSchema(column)]; - }), - ); - - if (refine) { - schemaEntries = Object.assign( - schemaEntries, - Object.fromEntries( - Object.entries(refine).map(([name, refineColumn]) => { - return [ - name, - typeof refineColumn === 'function' - ? refineColumn( - schemaEntries as BuildSelectSchema< - TTable, - {}, - true - >, - ) - : refineColumn, - ]; - }), - ), - ); - } - - for (const [name, column] of columnEntries) { - if (!column.notNull) { - schemaEntries[name] = Nullable(schemaEntries[name]!); - } - } - - return Type.Object(schemaEntries) as any; -} - -function isWithEnum( - column: AnyColumn, -): column is typeof column & { enumValues: [string, ...string[]] } { - return ( - 'enumValues' in column - && Array.isArray(column.enumValues) - && column.enumValues.length > 0 - ); -} - -const uuidPattern = /^[\dA-Fa-f]{8}(?:-[\dA-Fa-f]{4}){3}-[\dA-Fa-f]{12}$/; - -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); - } - } - - if (!type) { - type = Type.Any(); - } - - return type; -} +export * from './schema.ts'; +export * from './schema.types.ts'; diff --git a/drizzle-typebox/src/schema.ts b/drizzle-typebox/src/schema.ts new file mode 100644 index 000000000..b0291723e --- /dev/null +++ b/drizzle-typebox/src/schema.ts @@ -0,0 +1,144 @@ +import { Type as t } from '@sinclair/typebox'; +import type { TSchema } from '@sinclair/typebox'; +import { Column, getTableColumns, getViewSelectedFields, is, isTable, isView, SQL } from 'drizzle-orm'; +import type { Table, View } from 'drizzle-orm'; +import type { PgEnum } from 'drizzle-orm/pg-core'; +import { columnToSchema, mapEnumValues } from './column.ts'; +import type { Conditions } from './schema.types.internal.ts'; +import type { + CreateInsertSchema, + CreateSchemaFactoryOptions, + CreateSelectSchema, + CreateUpdateSchema, +} from './schema.types.ts'; +import { isPgEnum } from './utils.ts'; + +function getColumns(tableLike: Table | View) { + return isTable(tableLike) ? getTableColumns(tableLike) : getViewSelectedFields(tableLike); +} + +function handleColumns( + columns: Record, + refinements: Record, + conditions: Conditions, + factory?: CreateSchemaFactoryOptions, +): TSchema { + const columnSchemas: Record = {}; + + for (const [key, selected] of Object.entries(columns)) { + if (!is(selected, Column) && !is(selected, SQL) && !is(selected, SQL.Aliased) && typeof selected === 'object') { + const columns = isTable(selected) || isView(selected) ? getColumns(selected) : selected; + columnSchemas[key] = handleColumns(columns, refinements[key] ?? {}, conditions, factory); + continue; + } + + const refinement = refinements[key]; + if (refinement !== undefined && typeof refinement !== 'function') { + columnSchemas[key] = refinement; + continue; + } + + const column = is(selected, Column) ? selected : undefined; + const schema = column ? columnToSchema(column, factory?.typeboxInstance ?? t) : t.Any(); + const refined = typeof refinement === 'function' ? refinement(schema) : schema; + + if (conditions.never(column)) { + continue; + } else { + columnSchemas[key] = refined; + } + + if (column) { + if (conditions.nullable(column)) { + columnSchemas[key] = t.Union([columnSchemas[key]!, t.Null()]); + } + + if (conditions.optional(column)) { + columnSchemas[key] = t.Optional(columnSchemas[key]!); + } + } + } + + return t.Object(columnSchemas) as any; +} + +function handleEnum(enum_: PgEnum, factory?: CreateSchemaFactoryOptions) { + const typebox: typeof t = factory?.typeboxInstance ?? t; + return typebox.Enum(mapEnumValues(enum_.enumValues)); +} + +const selectConditions: Conditions = { + never: () => false, + optional: () => false, + nullable: (column) => !column.notNull, +}; + +const insertConditions: Conditions = { + never: (column) => column?.generated?.type === 'always' || column?.generatedIdentity?.type === 'always', + optional: (column) => !column.notNull || (column.notNull && column.hasDefault), + nullable: (column) => !column.notNull, +}; + +const updateConditions: Conditions = { + never: (column) => column?.generated?.type === 'always' || column?.generatedIdentity?.type === 'always', + optional: () => true, + nullable: (column) => !column.notNull, +}; + +export const createSelectSchema: CreateSelectSchema = ( + entity: Table | View | PgEnum<[string, ...string[]]>, + refine?: Record, +) => { + if (isPgEnum(entity)) { + return handleEnum(entity); + } + const columns = getColumns(entity); + return handleColumns(columns, refine ?? {}, selectConditions) as any; +}; + +export const createInsertSchema: CreateInsertSchema = ( + entity: Table, + refine?: Record, +) => { + const columns = getColumns(entity); + return handleColumns(columns, refine ?? {}, insertConditions) as any; +}; + +export const createUpdateSchema: CreateUpdateSchema = ( + entity: Table, + refine?: Record, +) => { + const columns = getColumns(entity); + return handleColumns(columns, refine ?? {}, updateConditions) as any; +}; + +export function createSchemaFactory(options?: CreateSchemaFactoryOptions) { + const createSelectSchema: CreateSelectSchema = ( + entity: Table | View | PgEnum<[string, ...string[]]>, + refine?: Record, + ) => { + if (isPgEnum(entity)) { + return handleEnum(entity, options); + } + const columns = getColumns(entity); + return handleColumns(columns, refine ?? {}, selectConditions, options) as any; + }; + + const createInsertSchema: CreateInsertSchema = ( + entity: Table, + refine?: Record, + ) => { + const columns = getColumns(entity); + return handleColumns(columns, refine ?? {}, insertConditions, options) as any; + }; + + const createUpdateSchema: CreateUpdateSchema = ( + entity: Table, + refine?: Record, + ) => { + const columns = getColumns(entity); + return handleColumns(columns, refine ?? {}, updateConditions, options) as any; + }; + + return { createSelectSchema, createInsertSchema, createUpdateSchema }; +} diff --git a/drizzle-typebox/src/schema.types.internal.ts b/drizzle-typebox/src/schema.types.internal.ts new file mode 100644 index 000000000..beccef94b --- /dev/null +++ b/drizzle-typebox/src/schema.types.internal.ts @@ -0,0 +1,88 @@ +import type * as t from '@sinclair/typebox'; +import type { Assume, Column, DrizzleTypeError, SelectedFieldsFlat, Simplify, Table, View } from 'drizzle-orm'; +import type { GetBaseColumn, GetEnumValuesFromColumn, GetTypeboxType, HandleColumn } from './column.types.ts'; +import type { GetSelection, RemoveNever } from './utils.ts'; + +export interface Conditions { + never: (column?: Column) => boolean; + optional: (column: Column) => boolean; + nullable: (column: Column) => boolean; +} + +export type BuildRefineColumns< + TColumns extends Record, +> = Simplify< + RemoveNever< + { + [K in keyof TColumns]: TColumns[K] extends infer TColumn extends Column ? GetTypeboxType< + TColumn['_']['data'], + TColumn['_']['dataType'], + TColumn['_']['columnType'], + GetEnumValuesFromColumn, + GetBaseColumn + > extends infer TSchema extends t.TSchema ? TSchema + : t.TAny + : TColumns[K] extends infer TObject extends SelectedFieldsFlat | Table | View + ? BuildRefineColumns> + : TColumns[K]; + } + > +>; + +export type BuildRefine< + TColumns extends Record, +> = BuildRefineColumns extends infer TBuildColumns ? { + [K in keyof TBuildColumns]?: TBuildColumns[K] extends t.TSchema + ? ((schema: TBuildColumns[K]) => t.TSchema) | t.TSchema + : TBuildColumns[K] extends Record ? Simplify> + : never; + } + : never; + +type HandleRefinement< + TRefinement extends t.TSchema | ((schema: t.TSchema) => t.TSchema), + TColumn extends Column, +> = TRefinement extends (schema: t.TSchema) => t.TSchema + ? TColumn['_']['notNull'] extends true ? ReturnType + : t.TTuple<[ReturnType, t.TNull]> + : TRefinement; + +export type BuildSchema< + TType extends 'select' | 'insert' | 'update', + TColumns extends Record, + TRefinements extends Record | undefined, +> = t.TObject< + Simplify< + RemoveNever< + { + [K in keyof TColumns]: TColumns[K] extends infer TColumn extends Column + ? TRefinements extends object + ? TRefinements[Assume] extends + infer TRefinement extends t.TSchema | ((schema: t.TSchema) => t.TSchema) + ? HandleRefinement + : HandleColumn + : HandleColumn + : TColumns[K] extends infer TObject extends SelectedFieldsFlat | Table | View ? BuildSchema< + TType, + GetSelection, + TRefinements extends object + ? TRefinements[Assume] extends infer TNestedRefinements extends object + ? TNestedRefinements + : undefined + : undefined + > + : t.TAny; + } + > + > +>; + +export type NoUnknownKeys< + TRefinement extends Record, + TCompare extends Record, +> = { + [K in keyof TRefinement]: K extends keyof TCompare ? TRefinement[K] extends t.TSchema ? TRefinement[K] + : TRefinement[K] extends Record ? NoUnknownKeys + : TRefinement[K] + : DrizzleTypeError<`Found unknown key in refinement: "${K & string}"`>; +}; diff --git a/drizzle-typebox/src/schema.types.ts b/drizzle-typebox/src/schema.types.ts new file mode 100644 index 000000000..abbb4f8eb --- /dev/null +++ b/drizzle-typebox/src/schema.types.ts @@ -0,0 +1,53 @@ +import type * as t from '@sinclair/typebox'; +import type { Table, View } from 'drizzle-orm'; +import type { PgEnum } from 'drizzle-orm/pg-core'; +import type { EnumValuesToEnum } from './column.types.ts'; +import type { BuildRefine, BuildSchema, NoUnknownKeys } from './schema.types.internal.ts'; + +export interface CreateSelectSchema { + (table: TTable): BuildSchema<'select', TTable['_']['columns'], undefined>; + < + TTable extends Table, + TRefine extends BuildRefine, + >( + table: TTable, + refine?: NoUnknownKeys, + ): BuildSchema<'select', TTable['_']['columns'], TRefine>; + + (view: TView): BuildSchema<'select', TView['_']['selectedFields'], undefined>; + < + TView extends View, + TRefine extends BuildRefine, + >( + view: TView, + refine: NoUnknownKeys, + ): BuildSchema<'select', TView['_']['selectedFields'], TRefine>; + + >(enum_: TEnum): t.TEnum>; +} + +export interface CreateInsertSchema { + (table: TTable): BuildSchema<'insert', TTable['_']['columns'], undefined>; + < + TTable extends Table, + TRefine extends BuildRefine>, + >( + table: TTable, + refine?: NoUnknownKeys, + ): BuildSchema<'insert', TTable['_']['columns'], TRefine>; +} + +export interface CreateUpdateSchema { + (table: TTable): BuildSchema<'update', TTable['_']['columns'], undefined>; + < + TTable extends Table, + TRefine extends BuildRefine>, + >( + table: TTable, + refine?: TRefine, + ): BuildSchema<'update', TTable['_']['columns'], TRefine>; +} + +export interface CreateSchemaFactoryOptions { + typeboxInstance?: any; +} diff --git a/drizzle-typebox/src/utils.ts b/drizzle-typebox/src/utils.ts new file mode 100644 index 000000000..686bf01b8 --- /dev/null +++ b/drizzle-typebox/src/utils.ts @@ -0,0 +1,50 @@ +import type { Kind, Static, TSchema } from '@sinclair/typebox'; +import type { Column, SelectedFieldsFlat, Table, View } from 'drizzle-orm'; +import type { PgEnum } from 'drizzle-orm/pg-core'; +import type { literalSchema } from './column.ts'; + +export function isColumnType(column: Column, columnTypes: string[]): column is T { + return columnTypes.includes(column.columnType); +} + +export function isWithEnum(column: Column): column is typeof column & { enumValues: [string, ...string[]] } { + return 'enumValues' in column && Array.isArray(column.enumValues) && column.enumValues.length > 0; +} + +export const isPgEnum: (entity: any) => entity is PgEnum<[string, ...string[]]> = isWithEnum as any; + +type Literal = Static; +export type Json = Literal | { [key: string]: Json } | Json[]; +export interface JsonSchema extends TSchema { + [Kind]: 'Union'; + static: Json; + anyOf: Json; +} +export interface BufferSchema extends TSchema { + [Kind]: 'Buffer'; + static: Buffer; + type: 'buffer'; +} + +export type IsNever = [T] extends [never] ? true : false; + +export type ArrayHasAtLeastOneValue = TEnum extends [infer TString, ...any[]] + ? TString extends `${infer TLiteral}` ? TLiteral extends any ? true + : false + : false + : false; + +export type ColumnIsGeneratedAlwaysAs = TColumn['_']['identity'] extends 'always' ? true + : TColumn['_']['generated'] extends undefined ? false + : TColumn['_']['generated'] extends infer TGenerated extends { type: string } + ? TGenerated['type'] extends 'byDefault' ? false + : true + : true; + +export type RemoveNever = { + [K in keyof T as T[K] extends never ? never : K]: T[K]; +}; + +export type GetSelection | Table | View> = T extends Table ? T['_']['columns'] + : T extends View ? T['_']['selectedFields'] + : T; diff --git a/drizzle-typebox/tests/mysql.test.ts b/drizzle-typebox/tests/mysql.test.ts index d6942a529..213240368 100644 --- a/drizzle-typebox/tests/mysql.test.ts +++ b/drizzle-typebox/tests/mysql.test.ts @@ -1,378 +1,469 @@ -import { Type } from '@sinclair/typebox'; -import { Value } from '@sinclair/typebox/value'; -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 { expect, test } from 'vitest'; -import { createInsertSchema, createSelectSchema, jsonSchema } from '../src'; -import { expectSchemaShape } from './utils.ts'; - -const customInt = customType<{ data: number }>({ - dataType() { - return 'int'; - }, +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 { test } from 'vitest'; +import { jsonSchema } from '~/column.ts'; +import { CONSTANTS } from '~/constants.ts'; +import { createInsertSchema, createSelectSchema, createUpdateSchema } from '../src'; +import { Expect, expectSchemaShape } from './utils.ts'; + +const intSchema = t.Integer({ + minimum: CONSTANTS.INT32_MIN, + maximum: CONSTANTS.INT32_MAX, +}); +const serialNumberModeSchema = t.Integer({ + minimum: 0, + maximum: Number.MAX_SAFE_INTEGER, }); +const textSchema = t.String({ maxLength: CONSTANTS.INT16_UNSIGNED_MAX }); + +test('table - select', (tc) => { + const table = mysqlTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); -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 result = createSelectSchema(table); + const expected = t.Object({ id: serialNumberModeSchema, name: textSchema }); + expectSchemaShape(tc, expected).from(result); + Expect>(); }); -const testTableRow = { - bigint: BigInt(1), - bigintNumber: 1, - binary: 'binary', - boolean: true, - char: 'char', - charEnum: 'a', - customInt: { data: 1 }, - date: new Date(), - dateString: new Date().toISOString(), - datetime: new Date(), - datetimeString: new Date().toISOString(), - decimal: '1.1', - double: 1.1, - enum: 'a', - float: 1.1, - int: 1, - json: { data: 1 }, - mediumint: 1, - real: 1.1, - serial: 1, - smallint: 1, - text: 'text', - textEnum: 'a', - tinytext: 'tinytext', - tinytextEnum: 'a', - mediumtext: 'mediumtext', - mediumtextEnum: 'a', - longtext: 'longtext', - longtextEnum: 'a', - time: '00:00:00', - timestamp: new Date(), - timestampString: new Date().toISOString(), - tinyint: 1, - varbinary: 'A'.repeat(200), - varchar: 'A'.repeat(200), - varcharEnum: 'a', - year: 2021, - autoIncrement: 1, -}; - -test('insert valid row', () => { - const schema = createInsertSchema(testTable); - - expect(Value.Check( - schema, - testTableRow, - )).toBeTruthy(); +test('table in schema - select', (tc) => { + const schema = mysqlSchema('test'); + const table = schema.table('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + + const result = createSelectSchema(table); + const expected = t.Object({ id: serialNumberModeSchema, name: textSchema }); + expectSchemaShape(tc, expected).from(result); + Expect>(); }); -test('insert invalid varchar length', () => { - const schema = createInsertSchema(testTable); +test('table - insert', (tc) => { + const table = mysqlTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + age: int(), + }); - expect(Value.Check(schema, { - ...testTableRow, - varchar: 'A'.repeat(201), - })).toBeFalsy(); + const result = createInsertSchema(table); + const expected = t.Object({ + id: t.Optional(serialNumberModeSchema), + name: textSchema, + age: t.Optional(t.Union([intSchema, t.Null()])), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); }); -test('insert smaller char length should work', () => { - const schema = createInsertSchema(testTable); +test('table - update', (tc) => { + const table = mysqlTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + age: int(), + }); - expect(Value.Check(schema, { ...testTableRow, char: 'abc' })).toBeTruthy(); + const result = createUpdateSchema(table); + const expected = t.Object({ + id: t.Optional(serialNumberModeSchema), + name: t.Optional(textSchema), + age: t.Optional(t.Union([intSchema, t.Null()])), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); }); -test('insert larger char length should fail', () => { - const schema = createInsertSchema(testTable); +test('view qb - select', (tc) => { + const table = mysqlTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + const view = mysqlView('test').as((qb) => qb.select({ id: table.id, age: sql``.as('age') }).from(table)); - expect(Value.Check(schema, { ...testTableRow, char: 'abcde' })).toBeFalsy(); + const result = createSelectSchema(view); + const expected = t.Object({ id: serialNumberModeSchema, age: t.Any() }); + expectSchemaShape(tc, expected).from(result); + Expect>(); }); -test('insert schema', (t) => { - const actual = createInsertSchema(testTable); - - const expected = Type.Object({ - bigint: Type.BigInt(), - bigintNumber: Type.Number(), - binary: Type.String(), - boolean: Type.Boolean(), - char: Type.String({ minLength: 4, maxLength: 4 }), - charEnum: Type.Union([Type.Literal('a'), Type.Literal('b'), Type.Literal('c')]), - customInt: Type.Any(), - date: Type.Date(), - dateString: Type.String(), - datetime: Type.Date(), - datetimeString: Type.String(), - decimal: Type.String(), - double: Type.Number(), - enum: Type.Union([ - Type.Literal('a'), - Type.Literal('b'), - Type.Literal('c'), - ]), - float: Type.Number(), - int: Type.Number(), - json: jsonSchema, - mediumint: Type.Number(), - real: Type.Number(), - serial: Type.Optional(Type.Number()), - smallint: Type.Number(), - text: Type.String(), - textEnum: Type.Union([ - Type.Literal('a'), - Type.Literal('b'), - Type.Literal('c'), - ]), - tinytext: Type.String(), - tinytextEnum: Type.Union([ - Type.Literal('a'), - Type.Literal('b'), - Type.Literal('c'), - ]), - mediumtext: Type.String(), - mediumtextEnum: Type.Union([ - Type.Literal('a'), - Type.Literal('b'), - Type.Literal('c'), - ]), - longtext: Type.String(), - longtextEnum: Type.Union([ - Type.Literal('a'), - Type.Literal('b'), - Type.Literal('c'), - ]), - time: Type.String(), - timestamp: Type.Date(), - timestampString: Type.String(), - tinyint: Type.Number(), - varbinary: Type.String({ maxLength: 200 }), - varchar: Type.String({ maxLength: 200 }), - varcharEnum: Type.Union([ - Type.Literal('a'), - Type.Literal('b'), - Type.Literal('c'), - ]), - year: Type.Number(), - autoIncrement: Type.Optional(Type.Number()), +test('view columns - select', (tc) => { + const view = mysqlView('test', { + id: serial().primaryKey(), + name: text().notNull(), + }).as(sql``); + + const result = createSelectSchema(view); + const expected = t.Object({ id: serialNumberModeSchema, name: textSchema }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('view with nested fields - select', (tc) => { + const table = mysqlTable('test', { + id: serial().primaryKey(), + name: text().notNull(), }); + const view = mysqlView('test').as((qb) => + qb.select({ + id: table.id, + nested: { + name: table.name, + age: sql``.as('age'), + }, + table, + }).from(table) + ); - expectSchemaShape(t, expected).from(actual); + const result = createSelectSchema(view); + const expected = t.Object({ + id: serialNumberModeSchema, + nested: t.Object({ name: textSchema, age: t.Any() }), + table: t.Object({ id: serialNumberModeSchema, name: textSchema }), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); }); -test('select schema', (t) => { - const actual = createSelectSchema(testTable); - - const expected = Type.Object({ - bigint: Type.BigInt(), - bigintNumber: Type.Number(), - binary: Type.String(), - boolean: Type.Boolean(), - char: Type.String({ minLength: 4, maxLength: 4 }), - charEnum: Type.Union([ - Type.Literal('a'), - Type.Literal('b'), - Type.Literal('c'), - ]), - customInt: Type.Any(), - date: Type.Date(), - dateString: Type.String(), - datetime: Type.Date(), - datetimeString: Type.String(), - decimal: Type.String(), - double: Type.Number(), - enum: Type.Union([ - Type.Literal('a'), - Type.Literal('b'), - Type.Literal('c'), - ]), - float: Type.Number(), - int: Type.Number(), - // - json: jsonSchema, - mediumint: Type.Number(), - real: Type.Number(), - serial: Type.Number(), - smallint: Type.Number(), - text: Type.String(), - textEnum: Type.Union([ - Type.Literal('a'), - Type.Literal('b'), - Type.Literal('c'), - ]), - tinytext: Type.String(), - tinytextEnum: Type.Union([ - Type.Literal('a'), - Type.Literal('b'), - Type.Literal('c'), - ]), - mediumtext: Type.String(), - mediumtextEnum: Type.Union([ - Type.Literal('a'), - Type.Literal('b'), - Type.Literal('c'), - ]), - longtext: Type.String(), - longtextEnum: Type.Union([ - Type.Literal('a'), - Type.Literal('b'), - Type.Literal('c'), - ]), - time: Type.String(), - timestamp: Type.Date(), - timestampString: Type.String(), - tinyint: Type.Number(), - varbinary: Type.String({ maxLength: 200 }), - varchar: Type.String({ maxLength: 200 }), - varcharEnum: Type.Union([ - Type.Literal('a'), - Type.Literal('b'), - Type.Literal('c'), - ]), - year: Type.Number(), - autoIncrement: Type.Number(), +test('nullability - select', (tc) => { + const table = mysqlTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().default(1), + c4: int().notNull().default(1), }); - expectSchemaShape(t, expected).from(actual); + const result = createSelectSchema(table); + const expected = t.Object({ + c1: t.Union([intSchema, t.Null()]), + c2: intSchema, + c3: t.Union([intSchema, t.Null()]), + c4: intSchema, + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); }); -test('select schema w/ refine', (t) => { - const actual = createSelectSchema(testTable, { - bigint: (_) => Type.BigInt({ minimum: 0n }), +test('nullability - insert', (tc) => { + const table = mysqlTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().default(1), + c4: int().notNull().default(1), + c5: int().generatedAlwaysAs(1), }); - const expected = Type.Object({ - bigint: Type.BigInt({ minimum: 0n }), - bigintNumber: Type.Number(), - binary: Type.String(), - boolean: Type.Boolean(), - char: Type.String({ minLength: 5, maxLength: 5 }), - charEnum: Type.Union([Type.Literal('a'), Type.Literal('b'), Type.Literal('c')]), - customInt: Type.Any(), - date: Type.Date(), - dateString: Type.String(), - datetime: Type.Date(), - datetimeString: Type.String(), - decimal: Type.String(), - double: Type.Number(), - enum: Type.Union([ - Type.Literal('a'), - Type.Literal('b'), - Type.Literal('c'), - ]), - float: Type.Number(), - int: Type.Number(), - json: jsonSchema, - mediumint: Type.Number(), - real: Type.Number(), - serial: Type.Number(), - smallint: Type.Number(), - text: Type.String(), - textEnum: Type.Union([ - Type.Literal('a'), - Type.Literal('b'), - Type.Literal('c'), - ]), - tinytext: Type.String(), - tinytextEnum: Type.Union([ - Type.Literal('a'), - Type.Literal('b'), - Type.Literal('c'), - ]), - mediumtext: Type.String(), - mediumtextEnum: Type.Union([ - Type.Literal('a'), - Type.Literal('b'), - Type.Literal('c'), - ]), - longtext: Type.String(), - longtextEnum: Type.Union([ - Type.Literal('a'), - Type.Literal('b'), - Type.Literal('c'), - ]), - time: Type.String(), - timestamp: Type.Date(), - timestampString: Type.String(), - tinyint: Type.Number(), - varbinary: Type.String({ maxLength: 200 }), - varchar: Type.String({ maxLength: 200 }), - varcharEnum: Type.Union([ - Type.Literal('a'), - Type.Literal('b'), - Type.Literal('c'), - ]), - year: Type.Number(), - autoIncrement: Type.Number(), + const result = createInsertSchema(table); + const expected = t.Object({ + c1: t.Optional(t.Union([intSchema, t.Null()])), + c2: intSchema, + c3: t.Optional(t.Union([intSchema, t.Null()])), + c4: t.Optional(intSchema), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('nullability - update', (tc) => { + const table = mysqlTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().default(1), + c4: int().notNull().default(1), + c5: int().generatedAlwaysAs(1), + }); + + const result = createUpdateSchema(table); + const expected = t.Object({ + c1: t.Optional(t.Union([intSchema, t.Null()])), + c2: t.Optional(intSchema), + c3: t.Optional(t.Union([intSchema, t.Null()])), + c4: t.Optional(intSchema), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('refine table - select', (tc) => { + const table = mysqlTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().notNull(), + }); + + const result = createSelectSchema(table, { + c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }), + c3: t.Integer({ minimum: 1, maximum: 10 }), + }); + 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 }), + }); + + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('refine table - insert', (tc) => { + const table = mysqlTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().notNull(), + c4: int().generatedAlwaysAs(1), }); - expectSchemaShape(t, expected).from(actual); + const result = createInsertSchema(table, { + c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }), + c3: t.Integer({ minimum: 1, maximum: 10 }), + }); + const expected = t.Object({ + c1: t.Optional(t.Union([intSchema, t.Null()])), + c2: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }), + c3: t.Integer({ minimum: 1, maximum: 10 }), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); }); + +test('refine table - update', (tc) => { + const table = mysqlTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().notNull(), + c4: int().generatedAlwaysAs(1), + }); + + const result = createUpdateSchema(table, { + c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }), + c3: t.Integer({ minimum: 1, maximum: 10 }), + }); + const expected = t.Object({ + c1: t.Optional(t.Union([intSchema, t.Null()])), + c2: t.Optional(t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 })), + c3: t.Integer({ minimum: 1, maximum: 10 }), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('refine view - select', (tc) => { + const table = mysqlTable('test', { + c1: int(), + c2: int(), + c3: int(), + c4: int(), + c5: int(), + c6: int(), + }); + const view = mysqlView('test').as((qb) => + qb.select({ + c1: table.c1, + c2: table.c2, + c3: table.c3, + nested: { + c4: table.c4, + c5: table.c5, + c6: table.c6, + }, + table, + }).from(table) + ); + + const result = createSelectSchema(view, { + c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }), + c3: t.Integer({ minimum: 1, maximum: 10 }), + nested: { + c5: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }), + c6: t.Integer({ minimum: 1, maximum: 10 }), + }, + table: { + c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }), + c3: t.Integer({ minimum: 1, maximum: 10 }), + }, + }); + const expected = t.Object({ + c1: t.Union([intSchema, t.Null()]), + c2: t.Union([t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }), t.Null()]), + c3: t.Integer({ minimum: 1, maximum: 10 }), + nested: t.Object({ + c4: t.Union([intSchema, t.Null()]), + c5: t.Union([t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }), t.Null()]), + c6: t.Integer({ minimum: 1, maximum: 10 }), + }), + table: t.Object({ + c1: t.Union([intSchema, t.Null()]), + c2: t.Union([t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }), t.Null()]), + c3: t.Integer({ minimum: 1, maximum: 10 }), + c4: t.Union([intSchema, t.Null()]), + c5: t.Union([intSchema, t.Null()]), + c6: t.Union([intSchema, t.Null()]), + }), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('all data types', (tc) => { + const table = mysqlTable('test', ({ + bigint, + binary, + boolean, + char, + date, + datetime, + decimal, + double, + float, + int, + json, + mediumint, + mysqlEnum, + real, + serial, + smallint, + text, + time, + timestamp, + tinyint, + varchar, + varbinary, + year, + longtext, + mediumtext, + tinytext, + }) => ({ + bigint1: bigint({ mode: 'number' }).notNull(), + bigint2: bigint({ mode: 'bigint' }).notNull(), + bigint3: bigint({ unsigned: true, mode: 'number' }).notNull(), + bigint4: bigint({ unsigned: true, mode: 'bigint' }).notNull(), + binary: binary({ length: 10 }).notNull(), + boolean: boolean().notNull(), + char1: char({ length: 10 }).notNull(), + char2: char({ length: 1, enum: ['a', 'b', 'c'] }).notNull(), + date1: date({ mode: 'date' }).notNull(), + date2: date({ mode: 'string' }).notNull(), + datetime1: datetime({ mode: 'date' }).notNull(), + datetime2: datetime({ mode: 'string' }).notNull(), + decimal1: decimal().notNull(), + decimal2: decimal({ unsigned: true }).notNull(), + double1: double().notNull(), + double2: double({ unsigned: true }).notNull(), + float1: float().notNull(), + float2: float({ unsigned: true }).notNull(), + int1: int().notNull(), + int2: int({ unsigned: true }).notNull(), + json: json().notNull(), + mediumint1: mediumint().notNull(), + mediumint2: mediumint({ unsigned: true }).notNull(), + enum: mysqlEnum('enum', ['a', 'b', 'c']).notNull(), + real: real().notNull(), + serial: serial().notNull(), + smallint1: smallint().notNull(), + smallint2: smallint({ unsigned: true }).notNull(), + text1: text().notNull(), + text2: text({ enum: ['a', 'b', 'c'] }).notNull(), + time: time().notNull(), + timestamp1: timestamp({ mode: 'date' }).notNull(), + timestamp2: timestamp({ mode: 'string' }).notNull(), + tinyint1: tinyint().notNull(), + tinyint2: tinyint({ unsigned: true }).notNull(), + varchar1: varchar({ length: 10 }).notNull(), + varchar2: varchar({ length: 1, enum: ['a', 'b', 'c'] }).notNull(), + varbinary: varbinary({ length: 10 }).notNull(), + year: year().notNull(), + longtext1: longtext().notNull(), + longtext2: longtext({ enum: ['a', 'b', 'c'] }).notNull(), + mediumtext1: mediumtext().notNull(), + mediumtext2: mediumtext({ enum: ['a', 'b', 'c'] }).notNull(), + tinytext1: tinytext().notNull(), + tinytext2: tinytext({ enum: ['a', 'b', 'c'] }).notNull(), + })); + + const result = createSelectSchema(table); + const expected = t.Object({ + bigint1: t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER }), + bigint2: t.BigInt({ minimum: CONSTANTS.INT64_MIN, maximum: CONSTANTS.INT64_MAX }), + bigint3: t.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }), + bigint4: t.BigInt({ minimum: 0n, maximum: CONSTANTS.INT64_UNSIGNED_MAX }), + binary: t.String(), + boolean: t.Boolean(), + char1: t.String({ minLength: 10, maxLength: 10 }), + char2: t.Enum({ a: 'a', b: 'b', c: 'c' }), + date1: t.Date(), + date2: t.String(), + datetime1: t.Date(), + datetime2: t.String(), + decimal1: t.String(), + decimal2: t.String(), + double1: t.Number({ minimum: CONSTANTS.INT48_MIN, maximum: CONSTANTS.INT48_MAX }), + double2: t.Number({ minimum: 0, maximum: CONSTANTS.INT48_UNSIGNED_MAX }), + float1: t.Number({ minimum: CONSTANTS.INT24_MIN, maximum: CONSTANTS.INT24_MAX }), + float2: t.Number({ minimum: 0, maximum: CONSTANTS.INT24_UNSIGNED_MAX }), + int1: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: CONSTANTS.INT32_MAX }), + int2: t.Integer({ minimum: 0, maximum: CONSTANTS.INT32_UNSIGNED_MAX }), + json: jsonSchema, + mediumint1: t.Integer({ minimum: CONSTANTS.INT24_MIN, maximum: CONSTANTS.INT24_MAX }), + mediumint2: t.Integer({ minimum: 0, maximum: CONSTANTS.INT24_UNSIGNED_MAX }), + enum: t.Enum({ a: 'a', b: 'b', c: 'c' }), + real: t.Number({ minimum: CONSTANTS.INT48_MIN, maximum: CONSTANTS.INT48_MAX }), + serial: t.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }), + smallint1: t.Integer({ minimum: CONSTANTS.INT16_MIN, maximum: CONSTANTS.INT16_MAX }), + smallint2: t.Integer({ minimum: 0, maximum: CONSTANTS.INT16_UNSIGNED_MAX }), + text1: t.String({ maxLength: CONSTANTS.INT16_UNSIGNED_MAX }), + text2: t.Enum({ a: 'a', b: 'b', c: 'c' }), + time: t.String(), + timestamp1: t.Date(), + timestamp2: t.String(), + tinyint1: t.Integer({ minimum: CONSTANTS.INT8_MIN, maximum: CONSTANTS.INT8_MAX }), + tinyint2: t.Integer({ minimum: 0, maximum: CONSTANTS.INT8_UNSIGNED_MAX }), + varchar1: t.String({ maxLength: 10 }), + varchar2: t.Enum({ a: 'a', b: 'b', c: 'c' }), + varbinary: t.String(), + year: t.Integer({ minimum: 1901, maximum: 2155 }), + longtext1: t.String({ maxLength: CONSTANTS.INT32_UNSIGNED_MAX }), + longtext2: t.Enum({ a: 'a', b: 'b', c: 'c' }), + mediumtext1: t.String({ maxLength: CONSTANTS.INT24_UNSIGNED_MAX }), + mediumtext2: t.Enum({ a: 'a', b: 'b', c: 'c' }), + tinytext1: t.String({ maxLength: CONSTANTS.INT8_UNSIGNED_MAX }), + tinytext2: t.Enum({ a: 'a', b: 'b', c: 'c' }), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +/* Disallow unknown keys in table refinement - select */ { + const table = mysqlTable('test', { id: int() }); + // @ts-expect-error + createSelectSchema(table, { unknown: t.String() }); +} + +/* Disallow unknown keys in table refinement - insert */ { + const table = mysqlTable('test', { id: int() }); + // @ts-expect-error + createInsertSchema(table, { unknown: t.String() }); +} + +/* Disallow unknown keys in table refinement - update */ { + const table = mysqlTable('test', { id: int() }); + // @ts-expect-error + createUpdateSchema(table, { unknown: t.String() }); +} + +/* Disallow unknown keys in view qb - select */ { + const table = mysqlTable('test', { id: int() }); + const view = mysqlView('test').as((qb) => qb.select().from(table)); + const nestedSelect = mysqlView('test').as((qb) => qb.select({ table }).from(table)); + // @ts-expect-error + createSelectSchema(view, { unknown: t.String() }); + // @ts-expect-error + createSelectSchema(nestedSelect, { table: { unknown: t.String() } }); +} + +/* Disallow unknown keys in view columns - select */ { + const view = mysqlView('test', { id: int() }).as(sql``); + // @ts-expect-error + createSelectSchema(view, { unknown: t.String() }); +} diff --git a/drizzle-typebox/tests/pg.test.ts b/drizzle-typebox/tests/pg.test.ts index 355dee531..3e9769aef 100644 --- a/drizzle-typebox/tests/pg.test.ts +++ b/drizzle-typebox/tests/pg.test.ts @@ -1,189 +1,504 @@ -import { Type } from '@sinclair/typebox'; -import { Value } from '@sinclair/typebox/value'; -import { char, date, integer, pgEnum, pgTable, serial, text, timestamp, varchar } from 'drizzle-orm/pg-core'; -import { expect, test } from 'vitest'; -import { createInsertSchema, createSelectSchema, Nullable } from '../src'; -import { expectSchemaShape } from './utils.ts'; - -export const roleEnum = pgEnum('role', ['admin', 'user']); - -const users = pgTable('users', { - a: integer('a').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 testUser = { - a: [1, 2, 3], - id: 1, - name: 'John Doe', - email: 'john.doe@example.com', - birthdayString: '1990-01-01', - birthdayDate: new Date('1990-01-01'), - createdAt: new Date(), - role: 'admin', - roleText: 'admin', - roleText2: 'admin', - profession: 'Software Engineer', - initials: 'JD', -}; - -test('users insert valid user', () => { - const schema = createInsertSchema(users); - - expect(Value.Check(schema, testUser)).toBeTruthy(); -}); - -test('users insert invalid varchar', () => { - const schema = createInsertSchema(users); - - expect(Value.Check(schema, { - ...testUser, - profession: 'Chief Executive Officer', - })).toBeFalsy(); -}); - -test('users insert invalid char', () => { - const schema = createInsertSchema(users); - - expect(Value.Check(schema, { ...testUser, initials: 'JoDo' })).toBeFalsy(); -}); - -test('users insert schema', (t) => { - const actual = createInsertSchema(users, { - id: () => Type.Number({ minimum: 0 }), - email: () => Type.String(), - roleText: Type.Union([ - Type.Literal('user'), - Type.Literal('manager'), - Type.Literal('admin'), - ]), - }); - - (() => { - { - createInsertSchema(users, { - // @ts-expect-error (missing property) - foobar: Type.Number(), - }); - } - - { - createInsertSchema(users, { - // @ts-expect-error (invalid type) - id: 123, - }); - } - }); - - const expected = Type.Object({ - a: Type.Optional(Nullable(Type.Array(Type.Number()))), - id: Type.Optional(Type.Number({ minimum: 0 })), - name: Type.Optional(Nullable(Type.String())), - email: Type.String(), - birthdayString: Type.String(), - birthdayDate: Type.Date(), - createdAt: Type.Optional(Type.Date()), - role: Type.Union([Type.Literal('admin'), Type.Literal('user')]), - roleText: Type.Union([ - Type.Literal('user'), - Type.Literal('manager'), - Type.Literal('admin'), - ]), - roleText2: Type.Optional( - Type.Union([Type.Literal('admin'), Type.Literal('user')]), - ), - profession: Type.String({ maxLength: 20, minLength: 1 }), - initials: Type.String({ maxLength: 2, minLength: 1 }), - }); - - expectSchemaShape(t, expected).from(actual); -}); - -test('users insert schema w/ defaults', (t) => { - const actual = createInsertSchema(users); - - const expected = Type.Object({ - a: Type.Optional(Nullable(Type.Array(Type.Number()))), - id: Type.Optional(Type.Number()), - name: Type.Optional(Nullable(Type.String())), - email: Type.String(), - birthdayString: Type.String(), - birthdayDate: Type.Date(), - createdAt: Type.Optional(Type.Date()), - role: Type.Union([Type.Literal('admin'), Type.Literal('user')]), - roleText: Type.Union([Type.Literal('admin'), Type.Literal('user')]), - roleText2: Type.Optional( - Type.Union([Type.Literal('admin'), Type.Literal('user')]), - ), - profession: Type.String({ maxLength: 20, minLength: 1 }), - initials: Type.String({ maxLength: 2, minLength: 1 }), - }); - - expectSchemaShape(t, expected).from(actual); -}); - -test('users select schema', (t) => { - const actual = createSelectSchema(users, { - id: () => Type.Number({ minimum: 0 }), - email: () => Type.String(), - roleText: Type.Union([ - Type.Literal('admin'), - Type.Literal('user'), - Type.Literal('manager'), - ]), - }); - - const expected = Type.Object({ - a: Nullable(Type.Array(Type.Number())), - id: Type.Number({ minimum: 0 }), - name: Nullable(Type.String()), - email: Type.String(), - birthdayString: Type.String(), - birthdayDate: Type.Date(), - createdAt: Type.Date(), - role: Type.Union([Type.Literal('admin'), Type.Literal('user')]), - roleText: Type.Union([ - Type.Literal('admin'), - Type.Literal('user'), - Type.Literal('manager'), - ]), - roleText2: Type.Union([Type.Literal('admin'), Type.Literal('user')]), - profession: Type.String({ maxLength: 20, minLength: 1 }), - initials: Type.String({ maxLength: 2, minLength: 1 }), - }); - - expectSchemaShape(t, expected).from(actual); -}); - -test('users select schema w/ defaults', (t) => { - const actual = createSelectSchema(users); - - const expected = Type.Object({ - a: Nullable(Type.Array(Type.Number())), - id: Type.Number(), - name: Nullable(Type.String()), - email: Type.String(), - birthdayString: Type.String(), - birthdayDate: Type.Date(), - createdAt: Type.Date(), - role: Type.Union([Type.Literal('admin'), Type.Literal('user')]), - roleText: Type.Union([Type.Literal('admin'), Type.Literal('user')]), - roleText2: Type.Union([Type.Literal('admin'), Type.Literal('user')]), - profession: Type.String({ maxLength: 20, minLength: 1 }), - initials: Type.String({ maxLength: 2, minLength: 1 }), - }); - - expectSchemaShape(t, expected).from(actual); +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 { test } from 'vitest'; +import { jsonSchema } from '~/column.ts'; +import { CONSTANTS } from '~/constants.ts'; +import { createInsertSchema, createSelectSchema, createUpdateSchema } from '../src'; +import { Expect, expectEnumValues, expectSchemaShape } from './utils.ts'; + +const integerSchema = t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: CONSTANTS.INT32_MAX }); +const textSchema = t.String(); + +test('table - select', (tc) => { + const table = pgTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + + const result = createSelectSchema(table); + const expected = t.Object({ id: integerSchema, name: textSchema }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('table in schema - select', (tc) => { + const schema = pgSchema('test'); + const table = schema.table('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + + const result = createSelectSchema(table); + const expected = t.Object({ id: integerSchema, name: textSchema }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('table - insert', (tc) => { + const table = pgTable('test', { + id: integer().generatedAlwaysAsIdentity().primaryKey(), + name: text().notNull(), + age: integer(), + }); + + const result = createInsertSchema(table); + const expected = t.Object({ name: textSchema, age: t.Optional(t.Union([integerSchema, t.Null()])) }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('table - update', (tc) => { + const table = pgTable('test', { + id: integer().generatedAlwaysAsIdentity().primaryKey(), + name: text().notNull(), + age: integer(), + }); + + const result = createUpdateSchema(table); + const expected = t.Object({ + name: t.Optional(textSchema), + age: t.Optional(t.Union([integerSchema, t.Null()])), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('view qb - select', (tc) => { + const table = pgTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + const view = pgView('test').as((qb) => qb.select({ id: table.id, age: sql``.as('age') }).from(table)); + + const result = createSelectSchema(view); + const expected = t.Object({ id: integerSchema, age: t.Any() }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('view columns - select', (tc) => { + const view = pgView('test', { + id: serial().primaryKey(), + name: text().notNull(), + }).as(sql``); + + const result = createSelectSchema(view); + const expected = t.Object({ id: integerSchema, name: textSchema }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('materialized view qb - select', (tc) => { + const table = pgTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + const view = pgMaterializedView('test').as((qb) => qb.select({ id: table.id, age: sql``.as('age') }).from(table)); + + const result = createSelectSchema(view); + const expected = t.Object({ id: integerSchema, age: t.Any() }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('materialized view columns - select', (tc) => { + const view = pgView('test', { + id: serial().primaryKey(), + name: text().notNull(), + }).as(sql``); + + const result = createSelectSchema(view); + const expected = t.Object({ id: integerSchema, name: textSchema }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('view with nested fields - select', (tc) => { + const table = pgTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + const view = pgMaterializedView('test').as((qb) => + qb.select({ + id: table.id, + nested: { + name: table.name, + age: sql``.as('age'), + }, + table, + }).from(table) + ); + + const result = createSelectSchema(view); + const expected = t.Object({ + id: integerSchema, + nested: t.Object({ name: textSchema, age: t.Any() }), + table: t.Object({ id: integerSchema, name: textSchema }), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('enum - select', (tc) => { + const enum_ = pgEnum('test', ['a', 'b', 'c']); + + const result = createSelectSchema(enum_); + const expected = t.Enum({ a: 'a', b: 'b', c: 'c' }); + expectEnumValues(tc, expected).from(result); + Expect>(); +}); + +test('nullability - select', (tc) => { + const table = pgTable('test', { + c1: integer(), + c2: integer().notNull(), + c3: integer().default(1), + c4: integer().notNull().default(1), + }); + + const result = createSelectSchema(table); + const expected = t.Object({ + c1: t.Union([integerSchema, t.Null()]), + c2: integerSchema, + c3: t.Union([integerSchema, t.Null()]), + c4: integerSchema, + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); }); + +test('nullability - insert', (tc) => { + const table = pgTable('test', { + c1: integer(), + c2: integer().notNull(), + c3: integer().default(1), + c4: integer().notNull().default(1), + c5: integer().generatedAlwaysAs(1), + c6: integer().generatedAlwaysAsIdentity(), + c7: integer().generatedByDefaultAsIdentity(), + }); + + const result = createInsertSchema(table); + const expected = t.Object({ + c1: t.Optional(t.Union([integerSchema, t.Null()])), + c2: integerSchema, + c3: t.Optional(t.Union([integerSchema, t.Null()])), + c4: t.Optional(integerSchema), + c7: t.Optional(integerSchema), + }); + expectSchemaShape(tc, expected).from(result); +}); + +test('nullability - update', (tc) => { + const table = pgTable('test', { + c1: integer(), + c2: integer().notNull(), + c3: integer().default(1), + c4: integer().notNull().default(1), + c5: integer().generatedAlwaysAs(1), + c6: integer().generatedAlwaysAsIdentity(), + c7: integer().generatedByDefaultAsIdentity(), + }); + + const result = createUpdateSchema(table); + const expected = t.Object({ + c1: t.Optional(t.Union([integerSchema, t.Null()])), + c2: t.Optional(integerSchema), + c3: t.Optional(t.Union([integerSchema, t.Null()])), + c4: t.Optional(integerSchema), + c7: t.Optional(integerSchema), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('refine table - select', (tc) => { + const table = pgTable('test', { + c1: integer(), + c2: integer().notNull(), + c3: integer().notNull(), + }); + + const result = createSelectSchema(table, { + c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }), + c3: t.Integer({ minimum: 1, maximum: 10 }), + }); + 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 }), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('refine table - insert', (tc) => { + const table = pgTable('test', { + c1: integer(), + c2: integer().notNull(), + c3: integer().notNull(), + c4: integer().generatedAlwaysAs(1), + }); + + const result = createInsertSchema(table, { + c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }), + c3: t.Integer({ minimum: 1, maximum: 10 }), + }); + const expected = t.Object({ + c1: t.Optional(t.Union([integerSchema, t.Null()])), + c2: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }), + c3: t.Integer({ minimum: 1, maximum: 10 }), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('refine table - update', (tc) => { + const table = pgTable('test', { + c1: integer(), + c2: integer().notNull(), + c3: integer().notNull(), + c4: integer().generatedAlwaysAs(1), + }); + + const result = createUpdateSchema(table, { + c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }), + c3: t.Integer({ minimum: 1, maximum: 10 }), + }); + const expected = t.Object({ + c1: t.Optional(t.Union([integerSchema, t.Null()])), + c2: t.Optional(t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 })), + c3: t.Integer({ minimum: 1, maximum: 10 }), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('refine view - select', (tc) => { + const table = pgTable('test', { + c1: integer(), + c2: integer(), + c3: integer(), + c4: integer(), + c5: integer(), + c6: integer(), + }); + const view = pgView('test').as((qb) => + qb.select({ + c1: table.c1, + c2: table.c2, + c3: table.c3, + nested: { + c4: table.c4, + c5: table.c5, + c6: table.c6, + }, + table, + }).from(table) + ); + + const result = createSelectSchema(view, { + c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }), + c3: t.Integer({ minimum: 1, maximum: 10 }), + nested: { + c5: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }), + c6: t.Integer({ minimum: 1, maximum: 10 }), + }, + table: { + c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }), + c3: t.Integer({ minimum: 1, maximum: 10 }), + }, + }); + const expected = t.Object({ + c1: t.Union([integerSchema, t.Null()]), + c2: t.Union([t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }), t.Null()]), + c3: t.Integer({ minimum: 1, maximum: 10 }), + nested: t.Object({ + c4: t.Union([integerSchema, t.Null()]), + c5: t.Union([t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }), t.Null()]), + c6: t.Integer({ minimum: 1, maximum: 10 }), + }), + table: t.Object({ + c1: t.Union([integerSchema, t.Null()]), + c2: t.Union([t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }), t.Null()]), + c3: t.Integer({ minimum: 1, maximum: 10 }), + c4: t.Union([integerSchema, t.Null()]), + c5: t.Union([integerSchema, t.Null()]), + c6: t.Union([integerSchema, t.Null()]), + }), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('all data types', (tc) => { + const table = pgTable('test', ({ + bigint, + bigserial, + bit, + boolean, + date, + char, + cidr, + doublePrecision, + geometry, + halfvec, + inet, + integer, + interval, + json, + jsonb, + line, + macaddr, + macaddr8, + numeric, + point, + real, + serial, + smallint, + smallserial, + text, + sparsevec, + time, + timestamp, + uuid, + varchar, + vector, + }) => ({ + bigint1: bigint({ mode: 'number' }).notNull(), + bigint2: bigint({ mode: 'bigint' }).notNull(), + bigserial1: bigserial({ mode: 'number' }).notNull(), + bigserial2: bigserial({ mode: 'bigint' }).notNull(), + bit: bit({ dimensions: 5 }).notNull(), + boolean: boolean().notNull(), + date1: date({ mode: 'date' }).notNull(), + date2: date({ mode: 'string' }).notNull(), + char1: char({ length: 10 }).notNull(), + char2: char({ length: 1, enum: ['a', 'b', 'c'] }).notNull(), + cidr: cidr().notNull(), + doublePrecision: doublePrecision().notNull(), + geometry1: geometry({ type: 'point', mode: 'tuple' }).notNull(), + geometry2: geometry({ type: 'point', mode: 'xy' }).notNull(), + halfvec: halfvec({ dimensions: 3 }).notNull(), + inet: inet().notNull(), + integer: integer().notNull(), + interval: interval().notNull(), + json: json().notNull(), + jsonb: jsonb().notNull(), + line1: line({ mode: 'abc' }).notNull(), + line2: line({ mode: 'tuple' }).notNull(), + macaddr: macaddr().notNull(), + macaddr8: macaddr8().notNull(), + numeric: numeric().notNull(), + point1: point({ mode: 'xy' }).notNull(), + point2: point({ mode: 'tuple' }).notNull(), + real: real().notNull(), + serial: serial().notNull(), + smallint: smallint().notNull(), + smallserial: smallserial().notNull(), + text1: text().notNull(), + text2: text({ enum: ['a', 'b', 'c'] }).notNull(), + sparsevec: sparsevec({ dimensions: 3 }).notNull(), + time: time().notNull(), + timestamp1: timestamp({ mode: 'date' }).notNull(), + timestamp2: timestamp({ mode: 'string' }).notNull(), + uuid: uuid().notNull(), + varchar1: varchar({ length: 10 }).notNull(), + varchar2: varchar({ length: 1, enum: ['a', 'b', 'c'] }).notNull(), + vector: vector({ dimensions: 3 }).notNull(), + array1: integer().array().notNull(), + array2: integer().array().array(2).notNull(), + array3: varchar({ length: 10 }).array().array(2).notNull(), + })); + + const result = createSelectSchema(table); + const expected = t.Object({ + bigint1: t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER }), + bigint2: t.BigInt({ minimum: CONSTANTS.INT64_MIN, maximum: CONSTANTS.INT64_MAX }), + bigserial1: t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER }), + bigserial2: t.BigInt({ minimum: CONSTANTS.INT64_MIN, maximum: CONSTANTS.INT64_MAX }), + bit: t.RegExp(/^[01]+$/, { maxLength: 5 }), + boolean: t.Boolean(), + date1: t.Date(), + date2: t.String(), + char1: t.String({ minLength: 10, maxLength: 10 }), + char2: t.Enum({ a: 'a', b: 'b', c: 'c' }), + cidr: t.String(), + doublePrecision: t.Number({ minimum: CONSTANTS.INT48_MIN, maximum: CONSTANTS.INT48_MAX }), + geometry1: t.Tuple([t.Number(), t.Number()]), + geometry2: t.Object({ x: t.Number(), y: t.Number() }), + halfvec: t.Array(t.Number(), { minItems: 3, maxItems: 3 }), + inet: t.String(), + integer: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: CONSTANTS.INT32_MAX }), + interval: t.String(), + json: jsonSchema, + jsonb: jsonSchema, + line1: t.Object({ a: t.Number(), b: t.Number(), c: t.Number() }), + line2: t.Tuple([t.Number(), t.Number(), t.Number()]), + macaddr: t.String(), + macaddr8: t.String(), + numeric: t.String(), + point1: t.Object({ x: t.Number(), y: t.Number() }), + point2: t.Tuple([t.Number(), t.Number()]), + real: t.Number({ minimum: CONSTANTS.INT24_MIN, maximum: CONSTANTS.INT24_MAX }), + serial: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: CONSTANTS.INT32_MAX }), + smallint: t.Integer({ minimum: CONSTANTS.INT16_MIN, maximum: CONSTANTS.INT16_MAX }), + smallserial: t.Integer({ minimum: CONSTANTS.INT16_MIN, maximum: CONSTANTS.INT16_MAX }), + text1: t.String(), + text2: t.Enum({ a: 'a', b: 'b', c: 'c' }), + sparsevec: t.String(), + time: t.String(), + timestamp1: t.Date(), + timestamp2: t.String(), + uuid: t.String({ format: 'uuid' }), + varchar1: t.String({ maxLength: 10 }), + varchar2: t.Enum({ a: 'a', b: 'b', c: 'c' }), + vector: t.Array(t.Number(), { minItems: 3, maxItems: 3 }), + array1: t.Array(integerSchema), + array2: t.Array(t.Array(integerSchema), { minItems: 2, maxItems: 2 }), + array3: t.Array(t.Array(t.String({ maxLength: 10 })), { minItems: 2, maxItems: 2 }), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +/* Disallow unknown keys in table refinement - select */ { + const table = pgTable('test', { id: integer() }); + // @ts-expect-error + createSelectSchema(table, { unknown: t.String() }); +} + +/* Disallow unknown keys in table refinement - insert */ { + const table = pgTable('test', { id: integer() }); + // @ts-expect-error + createInsertSchema(table, { unknown: t.String() }); +} + +/* Disallow unknown keys in table refinement - update */ { + const table = pgTable('test', { id: integer() }); + // @ts-expect-error + createUpdateSchema(table, { unknown: t.String() }); +} + +/* Disallow unknown keys in view qb - select */ { + const table = pgTable('test', { id: integer() }); + const view = pgView('test').as((qb) => qb.select().from(table)); + const mView = pgMaterializedView('test').as((qb) => qb.select().from(table)); + const nestedSelect = pgView('test').as((qb) => qb.select({ table }).from(table)); + // @ts-expect-error + createSelectSchema(view, { unknown: t.String() }); + // @ts-expect-error + createSelectSchema(mView, { unknown: t.String() }); + // @ts-expect-error + createSelectSchema(nestedSelect, { table: { unknown: t.String() } }); +} + +/* Disallow unknown keys in view columns - select */ { + const view = pgView('test', { id: integer() }).as(sql``); + const mView = pgView('test', { id: integer() }).as(sql``); + // @ts-expect-error + createSelectSchema(view, { unknown: t.String() }); + // @ts-expect-error + createSelectSchema(mView, { unknown: t.String() }); +} diff --git a/drizzle-typebox/tests/sqlite.test.ts b/drizzle-typebox/tests/sqlite.test.ts index a8506a269..ba2b55002 100644 --- a/drizzle-typebox/tests/sqlite.test.ts +++ b/drizzle-typebox/tests/sqlite.test.ts @@ -1,189 +1,363 @@ -import { type Static, Type } from '@sinclair/typebox'; -import { Value } from '@sinclair/typebox/value'; -import { blob, integer, numeric, real, sqliteTable, text } from 'drizzle-orm/sqlite-core'; -import { expect, test } from 'vitest'; -import { createInsertSchema, createSelectSchema, jsonSchema, Nullable } from '../src'; -import { expectSchemaShape } from './utils.ts'; - -const blobJsonSchema = Type.Object({ - foo: Type.String(), -}); +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 { test } from 'vitest'; +import { bufferSchema, jsonSchema } from '~/column.ts'; +import { CONSTANTS } from '~/constants.ts'; +import { createInsertSchema, createSelectSchema, createUpdateSchema } from '../src'; +import { Expect, expectSchemaShape } from './utils.ts'; + +const intSchema = t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER }); +const textSchema = t.String(); + +test('table - select', (tc) => { + const table = sqliteTable('test', { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + }); -const users = 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 result = createSelectSchema(table); + const expected = t.Object({ id: intSchema, name: textSchema }); + expectSchemaShape(tc, expected).from(result); + Expect>(); }); -const testUser = { - id: 1, - blobJson: { foo: 'bar' }, - blobBigInt: BigInt(123), - numeric: '123.45', - createdAt: new Date(), - createdAtMs: new Date(), - boolean: true, - real: 123.45, - text: 'foobar', - role: 'admin', -}; - -test('users insert valid user', () => { - const schema = createInsertSchema(users); - // - expect(Value.Check(schema, testUser)).toBeTruthy(); +test('table - insert', (tc) => { + const table = sqliteTable('test', { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + age: int(), + }); + + const result = createInsertSchema(table); + const expected = t.Object({ + id: t.Optional(intSchema), + name: textSchema, + age: t.Optional(t.Union([intSchema, t.Null()])), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); }); -test('users insert invalid text length', () => { - const schema = createInsertSchema(users); +test('table - update', (tc) => { + const table = sqliteTable('test', { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + age: int(), + }); - expect(Value.Check(schema, { ...testUser, text: 'a'.repeat(256) })).toBeFalsy(); + const result = createUpdateSchema(table); + const expected = t.Object({ + id: t.Optional(intSchema), + name: t.Optional(textSchema), + age: t.Optional(t.Union([intSchema, t.Null()])), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); }); -test('users insert schema', (t) => { - const actual = createInsertSchema(users, { - id: () => Type.Number({ minimum: 0 }), - blobJson: blobJsonSchema, - role: Type.Union([ - Type.Literal('admin'), - Type.Literal('user'), - Type.Literal('manager'), - ]), - }); - - (() => { - { - createInsertSchema(users, { - // @ts-expect-error (missing property) - foobar: Type.Number(), - }); - } - - { - createInsertSchema(users, { - // @ts-expect-error (invalid type) - id: 123, - }); - } - }); - - const expected = Type.Object({ - id: Type.Optional(Type.Number({ minimum: 0 })), - blobJson: blobJsonSchema, - blobBigInt: Type.BigInt(), - numeric: Type.String(), - createdAt: Type.Date(), - createdAtMs: Type.Date(), - boolean: Type.Boolean(), - real: Type.Number(), - text: Type.Optional(Nullable(Type.String())), - role: Type.Optional( - Type.Union([ - Type.Literal('admin'), - Type.Literal('user'), - Type.Literal('manager'), - ]), - ), - }); - - expectSchemaShape(t, expected).from(actual); +test('view qb - select', (tc) => { + const table = sqliteTable('test', { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + }); + const view = sqliteView('test').as((qb) => qb.select({ id: table.id, age: sql``.as('age') }).from(table)); + + const result = createSelectSchema(view); + const expected = t.Object({ id: intSchema, age: t.Any() }); + expectSchemaShape(tc, expected).from(result); + Expect>(); }); -test('users insert schema w/ defaults', (t) => { - const actual = createInsertSchema(users); - - const expected = Type.Object({ - id: Type.Optional(Type.Number()), - blobJson: jsonSchema, - blobBigInt: Type.BigInt(), - numeric: Type.String(), - createdAt: Type.Date(), - createdAtMs: Type.Date(), - boolean: Type.Boolean(), - real: Type.Number(), - text: Type.Optional(Nullable(Type.String())), - role: Type.Optional( - Type.Union([Type.Literal('admin'), Type.Literal('user')]), - ), - }); - - expectSchemaShape(t, expected).from(actual); +test('view columns - select', (tc) => { + const view = sqliteView('test', { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + }).as(sql``); + + const result = createSelectSchema(view); + const expected = t.Object({ id: intSchema, name: textSchema }); + expectSchemaShape(tc, expected).from(result); + Expect>(); }); -test('users select schema', (t) => { - const actual = createSelectSchema(users, { - blobJson: jsonSchema, - role: Type.Union([ - Type.Literal('admin'), - Type.Literal('user'), - Type.Literal('manager'), - ]), - }); - - (() => { - { - createSelectSchema(users, { - // @ts-expect-error (missing property) - foobar: Type.Number(), - }); - } - - { - createSelectSchema(users, { - // @ts-expect-error (invalid type) - id: 123, - }); - } - }); - - const expected = Type.Strict( - Type.Object({ - id: Type.Number(), - blobJson: jsonSchema, - blobBigInt: Type.BigInt(), - numeric: Type.String(), - createdAt: Type.Date(), - createdAtMs: Type.Date(), - boolean: Type.Boolean(), - real: Type.Number(), - text: Nullable(Type.String()), - role: Type.Union([ - Type.Literal('admin'), - Type.Literal('user'), - Type.Literal('manager'), - ]), - }), +test('view with nested fields - select', (tc) => { + const table = sqliteTable('test', { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + }); + const view = sqliteView('test').as((qb) => + qb.select({ + id: table.id, + nested: { + name: table.name, + age: sql``.as('age'), + }, + table, + }).from(table) ); - expectSchemaShape(t, expected).from(actual); + const result = createSelectSchema(view); + const expected = t.Object({ + id: intSchema, + nested: t.Object({ name: textSchema, age: t.Any() }), + table: t.Object({ id: intSchema, name: textSchema }), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); }); -test('users select schema w/ defaults', (t) => { - const actual = createSelectSchema(users); +test('nullability - select', (tc) => { + const table = sqliteTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().default(1), + c4: int().notNull().default(1), + }); - const expected = Type.Object({ - id: Type.Number(), - blobJson: jsonSchema, - blobBigInt: Type.BigInt(), - numeric: Type.String(), - createdAt: Type.Date(), - createdAtMs: Type.Date(), - boolean: Type.Boolean(), - real: Type.Number(), - text: Nullable(Type.String()), - role: Type.Union([Type.Literal('admin'), Type.Literal('user')]), + const result = createSelectSchema(table); + const expected = t.Object({ + c1: t.Union([intSchema, t.Null()]), + c2: intSchema, + c3: t.Union([intSchema, t.Null()]), + c4: intSchema, + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('nullability - insert', (tc) => { + const table = sqliteTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().default(1), + c4: int().notNull().default(1), + c5: int().generatedAlwaysAs(1), + }); + + const result = createInsertSchema(table); + const expected = t.Object({ + c1: t.Optional(t.Union([intSchema, t.Null()])), + c2: intSchema, + c3: t.Optional(t.Union([intSchema, t.Null()])), + c4: t.Optional(intSchema), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('nullability - update', (tc) => { + const table = sqliteTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().default(1), + c4: int().notNull().default(1), + c5: int().generatedAlwaysAs(1), }); - expectSchemaShape(t, expected).from(actual); + const result = createUpdateSchema(table); + const expected = t.Object({ + c1: t.Optional(t.Union([intSchema, t.Null()])), + c2: t.Optional(intSchema), + c3: t.Optional(t.Union([intSchema, t.Null()])), + c4: t.Optional(intSchema), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('refine table - select', (tc) => { + const table = sqliteTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().notNull(), + }); + + const result = createSelectSchema(table, { + c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }), + c3: t.Integer({ minimum: 1, maximum: 10 }), + }); + 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 }), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); }); + +test('refine table - insert', (tc) => { + const table = sqliteTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().notNull(), + c4: int().generatedAlwaysAs(1), + }); + + const result = createInsertSchema(table, { + c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }), + c3: t.Integer({ minimum: 1, maximum: 10 }), + }); + const expected = t.Object({ + c1: t.Optional(t.Union([intSchema, t.Null()])), + c2: t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: 1000 }), + c3: t.Integer({ minimum: 1, maximum: 10 }), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('refine table - update', (tc) => { + const table = sqliteTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().notNull(), + c4: int().generatedAlwaysAs(1), + }); + + const result = createUpdateSchema(table, { + c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }), + c3: t.Integer({ minimum: 1, maximum: 10 }), + }); + const expected = t.Object({ + c1: t.Optional(t.Union([intSchema, t.Null()])), + c2: t.Optional(t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: 1000 })), + c3: t.Integer({ minimum: 1, maximum: 10 }), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('refine view - select', (tc) => { + const table = sqliteTable('test', { + c1: int(), + c2: int(), + c3: int(), + c4: int(), + c5: int(), + c6: int(), + }); + const view = sqliteView('test').as((qb) => + qb.select({ + c1: table.c1, + c2: table.c2, + c3: table.c3, + nested: { + c4: table.c4, + c5: table.c5, + c6: table.c6, + }, + table, + }).from(table) + ); + + const result = createSelectSchema(view, { + c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }), + c3: t.Integer({ minimum: 1, maximum: 10 }), + nested: { + c5: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }), + c6: t.Integer({ minimum: 1, maximum: 10 }), + }, + table: { + c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }), + c3: t.Integer({ minimum: 1, maximum: 10 }), + }, + }); + const expected = t.Object({ + c1: t.Union([intSchema, t.Null()]), + c2: t.Union([t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: 1000 }), t.Null()]), + c3: t.Integer({ minimum: 1, maximum: 10 }), + nested: t.Object({ + c4: t.Union([intSchema, t.Null()]), + c5: t.Union([t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: 1000 }), t.Null()]), + c6: t.Integer({ minimum: 1, maximum: 10 }), + }), + table: t.Object({ + c1: t.Union([intSchema, t.Null()]), + c2: t.Union([t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: 1000 }), t.Null()]), + c3: t.Integer({ minimum: 1, maximum: 10 }), + c4: t.Union([intSchema, t.Null()]), + c5: t.Union([intSchema, t.Null()]), + c6: t.Union([intSchema, t.Null()]), + }), + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('all data types', (tc) => { + const table = sqliteTable('test', ({ + blob, + integer, + numeric, + real, + text, + }) => ({ + blob1: blob({ mode: 'buffer' }).notNull(), + blob2: blob({ mode: 'bigint' }).notNull(), + blob3: blob({ mode: 'json' }).notNull(), + integer1: integer({ mode: 'number' }).notNull(), + integer2: integer({ mode: 'boolean' }).notNull(), + integer3: integer({ mode: 'timestamp' }).notNull(), + integer4: integer({ mode: 'timestamp_ms' }).notNull(), + numeric: numeric().notNull(), + real: real().notNull(), + text1: text({ mode: 'text' }).notNull(), + text2: text({ mode: 'text', length: 10 }).notNull(), + text3: text({ mode: 'text', enum: ['a', 'b', 'c'] }).notNull(), + text4: text({ mode: 'json' }).notNull(), + })); + + const result = createSelectSchema(table); + const expected = t.Object({ + blob1: bufferSchema, + blob2: t.BigInt({ minimum: CONSTANTS.INT64_MIN, maximum: CONSTANTS.INT64_MAX }), + blob3: jsonSchema, + integer1: t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER }), + integer2: t.Boolean(), + integer3: t.Date(), + integer4: t.Date(), + numeric: t.String(), + real: t.Number({ minimum: CONSTANTS.INT48_MIN, maximum: CONSTANTS.INT48_MAX }), + text1: t.String(), + text2: t.String({ maxLength: 10 }), + text3: t.Enum({ a: 'a', b: 'b', c: 'c' }), + text4: jsonSchema, + }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +/* Disallow unknown keys in table refinement - select */ { + const table = sqliteTable('test', { id: int() }); + // @ts-expect-error + createSelectSchema(table, { unknown: t.String() }); +} + +/* Disallow unknown keys in table refinement - insert */ { + const table = sqliteTable('test', { id: int() }); + // @ts-expect-error + createInsertSchema(table, { unknown: t.String() }); +} + +/* Disallow unknown keys in table refinement - update */ { + const table = sqliteTable('test', { id: int() }); + // @ts-expect-error + createUpdateSchema(table, { unknown: t.String() }); +} + +/* Disallow unknown keys in view qb - select */ { + const table = sqliteTable('test', { id: int() }); + const view = sqliteView('test').as((qb) => qb.select().from(table)); + const nestedSelect = sqliteView('test').as((qb) => qb.select({ table }).from(table)); + // @ts-expect-error + createSelectSchema(view, { unknown: t.String() }); + // @ts-expect-error + createSelectSchema(nestedSelect, { table: { unknown: t.String() } }); +} + +/* Disallow unknown keys in view columns - select */ { + const view = sqliteView('test', { id: int() }).as(sql``); + // @ts-expect-error + createSelectSchema(view, { unknown: t.String() }); +} diff --git a/drizzle-typebox/tests/utils.ts b/drizzle-typebox/tests/utils.ts index e17e5f26d..46cd16a32 100644 --- a/drizzle-typebox/tests/utils.ts +++ b/drizzle-typebox/tests/utils.ts @@ -1,17 +1,34 @@ -import type { TSchema } from '@sinclair/typebox'; +import type * as t from '@sinclair/typebox'; import { expect, type TaskContext } from 'vitest'; -export function expectSchemaShape(t: TaskContext, expected: T) { +function removeKeysFromObject(obj: Record, keys: string[]) { + for (const key of keys) { + delete obj[key]; + } + return obj; +} + +export function expectSchemaShape(t: TaskContext, expected: T) { return { from(actual: T) { - expect(Object.keys(actual)).toStrictEqual(Object.keys(expected)); + expect(Object.keys(actual.properties)).toStrictEqual(Object.keys(expected.properties)); + const keys = ['$id', '$schema', 'title', 'description', 'default', 'examples', 'readOnly', 'writeOnly']; - for (const key of Object.keys(actual)) { - expect(actual[key].type).toStrictEqual(expected[key]?.type); - if (actual[key].optional) { - expect(actual[key].optional).toStrictEqual(expected[key]?.optional); - } + for (const key of Object.keys(actual.properties)) { + expect(removeKeysFromObject(actual.properties[key]!, keys)).toStrictEqual( + removeKeysFromObject(expected.properties[key]!, keys), + ); } }, }; } + +export function expectEnumValues>(t: TaskContext, expected: T) { + return { + from(actual: T) { + expect(actual.anyOf).toStrictEqual(expected.anyOf); + }, + }; +} + +export function Expect<_ extends true>() {} diff --git a/drizzle-typebox/tsconfig.json b/drizzle-typebox/tsconfig.json index 038d79591..c25379c37 100644 --- a/drizzle-typebox/tsconfig.json +++ b/drizzle-typebox/tsconfig.json @@ -4,6 +4,7 @@ "outDir": "dist", "baseUrl": ".", "declaration": true, + "noEmit": true, "paths": { "~/*": ["src/*"] } diff --git a/drizzle-valibot/README.md b/drizzle-valibot/README.md index 7696e0297..735e40b34 100644 --- a/drizzle-valibot/README.md +++ b/drizzle-valibot/README.md @@ -1,11 +1,17 @@ `drizzle-valibot` is a plugin for [Drizzle ORM](https://github.com/drizzle-team/drizzle-orm) that allows you to generate [valibot](https://valibot.dev/) schemas from Drizzle ORM schemas. +**Features** + +- Create a select schema for tables, views and enums. +- Create insert and update schemas for tables. +- Supports all dialects: PostgreSQL, MySQL and SQLite. + # Usage ```ts import { pgEnum, pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core'; import { createInsertSchema, createSelectSchema } from 'drizzle-valibot'; -import { string, parse, number } from 'valibot'; +import { string, parse, number, pipe } from 'valibot'; const users = pgTable('users', { id: serial('id').primaryKey(), @@ -18,6 +24,9 @@ const users = pgTable('users', { // Schema for inserting a user - can be used to validate API requests const insertUserSchema = createInsertSchema(users); +// Schema for updating a user - can be used to validate API requests +const updateUserSchema = createUpdateSchema(users); + // Schema for selecting a user - can be used to validate API responses const selectUserSchema = createSelectSchema(users); @@ -28,7 +37,7 @@ const insertUserSchema = createInsertSchema(users, { // Refining the fields - useful if you want to change the fields before they become nullable/optional in the final schema const insertUserSchema = createInsertSchema(users, { - id: (schema) => number([minValue(0)]), + id: (schema) => pipe([schema, minValue(0)]), role: string(), }); diff --git a/drizzle-valibot/package.json b/drizzle-valibot/package.json index 1d88fd26a..c9e6a02e9 100644 --- a/drizzle-valibot/package.json +++ b/drizzle-valibot/package.json @@ -1,6 +1,6 @@ { "name": "drizzle-valibot", - "version": "0.2.0", + "version": "0.3.0", "description": "Generate valibot schemas from Drizzle ORM schemas", "type": "module", "scripts": { @@ -55,18 +55,17 @@ "author": "Drizzle Team", "license": "Apache-2.0", "peerDependencies": { - "drizzle-orm": ">=0.23.13", - "valibot": ">=0.20" + "drizzle-orm": ">=0.36.0", + "valibot": ">=1.0.0-beta.7" }, "devDependencies": { - "@rollup/plugin-terser": "^0.4.1", "@rollup/plugin-typescript": "^11.1.0", "@types/node": "^18.15.10", "cpy": "^10.1.0", "drizzle-orm": "link:../drizzle-orm/dist", "rimraf": "^5.0.0", "rollup": "^3.20.7", - "valibot": "^0.30.0", + "valibot": "1.0.0-beta.7", "vite-tsconfig-paths": "^4.3.2", "vitest": "^1.6.0", "zx": "^7.2.2" diff --git a/drizzle-valibot/rollup.config.ts b/drizzle-valibot/rollup.config.ts index 2ed2d33d3..986da4883 100644 --- a/drizzle-valibot/rollup.config.ts +++ b/drizzle-valibot/rollup.config.ts @@ -1,4 +1,3 @@ -import terser from '@rollup/plugin-terser'; import typescript from '@rollup/plugin-typescript'; import { defineConfig } from 'rollup'; @@ -23,13 +22,12 @@ export default defineConfig([ ], external: [ /^drizzle-orm\/?/, - 'zod', + 'valibot', ], plugins: [ typescript({ tsconfig: 'tsconfig.build.json', }), - terser(), ], }, ]); diff --git a/drizzle-valibot/scripts/build.ts b/drizzle-valibot/scripts/build.ts index 1910feac6..07330ffd0 100755 --- a/drizzle-valibot/scripts/build.ts +++ b/drizzle-valibot/scripts/build.ts @@ -13,3 +13,4 @@ await cpy('dist/**/*.d.ts', 'dist', { rename: (basename) => basename.replace(/\.d\.ts$/, '.d.cts'), }); await fs.copy('package.json', 'dist/package.json'); +await $`scripts/fix-imports.ts`; diff --git a/drizzle-valibot/scripts/fix-imports.ts b/drizzle-valibot/scripts/fix-imports.ts new file mode 100755 index 000000000..a90057c5b --- /dev/null +++ b/drizzle-valibot/scripts/fix-imports.ts @@ -0,0 +1,136 @@ +#!/usr/bin/env -S pnpm tsx +import 'zx/globals'; + +import path from 'node:path'; +import { parse, print, visit } from 'recast'; +import parser from 'recast/parsers/typescript'; + +function resolvePathAlias(importPath: string, file: string) { + if (importPath.startsWith('~/')) { + const relativePath = path.relative(path.dirname(file), path.resolve('dist.new', importPath.slice(2))); + importPath = relativePath.startsWith('.') ? relativePath : './' + relativePath; + } + + return importPath; +} + +function fixImportPath(importPath: string, file: string, ext: string) { + importPath = resolvePathAlias(importPath, file); + + if (!/\..*\.(js|ts)$/.test(importPath)) { + return importPath; + } + + return importPath.replace(/\.(js|ts)$/, ext); +} + +const cjsFiles = await glob('dist/**/*.{cjs,d.cts}'); + +await Promise.all(cjsFiles.map(async (file) => { + const code = parse(await fs.readFile(file, 'utf8'), { parser }); + + visit(code, { + visitImportDeclaration(path) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.cjs'); + this.traverse(path); + }, + visitExportAllDeclaration(path) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.cjs'); + this.traverse(path); + }, + visitExportNamedDeclaration(path) { + if (path.value.source) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.cjs'); + } + this.traverse(path); + }, + visitCallExpression(path) { + if (path.value.callee.type === 'Identifier' && path.value.callee.name === 'require') { + path.value.arguments[0].value = fixImportPath(path.value.arguments[0].value, file, '.cjs'); + } + this.traverse(path); + }, + visitTSImportType(path) { + path.value.argument.value = resolvePathAlias(path.value.argument.value, file); + this.traverse(path); + }, + visitAwaitExpression(path) { + if (print(path.value).code.startsWith(`await import("./`)) { + path.value.argument.arguments[0].value = fixImportPath(path.value.argument.arguments[0].value, file, '.cjs'); + } + this.traverse(path); + }, + }); + + await fs.writeFile(file, print(code).code); +})); + +let esmFiles = await glob('dist/**/*.{js,d.ts}'); + +await Promise.all(esmFiles.map(async (file) => { + const code = parse(await fs.readFile(file, 'utf8'), { parser }); + + visit(code, { + visitImportDeclaration(path) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.js'); + this.traverse(path); + }, + visitExportAllDeclaration(path) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.js'); + this.traverse(path); + }, + visitExportNamedDeclaration(path) { + if (path.value.source) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.js'); + } + this.traverse(path); + }, + visitTSImportType(path) { + path.value.argument.value = fixImportPath(path.value.argument.value, file, '.js'); + this.traverse(path); + }, + visitAwaitExpression(path) { + if (print(path.value).code.startsWith(`await import("./`)) { + path.value.argument.arguments[0].value = fixImportPath(path.value.argument.arguments[0].value, file, '.js'); + } + this.traverse(path); + }, + }); + + await fs.writeFile(file, print(code).code); +})); + +esmFiles = await glob('dist/**/*.{mjs,d.mts}'); + +await Promise.all(esmFiles.map(async (file) => { + const code = parse(await fs.readFile(file, 'utf8'), { parser }); + + visit(code, { + visitImportDeclaration(path) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.mjs'); + this.traverse(path); + }, + visitExportAllDeclaration(path) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.mjs'); + this.traverse(path); + }, + visitExportNamedDeclaration(path) { + if (path.value.source) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.mjs'); + } + this.traverse(path); + }, + visitTSImportType(path) { + path.value.argument.value = fixImportPath(path.value.argument.value, file, '.mjs'); + this.traverse(path); + }, + visitAwaitExpression(path) { + if (print(path.value).code.startsWith(`await import("./`)) { + path.value.argument.arguments[0].value = fixImportPath(path.value.argument.arguments[0].value, file, '.mjs'); + } + this.traverse(path); + }, + }); + + await fs.writeFile(file, print(code).code); +})); diff --git a/drizzle-valibot/src/column.ts b/drizzle-valibot/src/column.ts new file mode 100644 index 000000000..e5716fe1e --- /dev/null +++ b/drizzle-valibot/src/column.ts @@ -0,0 +1,239 @@ +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 { SQLiteInteger, SQLiteReal, SQLiteText } from 'drizzle-orm/sqlite-core'; +import * as v from 'valibot'; +import { CONSTANTS } from './constants.ts'; +import { isColumnType, isWithEnum } from './utils.ts'; +import type { Json } from './utils.ts'; + +export const literalSchema = v.union([v.string(), v.number(), v.boolean(), v.null()]); +export const jsonSchema: v.GenericSchema = v.union([ + literalSchema, + v.array(v.lazy(() => jsonSchema)), + v.record(v.string(), v.lazy(() => jsonSchema)), +]); +export const bufferSchema: v.GenericSchema = v.custom((v) => v instanceof Buffer); // eslint-disable-line no-instanceof/no-instanceof + +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; + + if (isWithEnum(column)) { + schema = column.enumValues.length ? v.enum(mapEnumValues(column.enumValues)) : v.string(); + } + + if (!schema) { + // Handle specific types + if (isColumnType | PgPointTuple>(column, ['PgGeometry', 'PgPointTuple'])) { + schema = v.tuple([v.number(), v.number()]); + } else if ( + isColumnType | PgGeometryObject>(column, ['PgGeometryObject', 'PgPointObject']) + ) { + schema = v.object({ x: v.number(), y: v.number() }); + } else if (isColumnType | PgVector>(column, ['PgHalfVector', 'PgVector'])) { + schema = v.array(v.number()); + schema = column.dimensions ? v.pipe(schema as v.ArraySchema, v.length(column.dimensions)) : schema; + } else if (isColumnType>(column, ['PgLine'])) { + schema = v.tuple([v.number(), v.number(), v.number()]); + v.array(v.array(v.number())); + } else if (isColumnType>(column, ['PgLineABC'])) { + schema = v.object({ a: v.number(), b: v.number(), c: v.number() }); + } // Handle other types + else if (isColumnType>(column, ['PgArray'])) { + schema = v.array(columnToSchema(column.baseColumn)); + schema = column.size ? v.pipe(schema as v.ArraySchema, v.length(column.size)) : schema; + } else if (column.dataType === 'array') { + schema = v.array(v.any()); + } else if (column.dataType === 'number') { + schema = numberColumnToSchema(column); + } else if (column.dataType === 'bigint') { + schema = bigintColumnToSchema(column); + } else if (column.dataType === 'boolean') { + schema = v.boolean(); + } else if (column.dataType === 'date') { + schema = v.date(); + } else if (column.dataType === 'string') { + schema = stringColumnToSchema(column); + } else if (column.dataType === 'json') { + schema = jsonSchema; + } else if (column.dataType === 'custom') { + schema = v.any(); + } else if (column.dataType === 'buffer') { + schema = bufferSchema; + } + } + + if (!schema) { + schema = v.any(); + } + + return schema; +} + +function numberColumnToSchema(column: Column): v.GenericSchema { + let unsigned = column.getSQLType().includes('unsigned'); + let min!: number; + let max!: number; + let integer = false; + + if (isColumnType>(column, ['MySqlTinyInt'])) { + min = unsigned ? 0 : CONSTANTS.INT8_MIN; + max = unsigned ? CONSTANTS.INT8_UNSIGNED_MAX : CONSTANTS.INT8_MAX; + integer = true; + } else if ( + isColumnType | PgSmallSerial | MySqlSmallInt>(column, [ + 'PgSmallInt', + 'PgSmallSerial', + 'MySqlSmallInt', + ]) + ) { + min = unsigned ? 0 : CONSTANTS.INT16_MIN; + max = unsigned ? CONSTANTS.INT16_UNSIGNED_MAX : CONSTANTS.INT16_MAX; + integer = true; + } else if ( + isColumnType | MySqlFloat | MySqlMediumInt>(column, [ + 'PgReal', + 'MySqlFloat', + 'MySqlMediumInt', + ]) + ) { + min = unsigned ? 0 : CONSTANTS.INT24_MIN; + max = unsigned ? CONSTANTS.INT24_UNSIGNED_MAX : CONSTANTS.INT24_MAX; + integer = isColumnType(column, ['MySqlMediumInt']); + } else if ( + isColumnType | PgSerial | MySqlInt>(column, ['PgInteger', 'PgSerial', 'MySqlInt']) + ) { + min = unsigned ? 0 : CONSTANTS.INT32_MIN; + max = unsigned ? CONSTANTS.INT32_UNSIGNED_MAX : CONSTANTS.INT32_MAX; + integer = true; + } else if ( + isColumnType | MySqlReal | MySqlDouble | SQLiteReal>(column, [ + 'PgDoublePrecision', + 'MySqlReal', + 'MySqlDouble', + 'SQLiteReal', + ]) + ) { + min = unsigned ? 0 : CONSTANTS.INT48_MIN; + max = unsigned ? CONSTANTS.INT48_UNSIGNED_MAX : CONSTANTS.INT48_MAX; + } else if ( + isColumnType | PgBigSerial53 | MySqlBigInt53 | MySqlSerial | SQLiteInteger>( + column, + ['PgBigInt53', 'PgBigSerial53', 'MySqlBigInt53', 'MySqlSerial', 'SQLiteInteger'], + ) + ) { + unsigned = unsigned || isColumnType(column, ['MySqlSerial']); + min = unsigned ? 0 : Number.MIN_SAFE_INTEGER; + max = Number.MAX_SAFE_INTEGER; + integer = true; + } else if (isColumnType>(column, ['MySqlYear'])) { + min = 1901; + max = 2155; + integer = true; + } else { + min = Number.MIN_SAFE_INTEGER; + max = Number.MAX_SAFE_INTEGER; + } + + const actions: any[] = [v.minValue(min), v.maxValue(max)]; + if (integer) { + actions.push(v.integer()); + } + return v.pipe(v.number(), ...actions); +} + +function bigintColumnToSchema(column: Column): v.GenericSchema { + const unsigned = column.getSQLType().includes('unsigned'); + const min = unsigned ? 0n : CONSTANTS.INT64_MIN; + const max = unsigned ? CONSTANTS.INT64_UNSIGNED_MAX : CONSTANTS.INT64_MAX; + + return v.pipe(v.bigint(), v.minValue(min), v.maxValue(max)); +} + +function stringColumnToSchema(column: Column): v.GenericSchema { + if (isColumnType>>(column, ['PgUUID'])) { + return v.pipe(v.string(), v.uuid()); + } + + let max: number | undefined; + let regex: RegExp | undefined; + let fixed = false; + + if (isColumnType | SQLiteText>(column, ['PgVarchar', 'SQLiteText'])) { + max = column.length; + } else if (isColumnType>(column, ['MySqlVarChar'])) { + max = column.length ?? CONSTANTS.INT16_UNSIGNED_MAX; + } else if (isColumnType>(column, ['MySqlText'])) { + 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 | MySqlChar>(column, ['PgChar', 'MySqlChar'])) { + max = column.length; + fixed = true; + } + + if (isColumnType>(column, ['PgBinaryVector'])) { + regex = /^[01]+$/; + max = column.dimensions; + } + + const actions: any[] = []; + if (regex) { + actions.push(v.regex(regex)); + } + if (max && fixed) { + actions.push(v.length(max)); + } else if (max) { + actions.push(v.maxLength(max)); + } + return actions.length > 0 ? v.pipe(v.string(), ...actions) : v.string(); +} diff --git a/drizzle-valibot/src/column.types.ts b/drizzle-valibot/src/column.types.ts new file mode 100644 index 000000000..e6cd797ed --- /dev/null +++ b/drizzle-valibot/src/column.types.ts @@ -0,0 +1,202 @@ +import type { Assume, Column } from 'drizzle-orm'; +import type * as v from 'valibot'; +import type { + ArrayHasAtLeastOneValue, + ColumnIsGeneratedAlwaysAs, + IsNever, + Json, + RemoveNeverElements, +} from './utils.ts'; + +export type GetEnumValuesFromColumn = TColumn['_'] extends { enumValues: [string, ...string[]] } + ? TColumn['_']['enumValues'] + : undefined; + +export type GetBaseColumn = TColumn['_'] extends { baseColumn: Column | never | undefined } + ? IsNever extends false ? TColumn['_']['baseColumn'] + : undefined + : undefined; + +export type EnumValuesToEnum = { readonly [K in TEnumValues[number]]: K }; + +export type ExtractAdditionalProperties = { + max: TColumn['_']['columnType'] extends 'PgVarchar' | 'SQLiteText' | 'PgChar' | 'MySqlChar' + ? Assume['length'] + : TColumn['_']['columnType'] extends 'MySqlText' | 'MySqlVarChar' ? number + : TColumn['_']['columnType'] extends 'PgBinaryVector' | 'PgHalfVector' | 'PgVector' + ? Assume['dimensions'] + : TColumn['_']['columnType'] extends 'PgArray' ? Assume['size'] + : undefined; + fixedLength: TColumn['_']['columnType'] extends 'PgChar' | 'MySqlChar' | 'PgHalfVector' | 'PgVector' | 'PgArray' + ? true + : false; + arrayPipelines: []; +}; + +type RemovePipeIfNoElements> = T extends + infer TPiped extends { pipe: [any, ...any[]] } ? TPiped['pipe'][1] extends undefined ? T['pipe'][0] : TPiped + : never; + +type BuildArraySchema< + TWrapped extends v.GenericSchema, + TPipelines extends any[][], +> = TPipelines extends [infer TFirst extends any[], ...infer TRest extends any[][]] + ? BuildArraySchema, ...TFirst]>>, TRest> + : TPipelines extends [infer TFirst extends any[]] + ? BuildArraySchema, ...TFirst]>>, []> + : TWrapped; + +export type GetValibotType< + TData, + TDataType extends string, + TColumnType extends string, + TEnumValues extends [string, ...string[]] | undefined, + TBaseColumn extends Column | undefined, + TAdditionalProperties extends Record, +> = TColumnType extends 'PgHalfVector' | 'PgVector' ? RemovePipeIfNoElements< + v.SchemaWithPipe< + RemoveNeverElements<[ + v.ArraySchema, undefined>, + TAdditionalProperties['max'] extends number + ? TAdditionalProperties['fixedLength'] extends true ? v.LengthAction + : v.MaxLengthAction + : never, + ]> + > + > + : TColumnType extends 'PgUUID' ? v.SchemaWithPipe<[v.StringSchema, v.UuidAction]> + // 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 + > extends infer TSchema extends v.GenericSchema ? BuildArraySchema + : never + : TBaseColumn extends Column ? GetValibotType< + TBaseColumn['_']['data'], + TBaseColumn['_']['dataType'], + TBaseColumn['_']['columnType'], + GetEnumValuesFromColumn, + GetBaseColumn, + Omit, 'arrayPipelines'> & { + arrayPipelines: [ + RemoveNeverElements<[ + TAdditionalProperties['max'] extends number + ? TAdditionalProperties['fixedLength'] extends true + ? v.LengthAction[], number, undefined> + : v.MaxLengthAction[], number, undefined> + : never, + ]>, + ...TAdditionalProperties['arrayPipelines'], + ]; + arrayFinished: GetBaseColumn extends undefined ? true : false; + } + > + // PG array handling end + : ArrayHasAtLeastOneValue extends true + ? v.EnumSchema>, undefined> + : TData extends infer TTuple extends [any, ...any[]] ? v.TupleSchema< + Assume< + { [K in keyof TTuple]: GetValibotType }, + [any, ...any[]] + >, + undefined + > + : TData extends Date ? v.DateSchema + : TData extends Buffer ? v.GenericSchema + : TDataType extends 'array' ? v.ArraySchema< + GetValibotType[number], string, string, undefined, undefined, { noPipe: true }>, + undefined + > + : TData extends infer TDict extends Record ? v.ObjectSchema< + { readonly [K in keyof TDict]: GetValibotType }, + undefined + > + : TDataType extends 'json' ? v.GenericSchema + : TData extends number ? TAdditionalProperties['noPipe'] extends true ? v.NumberSchema : v.SchemaWithPipe< + RemoveNeverElements<[ + v.NumberSchema, + v.MinValueAction, + v.MaxValueAction, + TColumnType extends + | 'MySqlTinyInt' + | 'PgSmallInt' + | 'PgSmallSerial' + | 'MySqlSmallInt' + | 'MySqlMediumInt' + | 'PgInteger' + | 'PgSerial' + | 'MySqlInt' + | 'PgBigInt53' + | 'PgBigSerial53' + | 'MySqlBigInt53' + | 'MySqlSerial' + | 'SQLiteInteger' + | 'MySqlYear' ? v.IntegerAction + : never, + ]> + > + : TData extends bigint ? TAdditionalProperties['noPipe'] extends true ? v.BigintSchema : v.SchemaWithPipe<[ + v.BigintSchema, + v.MinValueAction, + v.MaxValueAction, + ]> + : TData extends boolean ? v.BooleanSchema + : TData extends string ? RemovePipeIfNoElements< + v.SchemaWithPipe< + RemoveNeverElements<[ + v.StringSchema, + TColumnType extends 'PgBinaryVector' ? v.RegexAction + : never, + TAdditionalProperties['max'] extends number + ? TAdditionalProperties['fixedLength'] extends true ? v.LengthAction + : v.MaxLengthAction + : never, + ]> + > + > + : v.AnySchema; + +type HandleSelectColumn< + TSchema extends v.GenericSchema, + TColumn extends Column, +> = TColumn['_']['notNull'] extends true ? TSchema + : v.NullableSchema; + +type HandleInsertColumn< + TSchema extends v.GenericSchema, + TColumn extends Column, +> = ColumnIsGeneratedAlwaysAs extends true ? never + : TColumn['_']['notNull'] extends true + ? TColumn['_']['hasDefault'] extends true ? v.OptionalSchema + : TSchema + : v.OptionalSchema, undefined>; + +type HandleUpdateColumn< + TSchema extends v.GenericSchema, + TColumn extends Column, +> = ColumnIsGeneratedAlwaysAs extends true ? never + : TColumn['_']['notNull'] extends true ? v.OptionalSchema + : v.OptionalSchema, undefined>; + +export type HandleColumn< + TType extends 'select' | 'insert' | 'update', + TColumn extends Column, +> = GetValibotType< + TColumn['_']['data'], + TColumn['_']['dataType'], + TColumn['_']['columnType'], + GetEnumValuesFromColumn, + GetBaseColumn, + ExtractAdditionalProperties +> extends infer TSchema extends v.GenericSchema ? TSchema extends v.AnySchema ? v.AnySchema + : TType extends 'select' ? HandleSelectColumn + : TType extends 'insert' ? HandleInsertColumn + : TType extends 'update' ? HandleUpdateColumn + : TSchema + : v.AnySchema; diff --git a/drizzle-valibot/src/constants.ts b/drizzle-valibot/src/constants.ts new file mode 100644 index 000000000..99f5d7a42 --- /dev/null +++ b/drizzle-valibot/src/constants.ts @@ -0,0 +1,20 @@ +export const CONSTANTS = { + INT8_MIN: -128, + INT8_MAX: 127, + INT8_UNSIGNED_MAX: 255, + INT16_MIN: -32768, + INT16_MAX: 32767, + INT16_UNSIGNED_MAX: 65535, + INT24_MIN: -8388608, + INT24_MAX: 8388607, + INT24_UNSIGNED_MAX: 16777215, + INT32_MIN: -2147483648, + INT32_MAX: 2147483647, + INT32_UNSIGNED_MAX: 4294967295, + INT48_MIN: -140737488355328, + INT48_MAX: 140737488355327, + INT48_UNSIGNED_MAX: 281474976710655, + INT64_MIN: -9223372036854775808n, + INT64_MAX: 9223372036854775807n, + INT64_UNSIGNED_MAX: 18446744073709551615n, +}; diff --git a/drizzle-valibot/src/index.ts b/drizzle-valibot/src/index.ts index 0c84c5052..0a6499e5b 100644 --- a/drizzle-valibot/src/index.ts +++ b/drizzle-valibot/src/index.ts @@ -1,332 +1,2 @@ -import { - type AnyColumn, - type Assume, - type Column, - type DrizzleTypeError, - type Equal, - getTableColumns, - is, - type Simplify, - type Table, -} from 'drizzle-orm'; -import { MySqlChar, MySqlVarBinary, MySqlVarChar } from 'drizzle-orm/mysql-core'; -import { type PgArray, PgChar, PgUUID, PgVarchar } from 'drizzle-orm/pg-core'; -import { SQLiteText } from 'drizzle-orm/sqlite-core'; - -import { - any, - type AnySchema, - array, - type ArraySchema, - type BaseSchema, - bigint, - type BigintSchema, - boolean, - type BooleanSchema, - date, - type DateSchema, - maxLength, - null_, - nullable, - type NullableSchema, - number, - type NumberSchema, - object, - type ObjectSchema, - optional, - type OptionalSchema, - picklist, - type PicklistSchema, - record, - string, - type StringSchema, - union, - uuid, -} from 'valibot'; - -const literalSchema = union([string(), number(), boolean(), null_()]); - -type Json = typeof jsonSchema; - -export const jsonSchema = union([literalSchema, array(any()), record(any())]); - -type MapInsertColumnToValibot< - TColumn extends Column, - TType extends BaseSchema, -> = TColumn['_']['notNull'] extends false ? OptionalSchema> - : TColumn['_']['hasDefault'] extends true ? OptionalSchema - : TType; - -type MapSelectColumnToValibot< - TColumn extends Column, - TType extends BaseSchema, -> = TColumn['_']['notNull'] extends false ? NullableSchema : TType; - -type MapColumnToValibot< - TColumn extends Column, - TType extends BaseSchema, - TMode extends 'insert' | 'select', -> = TMode extends 'insert' ? MapInsertColumnToValibot - : MapSelectColumnToValibot; - -type MaybeOptional< - TColumn extends Column, - TType extends BaseSchema, - TMode extends 'insert' | 'select', - TNoOptional extends boolean, -> = TNoOptional extends true ? TType - : MapColumnToValibot; - -type GetValibotType = TColumn['_']['dataType'] extends infer TDataType - ? TDataType extends 'custom' ? AnySchema - : TDataType extends 'json' ? Json - : 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 - : TDataType extends 'boolean' ? BooleanSchema - : TDataType extends 'date' ? DateSchema - : AnySchema - : never; - -type ValueOrUpdater = T | ((arg: TUpdaterArg) => T); - -type UnwrapValueOrUpdater = T extends ValueOrUpdater ? U - : never; - -export type Refine = { - [K in keyof TTable['_']['columns']]?: ValueOrUpdater< - BaseSchema, - TMode extends 'select' ? BuildSelectSchema - : BuildInsertSchema - >; -}; - -export type BuildInsertSchema< - TTable extends Table, - TRefine extends Refine | {}, - TNoOptional extends boolean = false, -> = TTable['_']['columns'] extends infer TColumns extends Record< - string, - Column -> ? { - [K in keyof TColumns & string]: MaybeOptional< - TColumns[K], - K extends keyof TRefine ? Assume, BaseSchema> - : GetValibotType, - 'insert', - TNoOptional - >; - } - : never; - -export type BuildSelectSchema< - TTable extends Table, - TRefine extends Refine, - TNoOptional extends boolean = false, -> = Simplify< - { - [K in keyof TTable['_']['columns']]: MaybeOptional< - TTable['_']['columns'][K], - K extends keyof TRefine ? Assume, BaseSchema> - : GetValibotType, - 'select', - TNoOptional - >; - } ->; - -export function createInsertSchema< - TTable extends Table, - TRefine extends Refine = Refine, ->( - table: TTable, - /** - * @param refine Refine schema fields - */ - refine?: { - [K in keyof TRefine]: K extends keyof TTable['_']['columns'] ? TRefine[K] - : DrizzleTypeError< - `Column '${ - & K - & string}' does not exist in table '${TTable['_']['name']}'` - >; - }, - // -): ObjectSchema< - BuildInsertSchema< - TTable, - Equal> extends true ? {} : TRefine - > -> { - const columns = getTableColumns(table); - const columnEntries = Object.entries(columns); - - let schemaEntries = Object.fromEntries( - columnEntries.map(([name, column]) => { - return [name, mapColumnToSchema(column)]; - }), - ); - - if (refine) { - schemaEntries = Object.assign( - schemaEntries, - Object.fromEntries( - Object.entries(refine).map(([name, refineColumn]) => { - return [ - name, - typeof refineColumn === 'function' - ? refineColumn( - schemaEntries as BuildInsertSchema< - TTable, - {}, - true - >, - ) - : refineColumn, - ]; - }), - ), - ); - } - - for (const [name, column] of columnEntries) { - if (!column.notNull) { - schemaEntries[name] = optional(nullable(schemaEntries[name]!)); - } else if (column.hasDefault) { - schemaEntries[name] = optional(schemaEntries[name]!); - } - } - - return object(schemaEntries) as any; -} - -export function createSelectSchema< - TTable extends Table, - TRefine extends Refine = Refine, ->( - table: TTable, - /** - * @param refine Refine schema fields - */ - refine?: { - [K in keyof TRefine]: K extends keyof TTable['_']['columns'] ? TRefine[K] - : DrizzleTypeError< - `Column '${ - & K - & string}' does not exist in table '${TTable['_']['name']}'` - >; - }, -): ObjectSchema< - BuildSelectSchema< - TTable, - Equal> extends true ? {} : TRefine - > -> { - const columns = getTableColumns(table); - const columnEntries = Object.entries(columns); - - let schemaEntries = Object.fromEntries( - columnEntries.map(([name, column]) => { - return [name, mapColumnToSchema(column)]; - }), - ); - - if (refine) { - schemaEntries = Object.assign( - schemaEntries, - Object.fromEntries( - Object.entries(refine).map(([name, refineColumn]) => { - return [ - name, - typeof refineColumn === 'function' - ? refineColumn( - schemaEntries as BuildSelectSchema< - TTable, - {}, - true - >, - ) - : refineColumn, - ]; - }), - ), - ); - } - - for (const [name, column] of columnEntries) { - if (!column.notNull) { - schemaEntries[name] = nullable(schemaEntries[name]!); - } - } - - return object(schemaEntries) as any; -} - -function isWithEnum( - column: AnyColumn, -): column is typeof column & { enumValues: [string, ...string[]] } { - return ( - 'enumValues' in column - && Array.isArray(column.enumValues) - && column.enumValues.length > 0 - ); -} - -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()]); - } - } - - if (!type) { - type = any(); - } - - return type; -} +export * from './schema.ts'; +export * from './schema.types.ts'; diff --git a/drizzle-valibot/src/schema.ts b/drizzle-valibot/src/schema.ts new file mode 100644 index 000000000..30a6f77ec --- /dev/null +++ b/drizzle-valibot/src/schema.ts @@ -0,0 +1,95 @@ +import { Column, getTableColumns, getViewSelectedFields, is, isTable, isView, SQL } from 'drizzle-orm'; +import type { Table, View } from 'drizzle-orm'; +import type { PgEnum } from 'drizzle-orm/pg-core'; +import * as v from 'valibot'; +import { columnToSchema, mapEnumValues } from './column.ts'; +import type { Conditions } from './schema.types.internal.ts'; +import type { CreateInsertSchema, CreateSelectSchema, CreateUpdateSchema } from './schema.types.ts'; +import { isPgEnum } from './utils.ts'; + +function getColumns(tableLike: Table | View) { + return isTable(tableLike) ? getTableColumns(tableLike) : getViewSelectedFields(tableLike); +} + +function handleColumns( + columns: Record, + refinements: Record, + conditions: Conditions, +): v.GenericSchema { + const columnSchemas: Record = {}; + + for (const [key, selected] of Object.entries(columns)) { + if (!is(selected, Column) && !is(selected, SQL) && !is(selected, SQL.Aliased) && typeof selected === 'object') { + const columns = isTable(selected) || isView(selected) ? getColumns(selected) : selected; + columnSchemas[key] = handleColumns(columns, refinements[key] ?? {}, conditions); + continue; + } + + const refinement = refinements[key]; + if (refinement !== undefined && typeof refinement !== 'function') { + columnSchemas[key] = refinement; + continue; + } + + const column = is(selected, Column) ? selected : undefined; + const schema = column ? columnToSchema(column) : v.any(); + const refined = typeof refinement === 'function' ? refinement(schema) : schema; + + if (conditions.never(column)) { + continue; + } else { + columnSchemas[key] = refined; + } + + if (column) { + if (conditions.nullable(column)) { + columnSchemas[key] = v.nullable(columnSchemas[key]!); + } + + if (conditions.optional(column)) { + columnSchemas[key] = v.optional(columnSchemas[key]!); + } + } + } + + return v.object(columnSchemas) as any; +} + +export const createSelectSchema: CreateSelectSchema = ( + entity: Table | View | PgEnum<[string, ...string[]]>, + refine?: Record, +) => { + if (isPgEnum(entity)) { + return v.enum(mapEnumValues(entity.enumValues)); + } + const columns = getColumns(entity); + return handleColumns(columns, refine ?? {}, { + never: () => false, + optional: () => false, + nullable: (column) => !column.notNull, + }) as any; +}; + +export const createInsertSchema: CreateInsertSchema = ( + entity: Table, + refine?: Record, +) => { + const columns = getColumns(entity); + return handleColumns(columns, refine ?? {}, { + never: (column) => column?.generated?.type === 'always' || column?.generatedIdentity?.type === 'always', + optional: (column) => !column.notNull || (column.notNull && column.hasDefault), + nullable: (column) => !column.notNull, + }) as any; +}; + +export const createUpdateSchema: CreateUpdateSchema = ( + entity: Table, + refine?: Record, +) => { + const columns = getColumns(entity); + return handleColumns(columns, refine ?? {}, { + never: (column) => column?.generated?.type === 'always' || column?.generatedIdentity?.type === 'always', + optional: () => true, + nullable: (column) => !column.notNull, + }) as any; +}; diff --git a/drizzle-valibot/src/schema.types.internal.ts b/drizzle-valibot/src/schema.types.internal.ts new file mode 100644 index 000000000..57dcedc7c --- /dev/null +++ b/drizzle-valibot/src/schema.types.internal.ts @@ -0,0 +1,105 @@ +import type { Assume, Column, DrizzleTypeError, SelectedFieldsFlat, Simplify, Table, View } from 'drizzle-orm'; +import type * as v from 'valibot'; +import type { + ExtractAdditionalProperties, + GetBaseColumn, + GetEnumValuesFromColumn, + GetValibotType, + HandleColumn, +} from './column.types.ts'; +import type { GetSelection, RemoveNever } from './utils.ts'; + +export interface Conditions { + never: (column?: Column) => boolean; + optional: (column: Column) => boolean; + nullable: (column: Column) => boolean; +} + +export type BuildRefineColumns< + TColumns extends Record, +> = Simplify< + RemoveNever< + { + [K in keyof TColumns]: TColumns[K] extends infer TColumn extends Column ? GetValibotType< + TColumn['_']['data'], + TColumn['_']['dataType'], + TColumn['_']['columnType'], + GetEnumValuesFromColumn, + GetBaseColumn, + ExtractAdditionalProperties + > extends infer TSchema extends v.GenericSchema ? TSchema + : v.AnySchema + : TColumns[K] extends infer TObject extends SelectedFieldsFlat | Table | View + ? BuildRefineColumns> + : TColumns[K]; + } + > +>; + +export type BuildRefine< + TColumns extends Record, +> = BuildRefineColumns extends infer TBuildColumns ? { + [K in keyof TBuildColumns]?: TBuildColumns[K] extends v.GenericSchema + ? ((schema: TBuildColumns[K]) => v.GenericSchema) | v.GenericSchema + : TBuildColumns[K] extends Record ? Simplify> + : never; + } + : never; + +type HandleRefinement< + TType extends 'select' | 'insert' | 'update', + TRefinement extends v.GenericSchema | ((schema: v.GenericSchema) => v.GenericSchema), + TColumn extends Column, +> = TRefinement extends (schema: any) => v.GenericSchema ? ( + TColumn['_']['notNull'] extends true ? ReturnType + : v.NullableSchema, undefined> + ) extends infer TSchema ? TType extends 'update' ? v.OptionalSchema, undefined> + : TSchema + : v.AnySchema + : TRefinement; + +type IsRefinementDefined = TKey extends keyof TRefinements + ? TRefinements[TKey] extends v.GenericSchema | ((schema: any) => any) ? true + : false + : false; + +export type BuildSchema< + TType extends 'select' | 'insert' | 'update', + TColumns extends Record, + TRefinements extends Record | undefined, +> // @ts-ignore false-positive + = v.ObjectSchema< + Simplify< + RemoveNever< + { + readonly [K in keyof TColumns]: TColumns[K] extends infer TColumn extends Column + ? TRefinements extends object + ? IsRefinementDefined> extends true + ? HandleRefinement], TColumn> + : HandleColumn + : HandleColumn + : TColumns[K] extends infer TObject extends SelectedFieldsFlat | Table | View ? BuildSchema< + TType, + GetSelection, + TRefinements extends object + ? TRefinements[Assume] extends infer TNestedRefinements extends object + ? TNestedRefinements + : undefined + : undefined + > + : v.AnySchema; + } + > + >, + undefined +>; + +export type NoUnknownKeys< + TRefinement extends Record, + TCompare extends Record, +> = { + [K in keyof TRefinement]: K extends keyof TCompare + ? TRefinement[K] extends Record ? NoUnknownKeys + : TRefinement[K] + : DrizzleTypeError<`Found unknown key in refinement: "${K & string}"`>; +}; diff --git a/drizzle-valibot/src/schema.types.ts b/drizzle-valibot/src/schema.types.ts new file mode 100644 index 000000000..c0b2ef82c --- /dev/null +++ b/drizzle-valibot/src/schema.types.ts @@ -0,0 +1,49 @@ +import type { Table, View } from 'drizzle-orm'; +import type { PgEnum } from 'drizzle-orm/pg-core'; +import type * as v from 'valibot'; +import type { EnumValuesToEnum } from './column.types.ts'; +import type { BuildRefine, BuildSchema, NoUnknownKeys } from './schema.types.internal.ts'; + +export interface CreateSelectSchema { + (table: TTable): BuildSchema<'select', TTable['_']['columns'], undefined>; + < + TTable extends Table, + TRefine extends BuildRefine, + >( + table: TTable, + refine?: NoUnknownKeys, + ): BuildSchema<'select', TTable['_']['columns'], TRefine>; + + (view: TView): BuildSchema<'select', TView['_']['selectedFields'], undefined>; + < + TView extends View, + TRefine extends BuildRefine, + >( + view: TView, + refine: NoUnknownKeys, + ): BuildSchema<'select', TView['_']['selectedFields'], TRefine>; + + >(enum_: TEnum): v.EnumSchema, undefined>; +} + +export interface CreateInsertSchema { + (table: TTable): BuildSchema<'insert', TTable['_']['columns'], undefined>; + < + TTable extends Table, + TRefine extends BuildRefine>, + >( + table: TTable, + refine?: NoUnknownKeys, + ): BuildSchema<'insert', TTable['_']['columns'], TRefine>; +} + +export interface CreateUpdateSchema { + (table: TTable): BuildSchema<'update', TTable['_']['columns'], undefined>; + < + TTable extends Table, + TRefine extends BuildRefine>, + >( + table: TTable, + refine?: TRefine, + ): BuildSchema<'update', TTable['_']['columns'], TRefine>; +} diff --git a/drizzle-valibot/src/utils.ts b/drizzle-valibot/src/utils.ts new file mode 100644 index 000000000..eb5034d6f --- /dev/null +++ b/drizzle-valibot/src/utils.ts @@ -0,0 +1,45 @@ +import type { Column, SelectedFieldsFlat, Table, View } from 'drizzle-orm'; +import type { PgEnum } from 'drizzle-orm/pg-core'; +import type * as v from 'valibot'; +import type { literalSchema } from './column.ts'; + +export function isColumnType(column: Column, columnTypes: string[]): column is T { + return columnTypes.includes(column.columnType); +} + +export function isWithEnum(column: Column): column is typeof column & { enumValues: [string, ...string[]] } { + return 'enumValues' in column && Array.isArray(column.enumValues) && column.enumValues.length > 0; +} + +export const isPgEnum: (entity: any) => entity is PgEnum<[string, ...string[]]> = isWithEnum as any; + +type Literal = v.InferOutput; +export type Json = Literal | { [key: string]: Json } | Json[]; + +export type IsNever = [T] extends [never] ? true : false; + +export type ArrayHasAtLeastOneValue = TEnum extends [infer TString, ...any[]] + ? TString extends `${infer TLiteral}` ? TLiteral extends any ? true + : false + : false + : false; + +export type ColumnIsGeneratedAlwaysAs = TColumn['_']['identity'] extends 'always' ? true + : TColumn['_']['generated'] extends undefined ? false + : TColumn['_']['generated'] extends infer TGenerated extends { type: string } + ? TGenerated['type'] extends 'byDefault' ? false + : true + : true; + +export type RemoveNever = { + [K in keyof T as T[K] extends never ? never : K]: T[K]; +}; + +export type RemoveNeverElements = T extends [infer First, ...infer Rest] + ? IsNever extends true ? RemoveNeverElements + : [First, ...RemoveNeverElements] + : []; + +export type GetSelection | Table | View> = T extends Table ? T['_']['columns'] + : T extends View ? T['_']['selectedFields'] + : T; diff --git a/drizzle-valibot/tests/mysql.test.ts b/drizzle-valibot/tests/mysql.test.ts index 9635ef8fa..5bf9520cb 100644 --- a/drizzle-valibot/tests/mysql.test.ts +++ b/drizzle-valibot/tests/mysql.test.ts @@ -1,400 +1,472 @@ -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 { - any, - bigint as valibigint, - boolean as valiboolean, - date as valiDate, - maxLength, - minLength, - minValue, - number, - object, - optional, - parse, - picklist, - string, -} from 'valibot'; -import { expect, test } from 'vitest'; -import { createInsertSchema, createSelectSchema, jsonSchema } from '../src'; -import { expectSchemaShape } from './utils.ts'; - -const customInt = customType<{ data: number }>({ - dataType() { - return 'int'; - }, +import { type Equal, sql } from 'drizzle-orm'; +import { 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'; +import { CONSTANTS } from '~/constants.ts'; +import { createInsertSchema, createSelectSchema, createUpdateSchema } from '../src'; +import { Expect, expectSchemaShape } from './utils.ts'; + +const intSchema = v.pipe( + v.number(), + v.minValue(CONSTANTS.INT32_MIN as number), + v.maxValue(CONSTANTS.INT32_MAX as number), + v.integer(), +); +const serialNumberModeSchema = v.pipe( + v.number(), + v.minValue(0 as number), + v.maxValue(Number.MAX_SAFE_INTEGER as number), + v.integer(), +); +const textSchema = v.pipe(v.string(), v.maxLength(CONSTANTS.INT16_UNSIGNED_MAX as number)); + +test('table - select', (t) => { + const table = mysqlTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + + const result = createSelectSchema(table); + const expected = v.object({ id: serialNumberModeSchema, name: textSchema }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -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(), +test('table in schema - select', (tc) => { + const schema = mysqlSchema('test'); + const table = schema.table('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + + const result = createSelectSchema(table); + const expected = v.object({ id: serialNumberModeSchema, name: textSchema }); + expectSchemaShape(tc, expected).from(result); + Expect>(); }); -const testTableRow = { - bigint: BigInt(1), - bigintNumber: 1, - binary: 'binary', - boolean: true, - char: 'char', - charEnum: 'a' as const, - customInt: { data: 1 }, - date: new Date(), - dateString: new Date().toISOString(), - datetime: new Date(), - datetimeString: new Date().toISOString(), - decimal: '1.1', - double: 1.1, - enum: 'a' as const, - float: 1.1, - int: 1, - json: { data: 1 }, - mediumint: 1, - real: 1.1, - serial: 1, - smallint: 1, - text: 'text', - textEnum: 'a' as const, - tinytext: 'tinytext', - tinytextEnum: 'a' as const, - mediumtext: 'mediumtext', - mediumtextEnum: 'a' as const, - longtext: 'longtext', - longtextEnum: 'a' as const, - time: '00:00:00', - timestamp: new Date(), - timestampString: new Date().toISOString(), - tinyint: 1, - varbinary: 'A'.repeat(200), - varchar: 'A'.repeat(200), - varcharEnum: 'a' as const, - year: 2021, - autoIncrement: 1, -}; - -test('insert valid row', () => { - const schema = createInsertSchema(testTable); - - expect(parse(schema, testTableRow)).toStrictEqual(testTableRow); +test('table - insert', (t) => { + const table = mysqlTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + age: int(), + }); + + const result = createInsertSchema(table); + const expected = v.object({ + id: v.optional(serialNumberModeSchema), + name: textSchema, + age: v.optional(v.nullable(intSchema)), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('insert invalid varchar length', () => { - const schema = createInsertSchema(testTable); +test('table - update', (t) => { + const table = mysqlTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + age: int(), + }); - expect(() => - parse(schema, { - ...testTableRow, - varchar: 'A'.repeat(201), - }) - ).toThrow(undefined); + const result = createUpdateSchema(table); + const expected = v.object({ + id: v.optional(serialNumberModeSchema), + name: v.optional(textSchema), + age: v.optional(v.nullable(intSchema)), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('insert smaller char length should work', () => { - const schema = createInsertSchema(testTable); +test('view qb - select', (t) => { + const table = mysqlTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + const view = mysqlView('test').as((qb) => qb.select({ id: table.id, age: sql``.as('age') }).from(table)); + + const result = createSelectSchema(view); + const expected = v.object({ id: serialNumberModeSchema, age: v.any() }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); - const input = { ...testTableRow, char: 'abc' }; +test('view columns - select', (t) => { + const view = mysqlView('test', { + id: serial().primaryKey(), + name: text().notNull(), + }).as(sql``); - expect(parse(schema, input)).toStrictEqual(input); + const result = createSelectSchema(view); + const expected = v.object({ id: serialNumberModeSchema, name: textSchema }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('insert larger char length should fail', () => { - const schema = createInsertSchema(testTable); +test('view with nested fields - select', (t) => { + const table = mysqlTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + const view = mysqlView('test').as((qb) => + qb.select({ + id: table.id, + nested: { + name: table.name, + age: sql``.as('age'), + }, + table, + }).from(table) + ); - expect(() => parse(schema, { ...testTableRow, char: 'abcde' })).toThrow(undefined); + const result = createSelectSchema(view); + const expected = v.object({ + id: serialNumberModeSchema, + nested: v.object({ name: textSchema, age: v.any() }), + table: v.object({ id: serialNumberModeSchema, name: textSchema }), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('insert schema', (t) => { - const actual = createInsertSchema(testTable); - - const expected = object({ - bigint: valibigint(), - bigintNumber: number(), - binary: string(), - boolean: valiboolean(), - char: string([minLength(4), maxLength(4)]), - charEnum: picklist([ - 'a', - 'b', - 'c', - ]), - customInt: any(), - date: valiDate(), - dateString: string(), - datetime: valiDate(), - datetimeString: string(), - decimal: string(), - double: number(), - enum: picklist([ - 'a', - 'b', - 'c', - ]), - float: number(), - int: number(), - json: jsonSchema, - mediumint: number(), - real: number(), - serial: optional(number()), - smallint: number(), - text: string(), - textEnum: picklist([ - 'a', - 'b', - 'c', - ]), - tinytext: string(), - tinytextEnum: picklist([ - 'a', - 'b', - 'c', - ]), - mediumtext: string(), - mediumtextEnum: picklist([ - 'a', - 'b', - 'c', - ]), - longtext: string(), - longtextEnum: picklist([ - 'a', - 'b', - 'c', - ]), - time: string(), - timestamp: valiDate(), - timestampString: string(), - tinyint: number(), - varbinary: string([maxLength(200)]), - varchar: string([maxLength(200)]), - varcharEnum: picklist([ - 'a', - 'b', - 'c', - ]), - year: number(), - autoIncrement: optional(number()), +test('nullability - select', (t) => { + const table = mysqlTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().default(1), + c4: int().notNull().default(1), }); - expectSchemaShape(t, expected).from(actual); + const result = createSelectSchema(table); + const expected = v.object({ + c1: v.nullable(intSchema), + c2: intSchema, + c3: v.nullable(intSchema), + c4: intSchema, + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('select schema', (t) => { - const actual = createSelectSchema(testTable); - - const expected = object({ - bigint: valibigint(), - bigintNumber: number(), - binary: string(), - boolean: valiboolean(), - char: string([minLength(4), maxLength(4)]), - charEnum: picklist([ - 'a', - 'b', - 'c', - ]), - customInt: any(), - date: valiDate(), - dateString: string(), - datetime: valiDate(), - datetimeString: string(), - decimal: string(), - double: number(), - enum: picklist([ - 'a', - 'b', - 'c', - ]), - float: number(), - int: number(), - // - json: jsonSchema, - mediumint: number(), - real: number(), - serial: number(), - smallint: number(), - text: string(), - textEnum: picklist([ - 'a', - 'b', - 'c', - ]), - tinytext: string(), - tinytextEnum: picklist([ - 'a', - 'b', - 'c', - ]), - mediumtext: string(), - mediumtextEnum: picklist([ - 'a', - 'b', - 'c', - ]), - longtext: string(), - longtextEnum: picklist([ - 'a', - 'b', - 'c', - ]), - time: string(), - timestamp: valiDate(), - timestampString: string(), - tinyint: number(), - varbinary: string([maxLength(200)]), - varchar: string([maxLength(200)]), - varcharEnum: picklist([ - 'a', - 'b', - 'c', - ]), - year: number(), - autoIncrement: number(), +test('nullability - insert', (t) => { + const table = mysqlTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().default(1), + c4: int().notNull().default(1), + c5: int().generatedAlwaysAs(1), }); - expectSchemaShape(t, expected).from(actual); + const result = createInsertSchema(table); + const expected = v.object({ + c1: v.optional(v.nullable(intSchema)), + c2: intSchema, + c3: v.optional(v.nullable(intSchema)), + c4: v.optional(intSchema), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('select schema w/ refine', (t) => { - const actual = createSelectSchema(testTable, { - bigint: (_) => valibigint([minValue(0n)]), +test('nullability - update', (t) => { + const table = mysqlTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().default(1), + c4: int().notNull().default(1), + c5: int().generatedAlwaysAs(1), }); - const expected = object({ - bigint: valibigint([minValue(0n)]), - bigintNumber: number(), - binary: string(), - boolean: valiboolean(), - char: string([minLength(5), maxLength(5)]), - charEnum: picklist([ - 'a', - 'b', - 'c', - ]), - customInt: any(), - date: valiDate(), - dateString: string(), - datetime: valiDate(), - datetimeString: string(), - decimal: string(), - double: number(), - enum: picklist([ - 'a', - 'b', - 'c', - ]), - float: number(), - int: number(), - json: jsonSchema, - mediumint: number(), - real: number(), - serial: number(), - smallint: number(), - text: string(), - textEnum: picklist([ - 'a', - 'b', - 'c', - ]), - tinytext: string(), - tinytextEnum: picklist([ - 'a', - 'b', - 'c', - ]), - mediumtext: string(), - mediumtextEnum: picklist([ - 'a', - 'b', - 'c', - ]), - longtext: string(), - longtextEnum: picklist([ - 'a', - 'b', - 'c', - ]), - time: string(), - timestamp: valiDate(), - timestampString: string(), - tinyint: number(), - varbinary: string([maxLength(200)]), - varchar: string([maxLength(200)]), - varcharEnum: picklist([ - 'a', - 'b', - 'c', - ]), - year: number(), - autoIncrement: number(), + const result = createUpdateSchema(table); + const expected = v.object({ + c1: v.optional(v.nullable(intSchema)), + c2: v.optional(intSchema), + c3: v.optional(v.nullable(intSchema)), + c4: v.optional(intSchema), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('refine table - select', (t) => { + const table = mysqlTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().notNull(), + }); + + const result = createSelectSchema(table, { + c2: (schema) => v.pipe(schema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + }); + const expected = v.object({ + c1: v.nullable(intSchema), + c2: v.pipe(intSchema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('refine table - insert', (t) => { + const table = mysqlTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().notNull(), + c4: int().generatedAlwaysAs(1), + }); + + const result = createInsertSchema(table, { + c2: (schema) => v.pipe(schema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + }); + const expected = v.object({ + c1: v.optional(v.nullable(intSchema)), + c2: v.pipe(intSchema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); - expectSchemaShape(t, expected).from(actual); +test('refine table - update', (t) => { + const table = mysqlTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().notNull(), + c4: int().generatedAlwaysAs(1), + }); + + const result = createUpdateSchema(table, { + c2: (schema) => v.pipe(schema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + }); + const expected = v.object({ + c1: v.optional(v.nullable(intSchema)), + c2: v.optional(v.pipe(intSchema, v.maxValue(1000))), + c3: v.pipe(v.string(), v.transform(Number)), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); + +test('refine view - select', (t) => { + const table = mysqlTable('test', { + c1: int(), + c2: int(), + c3: int(), + c4: int(), + c5: int(), + c6: int(), + }); + const view = mysqlView('test').as((qb) => + qb.select({ + c1: table.c1, + c2: table.c2, + c3: table.c3, + nested: { + c4: table.c4, + c5: table.c5, + c6: table.c6, + }, + table, + }).from(table) + ); + + const result = createSelectSchema(view, { + c2: (schema) => v.pipe(schema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + nested: { + c5: (schema) => v.pipe(schema, v.maxValue(1000)), + c6: v.pipe(v.string(), v.transform(Number)), + }, + table: { + c2: (schema) => v.pipe(schema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + }, + }); + const expected = v.object({ + c1: v.nullable(intSchema), + c2: v.nullable(v.pipe(intSchema, v.maxValue(1000))), + c3: v.pipe(v.string(), v.transform(Number)), + nested: v.object({ + c4: v.nullable(intSchema), + c5: v.nullable(v.pipe(intSchema, v.maxValue(1000))), + c6: v.pipe(v.string(), v.transform(Number)), + }), + table: v.object({ + c1: v.nullable(intSchema), + c2: v.nullable(v.pipe(intSchema, v.maxValue(1000))), + c3: v.pipe(v.string(), v.transform(Number)), + c4: v.nullable(intSchema), + c5: v.nullable(intSchema), + c6: v.nullable(intSchema), + }), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('all data types', (t) => { + const table = mysqlTable('test', ({ + bigint, + binary, + boolean, + char, + date, + datetime, + decimal, + double, + float, + int, + json, + mediumint, + mysqlEnum, + real, + serial, + smallint, + text, + time, + timestamp, + tinyint, + varchar, + varbinary, + year, + longtext, + mediumtext, + tinytext, + }) => ({ + bigint1: bigint({ mode: 'number' }).notNull(), + bigint2: bigint({ mode: 'bigint' }).notNull(), + bigint3: bigint({ unsigned: true, mode: 'number' }).notNull(), + bigint4: bigint({ unsigned: true, mode: 'bigint' }).notNull(), + binary: binary({ length: 10 }).notNull(), + boolean: boolean().notNull(), + char1: char({ length: 10 }).notNull(), + char2: char({ length: 1, enum: ['a', 'b', 'c'] }).notNull(), + date1: date({ mode: 'date' }).notNull(), + date2: date({ mode: 'string' }).notNull(), + datetime1: datetime({ mode: 'date' }).notNull(), + datetime2: datetime({ mode: 'string' }).notNull(), + decimal1: decimal().notNull(), + decimal2: decimal({ unsigned: true }).notNull(), + double1: double().notNull(), + double2: double({ unsigned: true }).notNull(), + float1: float().notNull(), + float2: float({ unsigned: true }).notNull(), + int1: int().notNull(), + int2: int({ unsigned: true }).notNull(), + json: json().notNull(), + mediumint1: mediumint().notNull(), + mediumint2: mediumint({ unsigned: true }).notNull(), + enum: mysqlEnum('enum', ['a', 'b', 'c']).notNull(), + real: real().notNull(), + serial: serial().notNull(), + smallint1: smallint().notNull(), + smallint2: smallint({ unsigned: true }).notNull(), + text1: text().notNull(), + text2: text({ enum: ['a', 'b', 'c'] }).notNull(), + time: time().notNull(), + timestamp1: timestamp({ mode: 'date' }).notNull(), + timestamp2: timestamp({ mode: 'string' }).notNull(), + tinyint1: tinyint().notNull(), + tinyint2: tinyint({ unsigned: true }).notNull(), + varchar1: varchar({ length: 10 }).notNull(), + varchar2: varchar({ length: 1, enum: ['a', 'b', 'c'] }).notNull(), + varbinary: varbinary({ length: 10 }).notNull(), + year: year().notNull(), + longtext1: longtext().notNull(), + longtext2: longtext({ enum: ['a', 'b', 'c'] }).notNull(), + mediumtext1: mediumtext().notNull(), + mediumtext2: mediumtext({ enum: ['a', 'b', 'c'] }).notNull(), + tinytext1: tinytext().notNull(), + tinytext2: tinytext({ enum: ['a', 'b', 'c'] }).notNull(), + })); + + const result = createSelectSchema(table); + const expected = v.object({ + bigint1: v.pipe(v.number(), v.minValue(Number.MIN_SAFE_INTEGER), v.maxValue(Number.MAX_SAFE_INTEGER), v.integer()), + bigint2: v.pipe(v.bigint(), v.minValue(CONSTANTS.INT64_MIN), v.maxValue(CONSTANTS.INT64_MAX)), + bigint3: v.pipe(v.number(), v.minValue(0 as number), v.maxValue(Number.MAX_SAFE_INTEGER), v.integer()), + bigint4: v.pipe(v.bigint(), v.minValue(0n as bigint), v.maxValue(CONSTANTS.INT64_UNSIGNED_MAX)), + binary: v.string(), + boolean: v.boolean(), + char1: v.pipe(v.string(), v.length(10 as number)), + char2: v.enum({ a: 'a', b: 'b', c: 'c' }), + date1: v.date(), + date2: v.string(), + datetime1: v.date(), + datetime2: v.string(), + decimal1: v.string(), + decimal2: v.string(), + double1: v.pipe(v.number(), v.minValue(CONSTANTS.INT48_MIN), v.maxValue(CONSTANTS.INT48_MAX)), + double2: v.pipe(v.number(), v.minValue(0 as number), v.maxValue(CONSTANTS.INT48_UNSIGNED_MAX)), + float1: v.pipe(v.number(), v.minValue(CONSTANTS.INT24_MIN), v.maxValue(CONSTANTS.INT24_MAX)), + float2: v.pipe(v.number(), v.minValue(0 as number), v.maxValue(CONSTANTS.INT24_UNSIGNED_MAX)), + int1: v.pipe(v.number(), v.minValue(CONSTANTS.INT32_MIN), v.maxValue(CONSTANTS.INT32_MAX), v.integer()), + int2: v.pipe(v.number(), v.minValue(0 as number), v.maxValue(CONSTANTS.INT32_UNSIGNED_MAX), v.integer()), + json: jsonSchema, + mediumint1: v.pipe(v.number(), v.minValue(CONSTANTS.INT24_MIN), v.maxValue(CONSTANTS.INT24_MAX), v.integer()), + mediumint2: v.pipe(v.number(), v.minValue(0 as number), v.maxValue(CONSTANTS.INT24_UNSIGNED_MAX), v.integer()), + enum: v.enum({ a: 'a', b: 'b', c: 'c' }), + real: v.pipe(v.number(), v.minValue(CONSTANTS.INT48_MIN), v.maxValue(CONSTANTS.INT48_MAX)), + serial: v.pipe(v.number(), v.minValue(0 as number), v.maxValue(Number.MAX_SAFE_INTEGER), v.integer()), + smallint1: v.pipe(v.number(), v.minValue(CONSTANTS.INT16_MIN), v.maxValue(CONSTANTS.INT16_MAX), v.integer()), + smallint2: v.pipe(v.number(), v.minValue(0 as number), v.maxValue(CONSTANTS.INT16_UNSIGNED_MAX), v.integer()), + text1: v.pipe(v.string(), v.maxLength(CONSTANTS.INT16_UNSIGNED_MAX)), + text2: v.enum({ a: 'a', b: 'b', c: 'c' }), + time: v.string(), + timestamp1: v.date(), + timestamp2: v.string(), + tinyint1: v.pipe(v.number(), v.minValue(CONSTANTS.INT8_MIN), v.maxValue(CONSTANTS.INT8_MAX), v.integer()), + tinyint2: v.pipe(v.number(), v.minValue(0 as number), v.maxValue(CONSTANTS.INT8_UNSIGNED_MAX), v.integer()), + varchar1: v.pipe(v.string(), v.maxLength(10 as number)), + varchar2: v.enum({ a: 'a', b: 'b', c: 'c' }), + varbinary: v.string(), + year: v.pipe(v.number(), v.minValue(1901 as number), v.maxValue(2155 as number), v.integer()), + longtext1: v.pipe(v.string(), v.maxLength(CONSTANTS.INT32_UNSIGNED_MAX)), + longtext2: v.enum({ a: 'a', b: 'b', c: 'c' }), + mediumtext1: v.pipe(v.string(), v.maxLength(CONSTANTS.INT24_UNSIGNED_MAX)), + mediumtext2: v.enum({ a: 'a', b: 'b', c: 'c' }), + tinytext1: v.pipe(v.string(), v.maxLength(CONSTANTS.INT8_UNSIGNED_MAX)), + tinytext2: v.enum({ a: 'a', b: 'b', c: 'c' }), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +/* Disallow unknown keys in table refinement - select */ { + const table = mysqlTable('test', { id: int() }); + // @ts-expect-error + createSelectSchema(table, { unknown: v.string() }); +} + +/* Disallow unknown keys in table refinement - insert */ { + const table = mysqlTable('test', { id: int() }); + // @ts-expect-error + createInsertSchema(table, { unknown: v.string() }); +} + +/* Disallow unknown keys in table refinement - update */ { + const table = mysqlTable('test', { id: int() }); + // @ts-expect-error + createUpdateSchema(table, { unknown: v.string() }); +} + +/* Disallow unknown keys in view qb - select */ { + const table = mysqlTable('test', { id: int() }); + const view = mysqlView('test').as((qb) => qb.select().from(table)); + const nestedSelect = mysqlView('test').as((qb) => qb.select({ table }).from(table)); + // @ts-expect-error + createSelectSchema(view, { unknown: v.string() }); + // @ts-expect-error + createSelectSchema(nestedSelect, { table: { unknown: v.string() } }); +} + +/* Disallow unknown keys in view columns - select */ { + const view = mysqlView('test', { id: int() }).as(sql``); + // @ts-expect-error + createSelectSchema(view, { unknown: v.string() }); +} diff --git a/drizzle-valibot/tests/pg.test.ts b/drizzle-valibot/tests/pg.test.ts index 659845fa1..4d1651a7c 100644 --- a/drizzle-valibot/tests/pg.test.ts +++ b/drizzle-valibot/tests/pg.test.ts @@ -1,184 +1,510 @@ -import { char, date, integer, pgEnum, pgTable, serial, text, timestamp, varchar } from 'drizzle-orm/pg-core'; -import { - array, - date as valiDate, - email, - maxLength, - minLength, - minValue, - nullable, - number, - object, - optional, - parse, - picklist, - string, -} from 'valibot'; -import { expect, test } from 'vitest'; -import { createInsertSchema, createSelectSchema } from '../src'; -import { expectSchemaShape } from './utils.ts'; - -export const roleEnum = pgEnum('role', ['admin', 'user']); - -const users = pgTable('users', { - a: integer('a').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 testUser = { - a: [1, 2, 3], - id: 1, - name: 'John Doe', - email: 'john.doe@example.com', - birthdayString: '1990-01-01', - birthdayDate: new Date('1990-01-01'), - createdAt: new Date(), - role: 'admin' as const, - roleText: 'admin' as const, - roleText2: 'admin' as const, - profession: 'Software Engineer', - initials: 'JD', -}; - -test('users insert valid user', () => { - const schema = createInsertSchema(users); - - expect(parse(schema, testUser)).toStrictEqual(testUser); -}); - -test('users insert invalid varchar', () => { - const schema = createInsertSchema(users); - - expect(() => - parse(schema, { - ...testUser, - profession: 'Chief Executive Officer', - }) - ).toThrow(undefined); -}); - -test('users insert invalid char', () => { - const schema = createInsertSchema(users); - - expect(() => parse(schema, { ...testUser, initials: 'JoDo' })).toThrow(undefined); -}); - -test('users insert schema', (t) => { - const actual = createInsertSchema(users, { - id: () => number([minValue(0)]), - email: () => string([email()]), - roleText: picklist(['user', 'manager', 'admin']), - }); - - (() => { - { - createInsertSchema(users, { - // @ts-expect-error (missing property) - foobar: number(), - }); - } - - { - createInsertSchema(users, { - // @ts-expect-error (invalid type) - id: 123, - }); - } - }); - - const expected = object({ - a: optional(nullable(array(number()))), - id: optional(number([minValue(0)])), - name: optional(nullable(string())), - email: string(), - birthdayString: string(), - birthdayDate: valiDate(), - createdAt: optional(valiDate()), - role: picklist(['admin', 'user']), - roleText: picklist(['user', 'manager', 'admin']), - roleText2: optional(picklist(['admin', 'user'])), - profession: string([maxLength(20), minLength(1)]), - initials: string([maxLength(2), minLength(1)]), - }); - - expectSchemaShape(t, expected).from(actual); -}); - -test('users insert schema w/ defaults', (t) => { - const actual = createInsertSchema(users); - - const expected = object({ - a: optional(nullable(array(number()))), - id: optional(number()), - name: optional(nullable(string())), - email: string(), - birthdayString: string(), - birthdayDate: valiDate(), - createdAt: optional(valiDate()), - role: picklist(['admin', 'user']), - roleText: picklist(['admin', 'user']), - roleText2: optional(picklist(['admin', 'user'])), - profession: string([maxLength(20), minLength(1)]), - initials: string([maxLength(2), minLength(1)]), - }); - - expectSchemaShape(t, expected).from(actual); +import { type Equal, sql } from 'drizzle-orm'; +import { integer, pgEnum, pgMaterializedView, pgSchema, pgTable, pgView, serial, text } from 'drizzle-orm/pg-core'; +import * as v from 'valibot'; +import { test } from 'vitest'; +import { jsonSchema } from '~/column.ts'; +import { CONSTANTS } from '~/constants.ts'; +import { createInsertSchema, createSelectSchema, createUpdateSchema } from '../src'; +import { Expect, expectEnumValues, expectSchemaShape } from './utils.ts'; + +const integerSchema = v.pipe(v.number(), v.minValue(CONSTANTS.INT32_MIN), v.maxValue(CONSTANTS.INT32_MAX), v.integer()); +const textSchema = v.string(); + +test('table - select', (t) => { + const table = pgTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + + const result = createSelectSchema(table); + const expected = v.object({ id: integerSchema, name: textSchema }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('table in schema - select', (tc) => { + const schema = pgSchema('test'); + const table = schema.table('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + + const result = createSelectSchema(table); + const expected = v.object({ id: integerSchema, name: textSchema }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); + +test('table - insert', (t) => { + const table = pgTable('test', { + id: integer().generatedAlwaysAsIdentity().primaryKey(), + name: text().notNull(), + age: integer(), + }); + + const result = createInsertSchema(table); + const expected = v.object({ name: textSchema, age: v.optional(v.nullable(integerSchema)) }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('table - update', (t) => { + const table = pgTable('test', { + id: integer().generatedAlwaysAsIdentity().primaryKey(), + name: text().notNull(), + age: integer(), + }); + + const result = createUpdateSchema(table); + const expected = v.object({ + name: v.optional(textSchema), + age: v.optional(v.nullable(integerSchema)), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('view qb - select', (t) => { + const table = pgTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + const view = pgView('test').as((qb) => qb.select({ id: table.id, age: sql``.as('age') }).from(table)); + + const result = createSelectSchema(view); + const expected = v.object({ id: integerSchema, age: v.any() }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('view columns - select', (t) => { + const view = pgView('test', { + id: serial().primaryKey(), + name: text().notNull(), + }).as(sql``); + + const result = createSelectSchema(view); + const expected = v.object({ id: integerSchema, name: textSchema }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('materialized view qb - select', (t) => { + const table = pgTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + const view = pgMaterializedView('test').as((qb) => qb.select({ id: table.id, age: sql``.as('age') }).from(table)); + + const result = createSelectSchema(view); + const expected = v.object({ id: integerSchema, age: v.any() }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('materialized view columns - select', (t) => { + const view = pgView('test', { + id: serial().primaryKey(), + name: text().notNull(), + }).as(sql``); + + const result = createSelectSchema(view); + const expected = v.object({ id: integerSchema, name: textSchema }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('view with nested fields - select', (t) => { + const table = pgTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + const view = pgMaterializedView('test').as((qb) => + qb.select({ + id: table.id, + nested: { + name: table.name, + age: sql``.as('age'), + }, + table, + }).from(table) + ); + + const result = createSelectSchema(view); + const expected = v.object({ + id: integerSchema, + nested: v.object({ name: textSchema, age: v.any() }), + table: v.object({ id: integerSchema, name: textSchema }), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('enum - select', (t) => { + const enum_ = pgEnum('test', ['a', 'b', 'c']); + + const result = createSelectSchema(enum_); + const expected = v.enum({ a: 'a', b: 'b', c: 'c' }); + expectEnumValues(t, expected).from(result); + Expect>(); +}); + +test('nullability - select', (t) => { + const table = pgTable('test', { + c1: integer(), + c2: integer().notNull(), + c3: integer().default(1), + c4: integer().notNull().default(1), + }); + + const result = createSelectSchema(table); + const expected = v.object({ + c1: v.nullable(integerSchema), + c2: integerSchema, + c3: v.nullable(integerSchema), + c4: integerSchema, + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('users select schema', (t) => { - const actual = createSelectSchema(users, { - id: () => number([minValue(0)]), - email: () => string(), - roleText: picklist(['user', 'manager', 'admin']), - }); - - const expected = object({ - a: nullable(array(number())), - id: number([minValue(0)]), - name: nullable(string()), - email: string(), - birthdayString: string(), - birthdayDate: valiDate(), - createdAt: valiDate(), - role: picklist(['admin', 'user']), - roleText: picklist(['user', 'manager', 'admin']), - roleText2: picklist(['admin', 'user']), - profession: string([maxLength(20), minLength(1)]), - initials: string([maxLength(2), minLength(1)]), +test('nullability - insert', (t) => { + const table = pgTable('test', { + c1: integer(), + c2: integer().notNull(), + c3: integer().default(1), + c4: integer().notNull().default(1), + c5: integer().generatedAlwaysAs(1), + c6: integer().generatedAlwaysAsIdentity(), + c7: integer().generatedByDefaultAsIdentity(), }); - - expectSchemaShape(t, expected).from(actual); + + const result = createInsertSchema(table); + const expected = v.object({ + c1: v.optional(v.nullable(integerSchema)), + c2: integerSchema, + c3: v.optional(v.nullable(integerSchema)), + c4: v.optional(integerSchema), + c7: v.optional(integerSchema), + }); + expectSchemaShape(t, expected).from(result); }); -test('users select schema w/ defaults', (t) => { - const actual = createSelectSchema(users); - - const expected = object({ - a: nullable(array(number())), - id: number(), - name: nullable(string()), - email: string(), - birthdayString: string(), - birthdayDate: valiDate(), - createdAt: valiDate(), - role: picklist(['admin', 'user']), - roleText: picklist(['admin', 'user']), - roleText2: picklist(['admin', 'user']), - profession: string([maxLength(20), minLength(1)]), - initials: string([maxLength(2), minLength(1)]), - }); - - expectSchemaShape(t, expected).from(actual); +test('nullability - update', (t) => { + const table = pgTable('test', { + c1: integer(), + c2: integer().notNull(), + c3: integer().default(1), + c4: integer().notNull().default(1), + c5: integer().generatedAlwaysAs(1), + c6: integer().generatedAlwaysAsIdentity(), + c7: integer().generatedByDefaultAsIdentity(), + }); + + const result = createUpdateSchema(table); + const expected = v.object({ + c1: v.optional(v.nullable(integerSchema)), + c2: v.optional(integerSchema), + c3: v.optional(v.nullable(integerSchema)), + c4: v.optional(integerSchema), + c7: v.optional(integerSchema), + }); + table.c5.generated?.type; + expectSchemaShape(t, expected).from(result); + Expect>(); }); + +test('refine table - select', (t) => { + const table = pgTable('test', { + c1: integer(), + c2: integer().notNull(), + c3: integer().notNull(), + }); + + const result = createSelectSchema(table, { + c2: (schema) => v.pipe(schema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + }); + const expected = v.object({ + c1: v.nullable(integerSchema), + c2: v.pipe(integerSchema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('refine table - insert', (t) => { + const table = pgTable('test', { + c1: integer(), + c2: integer().notNull(), + c3: integer().notNull(), + c4: integer().generatedAlwaysAs(1), + }); + + const result = createInsertSchema(table, { + c2: (schema) => v.pipe(schema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + }); + const expected = v.object({ + c1: v.optional(v.nullable(integerSchema)), + c2: v.pipe(integerSchema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('refine table - update', (t) => { + const table = pgTable('test', { + c1: integer(), + c2: integer().notNull(), + c3: integer().notNull(), + c4: integer().generatedAlwaysAs(1), + }); + + const result = createUpdateSchema(table, { + c2: (schema) => v.pipe(schema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + }); + const expected = v.object({ + c1: v.optional(v.nullable(integerSchema)), + c2: v.optional(v.pipe(integerSchema, v.maxValue(1000))), + c3: v.pipe(v.string(), v.transform(Number)), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('refine view - select', (t) => { + const table = pgTable('test', { + c1: integer(), + c2: integer(), + c3: integer(), + c4: integer(), + c5: integer(), + c6: integer(), + }); + const view = pgView('test').as((qb) => + qb.select({ + c1: table.c1, + c2: table.c2, + c3: table.c3, + nested: { + c4: table.c4, + c5: table.c5, + c6: table.c6, + }, + table, + }).from(table) + ); + + const result = createSelectSchema(view, { + c2: (schema) => v.pipe(schema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + nested: { + c5: (schema) => v.pipe(schema, v.maxValue(1000)), + c6: v.pipe(v.string(), v.transform(Number)), + }, + table: { + c2: (schema) => v.pipe(schema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + }, + }); + const expected = v.object({ + c1: v.nullable(integerSchema), + c2: v.nullable(v.pipe(integerSchema, v.maxValue(1000))), + c3: v.pipe(v.string(), v.transform(Number)), + nested: v.object({ + c4: v.nullable(integerSchema), + c5: v.nullable(v.pipe(integerSchema, v.maxValue(1000))), + c6: v.pipe(v.string(), v.transform(Number)), + }), + table: v.object({ + c1: v.nullable(integerSchema), + c2: v.nullable(v.pipe(integerSchema, v.maxValue(1000))), + c3: v.pipe(v.string(), v.transform(Number)), + c4: v.nullable(integerSchema), + c5: v.nullable(integerSchema), + c6: v.nullable(integerSchema), + }), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('all data types', (t) => { + const table = pgTable('test', ({ + bigint, + bigserial, + bit, + boolean, + date, + char, + cidr, + doublePrecision, + geometry, + halfvec, + inet, + integer, + interval, + json, + jsonb, + line, + macaddr, + macaddr8, + numeric, + point, + real, + serial, + smallint, + smallserial, + text, + sparsevec, + time, + timestamp, + uuid, + varchar, + vector, + }) => ({ + bigint1: bigint({ mode: 'number' }).notNull(), + bigint2: bigint({ mode: 'bigint' }).notNull(), + bigserial1: bigserial({ mode: 'number' }).notNull(), + bigserial2: bigserial({ mode: 'bigint' }).notNull(), + bit: bit({ dimensions: 5 }).notNull(), + boolean: boolean().notNull(), + date1: date({ mode: 'date' }).notNull(), + date2: date({ mode: 'string' }).notNull(), + char1: char({ length: 10 }).notNull(), + char2: char({ length: 1, enum: ['a', 'b', 'c'] }).notNull(), + cidr: cidr().notNull(), + doublePrecision: doublePrecision().notNull(), + geometry1: geometry({ type: 'point', mode: 'tuple' }).notNull(), + geometry2: geometry({ type: 'point', mode: 'xy' }).notNull(), + halfvec: halfvec({ dimensions: 3 }).notNull(), + inet: inet().notNull(), + integer: integer().notNull(), + interval: interval().notNull(), + json: json().notNull(), + jsonb: jsonb().notNull(), + line1: line({ mode: 'abc' }).notNull(), + line2: line({ mode: 'tuple' }).notNull(), + macaddr: macaddr().notNull(), + macaddr8: macaddr8().notNull(), + numeric: numeric().notNull(), + point1: point({ mode: 'xy' }).notNull(), + point2: point({ mode: 'tuple' }).notNull(), + real: real().notNull(), + serial: serial().notNull(), + smallint: smallint().notNull(), + smallserial: smallserial().notNull(), + text1: text().notNull(), + text2: text({ enum: ['a', 'b', 'c'] }).notNull(), + sparsevec: sparsevec({ dimensions: 3 }).notNull(), + time: time().notNull(), + timestamp1: timestamp({ mode: 'date' }).notNull(), + timestamp2: timestamp({ mode: 'string' }).notNull(), + uuid: uuid().notNull(), + varchar1: varchar({ length: 10 }).notNull(), + varchar2: varchar({ length: 1, enum: ['a', 'b', 'c'] }).notNull(), + vector: vector({ dimensions: 3 }).notNull(), + array1: integer().array().notNull(), + array2: integer().array().array(2).notNull(), + array3: varchar({ length: 10 }).array().array(2).notNull(), + })); + + const result = createSelectSchema(table); + const expected = v.object({ + bigint1: v.pipe(v.number(), v.minValue(Number.MIN_SAFE_INTEGER), v.maxValue(Number.MAX_SAFE_INTEGER), v.integer()), + bigint2: v.pipe(v.bigint(), v.minValue(CONSTANTS.INT64_MIN), v.maxValue(CONSTANTS.INT64_MAX)), + bigserial1: v.pipe( + v.number(), + v.minValue(Number.MIN_SAFE_INTEGER), + v.maxValue(Number.MAX_SAFE_INTEGER), + v.integer(), + ), + bigserial2: v.pipe(v.bigint(), v.minValue(CONSTANTS.INT64_MIN), v.maxValue(CONSTANTS.INT64_MAX)), + bit: v.pipe(v.string(), v.regex(/^[01]+$/), v.maxLength(5 as number)), + boolean: v.boolean(), + date1: v.date(), + date2: v.string(), + char1: v.pipe(v.string(), v.length(10 as number)), + char2: v.enum({ a: 'a', b: 'b', c: 'c' }), + cidr: v.string(), + doublePrecision: v.pipe(v.number(), v.minValue(CONSTANTS.INT48_MIN), v.maxValue(CONSTANTS.INT48_MAX)), + geometry1: v.tuple([v.number(), v.number()]), + geometry2: v.object({ x: v.number(), y: v.number() }), + halfvec: v.pipe(v.array(v.number()), v.length(3 as number)), + inet: v.string(), + integer: v.pipe(v.number(), v.minValue(CONSTANTS.INT32_MIN), v.maxValue(CONSTANTS.INT32_MAX), v.integer()), + interval: v.string(), + json: jsonSchema, + jsonb: jsonSchema, + line1: v.object({ a: v.number(), b: v.number(), c: v.number() }), + line2: v.tuple([v.number(), v.number(), v.number()]), + macaddr: v.string(), + macaddr8: v.string(), + numeric: v.string(), + point1: v.object({ x: v.number(), y: v.number() }), + point2: v.tuple([v.number(), v.number()]), + real: v.pipe(v.number(), v.minValue(CONSTANTS.INT24_MIN), v.maxValue(CONSTANTS.INT24_MAX)), + serial: v.pipe(v.number(), v.minValue(CONSTANTS.INT32_MIN), v.maxValue(CONSTANTS.INT32_MAX), v.integer()), + smallint: v.pipe(v.number(), v.minValue(CONSTANTS.INT16_MIN), v.maxValue(CONSTANTS.INT16_MAX), v.integer()), + smallserial: v.pipe(v.number(), v.minValue(CONSTANTS.INT16_MIN), v.maxValue(CONSTANTS.INT16_MAX), v.integer()), + text1: v.string(), + text2: v.enum({ a: 'a', b: 'b', c: 'c' }), + sparsevec: v.string(), + time: v.string(), + timestamp1: v.date(), + timestamp2: v.string(), + uuid: v.pipe(v.string(), v.uuid()), + varchar1: v.pipe(v.string(), v.maxLength(10 as number)), + varchar2: v.enum({ a: 'a', b: 'b', c: 'c' }), + vector: v.pipe(v.array(v.number()), v.length(3 as number)), + array1: v.array(integerSchema), + array2: v.pipe(v.array(v.array(integerSchema)), v.length(2 as number)), + array3: v.pipe(v.array(v.array(v.pipe(v.string(), v.maxLength(10 as number)))), v.length(2 as number)), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +/* Disallow unknown keys in table refinement - select */ { + const table = pgTable('test', { id: integer() }); + // @ts-expect-error + createSelectSchema(table, { unknown: v.string() }); +} + +/* Disallow unknown keys in table refinement - insert */ { + const table = pgTable('test', { id: integer() }); + // @ts-expect-error + createInsertSchema(table, { unknown: v.string() }); +} + +/* Disallow unknown keys in table refinement - update */ { + const table = pgTable('test', { id: integer() }); + // @ts-expect-error + createUpdateSchema(table, { unknown: v.string() }); +} + +/* Disallow unknown keys in view qb - select */ { + const table = pgTable('test', { id: integer() }); + const view = pgView('test').as((qb) => qb.select().from(table)); + const mView = pgMaterializedView('test').as((qb) => qb.select().from(table)); + const nestedSelect = pgView('test').as((qb) => qb.select({ table }).from(table)); + // @ts-expect-error + createSelectSchema(view, { unknown: v.string() }); + // @ts-expect-error + createSelectSchema(mView, { unknown: v.string() }); + // @ts-expect-error + createSelectSchema(nestedSelect, { table: { unknown: v.string() } }); +} + +/* Disallow unknown keys in view columns - select */ { + const view = pgView('test', { id: integer() }).as(sql``); + const mView = pgView('test', { id: integer() }).as(sql``); + // @ts-expect-error + createSelectSchema(view, { unknown: v.string() }); + // @ts-expect-error + createSelectSchema(mView, { unknown: v.string() }); +} diff --git a/drizzle-valibot/tests/sqlite.test.ts b/drizzle-valibot/tests/sqlite.test.ts index a520108f0..7eb5fc7bf 100644 --- a/drizzle-valibot/tests/sqlite.test.ts +++ b/drizzle-valibot/tests/sqlite.test.ts @@ -1,178 +1,364 @@ -import { blob, integer, numeric, real, sqliteTable, text } from 'drizzle-orm/sqlite-core'; -import { - bigint as valibigint, - boolean, - date as valiDate, - minValue, - nullable, - number, - object, - optional, - type Output, - parse, - picklist, - string, -} from 'valibot'; -import { expect, test } from 'vitest'; -import { createInsertSchema, createSelectSchema, jsonSchema } from '../src'; -import { expectSchemaShape } from './utils.ts'; - -const blobJsonSchema = object({ - foo: string(), +import { type Equal, sql } from 'drizzle-orm'; +import { int, sqliteTable, sqliteView, text } from 'drizzle-orm/sqlite-core'; +import * as v from 'valibot'; +import { test } from 'vitest'; +import { bufferSchema, jsonSchema } from '~/column.ts'; +import { CONSTANTS } from '~/constants.ts'; +import { createInsertSchema, createSelectSchema, createUpdateSchema } from '../src'; +import { Expect, expectSchemaShape } from './utils.ts'; + +const intSchema = v.pipe( + v.number(), + v.minValue(Number.MIN_SAFE_INTEGER), + v.maxValue(Number.MAX_SAFE_INTEGER), + v.integer(), +); +const textSchema = v.string(); + +test('table - select', (t) => { + const table = sqliteTable('test', { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + }); + + const result = createSelectSchema(table); + const expected = v.object({ id: intSchema, name: textSchema }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -const users = 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'), +test('table - insert', (t) => { + const table = sqliteTable('test', { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + age: int(), + }); + + const result = createInsertSchema(table); + const expected = v.object({ id: v.optional(intSchema), name: textSchema, age: v.optional(v.nullable(intSchema)) }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -const testUser = { - id: 1, - blobJson: { foo: 'bar' }, - blobBigInt: BigInt(123), - numeric: '123.45', - createdAt: new Date(), - createdAtMs: new Date(), - boolean: true, - real: 123.45, - text: 'foobar', - role: 'admin' as const, -}; - -test('users insert valid user', () => { - const schema = createInsertSchema(users); - // - expect(parse(schema, testUser)).toStrictEqual(testUser); +test('table - update', (t) => { + const table = sqliteTable('test', { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + age: int(), + }); + + const result = createUpdateSchema(table); + const expected = v.object({ + id: v.optional(intSchema), + name: v.optional(textSchema), + age: v.optional(v.nullable(intSchema)), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('users insert invalid text length', () => { - const schema = createInsertSchema(users); - expect(() => parse(schema, { ...testUser, text: 'a'.repeat(256) })).toThrow(undefined); +test('view qb - select', (t) => { + const table = sqliteTable('test', { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + }); + const view = sqliteView('test').as((qb) => qb.select({ id: table.id, age: sql``.as('age') }).from(table)); + + const result = createSelectSchema(view); + const expected = v.object({ id: intSchema, age: v.any() }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('users insert schema', (t) => { - const actual = createInsertSchema(users, { - id: () => number([minValue(0)]), - blobJson: blobJsonSchema, - role: picklist(['admin', 'user', 'manager']), - }); - - (() => { - { - createInsertSchema(users, { - // @ts-expect-error (missing property) - foobar: number(), - }); - } - - { - createInsertSchema(users, { - // @ts-expect-error (invalid type) - id: 123, - }); - } - }); - - const expected = object({ - id: optional(number([minValue(0)])), - blobJson: blobJsonSchema, - blobBigInt: valibigint(), - numeric: string(), - createdAt: valiDate(), - createdAtMs: valiDate(), - boolean: boolean(), - real: number(), - text: optional(nullable(string())), - role: optional(picklist(['admin', 'user', 'manager'])), - }); - - expectSchemaShape(t, expected).from(actual); +test('view columns - select', (t) => { + const view = sqliteView('test', { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + }).as(sql``); + + const result = createSelectSchema(view); + const expected = v.object({ id: intSchema, name: textSchema }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('users insert schema w/ defaults', (t) => { - const actual = createInsertSchema(users); +test('view with nested fields - select', (t) => { + const table = sqliteTable('test', { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + }); + const view = sqliteView('test').as((qb) => + qb.select({ + id: table.id, + nested: { + name: table.name, + age: sql``.as('age'), + }, + table, + }).from(table) + ); - const expected = object({ - id: optional(number()), - blobJson: jsonSchema, - blobBigInt: valibigint(), - numeric: string(), - createdAt: valiDate(), - createdAtMs: valiDate(), - boolean: boolean(), - real: number(), - text: optional(nullable(string())), - role: optional(picklist(['admin', 'user'])), + const result = createSelectSchema(view); + const expected = v.object({ + id: intSchema, + nested: v.object({ name: textSchema, age: v.any() }), + table: v.object({ id: intSchema, name: textSchema }), }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); - expectSchemaShape(t, expected).from(actual); +test('nullability - select', (t) => { + const table = sqliteTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().default(1), + c4: int().notNull().default(1), + }); + + const result = createSelectSchema(table); + const expected = v.object({ + c1: v.nullable(intSchema), + c2: intSchema, + c3: v.nullable(intSchema), + c4: intSchema, + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('users select schema', (t) => { - const actual = createSelectSchema(users, { - blobJson: jsonSchema, - role: picklist(['admin', 'user', 'manager']), - }); - - (() => { - { - createSelectSchema(users, { - // @ts-expect-error (missing property) - foobar: number(), - }); - } - - { - createSelectSchema(users, { - // @ts-expect-error (invalid type) - id: 123, - }); - } - }); - - const expected = object({ - id: number(), - blobJson: jsonSchema, - blobBigInt: valibigint(), - numeric: string(), - createdAt: valiDate(), - createdAtMs: valiDate(), - boolean: boolean(), - real: number(), - text: nullable(string()), - role: picklist(['admin', 'user', 'manager']), - }); - - expectSchemaShape(t, expected).from(actual); +test('nullability - insert', (t) => { + const table = sqliteTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().default(1), + c4: int().notNull().default(1), + c5: int().generatedAlwaysAs(1), + }); + + const result = createInsertSchema(table); + const expected = v.object({ + c1: v.optional(v.nullable(intSchema)), + c2: intSchema, + c3: v.optional(v.nullable(intSchema)), + c4: v.optional(intSchema), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('users select schema w/ defaults', (t) => { - const actual = createSelectSchema(users); +test('nullability - update', (t) => { + const table = sqliteTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().default(1), + c4: int().notNull().default(1), + c5: int().generatedAlwaysAs(1), + }); - const expected = object({ - id: number(), - blobJson: jsonSchema, - blobBigInt: valibigint(), - numeric: string(), - createdAt: valiDate(), - createdAtMs: valiDate(), - boolean: boolean(), - real: number(), - text: nullable(string()), - role: picklist(['admin', 'user']), + const result = createUpdateSchema(table); + const expected = v.object({ + c1: v.optional(v.nullable(intSchema)), + c2: v.optional(intSchema), + c3: v.optional(v.nullable(intSchema)), + c4: v.optional(intSchema), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('refine table - select', (t) => { + const table = sqliteTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().notNull(), }); - expectSchemaShape(t, expected).from(actual); + const result = createSelectSchema(table, { + c2: (schema) => v.pipe(schema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + }); + const expected = v.object({ + c1: v.nullable(intSchema), + c2: v.pipe(intSchema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); + +test('refine table - insert', (t) => { + const table = sqliteTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().notNull(), + c4: int().generatedAlwaysAs(1), + }); + + const result = createInsertSchema(table, { + c2: (schema) => v.pipe(schema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + }); + const expected = v.object({ + c1: v.optional(v.nullable(intSchema)), + c2: v.pipe(intSchema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('refine table - update', (t) => { + const table = sqliteTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().notNull(), + c4: int().generatedAlwaysAs(1), + }); + + const result = createUpdateSchema(table, { + c2: (schema) => v.pipe(schema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + }); + const expected = v.object({ + c1: v.optional(v.nullable(intSchema)), + c2: v.optional(v.pipe(intSchema, v.maxValue(1000))), + c3: v.pipe(v.string(), v.transform(Number)), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('refine view - select', (t) => { + const table = sqliteTable('test', { + c1: int(), + c2: int(), + c3: int(), + c4: int(), + c5: int(), + c6: int(), + }); + const view = sqliteView('test').as((qb) => + qb.select({ + c1: table.c1, + c2: table.c2, + c3: table.c3, + nested: { + c4: table.c4, + c5: table.c5, + c6: table.c6, + }, + table, + }).from(table) + ); + + const result = createSelectSchema(view, { + c2: (schema) => v.pipe(schema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + nested: { + c5: (schema) => v.pipe(schema, v.maxValue(1000)), + c6: v.pipe(v.string(), v.transform(Number)), + }, + table: { + c2: (schema) => v.pipe(schema, v.maxValue(1000)), + c3: v.pipe(v.string(), v.transform(Number)), + }, + }); + const expected = v.object({ + c1: v.nullable(intSchema), + c2: v.nullable(v.pipe(intSchema, v.maxValue(1000))), + c3: v.pipe(v.string(), v.transform(Number)), + nested: v.object({ + c4: v.nullable(intSchema), + c5: v.nullable(v.pipe(intSchema, v.maxValue(1000))), + c6: v.pipe(v.string(), v.transform(Number)), + }), + table: v.object({ + c1: v.nullable(intSchema), + c2: v.nullable(v.pipe(intSchema, v.maxValue(1000))), + c3: v.pipe(v.string(), v.transform(Number)), + c4: v.nullable(intSchema), + c5: v.nullable(intSchema), + c6: v.nullable(intSchema), + }), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('all data types', (t) => { + const table = sqliteTable('test', ({ + blob, + integer, + numeric, + real, + text, + }) => ({ + blob1: blob({ mode: 'buffer' }).notNull(), + blob2: blob({ mode: 'bigint' }).notNull(), + blob3: blob({ mode: 'json' }).notNull(), + integer1: integer({ mode: 'number' }).notNull(), + integer2: integer({ mode: 'boolean' }).notNull(), + integer3: integer({ mode: 'timestamp' }).notNull(), + integer4: integer({ mode: 'timestamp_ms' }).notNull(), + numeric: numeric().notNull(), + real: real().notNull(), + text1: text({ mode: 'text' }).notNull(), + text2: text({ mode: 'text', length: 10 }).notNull(), + text3: text({ mode: 'text', enum: ['a', 'b', 'c'] }).notNull(), + text4: text({ mode: 'json' }).notNull(), + })); + + const result = createSelectSchema(table); + const expected = v.object({ + blob1: bufferSchema, + blob2: v.pipe(v.bigint(), v.minValue(CONSTANTS.INT64_MIN), v.maxValue(CONSTANTS.INT64_MAX)), + blob3: jsonSchema, + integer1: v.pipe(v.number(), v.minValue(Number.MIN_SAFE_INTEGER), v.maxValue(Number.MAX_SAFE_INTEGER), v.integer()), + integer2: v.boolean(), + integer3: v.date(), + integer4: v.date(), + numeric: v.string(), + real: v.pipe(v.number(), v.minValue(CONSTANTS.INT48_MIN), v.maxValue(CONSTANTS.INT48_MAX)), + text1: v.string(), + text2: v.pipe(v.string(), v.maxLength(10 as number)), + text3: v.enum({ a: 'a', b: 'b', c: 'c' }), + text4: jsonSchema, + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +/* Disallow unknown keys in table refinement - select */ { + const table = sqliteTable('test', { id: int() }); + // @ts-expect-error + createSelectSchema(table, { unknown: v.string() }); +} + +/* Disallow unknown keys in table refinement - insert */ { + const table = sqliteTable('test', { id: int() }); + // @ts-expect-error + createInsertSchema(table, { unknown: v.string() }); +} + +/* Disallow unknown keys in table refinement - update */ { + const table = sqliteTable('test', { id: int() }); + // @ts-expect-error + createUpdateSchema(table, { unknown: v.string() }); +} + +/* Disallow unknown keys in view qb - select */ { + const table = sqliteTable('test', { id: int() }); + const view = sqliteView('test').as((qb) => qb.select().from(table)); + const nestedSelect = sqliteView('test').as((qb) => qb.select({ table }).from(table)); + // @ts-expect-error + createSelectSchema(view, { unknown: v.string() }); + // @ts-expect-error + createSelectSchema(nestedSelect, { table: { unknown: v.string() } }); +} + +/* Disallow unknown keys in view columns - select */ { + const view = sqliteView('test', { id: int() }).as(sql``); + // @ts-expect-error + createSelectSchema(view, { unknown: v.string() }); +} diff --git a/drizzle-valibot/tests/utils.ts b/drizzle-valibot/tests/utils.ts index 189731956..7e1eae757 100644 --- a/drizzle-valibot/tests/utils.ts +++ b/drizzle-valibot/tests/utils.ts @@ -1,10 +1,43 @@ -import type { BaseSchema } from 'valibot'; +import type * as v from 'valibot'; import { expect, type TaskContext } from 'vitest'; -export function expectSchemaShape>(t: TaskContext, expected: T) { +function onlySpecifiedKeys(obj: Record, keys: string[]) { + return Object.fromEntries(Object.entries(obj).filter(([key]) => keys.includes(key))); +} + +export function expectSchemaShape>(t: TaskContext, expected: T) { + return { + from(actual: T) { + expect(Object.keys(actual.entries)).toStrictEqual(Object.keys(expected.entries)); + + for (const key of Object.keys(actual.entries)) { + const actualEntry = actual.entries[key] as any; + const expectedEntry = expected.entries[key] as any; + const keys = ['kind', 'type', 'expects', 'async', 'message']; + actualEntry.pipe ??= []; + expectedEntry.pipe ??= []; + + expect(onlySpecifiedKeys(actualEntry, keys)).toStrictEqual(onlySpecifiedKeys(expectedEntry, keys)); + expect(actualEntry.pipe.length).toStrictEqual(expectedEntry.pipe.length); + + for (let i = 0; i < actualEntry.pipe.length; i++) { + const actualPipeElement = actualEntry.pipe[i]; + const expectedPipeElement = expectedEntry.pipe[i]; + expect(onlySpecifiedKeys(actualPipeElement, keys)).toStrictEqual( + onlySpecifiedKeys(expectedPipeElement, keys), + ); + } + } + }, + }; +} + +export function expectEnumValues>(t: TaskContext, expected: T) { return { from(actual: T) { - expect(Object.keys(actual)).toStrictEqual(Object.keys(expected)); + expect(actual.enum).toStrictEqual(expected.enum); }, }; } + +export function Expect<_ extends true>() {} diff --git a/drizzle-valibot/tsconfig.json b/drizzle-valibot/tsconfig.json index 038d79591..c25379c37 100644 --- a/drizzle-valibot/tsconfig.json +++ b/drizzle-valibot/tsconfig.json @@ -4,6 +4,7 @@ "outDir": "dist", "baseUrl": ".", "declaration": true, + "noEmit": true, "paths": { "~/*": ["src/*"] } diff --git a/drizzle-zod/README.md b/drizzle-zod/README.md index a935d18cd..46eced034 100644 --- a/drizzle-zod/README.md +++ b/drizzle-zod/README.md @@ -10,11 +10,11 @@ `drizzle-zod` is a plugin for [Drizzle ORM](https://github.com/drizzle-team/drizzle-orm) that allows you to generate [Zod](https://zod.dev/) schemas from Drizzle ORM schemas. -| Database | Insert schema | Select schema | -|:-----------|:-------------:|:-------------:| -| PostgreSQL | ✅ | ✅ | -| MySQL | ✅ | ✅ | -| SQLite | ✅ | ✅ | +**Features** + +- Create a select schema for tables, views and enums. +- Create insert and update schemas for tables. +- Supports all dialects: PostgreSQL, MySQL and SQLite. # Usage @@ -34,6 +34,9 @@ const users = pgTable('users', { // Schema for inserting a user - can be used to validate API requests const insertUserSchema = createInsertSchema(users); +// Schema for updating a user - can be used to validate API requests +const updateUserSchema = createUpdateSchema(users); + // Schema for selecting a user - can be used to validate API responses const selectUserSchema = createSelectSchema(users); @@ -44,8 +47,8 @@ const insertUserSchema = createInsertSchema(users, { // Refining the fields - useful if you want to change the fields before they become nullable/optional in the final schema const insertUserSchema = createInsertSchema(users, { - id: (schema) => schema.id.positive(), - email: (schema) => schema.email.email(), + id: (schema) => schema.positive(), + email: (schema) => schema.email(), role: z.string(), }); diff --git a/drizzle-zod/package.json b/drizzle-zod/package.json index 4d3acef81..ebbec398e 100644 --- a/drizzle-zod/package.json +++ b/drizzle-zod/package.json @@ -1,6 +1,6 @@ { "name": "drizzle-zod", - "version": "0.5.1", + "version": "0.6.0", "description": "Generate Zod schemas from Drizzle ORM schemas", "type": "module", "scripts": { @@ -64,11 +64,10 @@ "author": "Drizzle Team", "license": "Apache-2.0", "peerDependencies": { - "drizzle-orm": ">=0.23.13", - "zod": "*" + "drizzle-orm": ">=0.36.0", + "zod": ">=3.0.0" }, "devDependencies": { - "@rollup/plugin-terser": "^0.4.1", "@rollup/plugin-typescript": "^11.1.0", "@types/node": "^18.15.10", "cpy": "^10.1.0", diff --git a/drizzle-zod/rollup.config.ts b/drizzle-zod/rollup.config.ts index 2ed2d33d3..2049cc5ad 100644 --- a/drizzle-zod/rollup.config.ts +++ b/drizzle-zod/rollup.config.ts @@ -1,4 +1,3 @@ -import terser from '@rollup/plugin-terser'; import typescript from '@rollup/plugin-typescript'; import { defineConfig } from 'rollup'; @@ -29,7 +28,6 @@ export default defineConfig([ typescript({ tsconfig: 'tsconfig.build.json', }), - terser(), ], }, ]); diff --git a/drizzle-zod/scripts/build.ts b/drizzle-zod/scripts/build.ts index 1910feac6..07330ffd0 100755 --- a/drizzle-zod/scripts/build.ts +++ b/drizzle-zod/scripts/build.ts @@ -13,3 +13,4 @@ await cpy('dist/**/*.d.ts', 'dist', { rename: (basename) => basename.replace(/\.d\.ts$/, '.d.cts'), }); await fs.copy('package.json', 'dist/package.json'); +await $`scripts/fix-imports.ts`; diff --git a/drizzle-zod/scripts/fix-imports.ts b/drizzle-zod/scripts/fix-imports.ts new file mode 100755 index 000000000..a90057c5b --- /dev/null +++ b/drizzle-zod/scripts/fix-imports.ts @@ -0,0 +1,136 @@ +#!/usr/bin/env -S pnpm tsx +import 'zx/globals'; + +import path from 'node:path'; +import { parse, print, visit } from 'recast'; +import parser from 'recast/parsers/typescript'; + +function resolvePathAlias(importPath: string, file: string) { + if (importPath.startsWith('~/')) { + const relativePath = path.relative(path.dirname(file), path.resolve('dist.new', importPath.slice(2))); + importPath = relativePath.startsWith('.') ? relativePath : './' + relativePath; + } + + return importPath; +} + +function fixImportPath(importPath: string, file: string, ext: string) { + importPath = resolvePathAlias(importPath, file); + + if (!/\..*\.(js|ts)$/.test(importPath)) { + return importPath; + } + + return importPath.replace(/\.(js|ts)$/, ext); +} + +const cjsFiles = await glob('dist/**/*.{cjs,d.cts}'); + +await Promise.all(cjsFiles.map(async (file) => { + const code = parse(await fs.readFile(file, 'utf8'), { parser }); + + visit(code, { + visitImportDeclaration(path) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.cjs'); + this.traverse(path); + }, + visitExportAllDeclaration(path) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.cjs'); + this.traverse(path); + }, + visitExportNamedDeclaration(path) { + if (path.value.source) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.cjs'); + } + this.traverse(path); + }, + visitCallExpression(path) { + if (path.value.callee.type === 'Identifier' && path.value.callee.name === 'require') { + path.value.arguments[0].value = fixImportPath(path.value.arguments[0].value, file, '.cjs'); + } + this.traverse(path); + }, + visitTSImportType(path) { + path.value.argument.value = resolvePathAlias(path.value.argument.value, file); + this.traverse(path); + }, + visitAwaitExpression(path) { + if (print(path.value).code.startsWith(`await import("./`)) { + path.value.argument.arguments[0].value = fixImportPath(path.value.argument.arguments[0].value, file, '.cjs'); + } + this.traverse(path); + }, + }); + + await fs.writeFile(file, print(code).code); +})); + +let esmFiles = await glob('dist/**/*.{js,d.ts}'); + +await Promise.all(esmFiles.map(async (file) => { + const code = parse(await fs.readFile(file, 'utf8'), { parser }); + + visit(code, { + visitImportDeclaration(path) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.js'); + this.traverse(path); + }, + visitExportAllDeclaration(path) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.js'); + this.traverse(path); + }, + visitExportNamedDeclaration(path) { + if (path.value.source) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.js'); + } + this.traverse(path); + }, + visitTSImportType(path) { + path.value.argument.value = fixImportPath(path.value.argument.value, file, '.js'); + this.traverse(path); + }, + visitAwaitExpression(path) { + if (print(path.value).code.startsWith(`await import("./`)) { + path.value.argument.arguments[0].value = fixImportPath(path.value.argument.arguments[0].value, file, '.js'); + } + this.traverse(path); + }, + }); + + await fs.writeFile(file, print(code).code); +})); + +esmFiles = await glob('dist/**/*.{mjs,d.mts}'); + +await Promise.all(esmFiles.map(async (file) => { + const code = parse(await fs.readFile(file, 'utf8'), { parser }); + + visit(code, { + visitImportDeclaration(path) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.mjs'); + this.traverse(path); + }, + visitExportAllDeclaration(path) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.mjs'); + this.traverse(path); + }, + visitExportNamedDeclaration(path) { + if (path.value.source) { + path.value.source.value = fixImportPath(path.value.source.value, file, '.mjs'); + } + this.traverse(path); + }, + visitTSImportType(path) { + path.value.argument.value = fixImportPath(path.value.argument.value, file, '.mjs'); + this.traverse(path); + }, + visitAwaitExpression(path) { + if (print(path.value).code.startsWith(`await import("./`)) { + path.value.argument.arguments[0].value = fixImportPath(path.value.argument.arguments[0].value, file, '.mjs'); + } + this.traverse(path); + }, + }); + + await fs.writeFile(file, print(code).code); +})); diff --git a/drizzle-zod/src/column.ts b/drizzle-zod/src/column.ts new file mode 100644 index 000000000..4aae40e7e --- /dev/null +++ b/drizzle-zod/src/column.ts @@ -0,0 +1,227 @@ +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 { SQLiteInteger, SQLiteReal, SQLiteText } from 'drizzle-orm/sqlite-core'; +import { z } from 'zod'; +import type { z as zod } from 'zod'; +import { CONSTANTS } from './constants.ts'; +import { isColumnType, isWithEnum } from './utils.ts'; +import type { Json } from './utils.ts'; + +export const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); +export const jsonSchema: z.ZodType = z.lazy(() => + z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) +); +export const bufferSchema: z.ZodType = z.custom((v) => v instanceof Buffer); // eslint-disable-line no-instanceof/no-instanceof + +/** @internal */ +export function columnToSchema(column: Column, z: typeof zod): z.ZodTypeAny { + let schema!: z.ZodTypeAny; + + if (isWithEnum(column)) { + schema = column.enumValues.length ? z.enum(column.enumValues) : z.string(); + } + + if (!schema) { + // Handle specific types + if (isColumnType | PgPointTuple>(column, ['PgGeometry', 'PgPointTuple'])) { + schema = z.tuple([z.number(), z.number()]); + } else if ( + isColumnType | PgGeometryObject>(column, ['PgGeometryObject', 'PgPointObject']) + ) { + schema = z.object({ x: z.number(), y: z.number() }); + } else if (isColumnType | PgVector>(column, ['PgHalfVector', 'PgVector'])) { + schema = z.array(z.number()); + schema = column.dimensions ? (schema as z.ZodArray).length(column.dimensions) : schema; + } else if (isColumnType>(column, ['PgLine'])) { + schema = z.tuple([z.number(), z.number(), z.number()]); + } else if (isColumnType>(column, ['PgLineABC'])) { + schema = z.object({ + a: z.number(), + b: z.number(), + c: z.number(), + }); + } // Handle other types + else if (isColumnType>(column, ['PgArray'])) { + schema = z.array(columnToSchema(column.baseColumn, z)); + schema = column.size ? (schema as z.ZodArray).length(column.size) : schema; + } else if (column.dataType === 'array') { + schema = z.array(z.any()); + } else if (column.dataType === 'number') { + schema = numberColumnToSchema(column, z); + } else if (column.dataType === 'bigint') { + schema = bigintColumnToSchema(column, z); + } else if (column.dataType === 'boolean') { + schema = z.boolean(); + } else if (column.dataType === 'date') { + schema = z.date(); + } else if (column.dataType === 'string') { + schema = stringColumnToSchema(column, z); + } 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): z.ZodTypeAny { + let unsigned = column.getSQLType().includes('unsigned'); + let min!: number; + let max!: number; + let integer = false; + + if (isColumnType>(column, ['MySqlTinyInt'])) { + min = unsigned ? 0 : CONSTANTS.INT8_MIN; + max = unsigned ? CONSTANTS.INT8_UNSIGNED_MAX : CONSTANTS.INT8_MAX; + integer = true; + } else if ( + isColumnType | PgSmallSerial | MySqlSmallInt>(column, [ + 'PgSmallInt', + 'PgSmallSerial', + 'MySqlSmallInt', + ]) + ) { + min = unsigned ? 0 : CONSTANTS.INT16_MIN; + max = unsigned ? CONSTANTS.INT16_UNSIGNED_MAX : CONSTANTS.INT16_MAX; + integer = true; + } else if ( + isColumnType | MySqlFloat | MySqlMediumInt>(column, [ + 'PgReal', + 'MySqlFloat', + 'MySqlMediumInt', + ]) + ) { + min = unsigned ? 0 : CONSTANTS.INT24_MIN; + max = unsigned ? CONSTANTS.INT24_UNSIGNED_MAX : CONSTANTS.INT24_MAX; + integer = isColumnType(column, ['MySqlMediumInt']); + } else if ( + isColumnType | PgSerial | MySqlInt>(column, ['PgInteger', 'PgSerial', 'MySqlInt']) + ) { + min = unsigned ? 0 : CONSTANTS.INT32_MIN; + max = unsigned ? CONSTANTS.INT32_UNSIGNED_MAX : CONSTANTS.INT32_MAX; + integer = true; + } else if ( + isColumnType | MySqlReal | MySqlDouble | SQLiteReal>(column, [ + 'PgDoublePrecision', + 'MySqlReal', + 'MySqlDouble', + 'SQLiteReal', + ]) + ) { + min = unsigned ? 0 : CONSTANTS.INT48_MIN; + max = unsigned ? CONSTANTS.INT48_UNSIGNED_MAX : CONSTANTS.INT48_MAX; + } else if ( + isColumnType | PgBigSerial53 | MySqlBigInt53 | MySqlSerial | SQLiteInteger>( + column, + ['PgBigInt53', 'PgBigSerial53', 'MySqlBigInt53', 'MySqlSerial', 'SQLiteInteger'], + ) + ) { + unsigned = unsigned || isColumnType(column, ['MySqlSerial']); + min = unsigned ? 0 : Number.MIN_SAFE_INTEGER; + max = Number.MAX_SAFE_INTEGER; + integer = true; + } else if (isColumnType>(column, ['MySqlYear'])) { + min = 1901; + max = 2155; + integer = true; + } else { + min = Number.MIN_SAFE_INTEGER; + max = Number.MAX_SAFE_INTEGER; + } + + const schema = z.number().min(min).max(max); + return integer ? schema.int() : schema; +} + +function bigintColumnToSchema(column: Column, z: typeof zod): z.ZodTypeAny { + const unsigned = column.getSQLType().includes('unsigned'); + const min = unsigned ? 0n : CONSTANTS.INT64_MIN; + const max = unsigned ? CONSTANTS.INT64_UNSIGNED_MAX : CONSTANTS.INT64_MAX; + + return z.bigint().min(min).max(max); +} + +function stringColumnToSchema(column: Column, z: typeof zod): z.ZodTypeAny { + if (isColumnType>>(column, ['PgUUID'])) { + return z.string().uuid(); + } + + let max: number | undefined; + let regex: RegExp | undefined; + let fixed = false; + + if (isColumnType | SQLiteText>(column, ['PgVarchar', 'SQLiteText'])) { + max = column.length; + } else if (isColumnType>(column, ['MySqlVarChar'])) { + max = column.length ?? CONSTANTS.INT16_UNSIGNED_MAX; + } else if (isColumnType>(column, ['MySqlText'])) { + 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 | MySqlChar>(column, ['PgChar', 'MySqlChar'])) { + max = column.length; + fixed = true; + } + + if (isColumnType>(column, ['PgBinaryVector'])) { + regex = /^[01]+$/; + max = column.dimensions; + } + + let schema = z.string(); + schema = regex ? schema.regex(regex) : schema; + return max && fixed ? schema.length(max) : max ? schema.max(max) : schema; +} diff --git a/drizzle-zod/src/column.types.ts b/drizzle-zod/src/column.types.ts new file mode 100644 index 000000000..49c12cdbb --- /dev/null +++ b/drizzle-zod/src/column.types.ts @@ -0,0 +1,76 @@ +import type { Assume, Column } from 'drizzle-orm'; +import type { z } from 'zod'; +import type { ArrayHasAtLeastOneValue, ColumnIsGeneratedAlwaysAs, IsNever, Json } from './utils.ts'; + +export type GetEnumValuesFromColumn = TColumn['_'] extends { enumValues: [string, ...string[]] } + ? TColumn['_']['enumValues'] + : undefined; + +export type GetBaseColumn = TColumn['_'] extends { baseColumn: Column | never | undefined } + ? IsNever extends false ? TColumn['_']['baseColumn'] + : undefined + : undefined; + +export type GetZodType< + TData, + TDataType extends string, + TEnumValues extends [string, ...string[]] | undefined, + TBaseColumn extends Column | undefined, +> = TBaseColumn extends Column ? z.ZodArray< + GetZodType< + TBaseColumn['_']['data'], + TBaseColumn['_']['dataType'], + GetEnumValuesFromColumn, + GetBaseColumn + > + > + : ArrayHasAtLeastOneValue extends true ? z.ZodEnum> + : TData extends infer TTuple extends [any, ...any[]] + ? z.ZodTuple }, [any, ...any[]]>> + : TData extends Date ? z.ZodDate + : TData extends Buffer ? z.ZodType + : TDataType extends 'array' ? z.ZodArray[number], string, undefined, undefined>> + : TData extends infer TDict extends Record + ? z.ZodObject<{ [K in keyof TDict]: GetZodType }, 'strip'> + : TDataType extends 'json' ? z.ZodType + : TData extends number ? z.ZodNumber + : TData extends bigint ? z.ZodBigInt + : TData extends boolean ? z.ZodBoolean + : TData extends string ? z.ZodString + : z.ZodTypeAny; + +type HandleSelectColumn< + TSchema extends z.ZodTypeAny, + TColumn extends Column, +> = TColumn['_']['notNull'] extends true ? TSchema + : z.ZodNullable; + +type HandleInsertColumn< + TSchema extends z.ZodTypeAny, + TColumn extends Column, +> = ColumnIsGeneratedAlwaysAs extends true ? never + : TColumn['_']['notNull'] extends true ? TColumn['_']['hasDefault'] extends true ? z.ZodOptional + : TSchema + : z.ZodOptional>; + +type HandleUpdateColumn< + TSchema extends z.ZodTypeAny, + TColumn extends Column, +> = ColumnIsGeneratedAlwaysAs extends true ? never + : TColumn['_']['notNull'] extends true ? z.ZodOptional + : z.ZodOptional>; + +export type HandleColumn< + TType extends 'select' | 'insert' | 'update', + TColumn extends Column, +> = GetZodType< + TColumn['_']['data'], + TColumn['_']['dataType'], + GetEnumValuesFromColumn, + GetBaseColumn +> extends infer TSchema extends z.ZodTypeAny ? TSchema extends z.ZodAny ? z.ZodAny + : TType extends 'select' ? HandleSelectColumn + : TType extends 'insert' ? HandleInsertColumn + : TType extends 'update' ? HandleUpdateColumn + : TSchema + : z.ZodAny; diff --git a/drizzle-zod/src/constants.ts b/drizzle-zod/src/constants.ts new file mode 100644 index 000000000..99f5d7a42 --- /dev/null +++ b/drizzle-zod/src/constants.ts @@ -0,0 +1,20 @@ +export const CONSTANTS = { + INT8_MIN: -128, + INT8_MAX: 127, + INT8_UNSIGNED_MAX: 255, + INT16_MIN: -32768, + INT16_MAX: 32767, + INT16_UNSIGNED_MAX: 65535, + INT24_MIN: -8388608, + INT24_MAX: 8388607, + INT24_UNSIGNED_MAX: 16777215, + INT32_MIN: -2147483648, + INT32_MAX: 2147483647, + INT32_UNSIGNED_MAX: 4294967295, + INT48_MIN: -140737488355328, + INT48_MAX: 140737488355327, + INT48_UNSIGNED_MAX: 281474976710655, + INT64_MIN: -9223372036854775808n, + INT64_MAX: 9223372036854775807n, + INT64_UNSIGNED_MAX: 18446744073709551615n, +}; diff --git a/drizzle-zod/src/index.ts b/drizzle-zod/src/index.ts index 3f6547e0b..0a6499e5b 100644 --- a/drizzle-zod/src/index.ts +++ b/drizzle-zod/src/index.ts @@ -1,239 +1,2 @@ -import { - type Assume, - type Column, - type DrizzleTypeError, - type Equal, - getTableColumns, - is, - type Simplify, - type Table, -} from 'drizzle-orm'; -import { MySqlChar, MySqlVarBinary, MySqlVarChar } from 'drizzle-orm/mysql-core'; -import { type PgArray, PgChar, PgUUID, PgVarchar } from 'drizzle-orm/pg-core'; -import { SQLiteText } from 'drizzle-orm/sqlite-core'; -import { z } from 'zod'; - -const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); -type Literal = z.infer; -type Json = Literal | { [key: string]: Json } | Json[]; -export const jsonSchema: z.ZodType = z.lazy(() => - z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) -); - -type MapInsertColumnToZod = TColumn['_']['notNull'] extends false - ? z.ZodOptional> - : TColumn['_']['hasDefault'] extends true ? z.ZodOptional - : TType; - -type MapSelectColumnToZod = TColumn['_']['notNull'] extends false - ? z.ZodNullable - : TType; - -type MapColumnToZod = - TMode extends 'insert' ? MapInsertColumnToZod : MapSelectColumnToZod; - -type MaybeOptional< - TColumn extends Column, - TType extends z.ZodTypeAny, - TMode extends 'insert' | 'select', - TNoOptional extends boolean, -> = TNoOptional extends true ? TType - : MapColumnToZod; - -type GetZodType = TColumn['_']['dataType'] extends infer TDataType - ? TDataType extends 'custom' ? z.ZodAny - : TDataType extends 'json' ? z.ZodType - : 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 - : TDataType extends 'boolean' ? z.ZodBoolean - : TDataType extends 'date' ? z.ZodDate - : z.ZodAny - : never; - -type ValueOrUpdater = T | ((arg: TUpdaterArg) => T); - -type UnwrapValueOrUpdater = T extends ValueOrUpdater ? U : never; - -export type Refine = { - [K in keyof TTable['_']['columns']]?: ValueOrUpdater< - z.ZodTypeAny, - TMode extends 'select' ? BuildSelectSchema : BuildInsertSchema - >; -}; - -export type BuildInsertSchema< - TTable extends Table, - TRefine extends Refine | {}, - TNoOptional extends boolean = false, -> = TTable['_']['columns'] extends infer TColumns extends Record> ? { - [K in keyof TColumns & string]: MaybeOptional< - TColumns[K], - (K extends keyof TRefine ? Assume, z.ZodTypeAny> - : GetZodType), - 'insert', - TNoOptional - >; - } - : never; - -export type BuildSelectSchema< - TTable extends Table, - TRefine extends Refine, - TNoOptional extends boolean = false, -> = Simplify< - { - [K in keyof TTable['_']['columns']]: MaybeOptional< - TTable['_']['columns'][K], - (K extends keyof TRefine ? Assume, z.ZodTypeAny> - : GetZodType), - 'select', - TNoOptional - >; - } ->; - -export function createInsertSchema< - TTable extends Table, - TRefine extends Refine = Refine, ->( - table: TTable, - /** - * @param refine Refine schema fields - */ - refine?: { - [K in keyof TRefine]: K extends keyof TTable['_']['columns'] ? TRefine[K] - : DrizzleTypeError<`Column '${K & string}' does not exist in table '${TTable['_']['name']}'`>; - }, -): z.ZodObject> extends true ? {} : TRefine>> { - const columns = getTableColumns(table); - const columnEntries = Object.entries(columns); - - let schemaEntries = Object.fromEntries(columnEntries.map(([name, column]) => { - return [name, mapColumnToSchema(column)]; - })); - - if (refine) { - schemaEntries = Object.assign( - schemaEntries, - Object.fromEntries( - Object.entries(refine).map(([name, refineColumn]) => { - return [ - name, - typeof refineColumn === 'function' - ? refineColumn(schemaEntries as BuildInsertSchema) - : refineColumn, - ]; - }), - ), - ); - } - - for (const [name, column] of columnEntries) { - if (!column.notNull) { - schemaEntries[name] = schemaEntries[name]!.nullable().optional(); - } else if (column.hasDefault) { - schemaEntries[name] = schemaEntries[name]!.optional(); - } - } - - return z.object(schemaEntries) as any; -} - -export function createSelectSchema< - TTable extends Table, - TRefine extends Refine = Refine, ->( - table: TTable, - /** - * @param refine Refine schema fields - */ - refine?: { - [K in keyof TRefine]: K extends keyof TTable['_']['columns'] ? TRefine[K] - : DrizzleTypeError<`Column '${K & string}' does not exist in table '${TTable['_']['name']}'`>; - }, -): z.ZodObject> extends true ? {} : TRefine>> { - const columns = getTableColumns(table); - const columnEntries = Object.entries(columns); - - let schemaEntries = Object.fromEntries(columnEntries.map(([name, column]) => { - return [name, mapColumnToSchema(column)]; - })); - - if (refine) { - schemaEntries = Object.assign( - schemaEntries, - Object.fromEntries( - Object.entries(refine).map(([name, refineColumn]) => { - return [ - name, - typeof refineColumn === 'function' - ? refineColumn(schemaEntries as BuildSelectSchema) - : refineColumn, - ]; - }), - ), - ); - } - - for (const [name, column] of columnEntries) { - if (!column.notNull) { - schemaEntries[name] = schemaEntries[name]!.nullable(); - } - } - - return z.object(schemaEntries) as any; -} - -function isWithEnum(column: Column): column is typeof column & { enumValues: [string, ...string[]] } { - return 'enumValues' in column && Array.isArray(column.enumValues) && column.enumValues.length > 0; -} - -function mapColumnToSchema(column: Column): z.ZodTypeAny { - let type: z.ZodTypeAny | undefined; - - 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; - } - } - - if (!type) { - type = z.any(); - } - - return type; -} +export * from './schema.ts'; +export * from './schema.types.ts'; diff --git a/drizzle-zod/src/schema.ts b/drizzle-zod/src/schema.ts new file mode 100644 index 000000000..67a9cb733 --- /dev/null +++ b/drizzle-zod/src/schema.ts @@ -0,0 +1,143 @@ +import { Column, getTableColumns, getViewSelectedFields, is, isTable, isView, SQL } from 'drizzle-orm'; +import type { Table, View } from 'drizzle-orm'; +import type { PgEnum } from 'drizzle-orm/pg-core'; +import { z } from 'zod'; +import { columnToSchema } from './column.ts'; +import type { Conditions } from './schema.types.internal.ts'; +import type { + CreateInsertSchema, + CreateSchemaFactoryOptions, + CreateSelectSchema, + CreateUpdateSchema, +} from './schema.types.ts'; +import { isPgEnum } from './utils.ts'; + +function getColumns(tableLike: Table | View) { + return isTable(tableLike) ? getTableColumns(tableLike) : getViewSelectedFields(tableLike); +} + +function handleColumns( + columns: Record, + refinements: Record, + conditions: Conditions, + factory?: CreateSchemaFactoryOptions, +): z.ZodTypeAny { + const columnSchemas: Record = {}; + + for (const [key, selected] of Object.entries(columns)) { + if (!is(selected, Column) && !is(selected, SQL) && !is(selected, SQL.Aliased) && typeof selected === 'object') { + const columns = isTable(selected) || isView(selected) ? getColumns(selected) : selected; + columnSchemas[key] = handleColumns(columns, refinements[key] ?? {}, conditions, factory); + continue; + } + + const refinement = refinements[key]; + if (refinement !== undefined && typeof refinement !== 'function') { + columnSchemas[key] = refinement; + continue; + } + + const column = is(selected, Column) ? selected : undefined; + const schema = column ? columnToSchema(column, factory?.zodInstance ?? z) : z.any(); + const refined = typeof refinement === 'function' ? refinement(schema) : schema; + + if (conditions.never(column)) { + continue; + } else { + columnSchemas[key] = refined; + } + + if (column) { + if (conditions.nullable(column)) { + columnSchemas[key] = columnSchemas[key]!.nullable(); + } + + if (conditions.optional(column)) { + columnSchemas[key] = columnSchemas[key]!.optional(); + } + } + } + + return z.object(columnSchemas) as any; +} + +function handleEnum(enum_: PgEnum, factory?: CreateSchemaFactoryOptions) { + const zod: typeof z = factory?.zodInstance ?? z; + return zod.enum(enum_.enumValues); +} + +const selectConditions: Conditions = { + never: () => false, + optional: () => false, + nullable: (column) => !column.notNull, +}; + +const insertConditions: Conditions = { + never: (column) => column?.generated?.type === 'always' || column?.generatedIdentity?.type === 'always', + optional: (column) => !column.notNull || (column.notNull && column.hasDefault), + nullable: (column) => !column.notNull, +}; + +const updateConditions: Conditions = { + never: (column) => column?.generated?.type === 'always' || column?.generatedIdentity?.type === 'always', + optional: () => true, + nullable: (column) => !column.notNull, +}; + +export const createSelectSchema: CreateSelectSchema = ( + entity: Table | View | PgEnum<[string, ...string[]]>, + refine?: Record, +) => { + if (isPgEnum(entity)) { + return handleEnum(entity); + } + const columns = getColumns(entity); + return handleColumns(columns, refine ?? {}, selectConditions) as any; +}; + +export const createInsertSchema: CreateInsertSchema = ( + entity: Table, + refine?: Record, +) => { + const columns = getColumns(entity); + return handleColumns(columns, refine ?? {}, insertConditions) as any; +}; + +export const createUpdateSchema: CreateUpdateSchema = ( + entity: Table, + refine?: Record, +) => { + const columns = getColumns(entity); + return handleColumns(columns, refine ?? {}, updateConditions) as any; +}; + +export function createSchemaFactory(options?: CreateSchemaFactoryOptions) { + const createSelectSchema: CreateSelectSchema = ( + entity: Table | View | PgEnum<[string, ...string[]]>, + refine?: Record, + ) => { + if (isPgEnum(entity)) { + return handleEnum(entity, options); + } + const columns = getColumns(entity); + return handleColumns(columns, refine ?? {}, selectConditions, options) as any; + }; + + const createInsertSchema: CreateInsertSchema = ( + entity: Table, + refine?: Record, + ) => { + const columns = getColumns(entity); + return handleColumns(columns, refine ?? {}, insertConditions, options) as any; + }; + + const createUpdateSchema: CreateUpdateSchema = ( + entity: Table, + refine?: Record, + ) => { + const columns = getColumns(entity); + return handleColumns(columns, refine ?? {}, updateConditions, options) as any; + }; + + return { createSelectSchema, createInsertSchema, createUpdateSchema }; +} diff --git a/drizzle-zod/src/schema.types.internal.ts b/drizzle-zod/src/schema.types.internal.ts new file mode 100644 index 000000000..5732e2e0f --- /dev/null +++ b/drizzle-zod/src/schema.types.internal.ts @@ -0,0 +1,88 @@ +import type { Assume, Column, DrizzleTypeError, SelectedFieldsFlat, Simplify, Table, View } from 'drizzle-orm'; +import type { z } from 'zod'; +import type { GetBaseColumn, GetEnumValuesFromColumn, GetZodType, HandleColumn } from './column.types.ts'; +import type { GetSelection, RemoveNever } from './utils.ts'; + +export interface Conditions { + never: (column?: Column) => boolean; + optional: (column: Column) => boolean; + nullable: (column: Column) => boolean; +} + +export type BuildRefineColumns< + TColumns extends Record, +> = Simplify< + RemoveNever< + { + [K in keyof TColumns]: TColumns[K] extends infer TColumn extends Column ? GetZodType< + TColumn['_']['data'], + TColumn['_']['dataType'], + GetEnumValuesFromColumn, + GetBaseColumn + > extends infer TSchema extends z.ZodTypeAny ? TSchema + : z.ZodAny + : TColumns[K] extends infer TObject extends SelectedFieldsFlat | Table | View + ? BuildRefineColumns> + : TColumns[K]; + } + > +>; + +export type BuildRefine< + TColumns extends Record, +> = BuildRefineColumns extends infer TBuildColumns ? { + [K in keyof TBuildColumns]?: TBuildColumns[K] extends z.ZodTypeAny + ? ((schema: TBuildColumns[K]) => z.ZodTypeAny) | z.ZodTypeAny + : TBuildColumns[K] extends Record ? Simplify> + : never; + } + : never; + +type HandleRefinement< + TRefinement extends z.ZodTypeAny | ((schema: z.ZodTypeAny) => z.ZodTypeAny), + TColumn extends Column, +> = TRefinement extends (schema: z.ZodTypeAny) => z.ZodTypeAny + ? TColumn['_']['notNull'] extends true ? ReturnType + : z.ZodNullable> + : TRefinement; + +export type BuildSchema< + TType extends 'select' | 'insert' | 'update', + TColumns extends Record, + TRefinements extends Record | undefined, +> = z.ZodObject< + Simplify< + RemoveNever< + { + [K in keyof TColumns]: TColumns[K] extends infer TColumn extends Column + ? TRefinements extends object + ? TRefinements[Assume] extends + infer TRefinement extends z.ZodTypeAny | ((schema: z.ZodTypeAny) => z.ZodTypeAny) + ? HandleRefinement + : HandleColumn + : HandleColumn + : TColumns[K] extends infer TObject extends SelectedFieldsFlat | Table | View ? BuildSchema< + TType, + GetSelection, + TRefinements extends object + ? TRefinements[Assume] extends infer TNestedRefinements extends object + ? TNestedRefinements + : undefined + : undefined + > + : z.ZodAny; + } + > + >, + 'strip' +>; + +export type NoUnknownKeys< + TRefinement extends Record, + TCompare extends Record, +> = { + [K in keyof TRefinement]: K extends keyof TCompare + ? TRefinement[K] extends Record ? NoUnknownKeys + : TRefinement[K] + : DrizzleTypeError<`Found unknown key in refinement: "${K & string}"`>; +}; diff --git a/drizzle-zod/src/schema.types.ts b/drizzle-zod/src/schema.types.ts new file mode 100644 index 000000000..5873cd2a3 --- /dev/null +++ b/drizzle-zod/src/schema.types.ts @@ -0,0 +1,52 @@ +import type { Table, View } from 'drizzle-orm'; +import type { PgEnum } from 'drizzle-orm/pg-core'; +import type { z } from 'zod'; +import type { BuildRefine, BuildSchema, NoUnknownKeys } from './schema.types.internal.ts'; + +export interface CreateSelectSchema { + (table: TTable): BuildSchema<'select', TTable['_']['columns'], undefined>; + < + TTable extends Table, + TRefine extends BuildRefine, + >( + table: TTable, + refine?: NoUnknownKeys, + ): BuildSchema<'select', TTable['_']['columns'], TRefine>; + + (view: TView): BuildSchema<'select', TView['_']['selectedFields'], undefined>; + < + TView extends View, + TRefine extends BuildRefine, + >( + view: TView, + refine: NoUnknownKeys, + ): BuildSchema<'select', TView['_']['selectedFields'], TRefine>; + + >(enum_: TEnum): z.ZodEnum; +} + +export interface CreateInsertSchema { + (table: TTable): BuildSchema<'insert', TTable['_']['columns'], undefined>; + < + TTable extends Table, + TRefine extends BuildRefine>, + >( + table: TTable, + refine?: NoUnknownKeys, + ): BuildSchema<'insert', TTable['_']['columns'], TRefine>; +} + +export interface CreateUpdateSchema { + (table: TTable): BuildSchema<'update', TTable['_']['columns'], undefined>; + < + TTable extends Table, + TRefine extends BuildRefine>, + >( + table: TTable, + refine?: TRefine, + ): BuildSchema<'update', TTable['_']['columns'], TRefine>; +} + +export interface CreateSchemaFactoryOptions { + zodInstance?: any; +} diff --git a/drizzle-zod/src/utils.ts b/drizzle-zod/src/utils.ts new file mode 100644 index 000000000..506b80565 --- /dev/null +++ b/drizzle-zod/src/utils.ts @@ -0,0 +1,40 @@ +import type { Column, SelectedFieldsFlat, Table, View } from 'drizzle-orm'; +import type { PgEnum } from 'drizzle-orm/pg-core'; +import type { z } from 'zod'; +import type { literalSchema } from './column.ts'; + +export function isColumnType(column: Column, columnTypes: string[]): column is T { + return columnTypes.includes(column.columnType); +} + +export function isWithEnum(column: Column): column is typeof column & { enumValues: [string, ...string[]] } { + return 'enumValues' in column && Array.isArray(column.enumValues) && column.enumValues.length > 0; +} + +export const isPgEnum: (entity: any) => entity is PgEnum<[string, ...string[]]> = isWithEnum as any; + +type Literal = z.infer; +export type Json = Literal | { [key: string]: Json } | Json[]; + +export type IsNever = [T] extends [never] ? true : false; + +export type ArrayHasAtLeastOneValue = TEnum extends [infer TString, ...any[]] + ? TString extends `${infer TLiteral}` ? TLiteral extends any ? true + : false + : false + : false; + +export type ColumnIsGeneratedAlwaysAs = TColumn['_']['identity'] extends 'always' ? true + : TColumn['_']['generated'] extends undefined ? false + : TColumn['_']['generated'] extends infer TGenerated extends { type: string } + ? TGenerated['type'] extends 'byDefault' ? false + : true + : true; + +export type RemoveNever = { + [K in keyof T as T[K] extends never ? never : K]: T[K]; +}; + +export type GetSelection | Table | View> = T extends Table ? T['_']['columns'] + : T extends View ? T['_']['selectedFields'] + : T; diff --git a/drizzle-zod/tests/mysql.test.ts b/drizzle-zod/tests/mysql.test.ts index f28d6a768..37c9b7e64 100644 --- a/drizzle-zod/tests/mysql.test.ts +++ b/drizzle-zod/tests/mysql.test.ts @@ -1,289 +1,463 @@ -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 { expect, test } from 'vitest'; +import { type Equal, sql } from 'drizzle-orm'; +import { int, mysqlSchema, mysqlTable, mysqlView, serial, text } from 'drizzle-orm/mysql-core'; +import { test } from 'vitest'; import { z } from 'zod'; -import { createInsertSchema, createSelectSchema, jsonSchema } from '~/index'; -import { expectSchemaShape } from './utils.ts'; +import { jsonSchema } from '~/column.ts'; +import { CONSTANTS } from '~/constants.ts'; +import { createInsertSchema, createSelectSchema, createUpdateSchema } from '../src'; +import { Expect, expectSchemaShape } from './utils.ts'; -const customInt = customType<{ data: number }>({ - dataType() { - return 'int'; - }, +const intSchema = z.number().min(CONSTANTS.INT32_MIN).max(CONSTANTS.INT32_MAX).int(); +const serialNumberModeSchema = z.number().min(0).max(Number.MAX_SAFE_INTEGER).int(); +const textSchema = z.string().max(CONSTANTS.INT16_UNSIGNED_MAX); + +test('table - select', (t) => { + const table = mysqlTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + + const result = createSelectSchema(table); + const expected = z.object({ id: serialNumberModeSchema, name: textSchema }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -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(), +test('table in schema - select', (tc) => { + const schema = mysqlSchema('test'); + const table = schema.table('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + + const result = createSelectSchema(table); + const expected = z.object({ id: serialNumberModeSchema, name: textSchema }); + expectSchemaShape(tc, expected).from(result); + Expect>(); }); -const testTableRow = { - bigint: BigInt(1), - bigintNumber: 1, - binary: 'binary', - boolean: true, - char: 'char', - charEnum: 'a', - customInt: { data: 1 }, - date: new Date(), - dateString: new Date().toISOString(), - datetime: new Date(), - datetimeString: new Date().toISOString(), - decimal: '1.1', - double: 1.1, - enum: 'a', - float: 1.1, - int: 1, - json: { data: 1 }, - mediumint: 1, - real: 1.1, - serial: 1, - smallint: 1, - text: 'text', - textEnum: 'a', - tinytext: 'tinytext', - tinytextEnum: 'a', - mediumtext: 'mediumtext', - mediumtextEnum: 'a', - longtext: 'longtext', - longtextEnum: 'a', - time: '00:00:00', - timestamp: new Date(), - timestampString: new Date().toISOString(), - tinyint: 1, - varbinary: 'A'.repeat(200), - varchar: 'A'.repeat(200), - varcharEnum: 'a', - year: 2021, - autoIncrement: 1, -}; - -test('insert valid row', () => { - const schema = createInsertSchema(testTable); - - expect(schema.safeParse(testTableRow).success).toBeTruthy(); +test('table - insert', (t) => { + const table = mysqlTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + age: int(), + }); + + const result = createInsertSchema(table); + const expected = z.object({ + id: serialNumberModeSchema.optional(), + name: textSchema, + age: intSchema.nullable().optional(), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('insert invalid varchar length', () => { - const schema = createInsertSchema(testTable); +test('table - update', (t) => { + const table = mysqlTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + age: int(), + }); - expect(schema.safeParse({ ...testTableRow, varchar: 'A'.repeat(201) }).success).toBeFalsy(); + const result = createUpdateSchema(table); + const expected = z.object({ + id: serialNumberModeSchema.optional(), + name: textSchema.optional(), + age: intSchema.nullable().optional(), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('insert smaller char length should work', () => { - const schema = createInsertSchema(testTable); +test('view qb - select', (t) => { + const table = mysqlTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + const view = mysqlView('test').as((qb) => qb.select({ id: table.id, age: sql``.as('age') }).from(table)); - expect(schema.safeParse({ ...testTableRow, char: 'abc' }).success).toBeTruthy(); + const result = createSelectSchema(view); + const expected = z.object({ id: serialNumberModeSchema, age: z.any() }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('insert larger char length should fail', () => { - const schema = createInsertSchema(testTable); +test('view columns - select', (t) => { + const view = mysqlView('test', { + id: serial().primaryKey(), + name: text().notNull(), + }).as(sql``); - expect(schema.safeParse({ ...testTableRow, char: 'abcde' }).success).toBeFalsy(); + const result = createSelectSchema(view); + const expected = z.object({ id: serialNumberModeSchema, name: textSchema }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('insert schema', (t) => { - const actual = createInsertSchema(testTable); +test('view with nested fields - select', (t) => { + const table = mysqlTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + const view = mysqlView('test').as((qb) => + qb.select({ + id: table.id, + nested: { + name: table.name, + age: sql``.as('age'), + }, + table, + }).from(table) + ); + const result = createSelectSchema(view); const expected = z.object({ - bigint: z.bigint(), - bigintNumber: z.number(), - binary: z.string(), - boolean: z.boolean(), - char: z.string().length(4), - charEnum: z.enum(['a', 'b', 'c']), - customInt: z.any(), - date: z.date(), - dateString: z.string(), - datetime: z.date(), - datetimeString: z.string(), - decimal: z.string(), - double: z.number(), - enum: z.enum(['a', 'b', 'c']), - float: z.number(), - int: z.number(), - json: jsonSchema, - mediumint: z.number(), - real: z.number(), - serial: z.number().optional(), - smallint: z.number(), - text: z.string(), - textEnum: z.enum(['a', 'b', 'c']), - tinytext: z.string(), - tinytextEnum: z.enum(['a', 'b', 'c']), - mediumtext: z.string(), - mediumtextEnum: z.enum(['a', 'b', 'c']), - longtext: z.string(), - longtextEnum: z.enum(['a', 'b', 'c']), - time: z.string(), - timestamp: z.date(), - timestampString: z.string(), - tinyint: z.number(), - varbinary: z.string().max(200), - varchar: z.string().max(200), - varcharEnum: z.enum(['a', 'b', 'c']), - year: z.number(), - autoIncrement: z.number().optional(), + id: serialNumberModeSchema, + nested: z.object({ name: textSchema, age: z.any() }), + table: z.object({ id: serialNumberModeSchema, name: textSchema }), }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); - expectSchemaShape(t, expected).from(actual); +test('nullability - select', (t) => { + const table = mysqlTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().default(1), + c4: int().notNull().default(1), + }); + + const result = createSelectSchema(table); + const expected = z.object({ + c1: intSchema.nullable(), + c2: intSchema, + c3: intSchema.nullable(), + c4: intSchema, + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('select schema', (t) => { - const actual = createSelectSchema(testTable); +test('nullability - insert', (t) => { + const table = mysqlTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().default(1), + c4: int().notNull().default(1), + c5: int().generatedAlwaysAs(1), + }); + const result = createInsertSchema(table); const expected = z.object({ - bigint: z.bigint(), - bigintNumber: z.number(), - binary: z.string(), - boolean: z.boolean(), - char: z.string().length(4), - charEnum: z.enum(['a', 'b', 'c']), - customInt: z.any(), - date: z.date(), - dateString: z.string(), - datetime: z.date(), - datetimeString: z.string(), - decimal: z.string(), - double: z.number(), - enum: z.enum(['a', 'b', 'c']), - float: z.number(), - int: z.number(), - json: jsonSchema, - mediumint: z.number(), - real: z.number(), - serial: z.number(), - smallint: z.number(), - text: z.string(), - textEnum: z.enum(['a', 'b', 'c']), - tinytext: z.string(), - tinytextEnum: z.enum(['a', 'b', 'c']), - mediumtext: z.string(), - mediumtextEnum: z.enum(['a', 'b', 'c']), - longtext: z.string(), - longtextEnum: z.enum(['a', 'b', 'c']), - time: z.string(), - timestamp: z.date(), - timestampString: z.string(), - tinyint: z.number(), - varbinary: z.string().max(200), - varchar: z.string().max(200), - varcharEnum: z.enum(['a', 'b', 'c']), - year: z.number(), - autoIncrement: z.number(), + c1: intSchema.nullable().optional(), + c2: intSchema, + c3: intSchema.nullable().optional(), + c4: intSchema.optional(), }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); - expectSchemaShape(t, expected).from(actual); +test('nullability - update', (t) => { + const table = mysqlTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().default(1), + c4: int().notNull().default(1), + c5: int().generatedAlwaysAs(1), + }); + + const result = createUpdateSchema(table); + const expected = z.object({ + c1: intSchema.nullable().optional(), + c2: intSchema.optional(), + c3: intSchema.nullable().optional(), + c4: intSchema.optional(), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('select schema w/ refine', (t) => { - const actual = createSelectSchema(testTable, { - bigint: (schema) => schema.bigint.positive(), +test('refine table - select', (t) => { + const table = mysqlTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().notNull(), }); + const result = createSelectSchema(table, { + c2: (schema) => schema.max(1000), + c3: z.string().transform(Number), + }); const expected = z.object({ - bigint: z.bigint().positive(), - bigintNumber: z.number(), + c1: intSchema.nullable(), + c2: intSchema.max(1000), + c3: z.string().transform(Number), + }); + + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('refine table - insert', (t) => { + const table = mysqlTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().notNull(), + c4: int().generatedAlwaysAs(1), + }); + + const result = createInsertSchema(table, { + c2: (schema) => schema.max(1000), + c3: z.string().transform(Number), + }); + const expected = z.object({ + c1: intSchema.nullable().optional(), + c2: intSchema.max(1000), + c3: z.string().transform(Number), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('refine table - update', (t) => { + const table = mysqlTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().notNull(), + c4: int().generatedAlwaysAs(1), + }); + + const result = createUpdateSchema(table, { + c2: (schema) => schema.max(1000), + c3: z.string().transform(Number), + }); + const expected = z.object({ + c1: intSchema.nullable().optional(), + c2: intSchema.max(1000).optional(), + c3: z.string().transform(Number), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('refine view - select', (t) => { + const table = mysqlTable('test', { + c1: int(), + c2: int(), + c3: int(), + c4: int(), + c5: int(), + c6: int(), + }); + const view = mysqlView('test').as((qb) => + qb.select({ + c1: table.c1, + c2: table.c2, + c3: table.c3, + nested: { + c4: table.c4, + c5: table.c5, + c6: table.c6, + }, + table, + }).from(table) + ); + + const result = createSelectSchema(view, { + c2: (schema) => schema.max(1000), + c3: z.string().transform(Number), + nested: { + c5: (schema) => schema.max(1000), + c6: z.string().transform(Number), + }, + table: { + c2: (schema) => schema.max(1000), + c3: z.string().transform(Number), + }, + }); + const expected = z.object({ + c1: intSchema.nullable(), + c2: intSchema.max(1000).nullable(), + c3: z.string().transform(Number), + nested: z.object({ + c4: intSchema.nullable(), + c5: intSchema.max(1000).nullable(), + c6: z.string().transform(Number), + }), + table: z.object({ + c1: intSchema.nullable(), + c2: intSchema.max(1000).nullable(), + c3: z.string().transform(Number), + c4: intSchema.nullable(), + c5: intSchema.nullable(), + c6: intSchema.nullable(), + }), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('all data types', (t) => { + const table = mysqlTable('test', ({ + bigint, + binary, + boolean, + char, + date, + datetime, + decimal, + double, + float, + int, + json, + mediumint, + mysqlEnum, + real, + serial, + smallint, + text, + time, + timestamp, + tinyint, + varchar, + varbinary, + year, + longtext, + mediumtext, + tinytext, + }) => ({ + bigint1: bigint({ mode: 'number' }).notNull(), + bigint2: bigint({ mode: 'bigint' }).notNull(), + bigint3: bigint({ unsigned: true, mode: 'number' }).notNull(), + bigint4: bigint({ unsigned: true, mode: 'bigint' }).notNull(), + binary: binary({ length: 10 }).notNull(), + boolean: boolean().notNull(), + char1: char({ length: 10 }).notNull(), + char2: char({ length: 1, enum: ['a', 'b', 'c'] }).notNull(), + date1: date({ mode: 'date' }).notNull(), + date2: date({ mode: 'string' }).notNull(), + datetime1: datetime({ mode: 'date' }).notNull(), + datetime2: datetime({ mode: 'string' }).notNull(), + decimal1: decimal().notNull(), + decimal2: decimal({ unsigned: true }).notNull(), + double1: double().notNull(), + double2: double({ unsigned: true }).notNull(), + float1: float().notNull(), + float2: float({ unsigned: true }).notNull(), + int1: int().notNull(), + int2: int({ unsigned: true }).notNull(), + json: json().notNull(), + mediumint1: mediumint().notNull(), + mediumint2: mediumint({ unsigned: true }).notNull(), + enum: mysqlEnum('enum', ['a', 'b', 'c']).notNull(), + real: real().notNull(), + serial: serial().notNull(), + smallint1: smallint().notNull(), + smallint2: smallint({ unsigned: true }).notNull(), + text1: text().notNull(), + text2: text({ enum: ['a', 'b', 'c'] }).notNull(), + time: time().notNull(), + timestamp1: timestamp({ mode: 'date' }).notNull(), + timestamp2: timestamp({ mode: 'string' }).notNull(), + tinyint1: tinyint().notNull(), + tinyint2: tinyint({ unsigned: true }).notNull(), + varchar1: varchar({ length: 10 }).notNull(), + varchar2: varchar({ length: 1, enum: ['a', 'b', 'c'] }).notNull(), + varbinary: varbinary({ length: 10 }).notNull(), + year: year().notNull(), + longtext1: longtext().notNull(), + longtext2: longtext({ enum: ['a', 'b', 'c'] }).notNull(), + mediumtext1: mediumtext().notNull(), + mediumtext2: mediumtext({ enum: ['a', 'b', 'c'] }).notNull(), + tinytext1: tinytext().notNull(), + tinytext2: tinytext({ enum: ['a', 'b', 'c'] }).notNull(), + })); + + const result = createSelectSchema(table); + const expected = z.object({ + bigint1: z.number().min(Number.MIN_SAFE_INTEGER).max(Number.MAX_SAFE_INTEGER).int(), + bigint2: z.bigint().min(CONSTANTS.INT64_MIN).max(CONSTANTS.INT64_MAX), + bigint3: z.number().min(0).max(Number.MAX_SAFE_INTEGER).int(), + bigint4: z.bigint().min(0n).max(CONSTANTS.INT64_UNSIGNED_MAX), binary: z.string(), boolean: z.boolean(), - char: z.string().length(5), - charEnum: z.enum(['a', 'b', 'c']), - customInt: z.any(), - date: z.date(), - dateString: z.string(), - datetime: z.date(), - datetimeString: z.string(), - decimal: z.string(), - double: z.number(), - enum: z.enum(['a', 'b', 'c']), - float: z.number(), - int: z.number(), + char1: z.string().length(10), + char2: z.enum(['a', 'b', 'c']), + date1: z.date(), + date2: z.string(), + datetime1: z.date(), + datetime2: z.string(), + decimal1: z.string(), + decimal2: z.string(), + double1: z.number().min(CONSTANTS.INT48_MIN).max(CONSTANTS.INT48_MAX), + double2: z.number().min(0).max(CONSTANTS.INT48_UNSIGNED_MAX), + float1: z.number().min(CONSTANTS.INT24_MIN).max(CONSTANTS.INT24_MAX), + float2: z.number().min(0).max(CONSTANTS.INT24_UNSIGNED_MAX), + int1: z.number().min(CONSTANTS.INT32_MIN).max(CONSTANTS.INT32_MAX).int(), + int2: z.number().min(0).max(CONSTANTS.INT32_UNSIGNED_MAX).int(), json: jsonSchema, - mediumint: z.number(), - real: z.number(), - serial: z.number(), - smallint: z.number(), - text: z.string(), - textEnum: z.enum(['a', 'b', 'c']), - tinytext: z.string(), - tinytextEnum: z.enum(['a', 'b', 'c']), - mediumtext: z.string(), - mediumtextEnum: z.enum(['a', 'b', 'c']), - longtext: z.string(), - longtextEnum: z.enum(['a', 'b', 'c']), + mediumint1: z.number().min(CONSTANTS.INT24_MIN).max(CONSTANTS.INT24_MAX).int(), + mediumint2: z.number().min(0).max(CONSTANTS.INT24_UNSIGNED_MAX).int(), + enum: z.enum(['a', 'b', 'c']), + real: z.number().min(CONSTANTS.INT48_MIN).max(CONSTANTS.INT48_MAX), + serial: z.number().min(0).max(Number.MAX_SAFE_INTEGER).int(), + smallint1: z.number().min(CONSTANTS.INT16_MIN).max(CONSTANTS.INT16_MAX).int(), + smallint2: z.number().min(0).max(CONSTANTS.INT16_UNSIGNED_MAX).int(), + text1: z.string().max(CONSTANTS.INT16_UNSIGNED_MAX), + text2: z.enum(['a', 'b', 'c']), time: z.string(), - timestamp: z.date(), - timestampString: z.string(), - tinyint: z.number(), - varbinary: z.string().max(200), - varchar: z.string().max(200), - varcharEnum: z.enum(['a', 'b', 'c']), - year: z.number(), - autoIncrement: z.number(), + timestamp1: z.date(), + timestamp2: z.string(), + tinyint1: z.number().min(CONSTANTS.INT8_MIN).max(CONSTANTS.INT8_MAX).int(), + tinyint2: z.number().min(0).max(CONSTANTS.INT8_UNSIGNED_MAX).int(), + varchar1: z.string().max(10), + varchar2: z.enum(['a', 'b', 'c']), + varbinary: z.string(), + year: z.number().min(1901).max(2155).int(), + longtext1: z.string().max(CONSTANTS.INT32_UNSIGNED_MAX), + longtext2: z.enum(['a', 'b', 'c']), + mediumtext1: z.string().max(CONSTANTS.INT24_UNSIGNED_MAX), + mediumtext2: z.enum(['a', 'b', 'c']), + tinytext1: z.string().max(CONSTANTS.INT8_UNSIGNED_MAX), + tinytext2: z.enum(['a', 'b', 'c']), }); - - expectSchemaShape(t, expected).from(actual); + expectSchemaShape(t, expected).from(result); + Expect>(); }); + +/* Disallow unknown keys in table refinement - select */ { + const table = mysqlTable('test', { id: int() }); + // @ts-expect-error + createSelectSchema(table, { unknown: z.string() }); +} + +/* Disallow unknown keys in table refinement - insert */ { + const table = mysqlTable('test', { id: int() }); + // @ts-expect-error + createInsertSchema(table, { unknown: z.string() }); +} + +/* Disallow unknown keys in table refinement - update */ { + const table = mysqlTable('test', { id: int() }); + // @ts-expect-error + createUpdateSchema(table, { unknown: z.string() }); +} + +/* Disallow unknown keys in view qb - select */ { + const table = mysqlTable('test', { id: int() }); + const view = mysqlView('test').as((qb) => qb.select().from(table)); + const nestedSelect = mysqlView('test').as((qb) => qb.select({ table }).from(table)); + // @ts-expect-error + createSelectSchema(view, { unknown: z.string() }); + // @ts-expect-error + createSelectSchema(nestedSelect, { table: { unknown: z.string() } }); +} + +/* Disallow unknown keys in view columns - select */ { + const view = mysqlView('test', { id: int() }).as(sql``); + // @ts-expect-error + createSelectSchema(view, { unknown: z.string() }); +} diff --git a/drizzle-zod/tests/pg.test.ts b/drizzle-zod/tests/pg.test.ts index b1f6e0c20..dc703b4fc 100644 --- a/drizzle-zod/tests/pg.test.ts +++ b/drizzle-zod/tests/pg.test.ts @@ -1,163 +1,505 @@ -import { char, date, integer, pgEnum, pgTable, serial, text, timestamp, varchar } from 'drizzle-orm/pg-core'; -import { expect, test } from 'vitest'; +import { type Equal, sql } from 'drizzle-orm'; +import { integer, pgEnum, pgMaterializedView, pgSchema, pgTable, pgView, serial, text } from 'drizzle-orm/pg-core'; +import { test } from 'vitest'; import { z } from 'zod'; -import { createInsertSchema, createSelectSchema } from '../src'; -import { expectSchemaShape } from './utils.ts'; +import { jsonSchema } from '~/column.ts'; +import { CONSTANTS } from '~/constants.ts'; +import { createInsertSchema, createSelectSchema, createUpdateSchema } from '../src'; +import { Expect, expectEnumValues, expectSchemaShape } from './utils.ts'; -export const roleEnum = pgEnum('role', ['admin', 'user']); +const integerSchema = z.number().min(CONSTANTS.INT32_MIN).max(CONSTANTS.INT32_MAX).int(); +const textSchema = z.string(); -const users = pgTable('users', { - a: integer('a').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(), +test('table - select', (t) => { + const table = pgTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + + const result = createSelectSchema(table); + const expected = z.object({ id: integerSchema, name: textSchema }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -const testUser = { - a: [1, 2, 3], - id: 1, - name: 'John Doe', - email: 'john.doe@example.com', - birthdayString: '1990-01-01', - birthdayDate: new Date('1990-01-01'), - createdAt: new Date(), - role: 'admin', - roleText: 'admin', - roleText2: 'admin', - profession: 'Software Engineer', - initials: 'JD', -}; +test('table in schema - select', (tc) => { + const schema = pgSchema('test'); + const table = schema.table('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + + const result = createSelectSchema(table); + const expected = z.object({ id: integerSchema, name: textSchema }); + expectSchemaShape(tc, expected).from(result); + Expect>(); +}); -test('users insert valid user', () => { - const schema = createInsertSchema(users); +test('table - insert', (t) => { + const table = pgTable('test', { + id: integer().generatedAlwaysAsIdentity().primaryKey(), + name: text().notNull(), + age: integer(), + }); - expect(schema.safeParse(testUser).success).toBeTruthy(); + const result = createInsertSchema(table); + const expected = z.object({ name: textSchema, age: integerSchema.nullable().optional() }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('users insert invalid varchar', () => { - const schema = createInsertSchema(users); +test('table - update', (t) => { + const table = pgTable('test', { + id: integer().generatedAlwaysAsIdentity().primaryKey(), + name: text().notNull(), + age: integer(), + }); + + const result = createUpdateSchema(table); + const expected = z.object({ + name: textSchema.optional(), + age: integerSchema.nullable().optional(), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('view qb - select', (t) => { + const table = pgTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + const view = pgView('test').as((qb) => qb.select({ id: table.id, age: sql``.as('age') }).from(table)); + + const result = createSelectSchema(view); + const expected = z.object({ id: integerSchema, age: z.any() }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); - expect(schema.safeParse({ ...testUser, profession: 'Chief Executive Officer' }).success).toBeFalsy(); +test('view columns - select', (t) => { + const view = pgView('test', { + id: serial().primaryKey(), + name: text().notNull(), + }).as(sql``); + + const result = createSelectSchema(view); + const expected = z.object({ id: integerSchema, name: textSchema }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('materialized view qb - select', (t) => { + const table = pgTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + const view = pgMaterializedView('test').as((qb) => qb.select({ id: table.id, age: sql``.as('age') }).from(table)); + + const result = createSelectSchema(view); + const expected = z.object({ id: integerSchema, age: z.any() }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('materialized view columns - select', (t) => { + const view = pgView('test', { + id: serial().primaryKey(), + name: text().notNull(), + }).as(sql``); + + const result = createSelectSchema(view); + const expected = z.object({ id: integerSchema, name: textSchema }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('view with nested fields - select', (t) => { + const table = pgTable('test', { + id: serial().primaryKey(), + name: text().notNull(), + }); + const view = pgMaterializedView('test').as((qb) => + qb.select({ + id: table.id, + nested: { + name: table.name, + age: sql``.as('age'), + }, + table, + }).from(table) + ); + + const result = createSelectSchema(view); + const expected = z.object({ + id: integerSchema, + nested: z.object({ name: textSchema, age: z.any() }), + table: z.object({ id: integerSchema, name: textSchema }), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('users insert invalid char', () => { - const schema = createInsertSchema(users); +test('enum - select', (t) => { + const enum_ = pgEnum('test', ['a', 'b', 'c']); - expect(schema.safeParse({ ...testUser, initials: 'JoDo' }).success).toBeFalsy(); + const result = createSelectSchema(enum_); + const expected = z.enum(['a', 'b', 'c']); + expectEnumValues(t, expected).from(result); + Expect>(); }); -test('users insert schema', (t) => { - const actual = createInsertSchema(users, { - id: ({ id }) => id.positive(), - email: ({ email }) => email.email(), - roleText: z.enum(['user', 'manager', 'admin']), +test('nullability - select', (t) => { + const table = pgTable('test', { + c1: integer(), + c2: integer().notNull(), + c3: integer().default(1), + c4: integer().notNull().default(1), }); - (() => { - { - createInsertSchema(users, { - // @ts-expect-error (missing property) - foobar: z.number(), - }); - } + const result = createSelectSchema(table); + const expected = z.object({ + c1: integerSchema.nullable(), + c2: integerSchema, + c3: integerSchema.nullable(), + c4: integerSchema, + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); - { - createInsertSchema(users, { - // @ts-expect-error (invalid type) - id: 123, - }); - } +test('nullability - insert', (t) => { + const table = pgTable('test', { + c1: integer(), + c2: integer().notNull(), + c3: integer().default(1), + c4: integer().notNull().default(1), + c5: integer().generatedAlwaysAs(1), + c6: integer().generatedAlwaysAsIdentity(), + c7: integer().generatedByDefaultAsIdentity(), }); + const result = createInsertSchema(table); const expected = z.object({ - a: z.array(z.number()).nullable().optional(), - id: z.number().positive().optional(), - name: z.string().nullable().optional(), - email: z.string().email(), - birthdayString: z.string(), - birthdayDate: z.date(), - createdAt: z.date().optional(), - role: z.enum(['admin', 'user']), - roleText: z.enum(['user', 'manager', 'admin']), - roleText2: z.enum(['admin', 'user']).optional(), - profession: z.string().max(20).min(1), - initials: z.string().max(2).min(1), + c1: integerSchema.nullable().optional(), + c2: integerSchema, + c3: integerSchema.nullable().optional(), + c4: integerSchema.optional(), + c7: integerSchema.optional(), + }); + expectSchemaShape(t, expected).from(result); +}); + +test('nullability - update', (t) => { + const table = pgTable('test', { + c1: integer(), + c2: integer().notNull(), + c3: integer().default(1), + c4: integer().notNull().default(1), + c5: integer().generatedAlwaysAs(1), + c6: integer().generatedAlwaysAsIdentity(), + c7: integer().generatedByDefaultAsIdentity(), }); - expectSchemaShape(t, expected).from(actual); + const result = createUpdateSchema(table); + const expected = z.object({ + c1: integerSchema.nullable().optional(), + c2: integerSchema.optional(), + c3: integerSchema.nullable().optional(), + c4: integerSchema.optional(), + c7: integerSchema.optional(), + }); + table.c5.generated?.type; + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('users insert schema w/ defaults', (t) => { - const actual = createInsertSchema(users); +test('refine table - select', (t) => { + const table = pgTable('test', { + c1: integer(), + c2: integer().notNull(), + c3: integer().notNull(), + }); + const result = createSelectSchema(table, { + c2: (schema) => schema.max(1000), + c3: z.string().transform(Number), + }); const expected = z.object({ - a: z.array(z.number()).nullable().optional(), - id: z.number().optional(), - name: z.string().nullable().optional(), - email: z.string(), - birthdayString: z.string(), - birthdayDate: z.date(), - createdAt: z.date().optional(), - role: z.enum(['admin', 'user']), - roleText: z.enum(['admin', 'user']), - roleText2: z.enum(['admin', 'user']).optional(), - profession: z.string().max(20).min(1), - initials: z.string().max(2).min(1), + c1: integerSchema.nullable(), + c2: integerSchema.max(1000), + c3: z.string().transform(Number), }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); - expectSchemaShape(t, expected).from(actual); +test('refine table - insert', (t) => { + const table = pgTable('test', { + c1: integer(), + c2: integer().notNull(), + c3: integer().notNull(), + c4: integer().generatedAlwaysAs(1), + }); + + const result = createInsertSchema(table, { + c2: (schema) => schema.max(1000), + c3: z.string().transform(Number), + }); + const expected = z.object({ + c1: integerSchema.nullable().optional(), + c2: integerSchema.max(1000), + c3: z.string().transform(Number), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('users select schema', (t) => { - const actual = createSelectSchema(users, { - id: ({ id }) => id.positive(), - email: ({ email }) => email.email(), - roleText: z.enum(['user', 'manager', 'admin']), +test('refine table - update', (t) => { + const table = pgTable('test', { + c1: integer(), + c2: integer().notNull(), + c3: integer().notNull(), + c4: integer().generatedAlwaysAs(1), }); + const result = createUpdateSchema(table, { + c2: (schema) => schema.max(1000), + c3: z.string().transform(Number), + }); const expected = z.object({ - a: z.array(z.number()).nullable(), - id: z.number().positive(), - name: z.string().nullable(), - email: z.string().email(), - birthdayString: z.string(), - birthdayDate: z.date(), - createdAt: z.date(), - role: z.enum(['admin', 'user']), - roleText: z.enum(['user', 'manager', 'admin']), - roleText2: z.enum(['admin', 'user']), - profession: z.string().max(20).min(1), - initials: z.string().max(2).min(1), + c1: integerSchema.nullable().optional(), + c2: integerSchema.max(1000).optional(), + c3: z.string().transform(Number), }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); - expectSchemaShape(t, expected).from(actual); +test('refine view - select', (t) => { + const table = pgTable('test', { + c1: integer(), + c2: integer(), + c3: integer(), + c4: integer(), + c5: integer(), + c6: integer(), + }); + const view = pgView('test').as((qb) => + qb.select({ + c1: table.c1, + c2: table.c2, + c3: table.c3, + nested: { + c4: table.c4, + c5: table.c5, + c6: table.c6, + }, + table, + }).from(table) + ); + + const result = createSelectSchema(view, { + c2: (schema) => schema.max(1000), + c3: z.string().transform(Number), + nested: { + c5: (schema) => schema.max(1000), + c6: z.string().transform(Number), + }, + table: { + c2: (schema) => schema.max(1000), + c3: z.string().transform(Number), + }, + }); + const expected = z.object({ + c1: integerSchema.nullable(), + c2: integerSchema.max(1000).nullable(), + c3: z.string().transform(Number), + nested: z.object({ + c4: integerSchema.nullable(), + c5: integerSchema.max(1000).nullable(), + c6: z.string().transform(Number), + }), + table: z.object({ + c1: integerSchema.nullable(), + c2: integerSchema.max(1000).nullable(), + c3: z.string().transform(Number), + c4: integerSchema.nullable(), + c5: integerSchema.nullable(), + c6: integerSchema.nullable(), + }), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('users select schema w/ defaults', (t) => { - const actual = createSelectSchema(users); +test('all data types', (t) => { + const table = pgTable('test', ({ + bigint, + bigserial, + bit, + boolean, + date, + char, + cidr, + doublePrecision, + geometry, + halfvec, + inet, + integer, + interval, + json, + jsonb, + line, + macaddr, + macaddr8, + numeric, + point, + real, + serial, + smallint, + smallserial, + text, + sparsevec, + time, + timestamp, + uuid, + varchar, + vector, + }) => ({ + bigint1: bigint({ mode: 'number' }).notNull(), + bigint2: bigint({ mode: 'bigint' }).notNull(), + bigserial1: bigserial({ mode: 'number' }).notNull(), + bigserial2: bigserial({ mode: 'bigint' }).notNull(), + bit: bit({ dimensions: 5 }).notNull(), + boolean: boolean().notNull(), + date1: date({ mode: 'date' }).notNull(), + date2: date({ mode: 'string' }).notNull(), + char1: char({ length: 10 }).notNull(), + char2: char({ length: 1, enum: ['a', 'b', 'c'] }).notNull(), + cidr: cidr().notNull(), + doublePrecision: doublePrecision().notNull(), + geometry1: geometry({ type: 'point', mode: 'tuple' }).notNull(), + geometry2: geometry({ type: 'point', mode: 'xy' }).notNull(), + halfvec: halfvec({ dimensions: 3 }).notNull(), + inet: inet().notNull(), + integer: integer().notNull(), + interval: interval().notNull(), + json: json().notNull(), + jsonb: jsonb().notNull(), + line1: line({ mode: 'abc' }).notNull(), + line2: line({ mode: 'tuple' }).notNull(), + macaddr: macaddr().notNull(), + macaddr8: macaddr8().notNull(), + numeric: numeric().notNull(), + point1: point({ mode: 'xy' }).notNull(), + point2: point({ mode: 'tuple' }).notNull(), + real: real().notNull(), + serial: serial().notNull(), + smallint: smallint().notNull(), + smallserial: smallserial().notNull(), + text1: text().notNull(), + text2: text({ enum: ['a', 'b', 'c'] }).notNull(), + sparsevec: sparsevec({ dimensions: 3 }).notNull(), + time: time().notNull(), + timestamp1: timestamp({ mode: 'date' }).notNull(), + timestamp2: timestamp({ mode: 'string' }).notNull(), + uuid: uuid().notNull(), + varchar1: varchar({ length: 10 }).notNull(), + varchar2: varchar({ length: 1, enum: ['a', 'b', 'c'] }).notNull(), + vector: vector({ dimensions: 3 }).notNull(), + array1: integer().array().notNull(), + array2: integer().array().array(2).notNull(), + array3: varchar({ length: 10 }).array().array(2).notNull(), + })); + const result = createSelectSchema(table); const expected = z.object({ - a: z.array(z.number()).nullable(), - id: z.number(), - name: z.string().nullable(), - email: z.string(), - birthdayString: z.string(), - birthdayDate: z.date(), - createdAt: z.date(), - role: z.enum(['admin', 'user']), - roleText: z.enum(['admin', 'user']), - roleText2: z.enum(['admin', 'user']), - profession: z.string().max(20).min(1), - initials: z.string().max(2).min(1), - }); - - expectSchemaShape(t, expected).from(actual); + bigint1: z.number().min(Number.MIN_SAFE_INTEGER).max(Number.MAX_SAFE_INTEGER).int(), + bigint2: z.bigint().min(CONSTANTS.INT64_MIN).max(CONSTANTS.INT64_MAX), + bigserial1: z.number().min(Number.MIN_SAFE_INTEGER).max(Number.MAX_SAFE_INTEGER).int(), + bigserial2: z.bigint().min(CONSTANTS.INT64_MIN).max(CONSTANTS.INT64_MAX), + bit: z.string().regex(/^[01]+$/).max(5), + boolean: z.boolean(), + date1: z.date(), + date2: z.string(), + char1: z.string().length(10), + char2: z.enum(['a', 'b', 'c']), + cidr: z.string(), + doublePrecision: z.number().min(CONSTANTS.INT48_MIN).max(CONSTANTS.INT48_MAX), + geometry1: z.tuple([z.number(), z.number()]), + geometry2: z.object({ x: z.number(), y: z.number() }), + halfvec: z.array(z.number()).length(3), + inet: z.string(), + integer: z.number().min(CONSTANTS.INT32_MIN).max(CONSTANTS.INT32_MAX).int(), + interval: z.string(), + json: jsonSchema, + jsonb: jsonSchema, + line1: z.object({ a: z.number(), b: z.number(), c: z.number() }), + line2: z.tuple([z.number(), z.number(), z.number()]), + macaddr: z.string(), + macaddr8: z.string(), + numeric: z.string(), + point1: z.object({ x: z.number(), y: z.number() }), + point2: z.tuple([z.number(), z.number()]), + real: z.number().min(CONSTANTS.INT24_MIN).max(CONSTANTS.INT24_MAX), + serial: z.number().min(CONSTANTS.INT32_MIN).max(CONSTANTS.INT32_MAX).int(), + smallint: z.number().min(CONSTANTS.INT16_MIN).max(CONSTANTS.INT16_MAX).int(), + smallserial: z.number().min(CONSTANTS.INT16_MIN).max(CONSTANTS.INT16_MAX).int(), + text1: z.string(), + text2: z.enum(['a', 'b', 'c']), + sparsevec: z.string(), + time: z.string(), + timestamp1: z.date(), + timestamp2: z.string(), + uuid: z.string().uuid(), + varchar1: z.string().max(10), + varchar2: z.enum(['a', 'b', 'c']), + vector: z.array(z.number()).length(3), + array1: z.array(integerSchema), + array2: z.array(z.array(integerSchema).length(2)), + array3: z.array(z.array(z.string().max(10)).length(2)), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); + +/* Disallow unknown keys in table refinement - select */ { + const table = pgTable('test', { id: integer() }); + // @ts-expect-error + createSelectSchema(table, { unknown: z.string() }); +} + +/* Disallow unknown keys in table refinement - insert */ { + const table = pgTable('test', { id: integer() }); + // @ts-expect-error + createInsertSchema(table, { unknown: z.string() }); +} + +/* Disallow unknown keys in table refinement - update */ { + const table = pgTable('test', { id: integer() }); + // @ts-expect-error + createUpdateSchema(table, { unknown: z.string() }); +} + +/* Disallow unknown keys in view qb - select */ { + const table = pgTable('test', { id: integer() }); + const view = pgView('test').as((qb) => qb.select().from(table)); + const mView = pgMaterializedView('test').as((qb) => qb.select().from(table)); + const nestedSelect = pgView('test').as((qb) => qb.select({ table }).from(table)); + // @ts-expect-error + createSelectSchema(view, { unknown: z.string() }); + // @ts-expect-error + createSelectSchema(mView, { unknown: z.string() }); + // @ts-expect-error + createSelectSchema(nestedSelect, { table: { unknown: z.string() } }); +} + +/* Disallow unknown keys in view columns - select */ { + const view = pgView('test', { id: integer() }).as(sql``); + const mView = pgView('test', { id: integer() }).as(sql``); + // @ts-expect-error + createSelectSchema(view, { unknown: z.string() }); + // @ts-expect-error + createSelectSchema(mView, { unknown: z.string() }); +} diff --git a/drizzle-zod/tests/sqlite.test.ts b/drizzle-zod/tests/sqlite.test.ts index 5a2c3a04e..45e64bbde 100644 --- a/drizzle-zod/tests/sqlite.test.ts +++ b/drizzle-zod/tests/sqlite.test.ts @@ -1,162 +1,359 @@ -import { blob, integer, numeric, real, sqliteTable, text } from 'drizzle-orm/sqlite-core'; -import { expect, test } from 'vitest'; +import { type Equal, sql } from 'drizzle-orm'; +import { int, sqliteTable, sqliteView, text } from 'drizzle-orm/sqlite-core'; +import { test } from 'vitest'; import { z } from 'zod'; -import { createInsertSchema, createSelectSchema, jsonSchema } from '../src'; -import { expectSchemaShape } from './utils.ts'; +import { bufferSchema, jsonSchema } from '~/column.ts'; +import { CONSTANTS } from '~/constants.ts'; +import { createInsertSchema, createSelectSchema, createUpdateSchema } from '../src'; +import { Expect, expectSchemaShape } from './utils.ts'; -const blobJsonSchema = z.object({ - foo: z.string(), +const intSchema = z.number().min(Number.MIN_SAFE_INTEGER).max(Number.MAX_SAFE_INTEGER).int(); +const textSchema = z.string(); + +test('table - select', (t) => { + const table = sqliteTable('test', { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + }); + + const result = createSelectSchema(table); + const expected = z.object({ id: intSchema, name: textSchema }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -const users = 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'), +test('table - insert', (t) => { + const table = sqliteTable('test', { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + age: int(), + }); + + const result = createInsertSchema(table); + const expected = z.object({ id: intSchema.optional(), name: textSchema, age: intSchema.nullable().optional() }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -const testUser = { - id: 1, - blobJson: { foo: 'bar' }, - blobBigInt: BigInt(123), - numeric: '123.45', - createdAt: new Date(), - createdAtMs: new Date(), - boolean: true, - real: 123.45, - text: 'foobar', - role: 'admin', -}; - -test('users insert valid user', () => { - const schema = createInsertSchema(users); - - expect(schema.safeParse(testUser).success).toBeTruthy(); +test('table - update', (t) => { + const table = sqliteTable('test', { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + age: int(), + }); + + const result = createUpdateSchema(table); + const expected = z.object({ + id: intSchema.optional(), + name: textSchema.optional(), + age: intSchema.nullable().optional(), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('view qb - select', (t) => { + const table = sqliteTable('test', { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + }); + const view = sqliteView('test').as((qb) => qb.select({ id: table.id, age: sql``.as('age') }).from(table)); + + const result = createSelectSchema(view); + const expected = z.object({ id: intSchema, age: z.any() }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('users insert invalid text length', () => { - const schema = createInsertSchema(users); +test('view columns - select', (t) => { + const view = sqliteView('test', { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), + }).as(sql``); - expect(schema.safeParse({ ...testUser, text: 'a'.repeat(256) }).success).toBeFalsy(); + const result = createSelectSchema(view); + const expected = z.object({ id: intSchema, name: textSchema }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('users insert schema', (t) => { - const actual = createInsertSchema(users, { - id: ({ id }) => id.positive(), - blobJson: blobJsonSchema, - role: z.enum(['admin', 'manager', 'user']), +test('view with nested fields - select', (t) => { + const table = sqliteTable('test', { + id: int().primaryKey({ autoIncrement: true }), + name: text().notNull(), }); + const view = sqliteView('test').as((qb) => + qb.select({ + id: table.id, + nested: { + name: table.name, + age: sql``.as('age'), + }, + table, + }).from(table) + ); - (() => { - { - createInsertSchema(users, { - // @ts-expect-error (missing property) - foobar: z.number(), - }); - } + const result = createSelectSchema(view); + const expected = z.object({ + id: intSchema, + nested: z.object({ name: textSchema, age: z.any() }), + table: z.object({ id: intSchema, name: textSchema }), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); - { - createInsertSchema(users, { - // @ts-expect-error (invalid type) - id: 123, - }); - } +test('nullability - select', (t) => { + const table = sqliteTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().default(1), + c4: int().notNull().default(1), }); + const result = createSelectSchema(table); const expected = z.object({ - id: z.number().positive().optional(), - blobJson: blobJsonSchema, - blobBigInt: z.bigint(), - numeric: z.string(), - createdAt: z.date(), - createdAtMs: z.date(), - boolean: z.boolean(), - real: z.number(), - text: z.string().nullable().optional(), - role: z.enum(['admin', 'manager', 'user']).optional(), + c1: intSchema.nullable(), + c2: intSchema, + c3: intSchema.nullable(), + c4: intSchema, + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('nullability - insert', (t) => { + const table = sqliteTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().default(1), + c4: int().notNull().default(1), + c5: int().generatedAlwaysAs(1), }); - expectSchemaShape(t, expected).from(actual); + const result = createInsertSchema(table); + const expected = z.object({ + c1: intSchema.nullable().optional(), + c2: intSchema, + c3: intSchema.nullable().optional(), + c4: intSchema.optional(), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('users insert schema w/ defaults', (t) => { - const actual = createInsertSchema(users); +test('nullability - update', (t) => { + const table = sqliteTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().default(1), + c4: int().notNull().default(1), + c5: int().generatedAlwaysAs(1), + }); + const result = createUpdateSchema(table); const expected = z.object({ - id: z.number().optional(), - blobJson: jsonSchema, - blobBigInt: z.bigint(), - numeric: z.string(), - createdAt: z.date(), - createdAtMs: z.date(), - boolean: z.boolean(), - real: z.number(), - text: z.string().nullable().optional(), - role: z.enum(['admin', 'user']).optional(), + c1: intSchema.nullable().optional(), + c2: intSchema.optional(), + c3: intSchema.nullable().optional(), + c4: intSchema.optional(), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('refine table - select', (t) => { + const table = sqliteTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().notNull(), }); - expectSchemaShape(t, expected).from(actual); + const result = createSelectSchema(table, { + c2: (schema) => schema.max(1000), + c3: z.string().transform(Number), + }); + const expected = z.object({ + c1: intSchema.nullable(), + c2: intSchema.max(1000), + c3: z.string().transform(Number), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('users select schema', (t) => { - const actual = createSelectSchema(users, { - blobJson: jsonSchema, - role: z.enum(['admin', 'manager', 'user']), +test('refine table - insert', (t) => { + const table = sqliteTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().notNull(), + c4: int().generatedAlwaysAs(1), }); - (() => { - { - createSelectSchema(users, { - // @ts-expect-error (missing property) - foobar: z.number(), - }); - } + const result = createInsertSchema(table, { + c2: (schema) => schema.max(1000), + c3: z.string().transform(Number), + }); + const expected = z.object({ + c1: intSchema.nullable().optional(), + c2: intSchema.max(1000), + c3: z.string().transform(Number), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); - { - createSelectSchema(users, { - // @ts-expect-error (invalid type) - id: 123, - }); - } +test('refine table - update', (t) => { + const table = sqliteTable('test', { + c1: int(), + c2: int().notNull(), + c3: int().notNull(), + c4: int().generatedAlwaysAs(1), }); + const result = createUpdateSchema(table, { + c2: (schema) => schema.max(1000), + c3: z.string().transform(Number), + }); const expected = z.object({ - id: z.number(), - blobJson: jsonSchema, - blobBigInt: z.bigint(), - numeric: z.string(), - createdAt: z.date(), - createdAtMs: z.date(), - boolean: z.boolean(), - real: z.number(), - text: z.string().nullable(), - role: z.enum(['admin', 'manager', 'user']), - }).required(); - - expectSchemaShape(t, expected).from(actual); + c1: intSchema.nullable().optional(), + c2: intSchema.max(1000).optional(), + c3: z.string().transform(Number), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); -test('users select schema w/ defaults', (t) => { - const actual = createSelectSchema(users); +test('refine view - select', (t) => { + const table = sqliteTable('test', { + c1: int(), + c2: int(), + c3: int(), + c4: int(), + c5: int(), + c6: int(), + }); + const view = sqliteView('test').as((qb) => + qb.select({ + c1: table.c1, + c2: table.c2, + c3: table.c3, + nested: { + c4: table.c4, + c5: table.c5, + c6: table.c6, + }, + table, + }).from(table) + ); + const result = createSelectSchema(view, { + c2: (schema) => schema.max(1000), + c3: z.string().transform(Number), + nested: { + c5: (schema) => schema.max(1000), + c6: z.string().transform(Number), + }, + table: { + c2: (schema) => schema.max(1000), + c3: z.string().transform(Number), + }, + }); + const expected = z.object({ + c1: intSchema.nullable(), + c2: intSchema.max(1000).nullable(), + c3: z.string().transform(Number), + nested: z.object({ + c4: intSchema.nullable(), + c5: intSchema.max(1000).nullable(), + c6: z.string().transform(Number), + }), + table: z.object({ + c1: intSchema.nullable(), + c2: intSchema.max(1000).nullable(), + c3: z.string().transform(Number), + c4: intSchema.nullable(), + c5: intSchema.nullable(), + c6: intSchema.nullable(), + }), + }); + expectSchemaShape(t, expected).from(result); + Expect>(); +}); + +test('all data types', (t) => { + const table = sqliteTable('test', ({ + blob, + integer, + numeric, + real, + text, + }) => ({ + blob1: blob({ mode: 'buffer' }).notNull(), + blob2: blob({ mode: 'bigint' }).notNull(), + blob3: blob({ mode: 'json' }).notNull(), + integer1: integer({ mode: 'number' }).notNull(), + integer2: integer({ mode: 'boolean' }).notNull(), + integer3: integer({ mode: 'timestamp' }).notNull(), + integer4: integer({ mode: 'timestamp_ms' }).notNull(), + numeric: numeric().notNull(), + real: real().notNull(), + text1: text({ mode: 'text' }).notNull(), + text2: text({ mode: 'text', length: 10 }).notNull(), + text3: text({ mode: 'text', enum: ['a', 'b', 'c'] }).notNull(), + text4: text({ mode: 'json' }).notNull(), + })); + + const result = createSelectSchema(table); const expected = z.object({ - id: z.number(), - blobJson: jsonSchema, - blobBigInt: z.bigint(), + blob1: bufferSchema, + blob2: z.bigint().min(CONSTANTS.INT64_MIN).max(CONSTANTS.INT64_MAX), + blob3: jsonSchema, + integer1: z.number().min(Number.MIN_SAFE_INTEGER).max(Number.MAX_SAFE_INTEGER).int(), + integer2: z.boolean(), + integer3: z.date(), + integer4: z.date(), numeric: z.string(), - createdAt: z.date(), - createdAtMs: z.date(), - boolean: z.boolean(), - real: z.number(), - text: z.string().nullable(), - role: z.enum(['admin', 'user']), - }).required(); - - expectSchemaShape(t, expected).from(actual); + real: z.number().min(CONSTANTS.INT48_MIN).max(CONSTANTS.INT48_MAX), + text1: z.string(), + text2: z.string().max(10), + text3: z.enum(['a', 'b', 'c']), + text4: jsonSchema, + }); + expectSchemaShape(t, expected).from(result); + Expect>(); }); + +/* Disallow unknown keys in table refinement - select */ { + const table = sqliteTable('test', { id: int() }); + // @ts-expect-error + createSelectSchema(table, { unknown: z.string() }); +} + +/* Disallow unknown keys in table refinement - insert */ { + const table = sqliteTable('test', { id: int() }); + // @ts-expect-error + createInsertSchema(table, { unknown: z.string() }); +} + +/* Disallow unknown keys in table refinement - update */ { + const table = sqliteTable('test', { id: int() }); + // @ts-expect-error + createUpdateSchema(table, { unknown: z.string() }); +} + +/* Disallow unknown keys in view qb - select */ { + const table = sqliteTable('test', { id: int() }); + const view = sqliteView('test').as((qb) => qb.select().from(table)); + const nestedSelect = sqliteView('test').as((qb) => qb.select({ table }).from(table)); + // @ts-expect-error + createSelectSchema(view, { unknown: z.string() }); + // @ts-expect-error + createSelectSchema(nestedSelect, { table: { unknown: z.string() } }); +} + +/* Disallow unknown keys in view columns - select */ { + const view = sqliteView('test', { id: int() }).as(sql``); + // @ts-expect-error + createSelectSchema(view, { unknown: z.string() }); +} diff --git a/drizzle-zod/tests/utils.ts b/drizzle-zod/tests/utils.ts index 1c28be260..6a36f66c5 100644 --- a/drizzle-zod/tests/utils.ts +++ b/drizzle-zod/tests/utils.ts @@ -1,13 +1,14 @@ import { expect, type TaskContext } from 'vitest'; import type { z } from 'zod'; -export function expectSchemaShape(t: TaskContext, expected: z.ZodObject) { +export function expectSchemaShape>(t: TaskContext, expected: T) { return { - from(actual: z.ZodObject) { + from(actual: T) { expect(Object.keys(actual.shape)).toStrictEqual(Object.keys(expected.shape)); for (const key of Object.keys(actual.shape)) { expect(actual.shape[key]!._def.typeName).toStrictEqual(expected.shape[key]?._def.typeName); + expect(actual.shape[key]!._def?.checks).toEqual(expected.shape[key]?._def?.checks); if (actual.shape[key]?._def.typeName === 'ZodOptional') { expect(actual.shape[key]!._def.innerType._def.typeName).toStrictEqual( actual.shape[key]!._def.innerType._def.typeName, @@ -17,3 +18,13 @@ export function expectSchemaShape(t: TaskContext, expec }, }; } + +export function expectEnumValues>(t: TaskContext, expected: T) { + return { + from(actual: T) { + expect(actual._def.values).toStrictEqual(expected._def.values); + }, + }; +} + +export function Expect<_ extends true>() {} diff --git a/drizzle-zod/tsconfig.build.json b/drizzle-zod/tsconfig.build.json index 3377281ba..1af9f8b40 100644 --- a/drizzle-zod/tsconfig.build.json +++ b/drizzle-zod/tsconfig.build.json @@ -1,7 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "rootDir": "src" + "rootDir": "src", + "stripInternal": true }, "include": ["src"] } diff --git a/drizzle-zod/tsconfig.json b/drizzle-zod/tsconfig.json index 038d79591..c25379c37 100644 --- a/drizzle-zod/tsconfig.json +++ b/drizzle-zod/tsconfig.json @@ -4,6 +4,7 @@ "outDir": "dist", "baseUrl": ".", "declaration": true, + "noEmit": true, "paths": { "~/*": ["src/*"] } diff --git a/integration-tests/tests/mysql/mysql-common.ts b/integration-tests/tests/mysql/mysql-common.ts index a2a0baeb0..521e70407 100644 --- a/integration-tests/tests/mysql/mysql-common.ts +++ b/integration-tests/tests/mysql/mysql-common.ts @@ -37,6 +37,7 @@ import { foreignKey, getTableConfig, getViewConfig, + index, int, intersect, intersectAll, @@ -3888,6 +3889,37 @@ export function tests(driver?: string) { expect(users.length).toBeGreaterThan(0); }); + test('define constraints as array', async (ctx) => { + const { db } = ctx.mysql; + + const table = mysqlTable('name', { + id: int(), + }, (t) => [ + index('name').on(t.id), + primaryKey({ columns: [t.id], name: 'custom' }), + ]); + + const { indexes, primaryKeys } = getTableConfig(table); + + expect(indexes.length).toBe(1); + expect(primaryKeys.length).toBe(1); + }); + + test('define constraints as array inside third param', async (ctx) => { + const { db } = ctx.mysql; + + const table = mysqlTable('name', { + id: int(), + }, (t) => [ + [index('name').on(t.id), primaryKey({ columns: [t.id], name: 'custom' })], + ]); + + const { indexes, primaryKeys } = getTableConfig(table); + + expect(indexes.length).toBe(1); + expect(primaryKeys.length).toBe(1); + }); + test('update with limit and order by', async (ctx) => { const { db } = ctx.mysql; diff --git a/integration-tests/tests/singlestore/singlestore-common.ts b/integration-tests/tests/singlestore/singlestore-common.ts index 6335bf9ec..fe7c2afb4 100644 --- a/integration-tests/tests/singlestore/singlestore-common.ts +++ b/integration-tests/tests/singlestore/singlestore-common.ts @@ -35,6 +35,7 @@ import { decimal, except, getTableConfig, + index, int, intersect, json, @@ -2701,6 +2702,37 @@ export function tests(driver?: string) { })()).rejects.toThrowError(); }); + test('define constraints as array', async (ctx) => { + const { db } = ctx.singlestore; + + const table = singlestoreTable('name', { + id: int(), + }, (t) => [ + index('name').on(t.id), + primaryKey({ columns: [t.id], name: 'custom' }), + ]); + + const { indexes, primaryKeys } = getTableConfig(table); + + expect(indexes.length).toBe(1); + expect(primaryKeys.length).toBe(1); + }); + + test('define constraints as array inside third param', async (ctx) => { + const { db } = ctx.singlestore; + + const table = singlestoreTable('name', { + id: int(), + }, (t) => [ + [index('name').on(t.id), primaryKey({ columns: [t.id], name: 'custom' })], + ]); + + const { indexes, primaryKeys } = getTableConfig(table); + + expect(indexes.length).toBe(1); + expect(primaryKeys.length).toBe(1); + }); + test.skip('set operations (mixed) from query builder', async (ctx) => { const { db } = ctx.singlestore; diff --git a/integration-tests/tests/sqlite/sqlite-common.ts b/integration-tests/tests/sqlite/sqlite-common.ts index 1c62181dd..c6d67cee3 100644 --- a/integration-tests/tests/sqlite/sqlite-common.ts +++ b/integration-tests/tests/sqlite/sqlite-common.ts @@ -29,6 +29,7 @@ import { foreignKey, getTableConfig, getViewConfig, + index, int, integer, intersect, @@ -2592,6 +2593,34 @@ export function tests() { }).rejects.toThrowError(); }); + test('define constraints as array', async (_ctx) => { + const table = sqliteTable('name', { + id: int(), + }, (t) => [ + index('name').on(t.id), + primaryKey({ columns: [t.id], name: 'custom' }), + ]); + + const { indexes, primaryKeys } = getTableConfig(table); + + expect(indexes.length).toBe(1); + expect(primaryKeys.length).toBe(1); + }); + + test('define constraints as array inside third param', async (_ctx) => { + const table = sqliteTable('name', { + id: int(), + }, (t) => [ + index('name').on(t.id), + primaryKey({ columns: [t.id], name: 'custom' }), + ]); + + const { indexes, primaryKeys } = getTableConfig(table); + + expect(indexes.length).toBe(1); + expect(primaryKeys.length).toBe(1); + }); + test('aggregate function: count', async (ctx) => { const { db } = ctx.sqlite; const table = aggregateTable; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c4b13880..a267d4379 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -502,15 +502,12 @@ importers: drizzle-typebox: devDependencies: - '@rollup/plugin-terser': - specifier: ^0.4.1 - version: 0.4.1(rollup@3.27.2) '@rollup/plugin-typescript': specifier: ^11.1.0 version: 11.1.1(rollup@3.27.2)(tslib@2.8.1)(typescript@5.6.3) '@sinclair/typebox': - specifier: ^0.29.6 - version: 0.29.6 + specifier: ^0.34.8 + version: 0.34.10 '@types/node': specifier: ^18.15.10 version: 18.15.10 @@ -538,9 +535,6 @@ importers: drizzle-valibot: devDependencies: - '@rollup/plugin-terser': - specifier: ^0.4.1 - version: 0.4.1(rollup@3.27.2) '@rollup/plugin-typescript': specifier: ^11.1.0 version: 11.1.1(rollup@3.27.2)(tslib@2.8.1)(typescript@5.6.3) @@ -560,8 +554,8 @@ importers: specifier: ^3.20.7 version: 3.27.2 valibot: - specifier: ^0.30.0 - version: 0.30.0 + specifier: 1.0.0-beta.7 + version: 1.0.0-beta.7(typescript@5.6.3) vite-tsconfig-paths: specifier: ^4.3.2 version: 4.3.2(typescript@5.6.3)(vite@5.3.3(@types/node@18.15.10)(lightningcss@1.25.1)(terser@5.31.0)) @@ -574,9 +568,6 @@ importers: drizzle-zod: devDependencies: - '@rollup/plugin-terser': - specifier: ^0.4.1 - version: 0.4.1(rollup@3.20.7) '@rollup/plugin-typescript': specifier: ^11.1.0 version: 11.1.0(rollup@3.20.7)(tslib@2.8.1)(typescript@5.6.3) @@ -3182,9 +3173,6 @@ packages: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.3': - resolution: {integrity: sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==} - '@jridgewell/source-map@0.3.6': resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} @@ -3523,15 +3511,6 @@ packages: resolution: {integrity: sha512-lzD84av1ZQhYUS+jsGqJiCMaJO2dn9u+RTT9n9q6D3SaKVwWqv+7AoRKqBu19bkwyE+iFRl1ymr40QS90jVFYg==} engines: {node: '>=14.15'} - '@rollup/plugin-terser@0.4.1': - resolution: {integrity: sha512-aKS32sw5a7hy+fEXVy+5T95aDIwjpGHCTv833HXVtyKMDoVS7pBr5K3L9hEQoNqbJFjfANPrNpIXlTQ7is00eA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.x || ^3.x - peerDependenciesMeta: - rollup: - optional: true - '@rollup/plugin-terser@0.4.4': resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} engines: {node: '>=14.0.0'} @@ -3783,8 +3762,8 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - '@sinclair/typebox@0.29.6': - resolution: {integrity: sha512-aX5IFYWlMa7tQ8xZr3b2gtVReCvg7f3LEhjir/JAjX2bJCMVJA5tIPv30wTD4KDfcwMd7DDYY3hFDeGmOgtrZQ==} + '@sinclair/typebox@0.34.10': + resolution: {integrity: sha512-bJ3mIrYjEwenwwt+xAUq3GnOf1O4r2sApPzmfmF90XYMiKxjDzFSWSpWxqzSlQq3pCXuHP2UPxVPKeUFGJxb+A==} '@sindresorhus/is@4.6.0': resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} @@ -4642,11 +4621,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.8.2: - resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} - engines: {node: '>=0.4.0'} - hasBin: true - agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -9215,9 +9189,6 @@ packages: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - smob@0.0.6: - resolution: {integrity: sha512-V21+XeNni+tTyiST1MHsa84AQhT1aFZipzPpOFAVB8DkHzwJyjjAmt9bgwnuZiZWnIbMo2duE29wybxv/7HWUw==} - smob@1.5.0: resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==} @@ -9550,11 +9521,6 @@ packages: resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} engines: {node: '>=8'} - terser@5.17.1: - resolution: {integrity: sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw==} - engines: {node: '>=10'} - hasBin: true - terser@5.31.0: resolution: {integrity: sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==} engines: {node: '>=10'} @@ -10070,8 +10036,13 @@ packages: v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - valibot@0.30.0: - resolution: {integrity: sha512-5POBdbSkM+3nvJ6ZlyQHsggisfRtyT4tVTo1EIIShs6qCdXJnyWU5TJ68vr8iTg5zpOLjXLRiBqNx+9zwZz/rA==} + valibot@1.0.0-beta.7: + resolution: {integrity: sha512-8CsDu3tqyg7quEHMzCOYdQ/d9NlmVQKtd4AlFje6oJpvqo70EIZjSakKIeWltJyNAiUtdtLe0LAk4625gavoeQ==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true valid-url@1.0.9: resolution: {integrity: sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==} @@ -13796,11 +13767,6 @@ snapshots: '@jridgewell/set-array@1.2.1': {} - '@jridgewell/source-map@0.3.3': - dependencies: - '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.18 - '@jridgewell/source-map@0.3.6': dependencies: '@jridgewell/gen-mapping': 0.3.5 @@ -14345,22 +14311,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@rollup/plugin-terser@0.4.1(rollup@3.20.7)': - dependencies: - serialize-javascript: 6.0.1 - smob: 0.0.6 - terser: 5.17.1 - optionalDependencies: - rollup: 3.20.7 - - '@rollup/plugin-terser@0.4.1(rollup@3.27.2)': - dependencies: - serialize-javascript: 6.0.1 - smob: 0.0.6 - terser: 5.17.1 - optionalDependencies: - rollup: 3.27.2 - '@rollup/plugin-terser@0.4.4(rollup@4.27.3)': dependencies: serialize-javascript: 6.0.1 @@ -14537,7 +14487,7 @@ snapshots: '@sinclair/typebox@0.27.8': {} - '@sinclair/typebox@0.29.6': {} + '@sinclair/typebox@0.34.10': {} '@sindresorhus/is@4.6.0': {} @@ -15766,8 +15716,6 @@ snapshots: acorn@8.11.3: {} - acorn@8.8.2: {} - agent-base@6.0.2: dependencies: debug: 4.3.4 @@ -21036,8 +20984,6 @@ snapshots: smart-buffer@4.2.0: optional: true - smob@0.0.6: {} - smob@1.5.0: {} socks-proxy-agent@6.2.1: @@ -21412,13 +21358,6 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - terser@5.17.1: - dependencies: - '@jridgewell/source-map': 0.3.3 - acorn: 8.8.2 - commander: 2.20.3 - source-map-support: 0.5.21 - terser@5.31.0: dependencies: '@jridgewell/source-map': 0.3.6 @@ -21904,7 +21843,9 @@ snapshots: v8-compile-cache-lib@3.0.1: {} - valibot@0.30.0: {} + valibot@1.0.0-beta.7(typescript@5.6.3): + optionalDependencies: + typescript: 5.6.3 valid-url@1.0.9: {}