chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:10:05 +08:00
commit d37d8d293b
1388 changed files with 484182 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
`drizzle-typebox` is a plugin for [Drizzle ORM](https://github.com/drizzle-team/drizzle-orm) that allows you to generate [@sinclair/typebox](https://github.com/sinclairzx81/typebox) schemas from Drizzle ORM schemas.
**Features**
- Create a select schema for tables, views and enums.
- Create insert and update schemas for tables.
- Supports all dialects: PostgreSQL, MySQL and SQLite.
# Usage
```ts
import { pgEnum, pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
import { createInsertSchema, createSelectSchema } from 'drizzle-typebox';
import { Type } from '@sinclair/typebox';
import { Value } from '@sinclair/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 - can be used to validate API requests
const insertUserSchema = createInsertSchema(users);
// Schema for updating a user - can be used to validate API requests
const updateUserSchema = createUpdateSchema(users);
// Schema for selecting a user - can be used to validate API responses
const selectUserSchema = createSelectSchema(users);
// Overriding the fields
const insertUserSchema = createInsertSchema(users, {
role: Type.String(),
});
// Refining the fields - useful if you want to change the fields before they become nullable/optional in the final schema
const insertUserSchema = createInsertSchema(users, {
id: (schema) => Type.Number({ ...schema, minimum: 0 }),
role: Type.String(),
});
// Usage
const isUserValid: boolean = Value.Check(insertUserSchema, {
name: 'John Doe',
email: 'johndoe@test.com',
role: 'admin',
});
```
+74
View File
@@ -0,0 +1,74 @@
{
"name": "drizzle-typebox",
"version": "0.3.3",
"description": "Generate Typebox schemas from Drizzle ORM schemas",
"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",
"publish": "npm publish 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",
"publishConfig": {
"provenance": true
},
"repository": {
"type": "git",
"url": "git+https://github.com/drizzle-team/drizzle-orm.git"
},
"keywords": [
"typebox",
"validate",
"validation",
"schema",
"drizzle",
"orm",
"pg",
"mysql",
"postgresql",
"postgres",
"sqlite",
"database",
"sql",
"typescript",
"ts"
],
"author": "Drizzle Team",
"license": "Apache-2.0",
"peerDependencies": {
"@sinclair/typebox": ">=0.34.8",
"drizzle-orm": ">=0.36.0"
},
"devDependencies": {
"@rollup/plugin-typescript": "^11.1.0",
"@sinclair/typebox": "^0.34.8",
"@types/node": "^18.15.10",
"cpy": "^10.1.0",
"drizzle-orm": "link:../drizzle-orm/dist",
"json-rules-engine": "^7.3.1",
"rimraf": "^5.0.0",
"rollup": "^3.29.5",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^3.1.3",
"zx": "^7.2.2"
}
}
+33
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\/?/,
'@sinclair/typebox',
],
plugins: [
typescript({
tsconfig: 'tsconfig.build.json',
}),
],
},
]);
+16
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
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.new', importPath.slice(2)));
importPath = relativePath.startsWith('.') ? relativePath : './' + relativePath;
}
return importPath;
}
function fixImportPath(importPath: string, file: string, ext: string) {
importPath = resolvePathAlias(importPath, file);
if (!/\..*\.(js|ts)$/.test(importPath)) {
return importPath;
}
return importPath.replace(/\.(js|ts)$/, ext);
}
const cjsFiles = await glob('dist/**/*.{cjs,d.cts}');
await Promise.all(cjsFiles.map(async (file) => {
const code = parse(await fs.readFile(file, 'utf8'), { parser });
visit(code, {
visitImportDeclaration(path) {
path.value.source.value = fixImportPath(path.value.source.value, file, '.cjs');
this.traverse(path);
},
visitExportAllDeclaration(path) {
path.value.source.value = fixImportPath(path.value.source.value, file, '.cjs');
this.traverse(path);
},
visitExportNamedDeclaration(path) {
if (path.value.source) {
path.value.source.value = fixImportPath(path.value.source.value, file, '.cjs');
}
this.traverse(path);
},
visitCallExpression(path) {
if (path.value.callee.type === 'Identifier' && path.value.callee.name === 'require') {
path.value.arguments[0].value = fixImportPath(path.value.arguments[0].value, file, '.cjs');
}
this.traverse(path);
},
visitTSImportType(path) {
path.value.argument.value = resolvePathAlias(path.value.argument.value, file);
this.traverse(path);
},
visitAwaitExpression(path) {
if (print(path.value).code.startsWith(`await import("./`)) {
path.value.argument.arguments[0].value = fixImportPath(path.value.argument.arguments[0].value, file, '.cjs');
}
this.traverse(path);
},
});
await fs.writeFile(file, print(code).code);
}));
let esmFiles = await glob('dist/**/*.{js,d.ts}');
await Promise.all(esmFiles.map(async (file) => {
const code = parse(await fs.readFile(file, 'utf8'), { parser });
visit(code, {
visitImportDeclaration(path) {
path.value.source.value = fixImportPath(path.value.source.value, file, '.js');
this.traverse(path);
},
visitExportAllDeclaration(path) {
path.value.source.value = fixImportPath(path.value.source.value, file, '.js');
this.traverse(path);
},
visitExportNamedDeclaration(path) {
if (path.value.source) {
path.value.source.value = fixImportPath(path.value.source.value, file, '.js');
}
this.traverse(path);
},
visitTSImportType(path) {
path.value.argument.value = fixImportPath(path.value.argument.value, file, '.js');
this.traverse(path);
},
visitAwaitExpression(path) {
if (print(path.value).code.startsWith(`await import("./`)) {
path.value.argument.arguments[0].value = fixImportPath(path.value.argument.arguments[0].value, file, '.js');
}
this.traverse(path);
},
});
await fs.writeFile(file, print(code).code);
}));
esmFiles = await glob('dist/**/*.{mjs,d.mts}');
await Promise.all(esmFiles.map(async (file) => {
const code = parse(await fs.readFile(file, 'utf8'), { parser });
visit(code, {
visitImportDeclaration(path) {
path.value.source.value = fixImportPath(path.value.source.value, file, '.mjs');
this.traverse(path);
},
visitExportAllDeclaration(path) {
path.value.source.value = fixImportPath(path.value.source.value, file, '.mjs');
this.traverse(path);
},
visitExportNamedDeclaration(path) {
if (path.value.source) {
path.value.source.value = fixImportPath(path.value.source.value, file, '.mjs');
}
this.traverse(path);
},
visitTSImportType(path) {
path.value.argument.value = fixImportPath(path.value.argument.value, file, '.mjs');
this.traverse(path);
},
visitAwaitExpression(path) {
if (print(path.value).code.startsWith(`await import("./`)) {
path.value.argument.arguments[0].value = fixImportPath(path.value.argument.arguments[0].value, file, '.mjs');
}
this.traverse(path);
},
});
await fs.writeFile(file, print(code).code);
}));
+314
View File
@@ -0,0 +1,314 @@
import { Kind, Type as t, TypeRegistry } from '@sinclair/typebox';
import type { StringOptions, TSchema, Type as typebox } from '@sinclair/typebox';
import type { Column, ColumnBaseConfig } from 'drizzle-orm';
import type {
MySqlBigInt53,
MySqlChar,
MySqlDouble,
MySqlFloat,
MySqlInt,
MySqlMediumInt,
MySqlReal,
MySqlSerial,
MySqlSmallInt,
MySqlText,
MySqlTinyInt,
MySqlVarChar,
MySqlYear,
} from 'drizzle-orm/mysql-core';
import type {
PgArray,
PgBigInt53,
PgBigSerial53,
PgBinaryVector,
PgChar,
PgDoublePrecision,
PgGeometry,
PgGeometryObject,
PgHalfVector,
PgInteger,
PgLineABC,
PgLineTuple,
PgPointObject,
PgPointTuple,
PgReal,
PgSerial,
PgSmallInt,
PgSmallSerial,
PgUUID,
PgVarchar,
PgVector,
} from 'drizzle-orm/pg-core';
import type {
SingleStoreBigInt53,
SingleStoreChar,
SingleStoreDouble,
SingleStoreFloat,
SingleStoreInt,
SingleStoreMediumInt,
SingleStoreReal,
SingleStoreSerial,
SingleStoreSmallInt,
SingleStoreText,
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.Union([literalSchema, t.Array(t.Any()), t.Record(t.String(), t.Any())]) 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);
}
+91
View File
@@ -0,0 +1,91 @@
import type * as t from '@sinclair/typebox';
import type { Assume, Column } from 'drizzle-orm';
import type { BufferSchema, IsEnumDefined, IsNever, JsonSchema } from './utils.ts';
type HasBaseColumn<TColumn> = TColumn extends { _: { baseColumn: Column | undefined } }
? IsNever<TColumn['_']['baseColumn']> extends false ? true
: false
: false;
export type EnumValuesToEnum<TEnumValues extends [string, ...string[]]> = { [K in TEnumValues[number]]: K };
export interface GenericSchema<T> extends t.TSchema {
static: T;
}
export type GetTypeboxType<
TColumn extends Column,
> = TColumn['_']['columnType'] extends
| 'MySqlTinyInt'
| 'SingleStoreTinyInt'
| 'PgSmallInt'
| 'PgSmallSerial'
| 'MySqlSmallInt'
| 'MySqlMediumInt'
| 'SingleStoreSmallInt'
| 'SingleStoreMediumInt'
| 'PgInteger'
| 'PgSerial'
| 'MySqlInt'
| 'SingleStoreInt'
| 'PgBigInt53'
| 'PgBigSerial53'
| 'MySqlBigInt53'
| 'MySqlSerial'
| 'SingleStoreBigInt53'
| 'SingleStoreSerial'
| 'SQLiteInteger'
| 'MySqlYear'
| 'SingleStoreYear' ? t.TInteger
: TColumn['_']['columnType'] extends 'PgBinaryVector' ? t.TRegExp
: HasBaseColumn<TColumn> extends true ? t.TArray<
GetTypeboxType<Assume<TColumn['_']['baseColumn'], Column>>
>
: IsEnumDefined<TColumn['_']['enumValues']> extends true
? t.TEnum<{ [K in Assume<TColumn['_']['enumValues'], string[]>[number]]: K }>
: TColumn['_']['columnType'] extends 'PgGeometry' | 'PgPointTuple' ? t.TTuple<[t.TNumber, t.TNumber]>
: TColumn['_']['columnType'] extends 'PgLine' ? t.TTuple<[t.TNumber, t.TNumber, t.TNumber]>
: TColumn['_']['data'] extends Date ? t.TDate
: TColumn['_']['data'] extends Buffer ? BufferSchema
: TColumn['_']['dataType'] extends 'array'
? t.TArray<GetTypeboxPrimitiveType<Assume<TColumn['_']['data'], any[]>[number]>>
: TColumn['_']['data'] extends Record<string, any>
? TColumn['_']['columnType'] extends
'PgJson' | 'PgJsonb' | 'MySqlJson' | 'SingleStoreJson' | 'SQLiteTextJson' | 'SQLiteBlobJson'
? GenericSchema<TColumn['_']['data']>
: t.TObject<{ [K in keyof TColumn['_']['data']]: GetTypeboxPrimitiveType<TColumn['_']['data'][K]> }>
: TColumn['_']['dataType'] extends 'json' ? JsonSchema
: GetTypeboxPrimitiveType<TColumn['_']['data']>;
type GetTypeboxPrimitiveType<TData> = 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,
> = 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,
> = 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,
> = TType extends 'select' ? HandleSelectColumn<GetTypeboxType<TColumn>, TColumn>
: TType extends 'insert' ? HandleInsertColumn<GetTypeboxType<TColumn>, TColumn>
: TType extends 'update' ? HandleUpdateColumn<GetTypeboxType<TColumn>, TColumn>
: GetTypeboxType<TColumn>;
+20
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,
};
+6
View File
@@ -0,0 +1,6 @@
export { bufferSchema, jsonSchema, literalSchema } from './column.ts';
export * from './column.types.ts';
export * from './schema.ts';
export * from './schema.types.internal.ts';
export * from './schema.types.ts';
export * from './utils.ts';
+144
View File
@@ -0,0 +1,144 @@
import { Type as t } from '@sinclair/typebox';
import type { TSchema } from '@sinclair/typebox';
import { Column, getTableColumns, getViewSelectedFields, is, isTable, isView, SQL } from 'drizzle-orm';
import type { Table, View } from 'drizzle-orm';
import type { PgEnum } from 'drizzle-orm/pg-core';
import { columnToSchema, mapEnumValues } from './column.ts';
import type { Conditions } from './schema.types.internal.ts';
import type {
CreateInsertSchema,
CreateSchemaFactoryOptions,
CreateSelectSchema,
CreateUpdateSchema,
} from './schema.types.ts';
import { isPgEnum } from './utils.ts';
export function getColumns(tableLike: Table | View) {
return isTable(tableLike) ? getTableColumns(tableLike) : getViewSelectedFields(tableLike);
}
export 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;
}
export 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 };
}
@@ -0,0 +1,69 @@
import type * as t from '@sinclair/typebox';
import type { Assume, Column, DrizzleTypeError, SelectedFieldsFlat, Simplify, Table, View } from 'drizzle-orm';
import type { GetTypeboxType, HandleColumn } from './column.types.ts';
import type { ColumnIsGeneratedAlwaysAs, GetSelection } from './utils.ts';
export interface Conditions {
never: (column?: Column) => boolean;
optional: (column: Column) => boolean;
nullable: (column: Column) => boolean;
}
type BuildRefineField<T> = T extends t.TSchema ? ((schema: T) => t.TSchema) | t.TSchema : never;
export type BuildRefine<
TColumns extends Record<string, any>,
> = {
[K in keyof TColumns as TColumns[K] extends Column | SelectedFieldsFlat<Column> | Table | View ? K : never]?:
TColumns[K] extends Column ? BuildRefineField<GetTypeboxType<TColumns[K]>>
: BuildRefine<GetSelection<TColumns[K]>>;
};
type HandleRefinement<
TType extends 'select' | 'insert' | 'update',
TRefinement,
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 extends Record<string | symbol | number, any> | undefined,
TKey extends string | symbol | number,
> = TRefinements extends object ? 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<
{
[K in keyof TColumns as ColumnIsGeneratedAlwaysAs<TColumns[K]> extends true ? never : K]: TColumns[K] extends
infer TColumn extends Column
? IsRefinementDefined<TRefinements, K> extends true
? Assume<HandleRefinement<TType, TRefinements[K & keyof TRefinements], TColumn>, t.TSchema>
: HandleColumn<TType, TColumn>
: TColumns[K] extends infer TObject extends SelectedFieldsFlat<Column> | Table | View ? BuildSchema<
TType,
GetSelection<TObject>,
TRefinements extends object ? TRefinements[K & keyof TRefinements] : 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
View File
@@ -0,0 +1,53 @@
import type * as t from '@sinclair/typebox';
import type { Table, View } from 'drizzle-orm';
import type { PgEnum } from 'drizzle-orm/pg-core';
import type { EnumValuesToEnum } from './column.types.ts';
import type { BuildRefine, BuildSchema, NoUnknownKeys } from './schema.types.internal.ts';
export interface CreateSelectSchema {
<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;
}
+43
View File
@@ -0,0 +1,43 @@
import type { Kind, Static, TSchema } from '@sinclair/typebox';
import type { Column, SelectedFieldsFlat, Table, View } from 'drizzle-orm';
import type { PgEnum } from 'drizzle-orm/pg-core';
import type { literalSchema } from './column.ts';
export function isColumnType<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]: any } | any[];
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 IsEnumDefined<TEnum extends string[] | undefined> = [string, ...string[]] extends TEnum ? false
: undefined extends TEnum ? false
: true;
export type ColumnIsGeneratedAlwaysAs<TColumn> = TColumn extends Column
? TColumn['_']['identity'] extends 'always' ? true
: TColumn['_']['generated'] extends { type: 'byDefault' } | undefined ? false
: true
: false;
export type GetSelection<T extends SelectedFieldsFlat<Column> | Table | View> = T extends Table ? T['_']['columns']
: T extends View ? T['_']['selectedFields']
: T;
+508
View File
@@ -0,0 +1,508 @@
import { type Static, Type as t } from '@sinclair/typebox';
import { type Equal, sql } from 'drizzle-orm';
import { customType, int, json, mysqlSchema, mysqlTable, mysqlView, serial, text } from 'drizzle-orm/mysql-core';
import type { TopLevelCondition } from 'json-rules-engine';
import { test } from 'vitest';
import { jsonSchema } from '~/column.ts';
import { CONSTANTS } from '~/constants.ts';
import { createInsertSchema, createSelectSchema, createUpdateSchema, type GenericSchema } 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>>();
});
/* Infinitely recursive type */ {
const TopLevelCondition: GenericSchema<TopLevelCondition> = t.Any() as any;
const table = mysqlTable('test', {
json: json().$type<TopLevelCondition>(),
});
const result = createSelectSchema(table);
const expected = t.Object({
json: t.Union([TopLevelCondition, t.Null()]),
});
Expect<Equal<Static<typeof result>, Static<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() });
}
+557
View File
@@ -0,0 +1,557 @@
import { type Static, Type as t } from '@sinclair/typebox';
import { type Equal, sql } from 'drizzle-orm';
import {
customType,
integer,
json,
jsonb,
pgEnum,
pgMaterializedView,
pgSchema,
pgTable,
pgView,
serial,
text,
} from 'drizzle-orm/pg-core';
import type { TopLevelCondition } from 'json-rules-engine';
import { test } from 'vitest';
import { jsonSchema } from '~/column.ts';
import { CONSTANTS } from '~/constants.ts';
import { createInsertSchema, createSelectSchema, createUpdateSchema, type GenericSchema } 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>>();
});
/* Infinitely recursive type */ {
const TopLevelCondition: GenericSchema<TopLevelCondition> = t.Any() as any;
const table = pgTable('test', {
json: json().$type<TopLevelCondition>().notNull(),
jsonb: jsonb().$type<TopLevelCondition>(),
});
const result = createSelectSchema(table);
const expected = t.Object({
json: TopLevelCondition,
jsonb: t.Union([TopLevelCondition, t.Null()]),
});
Expect<Equal<Static<typeof result>, Static<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() });
}
+510
View File
@@ -0,0 +1,510 @@
import { type Static, Type as t } from '@sinclair/typebox';
import { type Equal } from 'drizzle-orm';
import { customType, int, json, serial, singlestoreSchema, singlestoreTable, text } from 'drizzle-orm/singlestore-core';
import type { TopLevelCondition } from 'json-rules-engine';
import { test } from 'vitest';
import { jsonSchema } from '~/column.ts';
import { CONSTANTS } from '~/constants.ts';
import { createInsertSchema, createSelectSchema, createUpdateSchema, type GenericSchema } 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>>();
});
/* Infinitely recursive type */ {
const TopLevelCondition: GenericSchema<TopLevelCondition> = t.Any() as any;
const table = singlestoreTable('test', {
json: json().$type<TopLevelCondition>(),
});
const result = createSelectSchema(table);
const expected = t.Object({
json: t.Union([TopLevelCondition, t.Null()]),
});
Expect<Equal<Static<typeof result>, Static<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() });
// }
+404
View File
@@ -0,0 +1,404 @@
import { type Static, Type as t } from '@sinclair/typebox';
import { type Equal, sql } from 'drizzle-orm';
import { blob, customType, int, sqliteTable, sqliteView, text } from 'drizzle-orm/sqlite-core';
import type { TopLevelCondition } from 'json-rules-engine';
import { test } from 'vitest';
import { bufferSchema, jsonSchema } from '~/column.ts';
import { CONSTANTS } from '~/constants.ts';
import { createInsertSchema, createSelectSchema, createUpdateSchema, type GenericSchema } 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>>();
});
/* Infinitely recursive type */ {
const TopLevelCondition: GenericSchema<TopLevelCondition> = t.Any() as any;
const table = sqliteTable('test', {
json1: text({ mode: 'json' }).$type<TopLevelCondition>().notNull(),
json2: blob({ mode: 'json' }).$type<TopLevelCondition>(),
});
const result = createSelectSchema(table);
const expected = t.Object({
json1: TopLevelCondition,
json2: t.Union([TopLevelCondition, t.Null()]),
});
Expect<Equal<Static<typeof result>, Static<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
View File
@@ -0,0 +1,11 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"noEmit": true,
"rootDir": "..",
"outDir": "./.cache"
},
"include": [".", "../src"]
}
+34
View File
@@ -0,0 +1,34 @@
import type * as t from '@sinclair/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
View File
@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "src"
},
"include": ["src"]
}
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"baseUrl": ".",
"declaration": true,
"noEmit": true,
"paths": {
"~/*": ["src/*"]
}
},
"include": ["src", "*.ts"]
}
+25
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()],
});