feat: fork drizzle-typebox as @alkdev/drizzlebox
- Rebrand package from drizzle-typebox to @alkdev/drizzlebox - Replace @sinclair/typebox with @alkdev/typebox in all source and test files - Replace @sinclair/typebox with @alkdev/typebox in rollup externals - Convert tsconfig.json from monorepo extends to standalone config - Fix build script monorepo remnant (dist.new -> dist) - Add missing devDependencies (recast, tsx, typescript, resolve-tspaths) - Replace monorepo link dependency for drizzle-orm with ^0.38.4 - Add .gitignore, LICENSE (Apache-2.0 with attribution), and README - Initialize git repo with remote at git.alk.dev:alkdev/drizzlebox
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.tgz
|
||||||
|
.cache/
|
||||||
27
LICENSE
Normal file
27
LICENSE
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
Copyright 2024 Drizzle Team
|
||||||
|
Copyright 2025 alkdev
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
NOTICE
|
||||||
|
|
||||||
|
This project is a fork of drizzle-typebox, originally part of the
|
||||||
|
drizzle-orm monorepo by the Drizzle Team (https://github.com/drizzle-team/drizzle-orm).
|
||||||
|
|
||||||
|
The original work is Copyright 2024 Drizzle Team and is licensed under the
|
||||||
|
Apache License, Version 2.0.
|
||||||
|
|
||||||
|
Modifications (rebranding to @alkdev/drizzlebox, dependency migration to
|
||||||
|
@alkdev/typebox, and standalone packaging) are Copyright 2025 alkdev.
|
||||||
74
README.md
Normal file
74
README.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# @alkdev/drizzlebox
|
||||||
|
|
||||||
|
Generate [TypeBox](https://github.com/alkdev/typebox) schemas from [Drizzle ORM](https://orm.drizzle.team) schemas.
|
||||||
|
|
||||||
|
This is a fork of [drizzle-typebox](https://github.com/drizzle-team/drizzle-orm/tree/main/drizzle-typebox) by the Drizzle Team, adapted for use with `@alkdev/typebox` (a maintained fork of `@sinclair/typebox`).
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @alkdev/drizzlebox
|
||||||
|
npm install @alkdev/typebox
|
||||||
|
npm install drizzle-orm
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Create select schemas for tables, views, and enums
|
||||||
|
- Create insert and update schemas for tables
|
||||||
|
- Supports all dialects: PostgreSQL, MySQL, and SQLite
|
||||||
|
- Custom TypeBox instance support via `createSchemaFactory`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { pgEnum, pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
|
||||||
|
import { createInsertSchema, createSelectSchema, createUpdateSchema } from '@alkdev/drizzlebox';
|
||||||
|
import { Type } from '@alkdev/typebox';
|
||||||
|
import { Value } from '@alkdev/typebox/value';
|
||||||
|
|
||||||
|
const users = pgTable('users', {
|
||||||
|
id: serial('id').primaryKey(),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
email: text('email').notNull(),
|
||||||
|
role: text('role', { enum: ['admin', 'user'] }).notNull(),
|
||||||
|
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schema for inserting a user
|
||||||
|
const insertUserSchema = createInsertSchema(users);
|
||||||
|
|
||||||
|
// Schema for updating a user
|
||||||
|
const updateUserSchema = createUpdateSchema(users);
|
||||||
|
|
||||||
|
// Schema for selecting a user
|
||||||
|
const selectUserSchema = createSelectSchema(users);
|
||||||
|
|
||||||
|
// Overriding fields
|
||||||
|
const insertUserSchema = createInsertSchema(users, {
|
||||||
|
role: Type.String(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refining fields
|
||||||
|
const insertUserSchema = createInsertSchema(users, {
|
||||||
|
id: (schema) => Type.Number({ ...schema, minimum: 0 }),
|
||||||
|
role: Type.String(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
const isUserValid: boolean = Value.Check(insertUserSchema, {
|
||||||
|
name: 'John Doe',
|
||||||
|
email: 'johndoe@test.com',
|
||||||
|
role: 'admin',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Differences from drizzle-typebox
|
||||||
|
|
||||||
|
- Uses `@alkdev/typebox` instead of `@sinclair/typebox`
|
||||||
|
- Standalone package (no monorepo dependency)
|
||||||
|
- Published as `@alkdev/drizzlebox` on npm
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
Based on [drizzle-typebox](https://github.com/drizzle-team/drizzle-orm/tree/main/drizzle-typebox) by the Drizzle Team, licensed under Apache-2.0.
|
||||||
75
package.json
Normal file
75
package.json
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"name": "@alkdev/drizzlebox",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Generate Typebox schemas from Drizzle ORM schemas — fork of drizzle-typebox with @alkdev/typebox support",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsx scripts/build.ts",
|
||||||
|
"b": "pnpm build",
|
||||||
|
"test:types": "cd tests && tsc",
|
||||||
|
"pack": "(cd dist && npm pack --pack-destination ..) && rm -f package.tgz && mv *.tgz package.tgz",
|
||||||
|
"test": "vitest run"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": {
|
||||||
|
"types": "./index.d.mts",
|
||||||
|
"default": "./index.mjs"
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"types": "./index.d.cjs",
|
||||||
|
"default": "./index.cjs"
|
||||||
|
},
|
||||||
|
"types": "./index.d.ts",
|
||||||
|
"default": "./index.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"main": "./index.cjs",
|
||||||
|
"module": "./index.mjs",
|
||||||
|
"types": "./index.d.ts",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://git.alk.dev/alkdev/drizzlebox.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"typebox",
|
||||||
|
"drizzlebox",
|
||||||
|
"validate",
|
||||||
|
"validation",
|
||||||
|
"schema",
|
||||||
|
"drizzle",
|
||||||
|
"orm",
|
||||||
|
"pg",
|
||||||
|
"mysql",
|
||||||
|
"postgresql",
|
||||||
|
"postgres",
|
||||||
|
"sqlite",
|
||||||
|
"database",
|
||||||
|
"sql",
|
||||||
|
"typescript",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"author": "Based on drizzle-typebox by Drizzle Team; fork maintained by alkdev",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@alkdev/typebox": ">=0.34.49",
|
||||||
|
"drizzle-orm": ">=0.36.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@alkdev/typebox": "^0.34.49",
|
||||||
|
"@rollup/plugin-typescript": "^11.1.0",
|
||||||
|
"@types/node": "^18.15.10",
|
||||||
|
"@types/recast": "^0.23.0",
|
||||||
|
"cpy": "^10.1.0",
|
||||||
|
"drizzle-orm": "^0.38.4",
|
||||||
|
"recast": "^0.23.0",
|
||||||
|
"resolve-tspaths": "^0.8.23",
|
||||||
|
"rimraf": "^5.0.0",
|
||||||
|
"rollup": "^3.20.7",
|
||||||
|
"tsx": "^4.0.0",
|
||||||
|
"typescript": "^5.4.0",
|
||||||
|
"vite-tsconfig-paths": "^4.3.2",
|
||||||
|
"vitest": "^1.6.0",
|
||||||
|
"zx": "^7.2.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
33
rollup.config.ts
Normal file
33
rollup.config.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import typescript from '@rollup/plugin-typescript';
|
||||||
|
import { defineConfig } from 'rollup';
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
{
|
||||||
|
input: 'src/index.ts',
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
format: 'esm',
|
||||||
|
dir: 'dist',
|
||||||
|
entryFileNames: '[name].mjs',
|
||||||
|
chunkFileNames: '[name]-[hash].mjs',
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: 'cjs',
|
||||||
|
dir: 'dist',
|
||||||
|
entryFileNames: '[name].cjs',
|
||||||
|
chunkFileNames: '[name]-[hash].cjs',
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
external: [
|
||||||
|
/^drizzle-orm\/?/,
|
||||||
|
'@alkdev/typebox',
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
typescript({
|
||||||
|
tsconfig: 'tsconfig.build.json',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
16
scripts/build.ts
Normal file
16
scripts/build.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env -S pnpm tsx
|
||||||
|
import 'zx/globals';
|
||||||
|
import cpy from 'cpy';
|
||||||
|
|
||||||
|
await fs.remove('dist');
|
||||||
|
await $`rollup --config rollup.config.ts --configPlugin typescript`;
|
||||||
|
await $`resolve-tspaths`;
|
||||||
|
await fs.copy('README.md', 'dist/README.md');
|
||||||
|
await cpy('dist/**/*.d.ts', 'dist', {
|
||||||
|
rename: (basename) => basename.replace(/\.d\.ts$/, '.d.mts'),
|
||||||
|
});
|
||||||
|
await cpy('dist/**/*.d.ts', 'dist', {
|
||||||
|
rename: (basename) => basename.replace(/\.d\.ts$/, '.d.cts'),
|
||||||
|
});
|
||||||
|
await fs.copy('package.json', 'dist/package.json');
|
||||||
|
await $`scripts/fix-imports.ts`;
|
||||||
136
scripts/fix-imports.ts
Executable file
136
scripts/fix-imports.ts
Executable file
@@ -0,0 +1,136 @@
|
|||||||
|
#!/usr/bin/env -S pnpm tsx
|
||||||
|
import 'zx/globals';
|
||||||
|
|
||||||
|
import path from 'node:path';
|
||||||
|
import { parse, print, visit } from 'recast';
|
||||||
|
import parser from 'recast/parsers/typescript';
|
||||||
|
|
||||||
|
function resolvePathAlias(importPath: string, file: string) {
|
||||||
|
if (importPath.startsWith('~/')) {
|
||||||
|
const relativePath = path.relative(path.dirname(file), path.resolve('dist', importPath.slice(2)));
|
||||||
|
importPath = relativePath.startsWith('.') ? relativePath : './' + relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return importPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixImportPath(importPath: string, file: string, ext: string) {
|
||||||
|
importPath = resolvePathAlias(importPath, file);
|
||||||
|
|
||||||
|
if (!/\..*\.(js|ts)$/.test(importPath)) {
|
||||||
|
return importPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return importPath.replace(/\.(js|ts)$/, ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cjsFiles = await glob('dist/**/*.{cjs,d.cts}');
|
||||||
|
|
||||||
|
await Promise.all(cjsFiles.map(async (file) => {
|
||||||
|
const code = parse(await fs.readFile(file, 'utf8'), { parser });
|
||||||
|
|
||||||
|
visit(code, {
|
||||||
|
visitImportDeclaration(path) {
|
||||||
|
path.value.source.value = fixImportPath(path.value.source.value, file, '.cjs');
|
||||||
|
this.traverse(path);
|
||||||
|
},
|
||||||
|
visitExportAllDeclaration(path) {
|
||||||
|
path.value.source.value = fixImportPath(path.value.source.value, file, '.cjs');
|
||||||
|
this.traverse(path);
|
||||||
|
},
|
||||||
|
visitExportNamedDeclaration(path) {
|
||||||
|
if (path.value.source) {
|
||||||
|
path.value.source.value = fixImportPath(path.value.source.value, file, '.cjs');
|
||||||
|
}
|
||||||
|
this.traverse(path);
|
||||||
|
},
|
||||||
|
visitCallExpression(path) {
|
||||||
|
if (path.value.callee.type === 'Identifier' && path.value.callee.name === 'require') {
|
||||||
|
path.value.arguments[0].value = fixImportPath(path.value.arguments[0].value, file, '.cjs');
|
||||||
|
}
|
||||||
|
this.traverse(path);
|
||||||
|
},
|
||||||
|
visitTSImportType(path) {
|
||||||
|
path.value.argument.value = resolvePathAlias(path.value.argument.value, file);
|
||||||
|
this.traverse(path);
|
||||||
|
},
|
||||||
|
visitAwaitExpression(path) {
|
||||||
|
if (print(path.value).code.startsWith(`await import("./`)) {
|
||||||
|
path.value.argument.arguments[0].value = fixImportPath(path.value.argument.arguments[0].value, file, '.cjs');
|
||||||
|
}
|
||||||
|
this.traverse(path);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await fs.writeFile(file, print(code).code);
|
||||||
|
}));
|
||||||
|
|
||||||
|
let esmFiles = await glob('dist/**/*.{js,d.ts}');
|
||||||
|
|
||||||
|
await Promise.all(esmFiles.map(async (file) => {
|
||||||
|
const code = parse(await fs.readFile(file, 'utf8'), { parser });
|
||||||
|
|
||||||
|
visit(code, {
|
||||||
|
visitImportDeclaration(path) {
|
||||||
|
path.value.source.value = fixImportPath(path.value.source.value, file, '.js');
|
||||||
|
this.traverse(path);
|
||||||
|
},
|
||||||
|
visitExportAllDeclaration(path) {
|
||||||
|
path.value.source.value = fixImportPath(path.value.source.value, file, '.js');
|
||||||
|
this.traverse(path);
|
||||||
|
},
|
||||||
|
visitExportNamedDeclaration(path) {
|
||||||
|
if (path.value.source) {
|
||||||
|
path.value.source.value = fixImportPath(path.value.source.value, file, '.js');
|
||||||
|
}
|
||||||
|
this.traverse(path);
|
||||||
|
},
|
||||||
|
visitTSImportType(path) {
|
||||||
|
path.value.argument.value = fixImportPath(path.value.argument.value, file, '.js');
|
||||||
|
this.traverse(path);
|
||||||
|
},
|
||||||
|
visitAwaitExpression(path) {
|
||||||
|
if (print(path.value).code.startsWith(`await import("./`)) {
|
||||||
|
path.value.argument.arguments[0].value = fixImportPath(path.value.argument.arguments[0].value, file, '.js');
|
||||||
|
}
|
||||||
|
this.traverse(path);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await fs.writeFile(file, print(code).code);
|
||||||
|
}));
|
||||||
|
|
||||||
|
esmFiles = await glob('dist/**/*.{mjs,d.mts}');
|
||||||
|
|
||||||
|
await Promise.all(esmFiles.map(async (file) => {
|
||||||
|
const code = parse(await fs.readFile(file, 'utf8'), { parser });
|
||||||
|
|
||||||
|
visit(code, {
|
||||||
|
visitImportDeclaration(path) {
|
||||||
|
path.value.source.value = fixImportPath(path.value.source.value, file, '.mjs');
|
||||||
|
this.traverse(path);
|
||||||
|
},
|
||||||
|
visitExportAllDeclaration(path) {
|
||||||
|
path.value.source.value = fixImportPath(path.value.source.value, file, '.mjs');
|
||||||
|
this.traverse(path);
|
||||||
|
},
|
||||||
|
visitExportNamedDeclaration(path) {
|
||||||
|
if (path.value.source) {
|
||||||
|
path.value.source.value = fixImportPath(path.value.source.value, file, '.mjs');
|
||||||
|
}
|
||||||
|
this.traverse(path);
|
||||||
|
},
|
||||||
|
visitTSImportType(path) {
|
||||||
|
path.value.argument.value = fixImportPath(path.value.argument.value, file, '.mjs');
|
||||||
|
this.traverse(path);
|
||||||
|
},
|
||||||
|
visitAwaitExpression(path) {
|
||||||
|
if (print(path.value).code.startsWith(`await import("./`)) {
|
||||||
|
path.value.argument.arguments[0].value = fixImportPath(path.value.argument.arguments[0].value, file, '.mjs');
|
||||||
|
}
|
||||||
|
this.traverse(path);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await fs.writeFile(file, print(code).code);
|
||||||
|
}));
|
||||||
316
src/column.ts
Normal file
316
src/column.ts
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
import { Kind, Type as t, TypeRegistry } from '@alkdev/typebox';
|
||||||
|
import type { StringOptions, TSchema, Type as typebox } from '@alkdev/typebox';
|
||||||
|
import type { Column, ColumnBaseConfig } from 'drizzle-orm';
|
||||||
|
import type {
|
||||||
|
MySqlBigInt53,
|
||||||
|
MySqlChar,
|
||||||
|
MySqlDouble,
|
||||||
|
MySqlFloat,
|
||||||
|
MySqlInt,
|
||||||
|
MySqlMediumInt,
|
||||||
|
MySqlReal,
|
||||||
|
MySqlSerial,
|
||||||
|
MySqlSmallInt,
|
||||||
|
MySqlText,
|
||||||
|
MySqlTinyInt,
|
||||||
|
MySqlVarChar,
|
||||||
|
MySqlYear,
|
||||||
|
} from 'drizzle-orm/mysql-core';
|
||||||
|
import type {
|
||||||
|
PgArray,
|
||||||
|
PgBigInt53,
|
||||||
|
PgBigSerial53,
|
||||||
|
PgBinaryVector,
|
||||||
|
PgChar,
|
||||||
|
PgDoublePrecision,
|
||||||
|
PgGeometry,
|
||||||
|
PgGeometryObject,
|
||||||
|
PgHalfVector,
|
||||||
|
PgInteger,
|
||||||
|
PgLineABC,
|
||||||
|
PgLineTuple,
|
||||||
|
PgPointObject,
|
||||||
|
PgPointTuple,
|
||||||
|
PgReal,
|
||||||
|
PgSerial,
|
||||||
|
PgSmallInt,
|
||||||
|
PgSmallSerial,
|
||||||
|
PgUUID,
|
||||||
|
PgVarchar,
|
||||||
|
PgVector,
|
||||||
|
} from 'drizzle-orm/pg-core';
|
||||||
|
import {
|
||||||
|
type SingleStoreBigInt53,
|
||||||
|
SingleStoreChar,
|
||||||
|
type SingleStoreDouble,
|
||||||
|
type SingleStoreFloat,
|
||||||
|
type SingleStoreInt,
|
||||||
|
type SingleStoreMediumInt,
|
||||||
|
type SingleStoreReal,
|
||||||
|
type SingleStoreSerial,
|
||||||
|
type SingleStoreSmallInt,
|
||||||
|
SingleStoreText,
|
||||||
|
type SingleStoreTinyInt,
|
||||||
|
SingleStoreVarChar,
|
||||||
|
SingleStoreYear,
|
||||||
|
} from 'drizzle-orm/singlestore-core';
|
||||||
|
import type { SQLiteInteger, SQLiteReal, SQLiteText } from 'drizzle-orm/sqlite-core';
|
||||||
|
import { CONSTANTS } from './constants.ts';
|
||||||
|
import { isColumnType, isWithEnum } from './utils.ts';
|
||||||
|
import type { BufferSchema, JsonSchema } from './utils.ts';
|
||||||
|
|
||||||
|
export const literalSchema = t.Union([t.String(), t.Number(), t.Boolean(), t.Null()]);
|
||||||
|
export const jsonSchema: JsonSchema = t.Recursive((self) =>
|
||||||
|
t.Union([literalSchema, t.Array(self), t.Record(t.String(), self)])
|
||||||
|
) as any;
|
||||||
|
TypeRegistry.Set('Buffer', (_, value) => value instanceof Buffer); // eslint-disable-line no-instanceof/no-instanceof
|
||||||
|
export const bufferSchema: BufferSchema = { [Kind]: 'Buffer', type: 'buffer' } as any;
|
||||||
|
|
||||||
|
export function mapEnumValues(values: string[]) {
|
||||||
|
return Object.fromEntries(values.map((value) => [value, value]));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function columnToSchema(column: Column, t: typeof typebox): TSchema {
|
||||||
|
let schema!: TSchema;
|
||||||
|
|
||||||
|
if (isWithEnum(column)) {
|
||||||
|
schema = column.enumValues.length ? t.Enum(mapEnumValues(column.enumValues)) : t.String();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!schema) {
|
||||||
|
// Handle specific types
|
||||||
|
if (isColumnType<PgGeometry<any> | PgPointTuple<any>>(column, ['PgGeometry', 'PgPointTuple'])) {
|
||||||
|
schema = t.Tuple([t.Number(), t.Number()]);
|
||||||
|
} else if (
|
||||||
|
isColumnType<PgPointObject<any> | PgGeometryObject<any>>(column, ['PgGeometryObject', 'PgPointObject'])
|
||||||
|
) {
|
||||||
|
schema = t.Object({ x: t.Number(), y: t.Number() });
|
||||||
|
} else if (isColumnType<PgHalfVector<any> | PgVector<any>>(column, ['PgHalfVector', 'PgVector'])) {
|
||||||
|
schema = t.Array(
|
||||||
|
t.Number(),
|
||||||
|
column.dimensions
|
||||||
|
? {
|
||||||
|
minItems: column.dimensions,
|
||||||
|
maxItems: column.dimensions,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
} else if (isColumnType<PgLineTuple<any>>(column, ['PgLine'])) {
|
||||||
|
schema = t.Tuple([t.Number(), t.Number(), t.Number()]);
|
||||||
|
} else if (isColumnType<PgLineABC<any>>(column, ['PgLineABC'])) {
|
||||||
|
schema = t.Object({
|
||||||
|
a: t.Number(),
|
||||||
|
b: t.Number(),
|
||||||
|
c: t.Number(),
|
||||||
|
});
|
||||||
|
} // Handle other types
|
||||||
|
else if (isColumnType<PgArray<any, any>>(column, ['PgArray'])) {
|
||||||
|
schema = t.Array(
|
||||||
|
columnToSchema(column.baseColumn, t),
|
||||||
|
column.size
|
||||||
|
? {
|
||||||
|
minItems: column.size,
|
||||||
|
maxItems: column.size,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
} else if (column.dataType === 'array') {
|
||||||
|
schema = t.Array(t.Any());
|
||||||
|
} else if (column.dataType === 'number') {
|
||||||
|
schema = numberColumnToSchema(column, t);
|
||||||
|
} else if (column.dataType === 'bigint') {
|
||||||
|
schema = bigintColumnToSchema(column, t);
|
||||||
|
} else if (column.dataType === 'boolean') {
|
||||||
|
schema = t.Boolean();
|
||||||
|
} else if (column.dataType === 'date') {
|
||||||
|
schema = t.Date();
|
||||||
|
} else if (column.dataType === 'string') {
|
||||||
|
schema = stringColumnToSchema(column, t);
|
||||||
|
} else if (column.dataType === 'json') {
|
||||||
|
schema = jsonSchema;
|
||||||
|
} else if (column.dataType === 'custom') {
|
||||||
|
schema = t.Any();
|
||||||
|
} else if (column.dataType === 'buffer') {
|
||||||
|
schema = bufferSchema;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!schema) {
|
||||||
|
schema = t.Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberColumnToSchema(column: Column, t: typeof typebox): TSchema {
|
||||||
|
let unsigned = column.getSQLType().includes('unsigned');
|
||||||
|
let min!: number;
|
||||||
|
let max!: number;
|
||||||
|
let integer = false;
|
||||||
|
|
||||||
|
if (isColumnType<MySqlTinyInt<any> | SingleStoreTinyInt<any>>(column, ['MySqlTinyInt', 'SingleStoreTinyInt'])) {
|
||||||
|
min = unsigned ? 0 : CONSTANTS.INT8_MIN;
|
||||||
|
max = unsigned ? CONSTANTS.INT8_UNSIGNED_MAX : CONSTANTS.INT8_MAX;
|
||||||
|
integer = true;
|
||||||
|
} else if (
|
||||||
|
isColumnType<PgSmallInt<any> | PgSmallSerial<any> | MySqlSmallInt<any> | SingleStoreSmallInt<any>>(column, [
|
||||||
|
'PgSmallInt',
|
||||||
|
'PgSmallSerial',
|
||||||
|
'MySqlSmallInt',
|
||||||
|
'SingleStoreSmallInt',
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
min = unsigned ? 0 : CONSTANTS.INT16_MIN;
|
||||||
|
max = unsigned ? CONSTANTS.INT16_UNSIGNED_MAX : CONSTANTS.INT16_MAX;
|
||||||
|
integer = true;
|
||||||
|
} else if (
|
||||||
|
isColumnType<
|
||||||
|
PgReal<any> | MySqlFloat<any> | MySqlMediumInt<any> | SingleStoreFloat<any> | SingleStoreMediumInt<any>
|
||||||
|
>(column, [
|
||||||
|
'PgReal',
|
||||||
|
'MySqlFloat',
|
||||||
|
'MySqlMediumInt',
|
||||||
|
'SingleStoreFloat',
|
||||||
|
'SingleStoreMediumInt',
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
min = unsigned ? 0 : CONSTANTS.INT24_MIN;
|
||||||
|
max = unsigned ? CONSTANTS.INT24_UNSIGNED_MAX : CONSTANTS.INT24_MAX;
|
||||||
|
integer = isColumnType(column, ['MySqlMediumInt', 'SingleStoreMediumInt']);
|
||||||
|
} else if (
|
||||||
|
isColumnType<PgInteger<any> | PgSerial<any> | MySqlInt<any> | SingleStoreInt<any>>(column, [
|
||||||
|
'PgInteger',
|
||||||
|
'PgSerial',
|
||||||
|
'MySqlInt',
|
||||||
|
'SingleStoreInt',
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
min = unsigned ? 0 : CONSTANTS.INT32_MIN;
|
||||||
|
max = unsigned ? CONSTANTS.INT32_UNSIGNED_MAX : CONSTANTS.INT32_MAX;
|
||||||
|
integer = true;
|
||||||
|
} else if (
|
||||||
|
isColumnType<
|
||||||
|
| PgDoublePrecision<any>
|
||||||
|
| MySqlReal<any>
|
||||||
|
| MySqlDouble<any>
|
||||||
|
| SingleStoreReal<any>
|
||||||
|
| SingleStoreDouble<any>
|
||||||
|
| SQLiteReal<any>
|
||||||
|
>(column, [
|
||||||
|
'PgDoublePrecision',
|
||||||
|
'MySqlReal',
|
||||||
|
'MySqlDouble',
|
||||||
|
'SingleStoreReal',
|
||||||
|
'SingleStoreDouble',
|
||||||
|
'SQLiteReal',
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
min = unsigned ? 0 : CONSTANTS.INT48_MIN;
|
||||||
|
max = unsigned ? CONSTANTS.INT48_UNSIGNED_MAX : CONSTANTS.INT48_MAX;
|
||||||
|
} else if (
|
||||||
|
isColumnType<
|
||||||
|
| PgBigInt53<any>
|
||||||
|
| PgBigSerial53<any>
|
||||||
|
| MySqlBigInt53<any>
|
||||||
|
| MySqlSerial<any>
|
||||||
|
| SingleStoreBigInt53<any>
|
||||||
|
| SingleStoreSerial<any>
|
||||||
|
| SQLiteInteger<any>
|
||||||
|
>(
|
||||||
|
column,
|
||||||
|
[
|
||||||
|
'PgBigInt53',
|
||||||
|
'PgBigSerial53',
|
||||||
|
'MySqlBigInt53',
|
||||||
|
'MySqlSerial',
|
||||||
|
'SingleStoreBigInt53',
|
||||||
|
'SingleStoreSerial',
|
||||||
|
'SQLiteInteger',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
unsigned = unsigned || isColumnType(column, ['MySqlSerial', 'SingleStoreSerial']);
|
||||||
|
min = unsigned ? 0 : Number.MIN_SAFE_INTEGER;
|
||||||
|
max = Number.MAX_SAFE_INTEGER;
|
||||||
|
integer = true;
|
||||||
|
} else if (isColumnType<MySqlYear<any> | SingleStoreYear<any>>(column, ['MySqlYear', 'SingleStoreYear'])) {
|
||||||
|
min = 1901;
|
||||||
|
max = 2155;
|
||||||
|
integer = true;
|
||||||
|
} else {
|
||||||
|
min = Number.MIN_SAFE_INTEGER;
|
||||||
|
max = Number.MAX_SAFE_INTEGER;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = integer ? 'Integer' : 'Number';
|
||||||
|
return t[key]({
|
||||||
|
minimum: min,
|
||||||
|
maximum: max,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bigintColumnToSchema(column: Column, t: typeof typebox): TSchema {
|
||||||
|
const unsigned = column.getSQLType().includes('unsigned');
|
||||||
|
const min = unsigned ? 0n : CONSTANTS.INT64_MIN;
|
||||||
|
const max = unsigned ? CONSTANTS.INT64_UNSIGNED_MAX : CONSTANTS.INT64_MAX;
|
||||||
|
|
||||||
|
return t.BigInt({
|
||||||
|
minimum: min,
|
||||||
|
maximum: max,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringColumnToSchema(column: Column, t: typeof typebox): TSchema {
|
||||||
|
if (isColumnType<PgUUID<ColumnBaseConfig<'string', 'PgUUID'>>>(column, ['PgUUID'])) {
|
||||||
|
return t.String({ format: 'uuid' });
|
||||||
|
} else if (
|
||||||
|
isColumnType<PgBinaryVector<ColumnBaseConfig<'string', 'PgBinaryVector'> & { dimensions: number }>>(column, [
|
||||||
|
'PgBinaryVector',
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
return t.RegExp(/^[01]+$/, column.dimensions ? { maxLength: column.dimensions } : undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
let max: number | undefined;
|
||||||
|
let fixed = false;
|
||||||
|
|
||||||
|
if (isColumnType<PgVarchar<any> | SQLiteText<any>>(column, ['PgVarchar', 'SQLiteText'])) {
|
||||||
|
max = column.length;
|
||||||
|
} else if (
|
||||||
|
isColumnType<MySqlVarChar<any> | SingleStoreVarChar<any>>(column, ['MySqlVarChar', 'SingleStoreVarChar'])
|
||||||
|
) {
|
||||||
|
max = column.length ?? CONSTANTS.INT16_UNSIGNED_MAX;
|
||||||
|
} else if (isColumnType<MySqlText<any> | SingleStoreText<any>>(column, ['MySqlText', 'SingleStoreText'])) {
|
||||||
|
if (column.textType === 'longtext') {
|
||||||
|
max = CONSTANTS.INT32_UNSIGNED_MAX;
|
||||||
|
} else if (column.textType === 'mediumtext') {
|
||||||
|
max = CONSTANTS.INT24_UNSIGNED_MAX;
|
||||||
|
} else if (column.textType === 'text') {
|
||||||
|
max = CONSTANTS.INT16_UNSIGNED_MAX;
|
||||||
|
} else {
|
||||||
|
max = CONSTANTS.INT8_UNSIGNED_MAX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isColumnType<PgChar<any> | MySqlChar<any> | SingleStoreChar<any>>(column, [
|
||||||
|
'PgChar',
|
||||||
|
'MySqlChar',
|
||||||
|
'SingleStoreChar',
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
max = column.length;
|
||||||
|
fixed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: Partial<StringOptions> = {};
|
||||||
|
|
||||||
|
if (max !== undefined && fixed) {
|
||||||
|
options.minLength = max;
|
||||||
|
options.maxLength = max;
|
||||||
|
} else if (max !== undefined) {
|
||||||
|
options.maxLength = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.String(Object.keys(options).length > 0 ? options : undefined);
|
||||||
|
}
|
||||||
107
src/column.types.ts
Normal file
107
src/column.types.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import type * as t from '@alkdev/typebox';
|
||||||
|
import type { Assume, Column } from 'drizzle-orm';
|
||||||
|
import type { ArrayHasAtLeastOneValue, BufferSchema, ColumnIsGeneratedAlwaysAs, IsNever, JsonSchema } from './utils.ts';
|
||||||
|
|
||||||
|
export type GetEnumValuesFromColumn<TColumn extends Column> = TColumn['_'] extends { enumValues: [string, ...string[]] }
|
||||||
|
? TColumn['_']['enumValues']
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
export type GetBaseColumn<TColumn extends Column> = TColumn['_'] extends { baseColumn: Column | never | undefined }
|
||||||
|
? IsNever<TColumn['_']['baseColumn']> extends false ? TColumn['_']['baseColumn']
|
||||||
|
: undefined
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
export type EnumValuesToEnum<TEnumValues extends [string, ...string[]]> = { [K in TEnumValues[number]]: K };
|
||||||
|
|
||||||
|
export type GetTypeboxType<
|
||||||
|
TData,
|
||||||
|
TDataType extends string,
|
||||||
|
TColumnType extends string,
|
||||||
|
TEnumValues extends [string, ...string[]] | undefined,
|
||||||
|
TBaseColumn extends Column | undefined,
|
||||||
|
> = TColumnType extends
|
||||||
|
| 'MySqlTinyInt'
|
||||||
|
| 'SingleStoreTinyInt'
|
||||||
|
| 'PgSmallInt'
|
||||||
|
| 'PgSmallSerial'
|
||||||
|
| 'MySqlSmallInt'
|
||||||
|
| 'MySqlMediumInt'
|
||||||
|
| 'SingleStoreSmallInt'
|
||||||
|
| 'SingleStoreMediumInt'
|
||||||
|
| 'PgInteger'
|
||||||
|
| 'PgSerial'
|
||||||
|
| 'MySqlInt'
|
||||||
|
| 'SingleStoreInt'
|
||||||
|
| 'PgBigInt53'
|
||||||
|
| 'PgBigSerial53'
|
||||||
|
| 'MySqlBigInt53'
|
||||||
|
| 'MySqlSerial'
|
||||||
|
| 'SingleStoreBigInt53'
|
||||||
|
| 'SingleStoreSerial'
|
||||||
|
| 'SQLiteInteger'
|
||||||
|
| 'MySqlYear'
|
||||||
|
| 'SingleStoreYear' ? t.TInteger
|
||||||
|
: TColumnType extends 'PgBinaryVector' ? t.TRegExp
|
||||||
|
: TBaseColumn extends Column ? t.TArray<
|
||||||
|
GetTypeboxType<
|
||||||
|
TBaseColumn['_']['data'],
|
||||||
|
TBaseColumn['_']['dataType'],
|
||||||
|
TBaseColumn['_']['columnType'],
|
||||||
|
GetEnumValuesFromColumn<TBaseColumn>,
|
||||||
|
GetBaseColumn<TBaseColumn>
|
||||||
|
>
|
||||||
|
>
|
||||||
|
: ArrayHasAtLeastOneValue<TEnumValues> extends true
|
||||||
|
? t.TEnum<EnumValuesToEnum<Assume<TEnumValues, [string, ...string[]]>>>
|
||||||
|
: TData extends infer TTuple extends [any, ...any[]] ? t.TTuple<
|
||||||
|
Assume<{ [K in keyof TTuple]: GetTypeboxType<TTuple[K], string, string, undefined, undefined> }, [any, ...any[]]>
|
||||||
|
>
|
||||||
|
: TData extends Date ? t.TDate
|
||||||
|
: TData extends Buffer ? BufferSchema
|
||||||
|
: TDataType extends 'array'
|
||||||
|
? t.TArray<GetTypeboxType<Assume<TData, any[]>[number], string, string, undefined, undefined>>
|
||||||
|
: TData extends infer TDict extends Record<string, any>
|
||||||
|
? t.TObject<{ [K in keyof TDict]: GetTypeboxType<TDict[K], string, string, undefined, undefined> }>
|
||||||
|
: TDataType extends 'json' ? JsonSchema
|
||||||
|
: TData extends number ? t.TNumber
|
||||||
|
: TData extends bigint ? t.TBigInt
|
||||||
|
: TData extends boolean ? t.TBoolean
|
||||||
|
: TData extends string ? t.TString
|
||||||
|
: t.TAny;
|
||||||
|
|
||||||
|
type HandleSelectColumn<
|
||||||
|
TSchema extends t.TSchema,
|
||||||
|
TColumn extends Column,
|
||||||
|
> = TColumn['_']['notNull'] extends true ? TSchema
|
||||||
|
: t.Union<[TSchema, t.TNull]>;
|
||||||
|
|
||||||
|
type HandleInsertColumn<
|
||||||
|
TSchema extends t.TSchema,
|
||||||
|
TColumn extends Column,
|
||||||
|
> = ColumnIsGeneratedAlwaysAs<TColumn> extends true ? never
|
||||||
|
: TColumn['_']['notNull'] extends true ? TColumn['_']['hasDefault'] extends true ? t.TOptional<TSchema>
|
||||||
|
: TSchema
|
||||||
|
: t.TOptional<t.Union<[TSchema, t.TNull]>>;
|
||||||
|
|
||||||
|
type HandleUpdateColumn<
|
||||||
|
TSchema extends t.TSchema,
|
||||||
|
TColumn extends Column,
|
||||||
|
> = ColumnIsGeneratedAlwaysAs<TColumn> extends true ? never
|
||||||
|
: TColumn['_']['notNull'] extends true ? t.TOptional<TSchema>
|
||||||
|
: t.TOptional<t.Union<[TSchema, t.TNull]>>;
|
||||||
|
|
||||||
|
export type HandleColumn<
|
||||||
|
TType extends 'select' | 'insert' | 'update',
|
||||||
|
TColumn extends Column,
|
||||||
|
> = GetTypeboxType<
|
||||||
|
TColumn['_']['data'],
|
||||||
|
TColumn['_']['dataType'],
|
||||||
|
TColumn['_']['columnType'],
|
||||||
|
GetEnumValuesFromColumn<TColumn>,
|
||||||
|
GetBaseColumn<TColumn>
|
||||||
|
> extends infer TSchema extends t.TSchema ? TSchema extends t.TAny ? t.TAny
|
||||||
|
: TType extends 'select' ? HandleSelectColumn<TSchema, TColumn>
|
||||||
|
: TType extends 'insert' ? HandleInsertColumn<TSchema, TColumn>
|
||||||
|
: TType extends 'update' ? HandleUpdateColumn<TSchema, TColumn>
|
||||||
|
: TSchema
|
||||||
|
: t.TAny;
|
||||||
20
src/constants.ts
Normal file
20
src/constants.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export const CONSTANTS = {
|
||||||
|
INT8_MIN: -128,
|
||||||
|
INT8_MAX: 127,
|
||||||
|
INT8_UNSIGNED_MAX: 255,
|
||||||
|
INT16_MIN: -32768,
|
||||||
|
INT16_MAX: 32767,
|
||||||
|
INT16_UNSIGNED_MAX: 65535,
|
||||||
|
INT24_MIN: -8388608,
|
||||||
|
INT24_MAX: 8388607,
|
||||||
|
INT24_UNSIGNED_MAX: 16777215,
|
||||||
|
INT32_MIN: -2147483648,
|
||||||
|
INT32_MAX: 2147483647,
|
||||||
|
INT32_UNSIGNED_MAX: 4294967295,
|
||||||
|
INT48_MIN: -140737488355328,
|
||||||
|
INT48_MAX: 140737488355327,
|
||||||
|
INT48_UNSIGNED_MAX: 281474976710655,
|
||||||
|
INT64_MIN: -9223372036854775808n,
|
||||||
|
INT64_MAX: 9223372036854775807n,
|
||||||
|
INT64_UNSIGNED_MAX: 18446744073709551615n,
|
||||||
|
};
|
||||||
2
src/index.ts
Normal file
2
src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './schema.ts';
|
||||||
|
export * from './schema.types.ts';
|
||||||
144
src/schema.ts
Normal file
144
src/schema.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import { Type as t } from '@alkdev/typebox';
|
||||||
|
import type { TSchema } from '@alkdev/typebox';
|
||||||
|
import { Column, getTableColumns, getViewSelectedFields, is, isTable, isView, SQL } from 'drizzle-orm';
|
||||||
|
import type { Table, View } from 'drizzle-orm';
|
||||||
|
import type { PgEnum } from 'drizzle-orm/pg-core';
|
||||||
|
import { columnToSchema, mapEnumValues } from './column.ts';
|
||||||
|
import type { Conditions } from './schema.types.internal.ts';
|
||||||
|
import type {
|
||||||
|
CreateInsertSchema,
|
||||||
|
CreateSchemaFactoryOptions,
|
||||||
|
CreateSelectSchema,
|
||||||
|
CreateUpdateSchema,
|
||||||
|
} from './schema.types.ts';
|
||||||
|
import { isPgEnum } from './utils.ts';
|
||||||
|
|
||||||
|
function getColumns(tableLike: Table | View) {
|
||||||
|
return isTable(tableLike) ? getTableColumns(tableLike) : getViewSelectedFields(tableLike);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleColumns(
|
||||||
|
columns: Record<string, any>,
|
||||||
|
refinements: Record<string, any>,
|
||||||
|
conditions: Conditions,
|
||||||
|
factory?: CreateSchemaFactoryOptions,
|
||||||
|
): TSchema {
|
||||||
|
const columnSchemas: Record<string, TSchema> = {};
|
||||||
|
|
||||||
|
for (const [key, selected] of Object.entries(columns)) {
|
||||||
|
if (!is(selected, Column) && !is(selected, SQL) && !is(selected, SQL.Aliased) && typeof selected === 'object') {
|
||||||
|
const columns = isTable(selected) || isView(selected) ? getColumns(selected) : selected;
|
||||||
|
columnSchemas[key] = handleColumns(columns, refinements[key] ?? {}, conditions, factory);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refinement = refinements[key];
|
||||||
|
if (refinement !== undefined && typeof refinement !== 'function') {
|
||||||
|
columnSchemas[key] = refinement;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const column = is(selected, Column) ? selected : undefined;
|
||||||
|
const schema = column ? columnToSchema(column, factory?.typeboxInstance ?? t) : t.Any();
|
||||||
|
const refined = typeof refinement === 'function' ? refinement(schema) : schema;
|
||||||
|
|
||||||
|
if (conditions.never(column)) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
columnSchemas[key] = refined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (column) {
|
||||||
|
if (conditions.nullable(column)) {
|
||||||
|
columnSchemas[key] = t.Union([columnSchemas[key]!, t.Null()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditions.optional(column)) {
|
||||||
|
columnSchemas[key] = t.Optional(columnSchemas[key]!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.Object(columnSchemas) as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEnum(enum_: PgEnum<any>, factory?: CreateSchemaFactoryOptions) {
|
||||||
|
const typebox: typeof t = factory?.typeboxInstance ?? t;
|
||||||
|
return typebox.Enum(mapEnumValues(enum_.enumValues));
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectConditions: Conditions = {
|
||||||
|
never: () => false,
|
||||||
|
optional: () => false,
|
||||||
|
nullable: (column) => !column.notNull,
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertConditions: Conditions = {
|
||||||
|
never: (column) => column?.generated?.type === 'always' || column?.generatedIdentity?.type === 'always',
|
||||||
|
optional: (column) => !column.notNull || (column.notNull && column.hasDefault),
|
||||||
|
nullable: (column) => !column.notNull,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateConditions: Conditions = {
|
||||||
|
never: (column) => column?.generated?.type === 'always' || column?.generatedIdentity?.type === 'always',
|
||||||
|
optional: () => true,
|
||||||
|
nullable: (column) => !column.notNull,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createSelectSchema: CreateSelectSchema = (
|
||||||
|
entity: Table | View | PgEnum<[string, ...string[]]>,
|
||||||
|
refine?: Record<string, any>,
|
||||||
|
) => {
|
||||||
|
if (isPgEnum(entity)) {
|
||||||
|
return handleEnum(entity);
|
||||||
|
}
|
||||||
|
const columns = getColumns(entity);
|
||||||
|
return handleColumns(columns, refine ?? {}, selectConditions) as any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createInsertSchema: CreateInsertSchema = (
|
||||||
|
entity: Table,
|
||||||
|
refine?: Record<string, any>,
|
||||||
|
) => {
|
||||||
|
const columns = getColumns(entity);
|
||||||
|
return handleColumns(columns, refine ?? {}, insertConditions) as any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createUpdateSchema: CreateUpdateSchema = (
|
||||||
|
entity: Table,
|
||||||
|
refine?: Record<string, any>,
|
||||||
|
) => {
|
||||||
|
const columns = getColumns(entity);
|
||||||
|
return handleColumns(columns, refine ?? {}, updateConditions) as any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createSchemaFactory(options?: CreateSchemaFactoryOptions) {
|
||||||
|
const createSelectSchema: CreateSelectSchema = (
|
||||||
|
entity: Table | View | PgEnum<[string, ...string[]]>,
|
||||||
|
refine?: Record<string, any>,
|
||||||
|
) => {
|
||||||
|
if (isPgEnum(entity)) {
|
||||||
|
return handleEnum(entity, options);
|
||||||
|
}
|
||||||
|
const columns = getColumns(entity);
|
||||||
|
return handleColumns(columns, refine ?? {}, selectConditions, options) as any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createInsertSchema: CreateInsertSchema = (
|
||||||
|
entity: Table,
|
||||||
|
refine?: Record<string, any>,
|
||||||
|
) => {
|
||||||
|
const columns = getColumns(entity);
|
||||||
|
return handleColumns(columns, refine ?? {}, insertConditions, options) as any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUpdateSchema: CreateUpdateSchema = (
|
||||||
|
entity: Table,
|
||||||
|
refine?: Record<string, any>,
|
||||||
|
) => {
|
||||||
|
const columns = getColumns(entity);
|
||||||
|
return handleColumns(columns, refine ?? {}, updateConditions, options) as any;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { createSelectSchema, createInsertSchema, createUpdateSchema };
|
||||||
|
}
|
||||||
94
src/schema.types.internal.ts
Normal file
94
src/schema.types.internal.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import type * as t from '@alkdev/typebox';
|
||||||
|
import type { Assume, Column, DrizzleTypeError, SelectedFieldsFlat, Simplify, Table, View } from 'drizzle-orm';
|
||||||
|
import type { GetBaseColumn, GetEnumValuesFromColumn, GetTypeboxType, HandleColumn } from './column.types.ts';
|
||||||
|
import type { GetSelection, RemoveNever } from './utils.ts';
|
||||||
|
|
||||||
|
export interface Conditions {
|
||||||
|
never: (column?: Column) => boolean;
|
||||||
|
optional: (column: Column) => boolean;
|
||||||
|
nullable: (column: Column) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BuildRefineColumns<
|
||||||
|
TColumns extends Record<string, any>,
|
||||||
|
> = Simplify<
|
||||||
|
RemoveNever<
|
||||||
|
{
|
||||||
|
[K in keyof TColumns]: TColumns[K] extends infer TColumn extends Column ? GetTypeboxType<
|
||||||
|
TColumn['_']['data'],
|
||||||
|
TColumn['_']['dataType'],
|
||||||
|
TColumn['_']['columnType'],
|
||||||
|
GetEnumValuesFromColumn<TColumn>,
|
||||||
|
GetBaseColumn<TColumn>
|
||||||
|
> extends infer TSchema extends t.TSchema ? TSchema
|
||||||
|
: t.TAny
|
||||||
|
: TColumns[K] extends infer TObject extends SelectedFieldsFlat<Column> | Table | View
|
||||||
|
? BuildRefineColumns<GetSelection<TObject>>
|
||||||
|
: TColumns[K];
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type BuildRefine<
|
||||||
|
TColumns extends Record<string, any>,
|
||||||
|
> = BuildRefineColumns<TColumns> extends infer TBuildColumns ? {
|
||||||
|
[K in keyof TBuildColumns]?: TBuildColumns[K] extends t.TSchema
|
||||||
|
? ((schema: TBuildColumns[K]) => t.TSchema) | t.TSchema
|
||||||
|
: TBuildColumns[K] extends Record<string, any> ? Simplify<BuildRefine<TBuildColumns[K]>>
|
||||||
|
: never;
|
||||||
|
}
|
||||||
|
: never;
|
||||||
|
|
||||||
|
type HandleRefinement<
|
||||||
|
TType extends 'select' | 'insert' | 'update',
|
||||||
|
TRefinement extends t.TSchema | ((schema: t.TSchema) => t.TSchema),
|
||||||
|
TColumn extends Column,
|
||||||
|
> = TRefinement extends (schema: any) => t.TSchema ? (TColumn['_']['notNull'] extends true ? ReturnType<TRefinement>
|
||||||
|
: t.TUnion<[ReturnType<TRefinement>, t.TNull]>) extends infer TSchema
|
||||||
|
? TType extends 'update' ? t.TOptional<Assume<TSchema, t.TSchema>> : TSchema
|
||||||
|
: t.TSchema
|
||||||
|
: TRefinement;
|
||||||
|
|
||||||
|
type IsRefinementDefined<TRefinements, TKey extends string> = TKey extends keyof TRefinements
|
||||||
|
? TRefinements[TKey] extends t.TSchema | ((schema: any) => any) ? true
|
||||||
|
: false
|
||||||
|
: false;
|
||||||
|
|
||||||
|
export type BuildSchema<
|
||||||
|
TType extends 'select' | 'insert' | 'update',
|
||||||
|
TColumns extends Record<string, any>,
|
||||||
|
TRefinements extends Record<string, any> | undefined,
|
||||||
|
> = t.TObject<
|
||||||
|
Simplify<
|
||||||
|
RemoveNever<
|
||||||
|
{
|
||||||
|
[K in keyof TColumns]: TColumns[K] extends infer TColumn extends Column
|
||||||
|
? TRefinements extends object
|
||||||
|
? IsRefinementDefined<TRefinements, Assume<K, string>> extends true
|
||||||
|
? HandleRefinement<TType, TRefinements[Assume<K, keyof TRefinements>], TColumn>
|
||||||
|
: HandleColumn<TType, TColumn>
|
||||||
|
: HandleColumn<TType, TColumn>
|
||||||
|
: TColumns[K] extends infer TObject extends SelectedFieldsFlat<Column> | Table | View ? BuildSchema<
|
||||||
|
TType,
|
||||||
|
GetSelection<TObject>,
|
||||||
|
TRefinements extends object
|
||||||
|
? TRefinements[Assume<K, keyof TRefinements>] extends infer TNestedRefinements extends object
|
||||||
|
? TNestedRefinements
|
||||||
|
: undefined
|
||||||
|
: undefined
|
||||||
|
>
|
||||||
|
: t.TAny;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type NoUnknownKeys<
|
||||||
|
TRefinement extends Record<string, any>,
|
||||||
|
TCompare extends Record<string, any>,
|
||||||
|
> = {
|
||||||
|
[K in keyof TRefinement]: K extends keyof TCompare ? TRefinement[K] extends t.TSchema ? TRefinement[K]
|
||||||
|
: TRefinement[K] extends Record<string, t.TSchema> ? NoUnknownKeys<TRefinement[K], TCompare[K]>
|
||||||
|
: TRefinement[K]
|
||||||
|
: DrizzleTypeError<`Found unknown key in refinement: "${K & string}"`>;
|
||||||
|
};
|
||||||
53
src/schema.types.ts
Normal file
53
src/schema.types.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import type * as t from '@alkdev/typebox';
|
||||||
|
import type { Table, View } from 'drizzle-orm';
|
||||||
|
import type { PgEnum } from 'drizzle-orm/pg-core';
|
||||||
|
import type { EnumValuesToEnum } from './column.types.ts';
|
||||||
|
import type { BuildRefine, BuildSchema, NoUnknownKeys } from './schema.types.internal.ts';
|
||||||
|
|
||||||
|
export interface CreateSelectSchema {
|
||||||
|
<TTable extends Table>(table: TTable): BuildSchema<'select', TTable['_']['columns'], undefined>;
|
||||||
|
<
|
||||||
|
TTable extends Table,
|
||||||
|
TRefine extends BuildRefine<TTable['_']['columns']>,
|
||||||
|
>(
|
||||||
|
table: TTable,
|
||||||
|
refine?: NoUnknownKeys<TRefine, TTable['$inferSelect']>,
|
||||||
|
): BuildSchema<'select', TTable['_']['columns'], TRefine>;
|
||||||
|
|
||||||
|
<TView extends View>(view: TView): BuildSchema<'select', TView['_']['selectedFields'], undefined>;
|
||||||
|
<
|
||||||
|
TView extends View,
|
||||||
|
TRefine extends BuildRefine<TView['_']['selectedFields']>,
|
||||||
|
>(
|
||||||
|
view: TView,
|
||||||
|
refine: NoUnknownKeys<TRefine, TView['$inferSelect']>,
|
||||||
|
): BuildSchema<'select', TView['_']['selectedFields'], TRefine>;
|
||||||
|
|
||||||
|
<TEnum extends PgEnum<any>>(enum_: TEnum): t.TEnum<EnumValuesToEnum<TEnum['enumValues']>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateInsertSchema {
|
||||||
|
<TTable extends Table>(table: TTable): BuildSchema<'insert', TTable['_']['columns'], undefined>;
|
||||||
|
<
|
||||||
|
TTable extends Table,
|
||||||
|
TRefine extends BuildRefine<Pick<TTable['_']['columns'], keyof TTable['$inferInsert']>>,
|
||||||
|
>(
|
||||||
|
table: TTable,
|
||||||
|
refine?: NoUnknownKeys<TRefine, TTable['$inferInsert']>,
|
||||||
|
): BuildSchema<'insert', TTable['_']['columns'], TRefine>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateUpdateSchema {
|
||||||
|
<TTable extends Table>(table: TTable): BuildSchema<'update', TTable['_']['columns'], undefined>;
|
||||||
|
<
|
||||||
|
TTable extends Table,
|
||||||
|
TRefine extends BuildRefine<Pick<TTable['_']['columns'], keyof TTable['$inferInsert']>>,
|
||||||
|
>(
|
||||||
|
table: TTable,
|
||||||
|
refine?: TRefine,
|
||||||
|
): BuildSchema<'update', TTable['_']['columns'], TRefine>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateSchemaFactoryOptions {
|
||||||
|
typeboxInstance?: any;
|
||||||
|
}
|
||||||
50
src/utils.ts
Normal file
50
src/utils.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import type { Kind, Static, TSchema } from '@alkdev/typebox';
|
||||||
|
import type { Column, SelectedFieldsFlat, Table, View } from 'drizzle-orm';
|
||||||
|
import type { PgEnum } from 'drizzle-orm/pg-core';
|
||||||
|
import type { literalSchema } from './column.ts';
|
||||||
|
|
||||||
|
export function isColumnType<T extends Column>(column: Column, columnTypes: string[]): column is T {
|
||||||
|
return columnTypes.includes(column.columnType);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWithEnum(column: Column): column is typeof column & { enumValues: [string, ...string[]] } {
|
||||||
|
return 'enumValues' in column && Array.isArray(column.enumValues) && column.enumValues.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isPgEnum: (entity: any) => entity is PgEnum<[string, ...string[]]> = isWithEnum as any;
|
||||||
|
|
||||||
|
type Literal = Static<typeof literalSchema>;
|
||||||
|
export type Json = Literal | { [key: string]: Json } | Json[];
|
||||||
|
export interface JsonSchema extends TSchema {
|
||||||
|
[Kind]: 'Union';
|
||||||
|
static: Json;
|
||||||
|
anyOf: Json;
|
||||||
|
}
|
||||||
|
export interface BufferSchema extends TSchema {
|
||||||
|
[Kind]: 'Buffer';
|
||||||
|
static: Buffer;
|
||||||
|
type: 'buffer';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IsNever<T> = [T] extends [never] ? true : false;
|
||||||
|
|
||||||
|
export type ArrayHasAtLeastOneValue<TEnum extends [any, ...any[]] | undefined> = TEnum extends [infer TString, ...any[]]
|
||||||
|
? TString extends `${infer TLiteral}` ? TLiteral extends any ? true
|
||||||
|
: false
|
||||||
|
: false
|
||||||
|
: false;
|
||||||
|
|
||||||
|
export type ColumnIsGeneratedAlwaysAs<TColumn extends Column> = TColumn['_']['identity'] extends 'always' ? true
|
||||||
|
: TColumn['_']['generated'] extends undefined ? false
|
||||||
|
: TColumn['_']['generated'] extends infer TGenerated extends { type: string }
|
||||||
|
? TGenerated['type'] extends 'byDefault' ? false
|
||||||
|
: true
|
||||||
|
: true;
|
||||||
|
|
||||||
|
export type RemoveNever<T> = {
|
||||||
|
[K in keyof T as T[K] extends never ? never : K]: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetSelection<T extends SelectedFieldsFlat<Column> | Table | View> = T extends Table ? T['_']['columns']
|
||||||
|
: T extends View ? T['_']['selectedFields']
|
||||||
|
: T;
|
||||||
495
tests/mysql.test.ts
Normal file
495
tests/mysql.test.ts
Normal file
@@ -0,0 +1,495 @@
|
|||||||
|
import { Type as t } from '@alkdev/typebox';
|
||||||
|
import { type Equal, sql } from 'drizzle-orm';
|
||||||
|
import { customType, int, mysqlSchema, mysqlTable, mysqlView, serial, text } from 'drizzle-orm/mysql-core';
|
||||||
|
import { test } from 'vitest';
|
||||||
|
import { jsonSchema } from '~/column.ts';
|
||||||
|
import { CONSTANTS } from '~/constants.ts';
|
||||||
|
import { createInsertSchema, createSelectSchema, createUpdateSchema } from '../src';
|
||||||
|
import { Expect, expectSchemaShape } from './utils.ts';
|
||||||
|
|
||||||
|
const intSchema = t.Integer({
|
||||||
|
minimum: CONSTANTS.INT32_MIN,
|
||||||
|
maximum: CONSTANTS.INT32_MAX,
|
||||||
|
});
|
||||||
|
const serialNumberModeSchema = t.Integer({
|
||||||
|
minimum: 0,
|
||||||
|
maximum: Number.MAX_SAFE_INTEGER,
|
||||||
|
});
|
||||||
|
const textSchema = t.String({ maxLength: CONSTANTS.INT16_UNSIGNED_MAX });
|
||||||
|
|
||||||
|
test('table - select', (tc) => {
|
||||||
|
const table = mysqlTable('test', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createSelectSchema(table);
|
||||||
|
const expected = t.Object({ id: serialNumberModeSchema, name: textSchema });
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('table in schema - select', (tc) => {
|
||||||
|
const schema = mysqlSchema('test');
|
||||||
|
const table = schema.table('test', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createSelectSchema(table);
|
||||||
|
const expected = t.Object({ id: serialNumberModeSchema, name: textSchema });
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('table - insert', (tc) => {
|
||||||
|
const table = mysqlTable('test', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
age: int(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createInsertSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
id: t.Optional(serialNumberModeSchema),
|
||||||
|
name: textSchema,
|
||||||
|
age: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('table - update', (tc) => {
|
||||||
|
const table = mysqlTable('test', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
age: int(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createUpdateSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
id: t.Optional(serialNumberModeSchema),
|
||||||
|
name: t.Optional(textSchema),
|
||||||
|
age: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('view qb - select', (tc) => {
|
||||||
|
const table = mysqlTable('test', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
});
|
||||||
|
const view = mysqlView('test').as((qb) => qb.select({ id: table.id, age: sql``.as('age') }).from(table));
|
||||||
|
|
||||||
|
const result = createSelectSchema(view);
|
||||||
|
const expected = t.Object({ id: serialNumberModeSchema, age: t.Any() });
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('view columns - select', (tc) => {
|
||||||
|
const view = mysqlView('test', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
}).as(sql``);
|
||||||
|
|
||||||
|
const result = createSelectSchema(view);
|
||||||
|
const expected = t.Object({ id: serialNumberModeSchema, name: textSchema });
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('view with nested fields - select', (tc) => {
|
||||||
|
const table = mysqlTable('test', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
});
|
||||||
|
const view = mysqlView('test').as((qb) =>
|
||||||
|
qb.select({
|
||||||
|
id: table.id,
|
||||||
|
nested: {
|
||||||
|
name: table.name,
|
||||||
|
age: sql``.as('age'),
|
||||||
|
},
|
||||||
|
table,
|
||||||
|
}).from(table)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = createSelectSchema(view);
|
||||||
|
const expected = t.Object({
|
||||||
|
id: serialNumberModeSchema,
|
||||||
|
nested: t.Object({ name: textSchema, age: t.Any() }),
|
||||||
|
table: t.Object({ id: serialNumberModeSchema, name: textSchema }),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nullability - select', (tc) => {
|
||||||
|
const table = mysqlTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().default(1),
|
||||||
|
c4: int().notNull().default(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createSelectSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Union([intSchema, t.Null()]),
|
||||||
|
c2: intSchema,
|
||||||
|
c3: t.Union([intSchema, t.Null()]),
|
||||||
|
c4: intSchema,
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nullability - insert', (tc) => {
|
||||||
|
const table = mysqlTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().default(1),
|
||||||
|
c4: int().notNull().default(1),
|
||||||
|
c5: int().generatedAlwaysAs(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createInsertSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
c2: intSchema,
|
||||||
|
c3: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
c4: t.Optional(intSchema),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nullability - update', (tc) => {
|
||||||
|
const table = mysqlTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().default(1),
|
||||||
|
c4: int().notNull().default(1),
|
||||||
|
c5: int().generatedAlwaysAs(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createUpdateSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
c2: t.Optional(intSchema),
|
||||||
|
c3: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
c4: t.Optional(intSchema),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refine table - select', (tc) => {
|
||||||
|
const table = mysqlTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createSelectSchema(table, {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Union([intSchema, t.Null()]),
|
||||||
|
c2: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refine table - select with custom data type', (tc) => {
|
||||||
|
const customText = customType({ dataType: () => 'text' });
|
||||||
|
const table = mysqlTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().notNull(),
|
||||||
|
c4: customText(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const customTextSchema = t.String({ minLength: 1, maxLength: 100 });
|
||||||
|
const result = createSelectSchema(table, {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
c4: customTextSchema,
|
||||||
|
});
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Union([intSchema, t.Null()]),
|
||||||
|
c2: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
c4: customTextSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refine table - insert', (tc) => {
|
||||||
|
const table = mysqlTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().notNull(),
|
||||||
|
c4: int().generatedAlwaysAs(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createInsertSchema(table, {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
c2: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refine table - update', (tc) => {
|
||||||
|
const table = mysqlTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().notNull(),
|
||||||
|
c4: int().generatedAlwaysAs(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createUpdateSchema(table, {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
c2: t.Optional(t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 })),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refine view - select', (tc) => {
|
||||||
|
const table = mysqlTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int(),
|
||||||
|
c3: int(),
|
||||||
|
c4: int(),
|
||||||
|
c5: int(),
|
||||||
|
c6: int(),
|
||||||
|
});
|
||||||
|
const view = mysqlView('test').as((qb) =>
|
||||||
|
qb.select({
|
||||||
|
c1: table.c1,
|
||||||
|
c2: table.c2,
|
||||||
|
c3: table.c3,
|
||||||
|
nested: {
|
||||||
|
c4: table.c4,
|
||||||
|
c5: table.c5,
|
||||||
|
c6: table.c6,
|
||||||
|
},
|
||||||
|
table,
|
||||||
|
}).from(table)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = createSelectSchema(view, {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
nested: {
|
||||||
|
c5: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c6: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Union([intSchema, t.Null()]),
|
||||||
|
c2: t.Union([t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }), t.Null()]),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
nested: t.Object({
|
||||||
|
c4: t.Union([intSchema, t.Null()]),
|
||||||
|
c5: t.Union([t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }), t.Null()]),
|
||||||
|
c6: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
}),
|
||||||
|
table: t.Object({
|
||||||
|
c1: t.Union([intSchema, t.Null()]),
|
||||||
|
c2: t.Union([t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }), t.Null()]),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
c4: t.Union([intSchema, t.Null()]),
|
||||||
|
c5: t.Union([intSchema, t.Null()]),
|
||||||
|
c6: t.Union([intSchema, t.Null()]),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('all data types', (tc) => {
|
||||||
|
const table = mysqlTable('test', ({
|
||||||
|
bigint,
|
||||||
|
binary,
|
||||||
|
boolean,
|
||||||
|
char,
|
||||||
|
date,
|
||||||
|
datetime,
|
||||||
|
decimal,
|
||||||
|
double,
|
||||||
|
float,
|
||||||
|
int,
|
||||||
|
json,
|
||||||
|
mediumint,
|
||||||
|
mysqlEnum,
|
||||||
|
real,
|
||||||
|
serial,
|
||||||
|
smallint,
|
||||||
|
text,
|
||||||
|
time,
|
||||||
|
timestamp,
|
||||||
|
tinyint,
|
||||||
|
varchar,
|
||||||
|
varbinary,
|
||||||
|
year,
|
||||||
|
longtext,
|
||||||
|
mediumtext,
|
||||||
|
tinytext,
|
||||||
|
}) => ({
|
||||||
|
bigint1: bigint({ mode: 'number' }).notNull(),
|
||||||
|
bigint2: bigint({ mode: 'bigint' }).notNull(),
|
||||||
|
bigint3: bigint({ unsigned: true, mode: 'number' }).notNull(),
|
||||||
|
bigint4: bigint({ unsigned: true, mode: 'bigint' }).notNull(),
|
||||||
|
binary: binary({ length: 10 }).notNull(),
|
||||||
|
boolean: boolean().notNull(),
|
||||||
|
char1: char({ length: 10 }).notNull(),
|
||||||
|
char2: char({ length: 1, enum: ['a', 'b', 'c'] }).notNull(),
|
||||||
|
date1: date({ mode: 'date' }).notNull(),
|
||||||
|
date2: date({ mode: 'string' }).notNull(),
|
||||||
|
datetime1: datetime({ mode: 'date' }).notNull(),
|
||||||
|
datetime2: datetime({ mode: 'string' }).notNull(),
|
||||||
|
decimal1: decimal().notNull(),
|
||||||
|
decimal2: decimal({ unsigned: true }).notNull(),
|
||||||
|
double1: double().notNull(),
|
||||||
|
double2: double({ unsigned: true }).notNull(),
|
||||||
|
float1: float().notNull(),
|
||||||
|
float2: float({ unsigned: true }).notNull(),
|
||||||
|
int1: int().notNull(),
|
||||||
|
int2: int({ unsigned: true }).notNull(),
|
||||||
|
json: json().notNull(),
|
||||||
|
mediumint1: mediumint().notNull(),
|
||||||
|
mediumint2: mediumint({ unsigned: true }).notNull(),
|
||||||
|
enum: mysqlEnum('enum', ['a', 'b', 'c']).notNull(),
|
||||||
|
real: real().notNull(),
|
||||||
|
serial: serial().notNull(),
|
||||||
|
smallint1: smallint().notNull(),
|
||||||
|
smallint2: smallint({ unsigned: true }).notNull(),
|
||||||
|
text1: text().notNull(),
|
||||||
|
text2: text({ enum: ['a', 'b', 'c'] }).notNull(),
|
||||||
|
time: time().notNull(),
|
||||||
|
timestamp1: timestamp({ mode: 'date' }).notNull(),
|
||||||
|
timestamp2: timestamp({ mode: 'string' }).notNull(),
|
||||||
|
tinyint1: tinyint().notNull(),
|
||||||
|
tinyint2: tinyint({ unsigned: true }).notNull(),
|
||||||
|
varchar1: varchar({ length: 10 }).notNull(),
|
||||||
|
varchar2: varchar({ length: 1, enum: ['a', 'b', 'c'] }).notNull(),
|
||||||
|
varbinary: varbinary({ length: 10 }).notNull(),
|
||||||
|
year: year().notNull(),
|
||||||
|
longtext1: longtext().notNull(),
|
||||||
|
longtext2: longtext({ enum: ['a', 'b', 'c'] }).notNull(),
|
||||||
|
mediumtext1: mediumtext().notNull(),
|
||||||
|
mediumtext2: mediumtext({ enum: ['a', 'b', 'c'] }).notNull(),
|
||||||
|
tinytext1: tinytext().notNull(),
|
||||||
|
tinytext2: tinytext({ enum: ['a', 'b', 'c'] }).notNull(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = createSelectSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
bigint1: t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER }),
|
||||||
|
bigint2: t.BigInt({ minimum: CONSTANTS.INT64_MIN, maximum: CONSTANTS.INT64_MAX }),
|
||||||
|
bigint3: t.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
|
||||||
|
bigint4: t.BigInt({ minimum: 0n, maximum: CONSTANTS.INT64_UNSIGNED_MAX }),
|
||||||
|
binary: t.String(),
|
||||||
|
boolean: t.Boolean(),
|
||||||
|
char1: t.String({ minLength: 10, maxLength: 10 }),
|
||||||
|
char2: t.Enum({ a: 'a', b: 'b', c: 'c' }),
|
||||||
|
date1: t.Date(),
|
||||||
|
date2: t.String(),
|
||||||
|
datetime1: t.Date(),
|
||||||
|
datetime2: t.String(),
|
||||||
|
decimal1: t.String(),
|
||||||
|
decimal2: t.String(),
|
||||||
|
double1: t.Number({ minimum: CONSTANTS.INT48_MIN, maximum: CONSTANTS.INT48_MAX }),
|
||||||
|
double2: t.Number({ minimum: 0, maximum: CONSTANTS.INT48_UNSIGNED_MAX }),
|
||||||
|
float1: t.Number({ minimum: CONSTANTS.INT24_MIN, maximum: CONSTANTS.INT24_MAX }),
|
||||||
|
float2: t.Number({ minimum: 0, maximum: CONSTANTS.INT24_UNSIGNED_MAX }),
|
||||||
|
int1: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: CONSTANTS.INT32_MAX }),
|
||||||
|
int2: t.Integer({ minimum: 0, maximum: CONSTANTS.INT32_UNSIGNED_MAX }),
|
||||||
|
json: jsonSchema,
|
||||||
|
mediumint1: t.Integer({ minimum: CONSTANTS.INT24_MIN, maximum: CONSTANTS.INT24_MAX }),
|
||||||
|
mediumint2: t.Integer({ minimum: 0, maximum: CONSTANTS.INT24_UNSIGNED_MAX }),
|
||||||
|
enum: t.Enum({ a: 'a', b: 'b', c: 'c' }),
|
||||||
|
real: t.Number({ minimum: CONSTANTS.INT48_MIN, maximum: CONSTANTS.INT48_MAX }),
|
||||||
|
serial: t.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
|
||||||
|
smallint1: t.Integer({ minimum: CONSTANTS.INT16_MIN, maximum: CONSTANTS.INT16_MAX }),
|
||||||
|
smallint2: t.Integer({ minimum: 0, maximum: CONSTANTS.INT16_UNSIGNED_MAX }),
|
||||||
|
text1: t.String({ maxLength: CONSTANTS.INT16_UNSIGNED_MAX }),
|
||||||
|
text2: t.Enum({ a: 'a', b: 'b', c: 'c' }),
|
||||||
|
time: t.String(),
|
||||||
|
timestamp1: t.Date(),
|
||||||
|
timestamp2: t.String(),
|
||||||
|
tinyint1: t.Integer({ minimum: CONSTANTS.INT8_MIN, maximum: CONSTANTS.INT8_MAX }),
|
||||||
|
tinyint2: t.Integer({ minimum: 0, maximum: CONSTANTS.INT8_UNSIGNED_MAX }),
|
||||||
|
varchar1: t.String({ maxLength: 10 }),
|
||||||
|
varchar2: t.Enum({ a: 'a', b: 'b', c: 'c' }),
|
||||||
|
varbinary: t.String(),
|
||||||
|
year: t.Integer({ minimum: 1901, maximum: 2155 }),
|
||||||
|
longtext1: t.String({ maxLength: CONSTANTS.INT32_UNSIGNED_MAX }),
|
||||||
|
longtext2: t.Enum({ a: 'a', b: 'b', c: 'c' }),
|
||||||
|
mediumtext1: t.String({ maxLength: CONSTANTS.INT24_UNSIGNED_MAX }),
|
||||||
|
mediumtext2: t.Enum({ a: 'a', b: 'b', c: 'c' }),
|
||||||
|
tinytext1: t.String({ maxLength: CONSTANTS.INT8_UNSIGNED_MAX }),
|
||||||
|
tinytext2: t.Enum({ a: 'a', b: 'b', c: 'c' }),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Disallow unknown keys in table refinement - select */ {
|
||||||
|
const table = mysqlTable('test', { id: int() });
|
||||||
|
// @ts-expect-error
|
||||||
|
createSelectSchema(table, { unknown: t.String() });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disallow unknown keys in table refinement - insert */ {
|
||||||
|
const table = mysqlTable('test', { id: int() });
|
||||||
|
// @ts-expect-error
|
||||||
|
createInsertSchema(table, { unknown: t.String() });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disallow unknown keys in table refinement - update */ {
|
||||||
|
const table = mysqlTable('test', { id: int() });
|
||||||
|
// @ts-expect-error
|
||||||
|
createUpdateSchema(table, { unknown: t.String() });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disallow unknown keys in view qb - select */ {
|
||||||
|
const table = mysqlTable('test', { id: int() });
|
||||||
|
const view = mysqlView('test').as((qb) => qb.select().from(table));
|
||||||
|
const nestedSelect = mysqlView('test').as((qb) => qb.select({ table }).from(table));
|
||||||
|
// @ts-expect-error
|
||||||
|
createSelectSchema(view, { unknown: t.String() });
|
||||||
|
// @ts-expect-error
|
||||||
|
createSelectSchema(nestedSelect, { table: { unknown: t.String() } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disallow unknown keys in view columns - select */ {
|
||||||
|
const view = mysqlView('test', { id: int() }).as(sql``);
|
||||||
|
// @ts-expect-error
|
||||||
|
createSelectSchema(view, { unknown: t.String() });
|
||||||
|
}
|
||||||
540
tests/pg.test.ts
Normal file
540
tests/pg.test.ts
Normal file
@@ -0,0 +1,540 @@
|
|||||||
|
import { Type as t } from '@alkdev/typebox';
|
||||||
|
import { type Equal, sql } from 'drizzle-orm';
|
||||||
|
import {
|
||||||
|
customType,
|
||||||
|
integer,
|
||||||
|
pgEnum,
|
||||||
|
pgMaterializedView,
|
||||||
|
pgSchema,
|
||||||
|
pgTable,
|
||||||
|
pgView,
|
||||||
|
serial,
|
||||||
|
text,
|
||||||
|
} from 'drizzle-orm/pg-core';
|
||||||
|
import { test } from 'vitest';
|
||||||
|
import { jsonSchema } from '~/column.ts';
|
||||||
|
import { CONSTANTS } from '~/constants.ts';
|
||||||
|
import { createInsertSchema, createSelectSchema, createUpdateSchema } from '../src';
|
||||||
|
import { Expect, expectEnumValues, expectSchemaShape } from './utils.ts';
|
||||||
|
|
||||||
|
const integerSchema = t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: CONSTANTS.INT32_MAX });
|
||||||
|
const textSchema = t.String();
|
||||||
|
|
||||||
|
test('table - select', (tc) => {
|
||||||
|
const table = pgTable('test', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createSelectSchema(table);
|
||||||
|
const expected = t.Object({ id: integerSchema, name: textSchema });
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('table in schema - select', (tc) => {
|
||||||
|
const schema = pgSchema('test');
|
||||||
|
const table = schema.table('test', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createSelectSchema(table);
|
||||||
|
const expected = t.Object({ id: integerSchema, name: textSchema });
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('table - insert', (tc) => {
|
||||||
|
const table = pgTable('test', {
|
||||||
|
id: integer().generatedAlwaysAsIdentity().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
age: integer(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createInsertSchema(table);
|
||||||
|
const expected = t.Object({ name: textSchema, age: t.Optional(t.Union([integerSchema, t.Null()])) });
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('table - update', (tc) => {
|
||||||
|
const table = pgTable('test', {
|
||||||
|
id: integer().generatedAlwaysAsIdentity().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
age: integer(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createUpdateSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
name: t.Optional(textSchema),
|
||||||
|
age: t.Optional(t.Union([integerSchema, t.Null()])),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('view qb - select', (tc) => {
|
||||||
|
const table = pgTable('test', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
});
|
||||||
|
const view = pgView('test').as((qb) => qb.select({ id: table.id, age: sql``.as('age') }).from(table));
|
||||||
|
|
||||||
|
const result = createSelectSchema(view);
|
||||||
|
const expected = t.Object({ id: integerSchema, age: t.Any() });
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('view columns - select', (tc) => {
|
||||||
|
const view = pgView('test', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
}).as(sql``);
|
||||||
|
|
||||||
|
const result = createSelectSchema(view);
|
||||||
|
const expected = t.Object({ id: integerSchema, name: textSchema });
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('materialized view qb - select', (tc) => {
|
||||||
|
const table = pgTable('test', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
});
|
||||||
|
const view = pgMaterializedView('test').as((qb) => qb.select({ id: table.id, age: sql``.as('age') }).from(table));
|
||||||
|
|
||||||
|
const result = createSelectSchema(view);
|
||||||
|
const expected = t.Object({ id: integerSchema, age: t.Any() });
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('materialized view columns - select', (tc) => {
|
||||||
|
const view = pgView('test', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
}).as(sql``);
|
||||||
|
|
||||||
|
const result = createSelectSchema(view);
|
||||||
|
const expected = t.Object({ id: integerSchema, name: textSchema });
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('view with nested fields - select', (tc) => {
|
||||||
|
const table = pgTable('test', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
});
|
||||||
|
const view = pgMaterializedView('test').as((qb) =>
|
||||||
|
qb.select({
|
||||||
|
id: table.id,
|
||||||
|
nested: {
|
||||||
|
name: table.name,
|
||||||
|
age: sql``.as('age'),
|
||||||
|
},
|
||||||
|
table,
|
||||||
|
}).from(table)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = createSelectSchema(view);
|
||||||
|
const expected = t.Object({
|
||||||
|
id: integerSchema,
|
||||||
|
nested: t.Object({ name: textSchema, age: t.Any() }),
|
||||||
|
table: t.Object({ id: integerSchema, name: textSchema }),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('enum - select', (tc) => {
|
||||||
|
const enum_ = pgEnum('test', ['a', 'b', 'c']);
|
||||||
|
|
||||||
|
const result = createSelectSchema(enum_);
|
||||||
|
const expected = t.Enum({ a: 'a', b: 'b', c: 'c' });
|
||||||
|
expectEnumValues(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nullability - select', (tc) => {
|
||||||
|
const table = pgTable('test', {
|
||||||
|
c1: integer(),
|
||||||
|
c2: integer().notNull(),
|
||||||
|
c3: integer().default(1),
|
||||||
|
c4: integer().notNull().default(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createSelectSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Union([integerSchema, t.Null()]),
|
||||||
|
c2: integerSchema,
|
||||||
|
c3: t.Union([integerSchema, t.Null()]),
|
||||||
|
c4: integerSchema,
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nullability - insert', (tc) => {
|
||||||
|
const table = pgTable('test', {
|
||||||
|
c1: integer(),
|
||||||
|
c2: integer().notNull(),
|
||||||
|
c3: integer().default(1),
|
||||||
|
c4: integer().notNull().default(1),
|
||||||
|
c5: integer().generatedAlwaysAs(1),
|
||||||
|
c6: integer().generatedAlwaysAsIdentity(),
|
||||||
|
c7: integer().generatedByDefaultAsIdentity(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createInsertSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Optional(t.Union([integerSchema, t.Null()])),
|
||||||
|
c2: integerSchema,
|
||||||
|
c3: t.Optional(t.Union([integerSchema, t.Null()])),
|
||||||
|
c4: t.Optional(integerSchema),
|
||||||
|
c7: t.Optional(integerSchema),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nullability - update', (tc) => {
|
||||||
|
const table = pgTable('test', {
|
||||||
|
c1: integer(),
|
||||||
|
c2: integer().notNull(),
|
||||||
|
c3: integer().default(1),
|
||||||
|
c4: integer().notNull().default(1),
|
||||||
|
c5: integer().generatedAlwaysAs(1),
|
||||||
|
c6: integer().generatedAlwaysAsIdentity(),
|
||||||
|
c7: integer().generatedByDefaultAsIdentity(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createUpdateSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Optional(t.Union([integerSchema, t.Null()])),
|
||||||
|
c2: t.Optional(integerSchema),
|
||||||
|
c3: t.Optional(t.Union([integerSchema, t.Null()])),
|
||||||
|
c4: t.Optional(integerSchema),
|
||||||
|
c7: t.Optional(integerSchema),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refine table - select', (tc) => {
|
||||||
|
const table = pgTable('test', {
|
||||||
|
c1: integer(),
|
||||||
|
c2: integer().notNull(),
|
||||||
|
c3: integer().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createSelectSchema(table, {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Union([integerSchema, t.Null()]),
|
||||||
|
c2: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refine table - select with custom data type', (tc) => {
|
||||||
|
const customText = customType({ dataType: () => 'text' });
|
||||||
|
const table = pgTable('test', {
|
||||||
|
c1: integer(),
|
||||||
|
c2: integer().notNull(),
|
||||||
|
c3: integer().notNull(),
|
||||||
|
c4: customText(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const customTextSchema = t.String({ minLength: 1, maxLength: 100 });
|
||||||
|
const result = createSelectSchema(table, {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
c4: customTextSchema,
|
||||||
|
});
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Union([integerSchema, t.Null()]),
|
||||||
|
c2: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
c4: customTextSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refine table - insert', (tc) => {
|
||||||
|
const table = pgTable('test', {
|
||||||
|
c1: integer(),
|
||||||
|
c2: integer().notNull(),
|
||||||
|
c3: integer().notNull(),
|
||||||
|
c4: integer().generatedAlwaysAs(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createInsertSchema(table, {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Optional(t.Union([integerSchema, t.Null()])),
|
||||||
|
c2: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refine table - update', (tc) => {
|
||||||
|
const table = pgTable('test', {
|
||||||
|
c1: integer(),
|
||||||
|
c2: integer().notNull(),
|
||||||
|
c3: integer().notNull(),
|
||||||
|
c4: integer().generatedAlwaysAs(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createUpdateSchema(table, {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Optional(t.Union([integerSchema, t.Null()])),
|
||||||
|
c2: t.Optional(t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 })),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refine view - select', (tc) => {
|
||||||
|
const table = pgTable('test', {
|
||||||
|
c1: integer(),
|
||||||
|
c2: integer(),
|
||||||
|
c3: integer(),
|
||||||
|
c4: integer(),
|
||||||
|
c5: integer(),
|
||||||
|
c6: integer(),
|
||||||
|
});
|
||||||
|
const view = pgView('test').as((qb) =>
|
||||||
|
qb.select({
|
||||||
|
c1: table.c1,
|
||||||
|
c2: table.c2,
|
||||||
|
c3: table.c3,
|
||||||
|
nested: {
|
||||||
|
c4: table.c4,
|
||||||
|
c5: table.c5,
|
||||||
|
c6: table.c6,
|
||||||
|
},
|
||||||
|
table,
|
||||||
|
}).from(table)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = createSelectSchema(view, {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
nested: {
|
||||||
|
c5: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c6: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Union([integerSchema, t.Null()]),
|
||||||
|
c2: t.Union([t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }), t.Null()]),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
nested: t.Object({
|
||||||
|
c4: t.Union([integerSchema, t.Null()]),
|
||||||
|
c5: t.Union([t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }), t.Null()]),
|
||||||
|
c6: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
}),
|
||||||
|
table: t.Object({
|
||||||
|
c1: t.Union([integerSchema, t.Null()]),
|
||||||
|
c2: t.Union([t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }), t.Null()]),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
c4: t.Union([integerSchema, t.Null()]),
|
||||||
|
c5: t.Union([integerSchema, t.Null()]),
|
||||||
|
c6: t.Union([integerSchema, t.Null()]),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('all data types', (tc) => {
|
||||||
|
const table = pgTable('test', ({
|
||||||
|
bigint,
|
||||||
|
bigserial,
|
||||||
|
bit,
|
||||||
|
boolean,
|
||||||
|
date,
|
||||||
|
char,
|
||||||
|
cidr,
|
||||||
|
doublePrecision,
|
||||||
|
geometry,
|
||||||
|
halfvec,
|
||||||
|
inet,
|
||||||
|
integer,
|
||||||
|
interval,
|
||||||
|
json,
|
||||||
|
jsonb,
|
||||||
|
line,
|
||||||
|
macaddr,
|
||||||
|
macaddr8,
|
||||||
|
numeric,
|
||||||
|
point,
|
||||||
|
real,
|
||||||
|
serial,
|
||||||
|
smallint,
|
||||||
|
smallserial,
|
||||||
|
text,
|
||||||
|
sparsevec,
|
||||||
|
time,
|
||||||
|
timestamp,
|
||||||
|
uuid,
|
||||||
|
varchar,
|
||||||
|
vector,
|
||||||
|
}) => ({
|
||||||
|
bigint1: bigint({ mode: 'number' }).notNull(),
|
||||||
|
bigint2: bigint({ mode: 'bigint' }).notNull(),
|
||||||
|
bigserial1: bigserial({ mode: 'number' }).notNull(),
|
||||||
|
bigserial2: bigserial({ mode: 'bigint' }).notNull(),
|
||||||
|
bit: bit({ dimensions: 5 }).notNull(),
|
||||||
|
boolean: boolean().notNull(),
|
||||||
|
date1: date({ mode: 'date' }).notNull(),
|
||||||
|
date2: date({ mode: 'string' }).notNull(),
|
||||||
|
char1: char({ length: 10 }).notNull(),
|
||||||
|
char2: char({ length: 1, enum: ['a', 'b', 'c'] }).notNull(),
|
||||||
|
cidr: cidr().notNull(),
|
||||||
|
doublePrecision: doublePrecision().notNull(),
|
||||||
|
geometry1: geometry({ type: 'point', mode: 'tuple' }).notNull(),
|
||||||
|
geometry2: geometry({ type: 'point', mode: 'xy' }).notNull(),
|
||||||
|
halfvec: halfvec({ dimensions: 3 }).notNull(),
|
||||||
|
inet: inet().notNull(),
|
||||||
|
integer: integer().notNull(),
|
||||||
|
interval: interval().notNull(),
|
||||||
|
json: json().notNull(),
|
||||||
|
jsonb: jsonb().notNull(),
|
||||||
|
line1: line({ mode: 'abc' }).notNull(),
|
||||||
|
line2: line({ mode: 'tuple' }).notNull(),
|
||||||
|
macaddr: macaddr().notNull(),
|
||||||
|
macaddr8: macaddr8().notNull(),
|
||||||
|
numeric: numeric().notNull(),
|
||||||
|
point1: point({ mode: 'xy' }).notNull(),
|
||||||
|
point2: point({ mode: 'tuple' }).notNull(),
|
||||||
|
real: real().notNull(),
|
||||||
|
serial: serial().notNull(),
|
||||||
|
smallint: smallint().notNull(),
|
||||||
|
smallserial: smallserial().notNull(),
|
||||||
|
text1: text().notNull(),
|
||||||
|
text2: text({ enum: ['a', 'b', 'c'] }).notNull(),
|
||||||
|
sparsevec: sparsevec({ dimensions: 3 }).notNull(),
|
||||||
|
time: time().notNull(),
|
||||||
|
timestamp1: timestamp({ mode: 'date' }).notNull(),
|
||||||
|
timestamp2: timestamp({ mode: 'string' }).notNull(),
|
||||||
|
uuid: uuid().notNull(),
|
||||||
|
varchar1: varchar({ length: 10 }).notNull(),
|
||||||
|
varchar2: varchar({ length: 1, enum: ['a', 'b', 'c'] }).notNull(),
|
||||||
|
vector: vector({ dimensions: 3 }).notNull(),
|
||||||
|
array1: integer().array().notNull(),
|
||||||
|
array2: integer().array().array(2).notNull(),
|
||||||
|
array3: varchar({ length: 10 }).array().array(2).notNull(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = createSelectSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
bigint1: t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER }),
|
||||||
|
bigint2: t.BigInt({ minimum: CONSTANTS.INT64_MIN, maximum: CONSTANTS.INT64_MAX }),
|
||||||
|
bigserial1: t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER }),
|
||||||
|
bigserial2: t.BigInt({ minimum: CONSTANTS.INT64_MIN, maximum: CONSTANTS.INT64_MAX }),
|
||||||
|
bit: t.RegExp(/^[01]+$/, { maxLength: 5 }),
|
||||||
|
boolean: t.Boolean(),
|
||||||
|
date1: t.Date(),
|
||||||
|
date2: t.String(),
|
||||||
|
char1: t.String({ minLength: 10, maxLength: 10 }),
|
||||||
|
char2: t.Enum({ a: 'a', b: 'b', c: 'c' }),
|
||||||
|
cidr: t.String(),
|
||||||
|
doublePrecision: t.Number({ minimum: CONSTANTS.INT48_MIN, maximum: CONSTANTS.INT48_MAX }),
|
||||||
|
geometry1: t.Tuple([t.Number(), t.Number()]),
|
||||||
|
geometry2: t.Object({ x: t.Number(), y: t.Number() }),
|
||||||
|
halfvec: t.Array(t.Number(), { minItems: 3, maxItems: 3 }),
|
||||||
|
inet: t.String(),
|
||||||
|
integer: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: CONSTANTS.INT32_MAX }),
|
||||||
|
interval: t.String(),
|
||||||
|
json: jsonSchema,
|
||||||
|
jsonb: jsonSchema,
|
||||||
|
line1: t.Object({ a: t.Number(), b: t.Number(), c: t.Number() }),
|
||||||
|
line2: t.Tuple([t.Number(), t.Number(), t.Number()]),
|
||||||
|
macaddr: t.String(),
|
||||||
|
macaddr8: t.String(),
|
||||||
|
numeric: t.String(),
|
||||||
|
point1: t.Object({ x: t.Number(), y: t.Number() }),
|
||||||
|
point2: t.Tuple([t.Number(), t.Number()]),
|
||||||
|
real: t.Number({ minimum: CONSTANTS.INT24_MIN, maximum: CONSTANTS.INT24_MAX }),
|
||||||
|
serial: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: CONSTANTS.INT32_MAX }),
|
||||||
|
smallint: t.Integer({ minimum: CONSTANTS.INT16_MIN, maximum: CONSTANTS.INT16_MAX }),
|
||||||
|
smallserial: t.Integer({ minimum: CONSTANTS.INT16_MIN, maximum: CONSTANTS.INT16_MAX }),
|
||||||
|
text1: t.String(),
|
||||||
|
text2: t.Enum({ a: 'a', b: 'b', c: 'c' }),
|
||||||
|
sparsevec: t.String(),
|
||||||
|
time: t.String(),
|
||||||
|
timestamp1: t.Date(),
|
||||||
|
timestamp2: t.String(),
|
||||||
|
uuid: t.String({ format: 'uuid' }),
|
||||||
|
varchar1: t.String({ maxLength: 10 }),
|
||||||
|
varchar2: t.Enum({ a: 'a', b: 'b', c: 'c' }),
|
||||||
|
vector: t.Array(t.Number(), { minItems: 3, maxItems: 3 }),
|
||||||
|
array1: t.Array(integerSchema),
|
||||||
|
array2: t.Array(t.Array(integerSchema), { minItems: 2, maxItems: 2 }),
|
||||||
|
array3: t.Array(t.Array(t.String({ maxLength: 10 })), { minItems: 2, maxItems: 2 }),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Disallow unknown keys in table refinement - select */ {
|
||||||
|
const table = pgTable('test', { id: integer() });
|
||||||
|
// @ts-expect-error
|
||||||
|
createSelectSchema(table, { unknown: t.String() });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disallow unknown keys in table refinement - insert */ {
|
||||||
|
const table = pgTable('test', { id: integer() });
|
||||||
|
// @ts-expect-error
|
||||||
|
createInsertSchema(table, { unknown: t.String() });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disallow unknown keys in table refinement - update */ {
|
||||||
|
const table = pgTable('test', { id: integer() });
|
||||||
|
// @ts-expect-error
|
||||||
|
createUpdateSchema(table, { unknown: t.String() });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disallow unknown keys in view qb - select */ {
|
||||||
|
const table = pgTable('test', { id: integer() });
|
||||||
|
const view = pgView('test').as((qb) => qb.select().from(table));
|
||||||
|
const mView = pgMaterializedView('test').as((qb) => qb.select().from(table));
|
||||||
|
const nestedSelect = pgView('test').as((qb) => qb.select({ table }).from(table));
|
||||||
|
// @ts-expect-error
|
||||||
|
createSelectSchema(view, { unknown: t.String() });
|
||||||
|
// @ts-expect-error
|
||||||
|
createSelectSchema(mView, { unknown: t.String() });
|
||||||
|
// @ts-expect-error
|
||||||
|
createSelectSchema(nestedSelect, { table: { unknown: t.String() } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disallow unknown keys in view columns - select */ {
|
||||||
|
const view = pgView('test', { id: integer() }).as(sql``);
|
||||||
|
const mView = pgView('test', { id: integer() }).as(sql``);
|
||||||
|
// @ts-expect-error
|
||||||
|
createSelectSchema(view, { unknown: t.String() });
|
||||||
|
// @ts-expect-error
|
||||||
|
createSelectSchema(mView, { unknown: t.String() });
|
||||||
|
}
|
||||||
497
tests/singlestore.test.ts
Normal file
497
tests/singlestore.test.ts
Normal file
@@ -0,0 +1,497 @@
|
|||||||
|
import { Type as t } from '@alkdev/typebox';
|
||||||
|
import { type Equal, sql } from 'drizzle-orm';
|
||||||
|
import { customType, int, serial, singlestoreSchema, singlestoreTable, text } from 'drizzle-orm/singlestore-core';
|
||||||
|
import { test } from 'vitest';
|
||||||
|
import { jsonSchema } from '~/column.ts';
|
||||||
|
import { CONSTANTS } from '~/constants.ts';
|
||||||
|
import { createInsertSchema, createSelectSchema, createUpdateSchema } from '../src';
|
||||||
|
import { Expect, expectSchemaShape } from './utils.ts';
|
||||||
|
|
||||||
|
const intSchema = t.Integer({
|
||||||
|
minimum: CONSTANTS.INT32_MIN,
|
||||||
|
maximum: CONSTANTS.INT32_MAX,
|
||||||
|
});
|
||||||
|
const serialNumberModeSchema = t.Integer({
|
||||||
|
minimum: 0,
|
||||||
|
maximum: Number.MAX_SAFE_INTEGER,
|
||||||
|
});
|
||||||
|
const textSchema = t.String({ maxLength: CONSTANTS.INT16_UNSIGNED_MAX });
|
||||||
|
|
||||||
|
test('table - select', (tc) => {
|
||||||
|
const table = singlestoreTable('test', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createSelectSchema(table);
|
||||||
|
const expected = t.Object({ id: serialNumberModeSchema, name: textSchema });
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('table in schema - select', (tc) => {
|
||||||
|
const schema = singlestoreSchema('test');
|
||||||
|
const table = schema.table('test', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createSelectSchema(table);
|
||||||
|
const expected = t.Object({ id: serialNumberModeSchema, name: textSchema });
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('table - insert', (tc) => {
|
||||||
|
const table = singlestoreTable('test', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
age: int(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createInsertSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
id: t.Optional(serialNumberModeSchema),
|
||||||
|
name: textSchema,
|
||||||
|
age: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('table - update', (tc) => {
|
||||||
|
const table = singlestoreTable('test', {
|
||||||
|
id: serial().primaryKey(),
|
||||||
|
name: text().notNull(),
|
||||||
|
age: int(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createUpdateSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
id: t.Optional(serialNumberModeSchema),
|
||||||
|
name: t.Optional(textSchema),
|
||||||
|
age: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: SingleStore doesn't support views yet. Add these tests when they're added
|
||||||
|
|
||||||
|
// test('view qb - select', (tc) => {
|
||||||
|
// const table = singlestoreTable('test', {
|
||||||
|
// id: serial().primaryKey(),
|
||||||
|
// name: text().notNull(),
|
||||||
|
// });
|
||||||
|
// const view = mysqlView('test').as((qb) => qb.select({ id: table.id, age: sql``.as('age') }).from(table));
|
||||||
|
|
||||||
|
// const result = createSelectSchema(view);
|
||||||
|
// const expected = t.Object({ id: serialNumberModeSchema, age: t.Any() });
|
||||||
|
// expectSchemaShape(tc, expected).from(result);
|
||||||
|
// Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// test('view columns - select', (tc) => {
|
||||||
|
// const view = mysqlView('test', {
|
||||||
|
// id: serial().primaryKey(),
|
||||||
|
// name: text().notNull(),
|
||||||
|
// }).as(sql``);
|
||||||
|
|
||||||
|
// const result = createSelectSchema(view);
|
||||||
|
// const expected = t.Object({ id: serialNumberModeSchema, name: textSchema });
|
||||||
|
// expectSchemaShape(tc, expected).from(result);
|
||||||
|
// Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// test('view with nested fields - select', (tc) => {
|
||||||
|
// const table = singlestoreTable('test', {
|
||||||
|
// id: serial().primaryKey(),
|
||||||
|
// name: text().notNull(),
|
||||||
|
// });
|
||||||
|
// const view = mysqlView('test').as((qb) =>
|
||||||
|
// qb.select({
|
||||||
|
// id: table.id,
|
||||||
|
// nested: {
|
||||||
|
// name: table.name,
|
||||||
|
// age: sql``.as('age'),
|
||||||
|
// },
|
||||||
|
// table,
|
||||||
|
// }).from(table)
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const result = createSelectSchema(view);
|
||||||
|
// const expected = t.Object({
|
||||||
|
// id: serialNumberModeSchema,
|
||||||
|
// nested: t.Object({ name: textSchema, age: t.Any() }),
|
||||||
|
// table: t.Object({ id: serialNumberModeSchema, name: textSchema }),
|
||||||
|
// });
|
||||||
|
// expectSchemaShape(tc, expected).from(result);
|
||||||
|
// Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
// });
|
||||||
|
|
||||||
|
test('nullability - select', (tc) => {
|
||||||
|
const table = singlestoreTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().default(1),
|
||||||
|
c4: int().notNull().default(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createSelectSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Union([intSchema, t.Null()]),
|
||||||
|
c2: intSchema,
|
||||||
|
c3: t.Union([intSchema, t.Null()]),
|
||||||
|
c4: intSchema,
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nullability - insert', (tc) => {
|
||||||
|
const table = singlestoreTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().default(1),
|
||||||
|
c4: int().notNull().default(1),
|
||||||
|
c5: int().generatedAlwaysAs(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createInsertSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
c2: intSchema,
|
||||||
|
c3: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
c4: t.Optional(intSchema),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nullability - update', (tc) => {
|
||||||
|
const table = singlestoreTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().default(1),
|
||||||
|
c4: int().notNull().default(1),
|
||||||
|
c5: int().generatedAlwaysAs(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createUpdateSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
c2: t.Optional(intSchema),
|
||||||
|
c3: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
c4: t.Optional(intSchema),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refine table - select', (tc) => {
|
||||||
|
const table = singlestoreTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createSelectSchema(table, {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Union([intSchema, t.Null()]),
|
||||||
|
c2: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refine table - select with custom data type', (tc) => {
|
||||||
|
const customText = customType({ dataType: () => 'text' });
|
||||||
|
const table = singlestoreTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().notNull(),
|
||||||
|
c4: customText(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const customTextSchema = t.String({ minLength: 1, maxLength: 100 });
|
||||||
|
const result = createSelectSchema(table, {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
c4: customTextSchema,
|
||||||
|
});
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Union([intSchema, t.Null()]),
|
||||||
|
c2: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
c4: customTextSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refine table - insert', (tc) => {
|
||||||
|
const table = singlestoreTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().notNull(),
|
||||||
|
c4: int().generatedAlwaysAs(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createInsertSchema(table, {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
c2: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refine table - update', (tc) => {
|
||||||
|
const table = singlestoreTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().notNull(),
|
||||||
|
c4: int().generatedAlwaysAs(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createUpdateSchema(table, {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
c2: t.Optional(t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 })),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
// test('refine view - select', (tc) => {
|
||||||
|
// const table = singlestoreTable('test', {
|
||||||
|
// c1: int(),
|
||||||
|
// c2: int(),
|
||||||
|
// c3: int(),
|
||||||
|
// c4: int(),
|
||||||
|
// c5: int(),
|
||||||
|
// c6: int(),
|
||||||
|
// });
|
||||||
|
// const view = mysqlView('test').as((qb) =>
|
||||||
|
// qb.select({
|
||||||
|
// c1: table.c1,
|
||||||
|
// c2: table.c2,
|
||||||
|
// c3: table.c3,
|
||||||
|
// nested: {
|
||||||
|
// c4: table.c4,
|
||||||
|
// c5: table.c5,
|
||||||
|
// c6: table.c6,
|
||||||
|
// },
|
||||||
|
// table,
|
||||||
|
// }).from(table)
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const result = createSelectSchema(view, {
|
||||||
|
// c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
// c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
// nested: {
|
||||||
|
// c5: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
// c6: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
// },
|
||||||
|
// table: {
|
||||||
|
// c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
// c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// const expected = t.Object({
|
||||||
|
// c1: t.Union([intSchema, t.Null()]),
|
||||||
|
// c2: t.Union([t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }), t.Null()]),
|
||||||
|
// c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
// nested: t.Object({
|
||||||
|
// c4: t.Union([intSchema, t.Null()]),
|
||||||
|
// c5: t.Union([t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }), t.Null()]),
|
||||||
|
// c6: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
// }),
|
||||||
|
// table: t.Object({
|
||||||
|
// c1: t.Union([intSchema, t.Null()]),
|
||||||
|
// c2: t.Union([t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: 1000 }), t.Null()]),
|
||||||
|
// c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
// c4: t.Union([intSchema, t.Null()]),
|
||||||
|
// c5: t.Union([intSchema, t.Null()]),
|
||||||
|
// c6: t.Union([intSchema, t.Null()]),
|
||||||
|
// }),
|
||||||
|
// });
|
||||||
|
// expectSchemaShape(tc, expected).from(result);
|
||||||
|
// Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
// });
|
||||||
|
|
||||||
|
test('all data types', (tc) => {
|
||||||
|
const table = singlestoreTable('test', ({
|
||||||
|
bigint,
|
||||||
|
binary,
|
||||||
|
boolean,
|
||||||
|
char,
|
||||||
|
date,
|
||||||
|
datetime,
|
||||||
|
decimal,
|
||||||
|
double,
|
||||||
|
float,
|
||||||
|
int,
|
||||||
|
json,
|
||||||
|
mediumint,
|
||||||
|
singlestoreEnum,
|
||||||
|
real,
|
||||||
|
serial,
|
||||||
|
smallint,
|
||||||
|
text,
|
||||||
|
time,
|
||||||
|
timestamp,
|
||||||
|
tinyint,
|
||||||
|
varchar,
|
||||||
|
varbinary,
|
||||||
|
year,
|
||||||
|
longtext,
|
||||||
|
mediumtext,
|
||||||
|
tinytext,
|
||||||
|
}) => ({
|
||||||
|
bigint1: bigint({ mode: 'number' }).notNull(),
|
||||||
|
bigint2: bigint({ mode: 'bigint' }).notNull(),
|
||||||
|
bigint3: bigint({ unsigned: true, mode: 'number' }).notNull(),
|
||||||
|
bigint4: bigint({ unsigned: true, mode: 'bigint' }).notNull(),
|
||||||
|
binary: binary({ length: 10 }).notNull(),
|
||||||
|
boolean: boolean().notNull(),
|
||||||
|
char1: char({ length: 10 }).notNull(),
|
||||||
|
char2: char({ length: 1, enum: ['a', 'b', 'c'] }).notNull(),
|
||||||
|
date1: date({ mode: 'date' }).notNull(),
|
||||||
|
date2: date({ mode: 'string' }).notNull(),
|
||||||
|
datetime1: datetime({ mode: 'date' }).notNull(),
|
||||||
|
datetime2: datetime({ mode: 'string' }).notNull(),
|
||||||
|
decimal1: decimal().notNull(),
|
||||||
|
decimal2: decimal({ unsigned: true }).notNull(),
|
||||||
|
double1: double().notNull(),
|
||||||
|
double2: double({ unsigned: true }).notNull(),
|
||||||
|
float1: float().notNull(),
|
||||||
|
float2: float({ unsigned: true }).notNull(),
|
||||||
|
int1: int().notNull(),
|
||||||
|
int2: int({ unsigned: true }).notNull(),
|
||||||
|
json: json().notNull(),
|
||||||
|
mediumint1: mediumint().notNull(),
|
||||||
|
mediumint2: mediumint({ unsigned: true }).notNull(),
|
||||||
|
enum: singlestoreEnum('enum', ['a', 'b', 'c']).notNull(),
|
||||||
|
real: real().notNull(),
|
||||||
|
serial: serial().notNull(),
|
||||||
|
smallint1: smallint().notNull(),
|
||||||
|
smallint2: smallint({ unsigned: true }).notNull(),
|
||||||
|
text1: text().notNull(),
|
||||||
|
text2: text({ enum: ['a', 'b', 'c'] }).notNull(),
|
||||||
|
time: time().notNull(),
|
||||||
|
timestamp1: timestamp({ mode: 'date' }).notNull(),
|
||||||
|
timestamp2: timestamp({ mode: 'string' }).notNull(),
|
||||||
|
tinyint1: tinyint().notNull(),
|
||||||
|
tinyint2: tinyint({ unsigned: true }).notNull(),
|
||||||
|
varchar1: varchar({ length: 10 }).notNull(),
|
||||||
|
varchar2: varchar({ length: 1, enum: ['a', 'b', 'c'] }).notNull(),
|
||||||
|
varbinary: varbinary({ length: 10 }).notNull(),
|
||||||
|
year: year().notNull(),
|
||||||
|
longtext1: longtext().notNull(),
|
||||||
|
longtext2: longtext({ enum: ['a', 'b', 'c'] }).notNull(),
|
||||||
|
mediumtext1: mediumtext().notNull(),
|
||||||
|
mediumtext2: mediumtext({ enum: ['a', 'b', 'c'] }).notNull(),
|
||||||
|
tinytext1: tinytext().notNull(),
|
||||||
|
tinytext2: tinytext({ enum: ['a', 'b', 'c'] }).notNull(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = createSelectSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
bigint1: t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER }),
|
||||||
|
bigint2: t.BigInt({ minimum: CONSTANTS.INT64_MIN, maximum: CONSTANTS.INT64_MAX }),
|
||||||
|
bigint3: t.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
|
||||||
|
bigint4: t.BigInt({ minimum: 0n, maximum: CONSTANTS.INT64_UNSIGNED_MAX }),
|
||||||
|
binary: t.String(),
|
||||||
|
boolean: t.Boolean(),
|
||||||
|
char1: t.String({ minLength: 10, maxLength: 10 }),
|
||||||
|
char2: t.Enum({ a: 'a', b: 'b', c: 'c' }),
|
||||||
|
date1: t.Date(),
|
||||||
|
date2: t.String(),
|
||||||
|
datetime1: t.Date(),
|
||||||
|
datetime2: t.String(),
|
||||||
|
decimal1: t.String(),
|
||||||
|
decimal2: t.String(),
|
||||||
|
double1: t.Number({ minimum: CONSTANTS.INT48_MIN, maximum: CONSTANTS.INT48_MAX }),
|
||||||
|
double2: t.Number({ minimum: 0, maximum: CONSTANTS.INT48_UNSIGNED_MAX }),
|
||||||
|
float1: t.Number({ minimum: CONSTANTS.INT24_MIN, maximum: CONSTANTS.INT24_MAX }),
|
||||||
|
float2: t.Number({ minimum: 0, maximum: CONSTANTS.INT24_UNSIGNED_MAX }),
|
||||||
|
int1: t.Integer({ minimum: CONSTANTS.INT32_MIN, maximum: CONSTANTS.INT32_MAX }),
|
||||||
|
int2: t.Integer({ minimum: 0, maximum: CONSTANTS.INT32_UNSIGNED_MAX }),
|
||||||
|
json: jsonSchema,
|
||||||
|
mediumint1: t.Integer({ minimum: CONSTANTS.INT24_MIN, maximum: CONSTANTS.INT24_MAX }),
|
||||||
|
mediumint2: t.Integer({ minimum: 0, maximum: CONSTANTS.INT24_UNSIGNED_MAX }),
|
||||||
|
enum: t.Enum({ a: 'a', b: 'b', c: 'c' }),
|
||||||
|
real: t.Number({ minimum: CONSTANTS.INT48_MIN, maximum: CONSTANTS.INT48_MAX }),
|
||||||
|
serial: t.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
|
||||||
|
smallint1: t.Integer({ minimum: CONSTANTS.INT16_MIN, maximum: CONSTANTS.INT16_MAX }),
|
||||||
|
smallint2: t.Integer({ minimum: 0, maximum: CONSTANTS.INT16_UNSIGNED_MAX }),
|
||||||
|
text1: t.String({ maxLength: CONSTANTS.INT16_UNSIGNED_MAX }),
|
||||||
|
text2: t.Enum({ a: 'a', b: 'b', c: 'c' }),
|
||||||
|
time: t.String(),
|
||||||
|
timestamp1: t.Date(),
|
||||||
|
timestamp2: t.String(),
|
||||||
|
tinyint1: t.Integer({ minimum: CONSTANTS.INT8_MIN, maximum: CONSTANTS.INT8_MAX }),
|
||||||
|
tinyint2: t.Integer({ minimum: 0, maximum: CONSTANTS.INT8_UNSIGNED_MAX }),
|
||||||
|
varchar1: t.String({ maxLength: 10 }),
|
||||||
|
varchar2: t.Enum({ a: 'a', b: 'b', c: 'c' }),
|
||||||
|
varbinary: t.String(),
|
||||||
|
year: t.Integer({ minimum: 1901, maximum: 2155 }),
|
||||||
|
longtext1: t.String({ maxLength: CONSTANTS.INT32_UNSIGNED_MAX }),
|
||||||
|
longtext2: t.Enum({ a: 'a', b: 'b', c: 'c' }),
|
||||||
|
mediumtext1: t.String({ maxLength: CONSTANTS.INT24_UNSIGNED_MAX }),
|
||||||
|
mediumtext2: t.Enum({ a: 'a', b: 'b', c: 'c' }),
|
||||||
|
tinytext1: t.String({ maxLength: CONSTANTS.INT8_UNSIGNED_MAX }),
|
||||||
|
tinytext2: t.Enum({ a: 'a', b: 'b', c: 'c' }),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Disallow unknown keys in table refinement - select */ {
|
||||||
|
const table = singlestoreTable('test', { id: int() });
|
||||||
|
// @ts-expect-error
|
||||||
|
createSelectSchema(table, { unknown: t.String() });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disallow unknown keys in table refinement - insert */ {
|
||||||
|
const table = singlestoreTable('test', { id: int() });
|
||||||
|
// @ts-expect-error
|
||||||
|
createInsertSchema(table, { unknown: t.String() });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disallow unknown keys in table refinement - update */ {
|
||||||
|
const table = singlestoreTable('test', { id: int() });
|
||||||
|
// @ts-expect-error
|
||||||
|
createUpdateSchema(table, { unknown: t.String() });
|
||||||
|
}
|
||||||
|
|
||||||
|
// /* Disallow unknown keys in view qb - select */ {
|
||||||
|
// const table = singlestoreTable('test', { id: int() });
|
||||||
|
// const view = mysqlView('test').as((qb) => qb.select().from(table));
|
||||||
|
// const nestedSelect = mysqlView('test').as((qb) => qb.select({ table }).from(table));
|
||||||
|
// // @ts-expect-error
|
||||||
|
// createSelectSchema(view, { unknown: t.String() });
|
||||||
|
// // @ts-expect-error
|
||||||
|
// createSelectSchema(nestedSelect, { table: { unknown: t.String() } });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /* Disallow unknown keys in view columns - select */ {
|
||||||
|
// const view = mysqlView('test', { id: int() }).as(sql``);
|
||||||
|
// // @ts-expect-error
|
||||||
|
// createSelectSchema(view, { unknown: t.String() });
|
||||||
|
// }
|
||||||
389
tests/sqlite.test.ts
Normal file
389
tests/sqlite.test.ts
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
import { Type as t } from '@alkdev/typebox';
|
||||||
|
import { type Equal, sql } from 'drizzle-orm';
|
||||||
|
import { customType, int, sqliteTable, sqliteView, text } from 'drizzle-orm/sqlite-core';
|
||||||
|
import { test } from 'vitest';
|
||||||
|
import { bufferSchema, jsonSchema } from '~/column.ts';
|
||||||
|
import { CONSTANTS } from '~/constants.ts';
|
||||||
|
import { createInsertSchema, createSelectSchema, createUpdateSchema } from '../src';
|
||||||
|
import { Expect, expectSchemaShape } from './utils.ts';
|
||||||
|
|
||||||
|
const intSchema = t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER });
|
||||||
|
const textSchema = t.String();
|
||||||
|
|
||||||
|
test('table - select', (tc) => {
|
||||||
|
const table = sqliteTable('test', {
|
||||||
|
id: int().primaryKey({ autoIncrement: true }),
|
||||||
|
name: text().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createSelectSchema(table);
|
||||||
|
const expected = t.Object({ id: intSchema, name: textSchema });
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('table - insert', (tc) => {
|
||||||
|
const table = sqliteTable('test', {
|
||||||
|
id: int().primaryKey({ autoIncrement: true }),
|
||||||
|
name: text().notNull(),
|
||||||
|
age: int(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createInsertSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
id: t.Optional(intSchema),
|
||||||
|
name: textSchema,
|
||||||
|
age: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('table - update', (tc) => {
|
||||||
|
const table = sqliteTable('test', {
|
||||||
|
id: int().primaryKey({ autoIncrement: true }),
|
||||||
|
name: text().notNull(),
|
||||||
|
age: int(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createUpdateSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
id: t.Optional(intSchema),
|
||||||
|
name: t.Optional(textSchema),
|
||||||
|
age: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('view qb - select', (tc) => {
|
||||||
|
const table = sqliteTable('test', {
|
||||||
|
id: int().primaryKey({ autoIncrement: true }),
|
||||||
|
name: text().notNull(),
|
||||||
|
});
|
||||||
|
const view = sqliteView('test').as((qb) => qb.select({ id: table.id, age: sql``.as('age') }).from(table));
|
||||||
|
|
||||||
|
const result = createSelectSchema(view);
|
||||||
|
const expected = t.Object({ id: intSchema, age: t.Any() });
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('view columns - select', (tc) => {
|
||||||
|
const view = sqliteView('test', {
|
||||||
|
id: int().primaryKey({ autoIncrement: true }),
|
||||||
|
name: text().notNull(),
|
||||||
|
}).as(sql``);
|
||||||
|
|
||||||
|
const result = createSelectSchema(view);
|
||||||
|
const expected = t.Object({ id: intSchema, name: textSchema });
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('view with nested fields - select', (tc) => {
|
||||||
|
const table = sqliteTable('test', {
|
||||||
|
id: int().primaryKey({ autoIncrement: true }),
|
||||||
|
name: text().notNull(),
|
||||||
|
});
|
||||||
|
const view = sqliteView('test').as((qb) =>
|
||||||
|
qb.select({
|
||||||
|
id: table.id,
|
||||||
|
nested: {
|
||||||
|
name: table.name,
|
||||||
|
age: sql``.as('age'),
|
||||||
|
},
|
||||||
|
table,
|
||||||
|
}).from(table)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = createSelectSchema(view);
|
||||||
|
const expected = t.Object({
|
||||||
|
id: intSchema,
|
||||||
|
nested: t.Object({ name: textSchema, age: t.Any() }),
|
||||||
|
table: t.Object({ id: intSchema, name: textSchema }),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nullability - select', (tc) => {
|
||||||
|
const table = sqliteTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().default(1),
|
||||||
|
c4: int().notNull().default(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createSelectSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Union([intSchema, t.Null()]),
|
||||||
|
c2: intSchema,
|
||||||
|
c3: t.Union([intSchema, t.Null()]),
|
||||||
|
c4: intSchema,
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nullability - insert', (tc) => {
|
||||||
|
const table = sqliteTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().default(1),
|
||||||
|
c4: int().notNull().default(1),
|
||||||
|
c5: int().generatedAlwaysAs(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createInsertSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
c2: intSchema,
|
||||||
|
c3: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
c4: t.Optional(intSchema),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nullability - update', (tc) => {
|
||||||
|
const table = sqliteTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().default(1),
|
||||||
|
c4: int().notNull().default(1),
|
||||||
|
c5: int().generatedAlwaysAs(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createUpdateSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
c2: t.Optional(intSchema),
|
||||||
|
c3: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
c4: t.Optional(intSchema),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refine table - select', (tc) => {
|
||||||
|
const table = sqliteTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createSelectSchema(table, {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Union([intSchema, t.Null()]),
|
||||||
|
c2: t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refine table - select with custom data type', (tc) => {
|
||||||
|
const customText = customType({ dataType: () => 'text' });
|
||||||
|
const table = sqliteTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().notNull(),
|
||||||
|
c4: customText(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const customTextSchema = t.String({ minLength: 1, maxLength: 100 });
|
||||||
|
const result = createSelectSchema(table, {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
c4: customTextSchema,
|
||||||
|
});
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Union([intSchema, t.Null()]),
|
||||||
|
c2: t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
c4: customTextSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refine table - insert', (tc) => {
|
||||||
|
const table = sqliteTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().notNull(),
|
||||||
|
c4: int().generatedAlwaysAs(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createInsertSchema(table, {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
c2: t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refine table - update', (tc) => {
|
||||||
|
const table = sqliteTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int().notNull(),
|
||||||
|
c3: int().notNull(),
|
||||||
|
c4: int().generatedAlwaysAs(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = createUpdateSchema(table, {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Optional(t.Union([intSchema, t.Null()])),
|
||||||
|
c2: t.Optional(t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: 1000 })),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refine view - select', (tc) => {
|
||||||
|
const table = sqliteTable('test', {
|
||||||
|
c1: int(),
|
||||||
|
c2: int(),
|
||||||
|
c3: int(),
|
||||||
|
c4: int(),
|
||||||
|
c5: int(),
|
||||||
|
c6: int(),
|
||||||
|
});
|
||||||
|
const view = sqliteView('test').as((qb) =>
|
||||||
|
qb.select({
|
||||||
|
c1: table.c1,
|
||||||
|
c2: table.c2,
|
||||||
|
c3: table.c3,
|
||||||
|
nested: {
|
||||||
|
c4: table.c4,
|
||||||
|
c5: table.c5,
|
||||||
|
c6: table.c6,
|
||||||
|
},
|
||||||
|
table,
|
||||||
|
}).from(table)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = createSelectSchema(view, {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
nested: {
|
||||||
|
c5: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c6: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
c2: (schema) => t.Integer({ minimum: schema.minimum, maximum: 1000 }),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const expected = t.Object({
|
||||||
|
c1: t.Union([intSchema, t.Null()]),
|
||||||
|
c2: t.Union([t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: 1000 }), t.Null()]),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
nested: t.Object({
|
||||||
|
c4: t.Union([intSchema, t.Null()]),
|
||||||
|
c5: t.Union([t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: 1000 }), t.Null()]),
|
||||||
|
c6: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
}),
|
||||||
|
table: t.Object({
|
||||||
|
c1: t.Union([intSchema, t.Null()]),
|
||||||
|
c2: t.Union([t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: 1000 }), t.Null()]),
|
||||||
|
c3: t.Integer({ minimum: 1, maximum: 10 }),
|
||||||
|
c4: t.Union([intSchema, t.Null()]),
|
||||||
|
c5: t.Union([intSchema, t.Null()]),
|
||||||
|
c6: t.Union([intSchema, t.Null()]),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('all data types', (tc) => {
|
||||||
|
const table = sqliteTable('test', ({
|
||||||
|
blob,
|
||||||
|
integer,
|
||||||
|
numeric,
|
||||||
|
real,
|
||||||
|
text,
|
||||||
|
}) => ({
|
||||||
|
blob1: blob({ mode: 'buffer' }).notNull(),
|
||||||
|
blob2: blob({ mode: 'bigint' }).notNull(),
|
||||||
|
blob3: blob({ mode: 'json' }).notNull(),
|
||||||
|
integer1: integer({ mode: 'number' }).notNull(),
|
||||||
|
integer2: integer({ mode: 'boolean' }).notNull(),
|
||||||
|
integer3: integer({ mode: 'timestamp' }).notNull(),
|
||||||
|
integer4: integer({ mode: 'timestamp_ms' }).notNull(),
|
||||||
|
numeric: numeric().notNull(),
|
||||||
|
real: real().notNull(),
|
||||||
|
text1: text({ mode: 'text' }).notNull(),
|
||||||
|
text2: text({ mode: 'text', length: 10 }).notNull(),
|
||||||
|
text3: text({ mode: 'text', enum: ['a', 'b', 'c'] }).notNull(),
|
||||||
|
text4: text({ mode: 'json' }).notNull(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = createSelectSchema(table);
|
||||||
|
const expected = t.Object({
|
||||||
|
blob1: bufferSchema,
|
||||||
|
blob2: t.BigInt({ minimum: CONSTANTS.INT64_MIN, maximum: CONSTANTS.INT64_MAX }),
|
||||||
|
blob3: jsonSchema,
|
||||||
|
integer1: t.Integer({ minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER }),
|
||||||
|
integer2: t.Boolean(),
|
||||||
|
integer3: t.Date(),
|
||||||
|
integer4: t.Date(),
|
||||||
|
numeric: t.String(),
|
||||||
|
real: t.Number({ minimum: CONSTANTS.INT48_MIN, maximum: CONSTANTS.INT48_MAX }),
|
||||||
|
text1: t.String(),
|
||||||
|
text2: t.String({ maxLength: 10 }),
|
||||||
|
text3: t.Enum({ a: 'a', b: 'b', c: 'c' }),
|
||||||
|
text4: jsonSchema,
|
||||||
|
});
|
||||||
|
expectSchemaShape(tc, expected).from(result);
|
||||||
|
Expect<Equal<typeof result, typeof expected>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Disallow unknown keys in table refinement - select */ {
|
||||||
|
const table = sqliteTable('test', { id: int() });
|
||||||
|
// @ts-expect-error
|
||||||
|
createSelectSchema(table, { unknown: t.String() });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disallow unknown keys in table refinement - insert */ {
|
||||||
|
const table = sqliteTable('test', { id: int() });
|
||||||
|
// @ts-expect-error
|
||||||
|
createInsertSchema(table, { unknown: t.String() });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disallow unknown keys in table refinement - update */ {
|
||||||
|
const table = sqliteTable('test', { id: int() });
|
||||||
|
// @ts-expect-error
|
||||||
|
createUpdateSchema(table, { unknown: t.String() });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disallow unknown keys in view qb - select */ {
|
||||||
|
const table = sqliteTable('test', { id: int() });
|
||||||
|
const view = sqliteView('test').as((qb) => qb.select().from(table));
|
||||||
|
const nestedSelect = sqliteView('test').as((qb) => qb.select({ table }).from(table));
|
||||||
|
// @ts-expect-error
|
||||||
|
createSelectSchema(view, { unknown: t.String() });
|
||||||
|
// @ts-expect-error
|
||||||
|
createSelectSchema(nestedSelect, { table: { unknown: t.String() } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disallow unknown keys in view columns - select */ {
|
||||||
|
const view = sqliteView('test', { id: int() }).as(sql``);
|
||||||
|
// @ts-expect-error
|
||||||
|
createSelectSchema(view, { unknown: t.String() });
|
||||||
|
}
|
||||||
11
tests/tsconfig.json
Normal file
11
tests/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "esnext",
|
||||||
|
"target": "esnext",
|
||||||
|
"noEmit": true,
|
||||||
|
"rootDir": "..",
|
||||||
|
"outDir": "./.cache"
|
||||||
|
},
|
||||||
|
"include": [".", "../src"]
|
||||||
|
}
|
||||||
34
tests/utils.ts
Normal file
34
tests/utils.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type * as t from '@alkdev/typebox';
|
||||||
|
import { expect, type TaskContext } from 'vitest';
|
||||||
|
|
||||||
|
function removeKeysFromObject(obj: Record<string, any>, keys: string[]) {
|
||||||
|
for (const key of keys) {
|
||||||
|
delete obj[key];
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expectSchemaShape<T extends t.TObject>(t: TaskContext, expected: T) {
|
||||||
|
return {
|
||||||
|
from(actual: T) {
|
||||||
|
expect(Object.keys(actual.properties)).toStrictEqual(Object.keys(expected.properties));
|
||||||
|
const keys = ['$id', '$schema', 'title', 'description', 'default', 'examples', 'readOnly', 'writeOnly'];
|
||||||
|
|
||||||
|
for (const key of Object.keys(actual.properties)) {
|
||||||
|
expect(removeKeysFromObject(actual.properties[key]!, keys)).toStrictEqual(
|
||||||
|
removeKeysFromObject(expected.properties[key]!, keys),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expectEnumValues<T extends t.TEnum<any>>(t: TaskContext, expected: T) {
|
||||||
|
return {
|
||||||
|
from(actual: T) {
|
||||||
|
expect(actual.anyOf).toStrictEqual(expected.anyOf);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Expect<_ extends true>() {}
|
||||||
7
tsconfig.build.json
Normal file
7
tsconfig.build.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
41
tsconfig.json
Normal file
41
tsconfig.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"isolatedModules": true,
|
||||||
|
"target": "esnext",
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["es2020", "es2018", "es2017", "es7", "es6", "es5"],
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strictFunctionTypes": true,
|
||||||
|
"strictBindCallApply": true,
|
||||||
|
"strictPropertyInitialization": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"useUnknownInCatchVariables": true,
|
||||||
|
"alwaysStrict": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"allowUnusedLabels": false,
|
||||||
|
"allowUnreachableCode": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noErrorTruncation": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src", "*.ts"]
|
||||||
|
}
|
||||||
25
vitest.config.ts
Normal file
25
vitest.config.ts
Normal 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()],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user