feat: fork drizzle-typebox as @alkdev/drizzlebox

- Rebrand package from drizzle-typebox to @alkdev/drizzlebox
- Replace @sinclair/typebox with @alkdev/typebox in all source and test files
- Replace @sinclair/typebox with @alkdev/typebox in rollup externals
- Convert tsconfig.json from monorepo extends to standalone config
- Fix build script monorepo remnant (dist.new -> dist)
- Add missing devDependencies (recast, tsx, typescript, resolve-tspaths)
- Replace monorepo link dependency for drizzle-orm with ^0.38.4
- Add .gitignore, LICENSE (Apache-2.0 with attribution), and README
- Initialize git repo with remote at git.alk.dev:alkdev/drizzlebox
This commit is contained in:
2026-04-25 09:45:14 +00:00
commit d0a0de766b
24 changed files with 3190 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
dist/
*.tgz
.cache/

27
LICENSE Normal file
View File

@@ -0,0 +1,27 @@
Copyright 2024 Drizzle Team
Copyright 2025 alkdev
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
---
NOTICE
This project is a fork of drizzle-typebox, originally part of the
drizzle-orm monorepo by the Drizzle Team (https://github.com/drizzle-team/drizzle-orm).
The original work is Copyright 2024 Drizzle Team and is licensed under the
Apache License, Version 2.0.
Modifications (rebranding to @alkdev/drizzlebox, dependency migration to
@alkdev/typebox, and standalone packaging) are Copyright 2025 alkdev.

74
README.md Normal file
View File

@@ -0,0 +1,74 @@
# @alkdev/drizzlebox
Generate [TypeBox](https://github.com/alkdev/typebox) schemas from [Drizzle ORM](https://orm.drizzle.team) schemas.
This is a fork of [drizzle-typebox](https://github.com/drizzle-team/drizzle-orm/tree/main/drizzle-typebox) by the Drizzle Team, adapted for use with `@alkdev/typebox` (a maintained fork of `@sinclair/typebox`).
## Install
```bash
npm install @alkdev/drizzlebox
npm install @alkdev/typebox
npm install drizzle-orm
```
## Features
- Create select schemas for tables, views, and enums
- Create insert and update schemas for tables
- Supports all dialects: PostgreSQL, MySQL, and SQLite
- Custom TypeBox instance support via `createSchemaFactory`
## Usage
```ts
import { pgEnum, pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
import { createInsertSchema, createSelectSchema, createUpdateSchema } from '@alkdev/drizzlebox';
import { Type } from '@alkdev/typebox';
import { Value } from '@alkdev/typebox/value';
const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull(),
role: text('role', { enum: ['admin', 'user'] }).notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
// Schema for inserting a user
const insertUserSchema = createInsertSchema(users);
// Schema for updating a user
const updateUserSchema = createUpdateSchema(users);
// Schema for selecting a user
const selectUserSchema = createSelectSchema(users);
// Overriding fields
const insertUserSchema = createInsertSchema(users, {
role: Type.String(),
});
// Refining fields
const insertUserSchema = createInsertSchema(users, {
id: (schema) => Type.Number({ ...schema, minimum: 0 }),
role: Type.String(),
});
// Validation
const isUserValid: boolean = Value.Check(insertUserSchema, {
name: 'John Doe',
email: 'johndoe@test.com',
role: 'admin',
});
```
## Differences from drizzle-typebox
- Uses `@alkdev/typebox` instead of `@sinclair/typebox`
- Standalone package (no monorepo dependency)
- Published as `@alkdev/drizzlebox` on npm
## Attribution
Based on [drizzle-typebox](https://github.com/drizzle-team/drizzle-orm/tree/main/drizzle-typebox) by the Drizzle Team, licensed under Apache-2.0.

75
package.json Normal file
View File

@@ -0,0 +1,75 @@
{
"name": "@alkdev/drizzlebox",
"version": "0.1.0",
"description": "Generate Typebox schemas from Drizzle ORM schemas — fork of drizzle-typebox with @alkdev/typebox support",
"type": "module",
"scripts": {
"build": "tsx scripts/build.ts",
"b": "pnpm build",
"test:types": "cd tests && tsc",
"pack": "(cd dist && npm pack --pack-destination ..) && rm -f package.tgz && mv *.tgz package.tgz",
"test": "vitest run"
},
"exports": {
".": {
"import": {
"types": "./index.d.mts",
"default": "./index.mjs"
},
"require": {
"types": "./index.d.cjs",
"default": "./index.cjs"
},
"types": "./index.d.ts",
"default": "./index.mjs"
}
},
"main": "./index.cjs",
"module": "./index.mjs",
"types": "./index.d.ts",
"repository": {
"type": "git",
"url": "git+https://git.alk.dev/alkdev/drizzlebox.git"
},
"keywords": [
"typebox",
"drizzlebox",
"validate",
"validation",
"schema",
"drizzle",
"orm",
"pg",
"mysql",
"postgresql",
"postgres",
"sqlite",
"database",
"sql",
"typescript",
"ts"
],
"author": "Based on drizzle-typebox by Drizzle Team; fork maintained by alkdev",
"license": "Apache-2.0",
"peerDependencies": {
"@alkdev/typebox": ">=0.34.49",
"drizzle-orm": ">=0.36.0"
},
"devDependencies": {
"@alkdev/typebox": "^0.34.49",
"@rollup/plugin-typescript": "^11.1.0",
"@types/node": "^18.15.10",
"@types/recast": "^0.23.0",
"cpy": "^10.1.0",
"drizzle-orm": "^0.38.4",
"recast": "^0.23.0",
"resolve-tspaths": "^0.8.23",
"rimraf": "^5.0.0",
"rollup": "^3.20.7",
"tsx": "^4.0.0",
"typescript": "^5.4.0",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0",
"zx": "^7.2.2"
}
}

33
rollup.config.ts Normal file
View File

@@ -0,0 +1,33 @@
import typescript from '@rollup/plugin-typescript';
import { defineConfig } from 'rollup';
export default defineConfig([
{
input: 'src/index.ts',
output: [
{
format: 'esm',
dir: 'dist',
entryFileNames: '[name].mjs',
chunkFileNames: '[name]-[hash].mjs',
sourcemap: true,
},
{
format: 'cjs',
dir: 'dist',
entryFileNames: '[name].cjs',
chunkFileNames: '[name]-[hash].cjs',
sourcemap: true,
},
],
external: [
/^drizzle-orm\/?/,
'@alkdev/typebox',
],
plugins: [
typescript({
tsconfig: 'tsconfig.build.json',
}),
],
},
]);

16
scripts/build.ts Normal file
View File

@@ -0,0 +1,16 @@
#!/usr/bin/env -S pnpm tsx
import 'zx/globals';
import cpy from 'cpy';
await fs.remove('dist');
await $`rollup --config rollup.config.ts --configPlugin typescript`;
await $`resolve-tspaths`;
await fs.copy('README.md', 'dist/README.md');
await cpy('dist/**/*.d.ts', 'dist', {
rename: (basename) => basename.replace(/\.d\.ts$/, '.d.mts'),
});
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`;

136
scripts/fix-imports.ts Executable file
View File

@@ -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', 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);
}));

316
src/column.ts Normal file
View File

@@ -0,0 +1,316 @@
import { Kind, Type as t, TypeRegistry } from '@alkdev/typebox';
import type { StringOptions, TSchema, Type as typebox } from '@alkdev/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 SingleStoreBigInt53,
SingleStoreChar,
type SingleStoreDouble,
type SingleStoreFloat,
type SingleStoreInt,
type SingleStoreMediumInt,
type SingleStoreReal,
type SingleStoreSerial,
type SingleStoreSmallInt,
SingleStoreText,
type SingleStoreTinyInt,
SingleStoreVarChar,
SingleStoreYear,
} from 'drizzle-orm/singlestore-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]));
}
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<PgGeometry<any> | PgPointTuple<any>>(column, ['PgGeometry', 'PgPointTuple'])) {
schema = t.Tuple([t.Number(), t.Number()]);
} else if (
isColumnType<PgPointObject<any> | PgGeometryObject<any>>(column, ['PgGeometryObject', 'PgPointObject'])
) {
schema = t.Object({ x: t.Number(), y: t.Number() });
} else if (isColumnType<PgHalfVector<any> | PgVector<any>>(column, ['PgHalfVector', 'PgVector'])) {
schema = t.Array(
t.Number(),
column.dimensions
? {
minItems: column.dimensions,
maxItems: column.dimensions,
}
: undefined,
);
} else if (isColumnType<PgLineTuple<any>>(column, ['PgLine'])) {
schema = t.Tuple([t.Number(), t.Number(), t.Number()]);
} else if (isColumnType<PgLineABC<any>>(column, ['PgLineABC'])) {
schema = t.Object({
a: t.Number(),
b: t.Number(),
c: t.Number(),
});
} // Handle other types
else if (isColumnType<PgArray<any, any>>(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<MySqlTinyInt<any> | SingleStoreTinyInt<any>>(column, ['MySqlTinyInt', 'SingleStoreTinyInt'])) {
min = unsigned ? 0 : CONSTANTS.INT8_MIN;
max = unsigned ? CONSTANTS.INT8_UNSIGNED_MAX : CONSTANTS.INT8_MAX;
integer = true;
} else if (
isColumnType<PgSmallInt<any> | PgSmallSerial<any> | MySqlSmallInt<any> | SingleStoreSmallInt<any>>(column, [
'PgSmallInt',
'PgSmallSerial',
'MySqlSmallInt',
'SingleStoreSmallInt',
])
) {
min = unsigned ? 0 : CONSTANTS.INT16_MIN;
max = unsigned ? CONSTANTS.INT16_UNSIGNED_MAX : CONSTANTS.INT16_MAX;
integer = true;
} else if (
isColumnType<
PgReal<any> | MySqlFloat<any> | MySqlMediumInt<any> | SingleStoreFloat<any> | SingleStoreMediumInt<any>
>(column, [
'PgReal',
'MySqlFloat',
'MySqlMediumInt',
'SingleStoreFloat',
'SingleStoreMediumInt',
])
) {
min = unsigned ? 0 : CONSTANTS.INT24_MIN;
max = unsigned ? CONSTANTS.INT24_UNSIGNED_MAX : CONSTANTS.INT24_MAX;
integer = isColumnType(column, ['MySqlMediumInt', 'SingleStoreMediumInt']);
} else if (
isColumnType<PgInteger<any> | PgSerial<any> | MySqlInt<any> | SingleStoreInt<any>>(column, [
'PgInteger',
'PgSerial',
'MySqlInt',
'SingleStoreInt',
])
) {
min = unsigned ? 0 : CONSTANTS.INT32_MIN;
max = unsigned ? CONSTANTS.INT32_UNSIGNED_MAX : CONSTANTS.INT32_MAX;
integer = true;
} else if (
isColumnType<
| PgDoublePrecision<any>
| MySqlReal<any>
| MySqlDouble<any>
| SingleStoreReal<any>
| SingleStoreDouble<any>
| SQLiteReal<any>
>(column, [
'PgDoublePrecision',
'MySqlReal',
'MySqlDouble',
'SingleStoreReal',
'SingleStoreDouble',
'SQLiteReal',
])
) {
min = unsigned ? 0 : CONSTANTS.INT48_MIN;
max = unsigned ? CONSTANTS.INT48_UNSIGNED_MAX : CONSTANTS.INT48_MAX;
} else if (
isColumnType<
| PgBigInt53<any>
| PgBigSerial53<any>
| MySqlBigInt53<any>
| MySqlSerial<any>
| SingleStoreBigInt53<any>
| SingleStoreSerial<any>
| SQLiteInteger<any>
>(
column,
[
'PgBigInt53',
'PgBigSerial53',
'MySqlBigInt53',
'MySqlSerial',
'SingleStoreBigInt53',
'SingleStoreSerial',
'SQLiteInteger',
],
)
) {
unsigned = unsigned || isColumnType(column, ['MySqlSerial', 'SingleStoreSerial']);
min = unsigned ? 0 : Number.MIN_SAFE_INTEGER;
max = Number.MAX_SAFE_INTEGER;
integer = true;
} else if (isColumnType<MySqlYear<any> | SingleStoreYear<any>>(column, ['MySqlYear', 'SingleStoreYear'])) {
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<PgUUID<ColumnBaseConfig<'string', 'PgUUID'>>>(column, ['PgUUID'])) {
return t.String({ format: 'uuid' });
} else if (
isColumnType<PgBinaryVector<ColumnBaseConfig<'string', 'PgBinaryVector'> & { dimensions: number }>>(column, [
'PgBinaryVector',
])
) {
return t.RegExp(/^[01]+$/, column.dimensions ? { maxLength: column.dimensions } : undefined);
}
let max: number | undefined;
let fixed = false;
if (isColumnType<PgVarchar<any> | SQLiteText<any>>(column, ['PgVarchar', 'SQLiteText'])) {
max = column.length;
} else if (
isColumnType<MySqlVarChar<any> | SingleStoreVarChar<any>>(column, ['MySqlVarChar', 'SingleStoreVarChar'])
) {
max = column.length ?? CONSTANTS.INT16_UNSIGNED_MAX;
} else if (isColumnType<MySqlText<any> | SingleStoreText<any>>(column, ['MySqlText', 'SingleStoreText'])) {
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<PgChar<any> | MySqlChar<any> | SingleStoreChar<any>>(column, [
'PgChar',
'MySqlChar',
'SingleStoreChar',
])
) {
max = column.length;
fixed = true;
}
const options: Partial<StringOptions> = {};
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);
}

107
src/column.types.ts Normal file
View File

@@ -0,0 +1,107 @@
import type * as t from '@alkdev/typebox';
import type { Assume, Column } from 'drizzle-orm';
import type { ArrayHasAtLeastOneValue, BufferSchema, ColumnIsGeneratedAlwaysAs, IsNever, JsonSchema } from './utils.ts';
export type GetEnumValuesFromColumn<TColumn extends Column> = TColumn['_'] extends { enumValues: [string, ...string[]] }
? TColumn['_']['enumValues']
: undefined;
export type GetBaseColumn<TColumn extends Column> = TColumn['_'] extends { baseColumn: Column | never | undefined }
? IsNever<TColumn['_']['baseColumn']> extends false ? TColumn['_']['baseColumn']
: undefined
: undefined;
export type EnumValuesToEnum<TEnumValues extends [string, ...string[]]> = { [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'
| 'SingleStoreTinyInt'
| 'PgSmallInt'
| 'PgSmallSerial'
| 'MySqlSmallInt'
| 'MySqlMediumInt'
| 'SingleStoreSmallInt'
| 'SingleStoreMediumInt'
| 'PgInteger'
| 'PgSerial'
| 'MySqlInt'
| 'SingleStoreInt'
| 'PgBigInt53'
| 'PgBigSerial53'
| 'MySqlBigInt53'
| 'MySqlSerial'
| 'SingleStoreBigInt53'
| 'SingleStoreSerial'
| 'SQLiteInteger'
| 'MySqlYear'
| 'SingleStoreYear' ? t.TInteger
: TColumnType extends 'PgBinaryVector' ? t.TRegExp
: TBaseColumn extends Column ? t.TArray<
GetTypeboxType<
TBaseColumn['_']['data'],
TBaseColumn['_']['dataType'],
TBaseColumn['_']['columnType'],
GetEnumValuesFromColumn<TBaseColumn>,
GetBaseColumn<TBaseColumn>
>
>
: ArrayHasAtLeastOneValue<TEnumValues> extends true
? t.TEnum<EnumValuesToEnum<Assume<TEnumValues, [string, ...string[]]>>>
: TData extends infer TTuple extends [any, ...any[]] ? t.TTuple<
Assume<{ [K in keyof TTuple]: GetTypeboxType<TTuple[K], string, string, undefined, undefined> }, [any, ...any[]]>
>
: TData extends Date ? t.TDate
: TData extends Buffer ? BufferSchema
: TDataType extends 'array'
? t.TArray<GetTypeboxType<Assume<TData, any[]>[number], string, string, undefined, undefined>>
: TData extends infer TDict extends Record<string, any>
? t.TObject<{ [K in keyof TDict]: GetTypeboxType<TDict[K], string, string, undefined, undefined> }>
: 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<TColumn> extends true ? never
: TColumn['_']['notNull'] extends true ? TColumn['_']['hasDefault'] extends true ? t.TOptional<TSchema>
: TSchema
: t.TOptional<t.Union<[TSchema, t.TNull]>>;
type HandleUpdateColumn<
TSchema extends t.TSchema,
TColumn extends Column,
> = ColumnIsGeneratedAlwaysAs<TColumn> extends true ? never
: TColumn['_']['notNull'] extends true ? t.TOptional<TSchema>
: t.TOptional<t.Union<[TSchema, t.TNull]>>;
export type HandleColumn<
TType extends 'select' | 'insert' | 'update',
TColumn extends Column,
> = GetTypeboxType<
TColumn['_']['data'],
TColumn['_']['dataType'],
TColumn['_']['columnType'],
GetEnumValuesFromColumn<TColumn>,
GetBaseColumn<TColumn>
> extends infer TSchema extends t.TSchema ? TSchema extends t.TAny ? t.TAny
: TType extends 'select' ? HandleSelectColumn<TSchema, TColumn>
: TType extends 'insert' ? HandleInsertColumn<TSchema, TColumn>
: TType extends 'update' ? HandleUpdateColumn<TSchema, TColumn>
: TSchema
: t.TAny;

20
src/constants.ts Normal file
View File

@@ -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,
};

2
src/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './schema.ts';
export * from './schema.types.ts';

144
src/schema.ts Normal file
View File

@@ -0,0 +1,144 @@
import { Type as t } from '@alkdev/typebox';
import type { TSchema } from '@alkdev/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<string, any>,
refinements: Record<string, any>,
conditions: Conditions,
factory?: CreateSchemaFactoryOptions,
): TSchema {
const columnSchemas: Record<string, TSchema> = {};
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<any>, 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<string, any>,
) => {
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<string, any>,
) => {
const columns = getColumns(entity);
return handleColumns(columns, refine ?? {}, insertConditions) as any;
};
export const createUpdateSchema: CreateUpdateSchema = (
entity: Table,
refine?: Record<string, any>,
) => {
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<string, any>,
) => {
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<string, any>,
) => {
const columns = getColumns(entity);
return handleColumns(columns, refine ?? {}, insertConditions, options) as any;
};
const createUpdateSchema: CreateUpdateSchema = (
entity: Table,
refine?: Record<string, any>,
) => {
const columns = getColumns(entity);
return handleColumns(columns, refine ?? {}, updateConditions, options) as any;
};
return { createSelectSchema, createInsertSchema, createUpdateSchema };
}

View File

@@ -0,0 +1,94 @@
import type * as t from '@alkdev/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<string, any>,
> = Simplify<
RemoveNever<
{
[K in keyof TColumns]: TColumns[K] extends infer TColumn extends Column ? GetTypeboxType<
TColumn['_']['data'],
TColumn['_']['dataType'],
TColumn['_']['columnType'],
GetEnumValuesFromColumn<TColumn>,
GetBaseColumn<TColumn>
> extends infer TSchema extends t.TSchema ? TSchema
: t.TAny
: TColumns[K] extends infer TObject extends SelectedFieldsFlat<Column> | Table | View
? BuildRefineColumns<GetSelection<TObject>>
: TColumns[K];
}
>
>;
export type BuildRefine<
TColumns extends Record<string, any>,
> = BuildRefineColumns<TColumns> extends infer TBuildColumns ? {
[K in keyof TBuildColumns]?: TBuildColumns[K] extends t.TSchema
? ((schema: TBuildColumns[K]) => t.TSchema) | t.TSchema
: TBuildColumns[K] extends Record<string, any> ? Simplify<BuildRefine<TBuildColumns[K]>>
: never;
}
: never;
type HandleRefinement<
TType extends 'select' | 'insert' | 'update',
TRefinement extends t.TSchema | ((schema: t.TSchema) => t.TSchema),
TColumn extends Column,
> = TRefinement extends (schema: any) => t.TSchema ? (TColumn['_']['notNull'] extends true ? ReturnType<TRefinement>
: t.TUnion<[ReturnType<TRefinement>, t.TNull]>) extends infer TSchema
? TType extends 'update' ? t.TOptional<Assume<TSchema, t.TSchema>> : TSchema
: t.TSchema
: TRefinement;
type IsRefinementDefined<TRefinements, TKey extends string> = TKey extends keyof TRefinements
? TRefinements[TKey] extends t.TSchema | ((schema: any) => any) ? true
: false
: false;
export type BuildSchema<
TType extends 'select' | 'insert' | 'update',
TColumns extends Record<string, any>,
TRefinements extends Record<string, any> | undefined,
> = t.TObject<
Simplify<
RemoveNever<
{
[K in keyof TColumns]: TColumns[K] extends infer TColumn extends Column
? TRefinements extends object
? IsRefinementDefined<TRefinements, Assume<K, string>> extends true
? HandleRefinement<TType, TRefinements[Assume<K, keyof TRefinements>], TColumn>
: HandleColumn<TType, TColumn>
: HandleColumn<TType, TColumn>
: TColumns[K] extends infer TObject extends SelectedFieldsFlat<Column> | Table | View ? BuildSchema<
TType,
GetSelection<TObject>,
TRefinements extends object
? TRefinements[Assume<K, keyof TRefinements>] extends infer TNestedRefinements extends object
? TNestedRefinements
: undefined
: undefined
>
: t.TAny;
}
>
>
>;
export type NoUnknownKeys<
TRefinement extends Record<string, any>,
TCompare extends Record<string, any>,
> = {
[K in keyof TRefinement]: K extends keyof TCompare ? TRefinement[K] extends t.TSchema ? TRefinement[K]
: TRefinement[K] extends Record<string, t.TSchema> ? NoUnknownKeys<TRefinement[K], TCompare[K]>
: TRefinement[K]
: DrizzleTypeError<`Found unknown key in refinement: "${K & string}"`>;
};

53
src/schema.types.ts Normal file
View File

@@ -0,0 +1,53 @@
import type * as t from '@alkdev/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 {
<TTable extends Table>(table: TTable): BuildSchema<'select', TTable['_']['columns'], undefined>;
<
TTable extends Table,
TRefine extends BuildRefine<TTable['_']['columns']>,
>(
table: TTable,
refine?: NoUnknownKeys<TRefine, TTable['$inferSelect']>,
): BuildSchema<'select', TTable['_']['columns'], TRefine>;
<TView extends View>(view: TView): BuildSchema<'select', TView['_']['selectedFields'], undefined>;
<
TView extends View,
TRefine extends BuildRefine<TView['_']['selectedFields']>,
>(
view: TView,
refine: NoUnknownKeys<TRefine, TView['$inferSelect']>,
): BuildSchema<'select', TView['_']['selectedFields'], TRefine>;
<TEnum extends PgEnum<any>>(enum_: TEnum): t.TEnum<EnumValuesToEnum<TEnum['enumValues']>>;
}
export interface CreateInsertSchema {
<TTable extends Table>(table: TTable): BuildSchema<'insert', TTable['_']['columns'], undefined>;
<
TTable extends Table,
TRefine extends BuildRefine<Pick<TTable['_']['columns'], keyof TTable['$inferInsert']>>,
>(
table: TTable,
refine?: NoUnknownKeys<TRefine, TTable['$inferInsert']>,
): BuildSchema<'insert', TTable['_']['columns'], TRefine>;
}
export interface CreateUpdateSchema {
<TTable extends Table>(table: TTable): BuildSchema<'update', TTable['_']['columns'], undefined>;
<
TTable extends Table,
TRefine extends BuildRefine<Pick<TTable['_']['columns'], keyof TTable['$inferInsert']>>,
>(
table: TTable,
refine?: TRefine,
): BuildSchema<'update', TTable['_']['columns'], TRefine>;
}
export interface CreateSchemaFactoryOptions {
typeboxInstance?: any;
}

50
src/utils.ts Normal file
View File

@@ -0,0 +1,50 @@
import type { Kind, Static, TSchema } from '@alkdev/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<T extends Column>(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<typeof literalSchema>;
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> = [T] extends [never] ? true : false;
export type ArrayHasAtLeastOneValue<TEnum extends [any, ...any[]] | undefined> = TEnum extends [infer TString, ...any[]]
? TString extends `${infer TLiteral}` ? TLiteral extends any ? true
: false
: false
: false;
export type ColumnIsGeneratedAlwaysAs<TColumn extends Column> = 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<T> = {
[K in keyof T as T[K] extends never ? never : K]: T[K];
};
export type GetSelection<T extends SelectedFieldsFlat<Column> | Table | View> = T extends Table ? T['_']['columns']
: T extends View ? T['_']['selectedFields']
: T;

495
tests/mysql.test.ts Normal file
View File

@@ -0,0 +1,495 @@
import { Type as t } from '@alkdev/typebox';
import { type Equal, sql } from 'drizzle-orm';
import { customType, int, mysqlSchema, mysqlTable, mysqlView, serial, text } from 'drizzle-orm/mysql-core';
import { test } from 'vitest';
import { jsonSchema } from '~/column.ts';
import { CONSTANTS } from '~/constants.ts';
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 result = createSelectSchema(table);
const expected = t.Object({ id: serialNumberModeSchema, name: textSchema });
expectSchemaShape(tc, expected).from(result);
Expect<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
test('table - insert', (tc) => {
const table = mysqlTable('test', {
id: serial().primaryKey(),
name: text().notNull(),
age: int(),
});
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<Equal<typeof result, typeof expected>>();
});
test('table - update', (tc) => {
const table = mysqlTable('test', {
id: serial().primaryKey(),
name: text().notNull(),
age: int(),
});
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<Equal<typeof result, typeof expected>>();
});
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));
const result = createSelectSchema(view);
const expected = t.Object({ id: serialNumberModeSchema, age: t.Any() });
expectSchemaShape(tc, expected).from(result);
Expect<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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)
);
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<Equal<typeof result, typeof expected>>();
});
test('nullability - select', (tc) => {
const table = mysqlTable('test', {
c1: int(),
c2: int().notNull(),
c3: int().default(1),
c4: int().notNull().default(1),
});
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<Equal<typeof result, typeof expected>>();
});
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 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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
test('refine table - select with custom data type', (tc) => {
const customText = customType({ dataType: () => 'text' });
const table = mysqlTable('test', {
c1: int(),
c2: int().notNull(),
c3: int().notNull(),
c4: customText(),
});
const customTextSchema = t.String({ minLength: 1, maxLength: 100 });
const result = createSelectSchema(table, {
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
c3: t.Integer({ minimum: 1, maximum: 10 }),
c4: customTextSchema,
});
const expected = t.Object({
c1: t.Union([intSchema, t.Null()]),
c2: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }),
c3: t.Integer({ minimum: 1, maximum: 10 }),
c4: customTextSchema,
});
expectSchemaShape(tc, expected).from(result);
Expect<Equal<typeof result, typeof expected>>();
});
test('refine table - insert', (tc) => {
const table = mysqlTable('test', {
c1: int(),
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: CONSTANTS.INT32_MIN, maximum: 1000 }),
c3: t.Integer({ minimum: 1, maximum: 10 }),
});
expectSchemaShape(tc, expected).from(result);
Expect<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
/* 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() });
}

540
tests/pg.test.ts Normal file
View File

@@ -0,0 +1,540 @@
import { Type as t } from '@alkdev/typebox';
import { type Equal, sql } from 'drizzle-orm';
import {
customType,
integer,
pgEnum,
pgMaterializedView,
pgSchema,
pgTable,
pgView,
serial,
text,
} from 'drizzle-orm/pg-core';
import { test } from 'vitest';
import { jsonSchema } from '~/column.ts';
import { CONSTANTS } from '~/constants.ts';
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
test('refine table - select with custom data type', (tc) => {
const customText = customType({ dataType: () => 'text' });
const table = pgTable('test', {
c1: integer(),
c2: integer().notNull(),
c3: integer().notNull(),
c4: customText(),
});
const customTextSchema = t.String({ minLength: 1, maxLength: 100 });
const result = createSelectSchema(table, {
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
c3: t.Integer({ minimum: 1, maximum: 10 }),
c4: customTextSchema,
});
const expected = t.Object({
c1: t.Union([integerSchema, t.Null()]),
c2: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }),
c3: t.Integer({ minimum: 1, maximum: 10 }),
c4: customTextSchema,
});
expectSchemaShape(tc, expected).from(result);
Expect<Equal<typeof result, typeof expected>>();
});
test('refine table - insert', (tc) => {
const table = pgTable('test', {
c1: integer(),
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
/* 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() });
}

497
tests/singlestore.test.ts Normal file
View File

@@ -0,0 +1,497 @@
import { Type as t } from '@alkdev/typebox';
import { type Equal, sql } from 'drizzle-orm';
import { customType, int, serial, singlestoreSchema, singlestoreTable, text } from 'drizzle-orm/singlestore-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 = singlestoreTable('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<Equal<typeof result, typeof expected>>();
});
test('table in schema - select', (tc) => {
const schema = singlestoreSchema('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<Equal<typeof result, typeof expected>>();
});
test('table - insert', (tc) => {
const table = singlestoreTable('test', {
id: serial().primaryKey(),
name: text().notNull(),
age: int(),
});
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<Equal<typeof result, typeof expected>>();
});
test('table - update', (tc) => {
const table = singlestoreTable('test', {
id: serial().primaryKey(),
name: text().notNull(),
age: int(),
});
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<Equal<typeof result, typeof expected>>();
});
// TODO: SingleStore doesn't support views yet. Add these tests when they're added
// test('view qb - select', (tc) => {
// const table = singlestoreTable('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 = t.Object({ id: serialNumberModeSchema, age: t.Any() });
// expectSchemaShape(tc, expected).from(result);
// Expect<Equal<typeof result, typeof expected>>();
// });
// 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<Equal<typeof result, typeof expected>>();
// });
// test('view with nested fields - select', (tc) => {
// const table = singlestoreTable('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 = 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<Equal<typeof result, typeof expected>>();
// });
test('nullability - select', (tc) => {
const table = singlestoreTable('test', {
c1: int(),
c2: int().notNull(),
c3: int().default(1),
c4: int().notNull().default(1),
});
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<Equal<typeof result, typeof expected>>();
});
test('nullability - insert', (tc) => {
const table = singlestoreTable('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<Equal<typeof result, typeof expected>>();
});
test('nullability - update', (tc) => {
const table = singlestoreTable('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<Equal<typeof result, typeof expected>>();
});
test('refine table - select', (tc) => {
const table = singlestoreTable('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<Equal<typeof result, typeof expected>>();
});
test('refine table - select with custom data type', (tc) => {
const customText = customType({ dataType: () => 'text' });
const table = singlestoreTable('test', {
c1: int(),
c2: int().notNull(),
c3: int().notNull(),
c4: customText(),
});
const customTextSchema = t.String({ minLength: 1, maxLength: 100 });
const result = createSelectSchema(table, {
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
c3: t.Integer({ minimum: 1, maximum: 10 }),
c4: customTextSchema,
});
const expected = t.Object({
c1: t.Union([intSchema, t.Null()]),
c2: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }),
c3: t.Integer({ minimum: 1, maximum: 10 }),
c4: customTextSchema,
});
expectSchemaShape(tc, expected).from(result);
Expect<Equal<typeof result, typeof expected>>();
});
test('refine table - insert', (tc) => {
const table = singlestoreTable('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: CONSTANTS.INT32_MIN, maximum: 1000 }),
c3: t.Integer({ minimum: 1, maximum: 10 }),
});
expectSchemaShape(tc, expected).from(result);
Expect<Equal<typeof result, typeof expected>>();
});
test('refine table - update', (tc) => {
const table = singlestoreTable('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<Equal<typeof result, typeof expected>>();
});
// test('refine view - select', (tc) => {
// const table = singlestoreTable('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<Equal<typeof result, typeof expected>>();
// });
test('all data types', (tc) => {
const table = singlestoreTable('test', ({
bigint,
binary,
boolean,
char,
date,
datetime,
decimal,
double,
float,
int,
json,
mediumint,
singlestoreEnum,
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: singlestoreEnum('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<Equal<typeof result, typeof expected>>();
});
/* Disallow unknown keys in table refinement - select */ {
const table = singlestoreTable('test', { id: int() });
// @ts-expect-error
createSelectSchema(table, { unknown: t.String() });
}
/* Disallow unknown keys in table refinement - insert */ {
const table = singlestoreTable('test', { id: int() });
// @ts-expect-error
createInsertSchema(table, { unknown: t.String() });
}
/* Disallow unknown keys in table refinement - update */ {
const table = singlestoreTable('test', { id: int() });
// @ts-expect-error
createUpdateSchema(table, { unknown: t.String() });
}
// /* Disallow unknown keys in view qb - select */ {
// const table = singlestoreTable('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() });
// }

389
tests/sqlite.test.ts Normal file
View File

@@ -0,0 +1,389 @@
import { Type as t } from '@alkdev/typebox';
import { type Equal, sql } from 'drizzle-orm';
import { customType, int, sqliteTable, sqliteView, text } from 'drizzle-orm/sqlite-core';
import { test } from 'vitest';
import { bufferSchema, jsonSchema } from '~/column.ts';
import { CONSTANTS } from '~/constants.ts';
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 result = createSelectSchema(table);
const expected = t.Object({ id: intSchema, name: textSchema });
expectSchemaShape(tc, expected).from(result);
Expect<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
test('table - update', (tc) => {
const table = sqliteTable('test', {
id: int().primaryKey({ autoIncrement: true }),
name: text().notNull(),
age: int(),
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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)
);
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<Equal<typeof result, typeof expected>>();
});
test('nullability - select', (tc) => {
const table = sqliteTable('test', {
c1: int(),
c2: int().notNull(),
c3: int().default(1),
c4: int().notNull().default(1),
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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),
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
test('refine table - select with custom data type', (tc) => {
const customText = customType({ dataType: () => 'text' });
const table = sqliteTable('test', {
c1: int(),
c2: int().notNull(),
c3: int().notNull(),
c4: customText(),
});
const customTextSchema = t.String({ minLength: 1, maxLength: 100 });
const result = createSelectSchema(table, {
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
c3: t.Integer({ minimum: 1, maximum: 10 }),
c4: customTextSchema,
});
const expected = t.Object({
c1: t.Union([intSchema, t.Null()]),
c2: t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: 1000 }),
c3: t.Integer({ minimum: 1, maximum: 10 }),
c4: customTextSchema,
});
expectSchemaShape(tc, expected).from(result);
Expect<Equal<typeof result, typeof expected>>();
});
test('refine table - insert', (tc) => {
const table = sqliteTable('test', {
c1: int(),
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
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<Equal<typeof result, typeof expected>>();
});
/* 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() });
}

11
tests/tsconfig.json Normal file
View File

@@ -0,0 +1,11 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"noEmit": true,
"rootDir": "..",
"outDir": "./.cache"
},
"include": [".", "../src"]
}

34
tests/utils.ts Normal file
View File

@@ -0,0 +1,34 @@
import type * as t from '@alkdev/typebox';
import { expect, type TaskContext } from 'vitest';
function removeKeysFromObject(obj: Record<string, any>, keys: string[]) {
for (const key of keys) {
delete obj[key];
}
return obj;
}
export function expectSchemaShape<T extends t.TObject>(t: TaskContext, expected: T) {
return {
from(actual: T) {
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.properties)) {
expect(removeKeysFromObject(actual.properties[key]!, keys)).toStrictEqual(
removeKeysFromObject(expected.properties[key]!, keys),
);
}
},
};
}
export function expectEnumValues<T extends t.TEnum<any>>(t: TaskContext, expected: T) {
return {
from(actual: T) {
expect(actual.anyOf).toStrictEqual(expected.anyOf);
},
};
}
export function Expect<_ extends true>() {}

7
tsconfig.build.json Normal file
View File

@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "src"
},
"include": ["src"]
}

41
tsconfig.json Normal file
View File

@@ -0,0 +1,41 @@
{
"compilerOptions": {
"isolatedModules": true,
"target": "esnext",
"module": "esnext",
"moduleResolution": "bundler",
"lib": ["es2020", "es2018", "es2017", "es7", "es6", "es5"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"allowJs": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"allowUnusedLabels": false,
"allowUnreachableCode": false,
"skipLibCheck": true,
"noErrorTruncation": true,
"noEmit": true,
"outDir": "dist",
"baseUrl": ".",
"paths": {
"~/*": ["src/*"]
}
},
"include": ["src", "*.ts"]
}

25
vitest.config.ts Normal file
View File

@@ -0,0 +1,25 @@
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
include: [
'tests/**/*.test.ts',
],
exclude: [
'tests/bun/**/*',
],
typecheck: {
tsconfig: 'tsconfig.json',
},
testTimeout: 100000,
hookTimeout: 100000,
isolate: false,
poolOptions: {
threads: {
singleThread: true,
},
},
},
plugins: [tsconfigPaths()],
});