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:
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;
|
||||
Reference in New Issue
Block a user