Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/arktype-drizzle/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# ArkType Drizzle Validators
23 changes: 23 additions & 0 deletions packages/arktype-drizzle/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@jhecht/arktype-drizzle",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"build": "echo 'Add build script here'",
"test": "vitest run",
"test:watch": "vitest --coverage",
"lint": "echo 'Add lint script here'"
},
"peerDependencies": {
"arktype": "^2.0.4",
"drizzle-orm": "0.32.1"
},
"devDependencies": {
"@jhecht/eslint-plugin": "workspace:*",
"@jhecht/typescript-config": "workspace:*",
"@vitest/coverage-v8": "^1.2.2",
"arktype": "^2.0.4",
"vitest": "^1.1.3"
}
}
91 changes: 91 additions & 0 deletions packages/arktype-drizzle/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { describe, it, expect } from 'vitest';
import { createInsertSchema, mapToArkType } from './main.js';
import {
pgTable,
text,
uuid,
integer,
date,
timestamp,
smallint,
bigint,
smallserial,
bigserial,
varchar,
} from 'drizzle-orm/pg-core';

describe('mapToArkType', () => {
const intTable = pgTable('integers', {
base: integer('base'),
smallInt: smallint('small_int'),
bigInt: bigint('big_int', { mode: 'number' }),
reallyBigInt: bigint('really_big_int', { mode: 'bigint' }),
smallSerial: smallserial('small_serial'),
bigSerial: bigserial('big_serial', { mode: 'number' }),
reallyBigSerial: bigserial('really_big_serial', { mode: 'bigint' }),
});

const textTable = pgTable('texts', {
text: text('text'),
textWithEnum: text('text_with_enums', { enum: ['a', 'b'] }),
varchar: varchar('varchar_column'),
varcharWithLimit: varchar('varchar_with_limit', { length: 10 }),
});

it('Works with integer columns', () => {
expect(mapToArkType(intTable.base)).toBe('number');
expect(mapToArkType(intTable.smallInt)).toBe('number');
expect(mapToArkType(intTable.bigInt)).toBe('number');
expect(mapToArkType(intTable.reallyBigInt)).toBe('bigint');
expect(mapToArkType(intTable.smallSerial)).toBe('number');
expect(mapToArkType(intTable.bigSerial)).toBe('number');
expect(mapToArkType(intTable.reallyBigSerial)).toBe('bigint');
});

it('Works with string/text columns', () => {
expect(mapToArkType(textTable.text)).toBe('string');
expect(mapToArkType(textTable.textWithEnum)).toBe("'a' | 'b'");
expect(mapToArkType(textTable.varchar)).toBe('string');
expect(mapToArkType(textTable.varcharWithLimit)).toBe('string<=10');
});
});

describe('createInsertSchema', () => {
const exampleWithArrayColumn = pgTable('example', {
id: uuid('id').primaryKey(),
things: integer('things').array(),
something: integer('something').notNull(),
date: date('date', { mode: 'string' }),
});

const exampleSimple = pgTable('users', {
id: uuid('id').primaryKey(),
name: text('name').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
varchar: varchar('varchar', { length: 100 }),
});

describe('createInsertSchema', () => {
it('Works with simple types', () => {
const insertSchema = createInsertSchema(exampleSimple);
const uid = crypto.randomUUID();
const d = new Date();
const output = insertSchema({
id: uid,
name: 'Testing',
createdAt: d,
varchar: 'a',
});
// No problems
expect(output.problems).toBeUndefined();

// data is valid
expect(output.data).toStrictEqual({
id: uid,
name: 'Testing',
createdAt: d,
varchar: 'a',
});
});
});
});
197 changes: 197 additions & 0 deletions packages/arktype-drizzle/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { type, type Type } from 'arktype';
import type {
Assume,
Column,
DrizzleTypeError,
Equal,
Simplify,
Table,
} from 'drizzle-orm';
import { getTableColumns, is } from 'drizzle-orm';
import {
PgArray,
PgDate,
PgTimestamp,
PgUUID,
PgVarchar,
pgTable,
text,
uuid,
} from 'drizzle-orm/pg-core';

const literalSchema = type('string | number | boolean | null');

type Literal = typeof literalSchema.infer;

type Json = Literal | { [key: string]: Json };

type MapInsertColumnsToArk<
TColumn extends Column,
TType extends Type<any>,
> = TColumn['_']['notNull'] extends false
? Type<Partial<TType>>
: TColumn['_']['hasDefault'] extends true
? Type<Partial<TType>>
: TType;

type MapSelectColumnToArk<
TColumn extends Column,
TType extends Type<any>,
> = TColumn['_']['notNull'] extends false ? Type<Partial<TType>> : TType;

type MapColumnToArk<
TColumn extends Column,
TType extends Type<any>,
TMode extends 'insert' | 'select',
> = TMode extends 'insert'
? MapInsertColumnsToArk<TColumn, TType>
: MapSelectColumnToArk<TColumn, TType>;

type MaybeOptional<
TColumn extends Column,
TType extends Type<any>,
TMode extends 'insert' | 'select',
TNoOptional extends boolean,
> = TNoOptional extends true ? TType : MapColumnToArk<TColumn, TType, TMode>;

let a = arrayOf('number | string');

type GetArkType<TColumn extends Column> =
TColumn['_']['dataType'] extends infer TDataType
? TDataType extends 'custom'
? PrecompiledDefaults['any']
: TDataType extends 'json'
? Type<Json>
: TColumn extends { enumValues: [string, ...string[]] }
? Equal<TColumn['enumValues'], [string, ...string[]]> extends true
? PrecompiledDefaults['string']
: Type<TColumn['enumValues']>
: TDataType extends 'array'
? GetArkType<
Assume<TColumn['_'], { baseColumn: Column }>['baseColumn']
>[]
: TDataType extends 'bigint'
? PrecompiledDefaults['bigint']
: TDataType extends 'number'
? PrecompiledDefaults['number']
: TDataType extends 'string'
? PrecompiledDefaults['string']
: TDataType extends 'boolean'
? PrecompiledDefaults['boolean']
: TDataType extends 'date'
? PrecompiledDefaults['Date']
: PrecompiledDefaults['any']
: never;

type ValueOrUpdater<T, TUpdaterArg> = T | ((arg: TUpdaterArg) => T);

type UnwrapValueOrUpdater<T> =
T extends ValueOrUpdater<infer U, any> ? U : never;

type Refine<TTable extends Table, TMode extends 'select' | 'insert'> = {
[K in keyof TTable['_']['columns']]?: ValueOrUpdater<
PrecompiledDefaults['any'],
TMode extends 'select'
? BuildSelectSchema<TTable, {}, true>
: BuildInsertSchema<TTable, {}, true>
>;
};

type BuildSelectSchema<
TTable extends Table,
TRefine extends Refine<TTable, 'select'>,
TNoOptional extends boolean = false,
> = Simplify<{
[K in keyof TTable['_']['columns']]: MaybeOptional<
TTable['_']['columns'][K],
K extends keyof TRefine
? Assume<UnwrapValueOrUpdater<TRefine[K]>, PrecompiledDefaults['any']>
: GetArkType<TTable['_']['columns'][K]>,
'select',
TNoOptional
>;
}>;

export type BuildInsertSchema<
TTable extends Table,
TRefine extends Refine<TTable, 'insert'> | {},
TNoOptional extends boolean = false,
> = TTable['_']['columns'] extends infer TColumns extends Record<
string,
Column<any>
>
? {
[K in keyof TColumns & string]: MaybeOptional<
TColumns[K],
K extends keyof TRefine
? Assume<UnwrapValueOrUpdater<TRefine[K]>, PrecompiledDefaults['any']>
: GetArkType<TColumns[K]>,
'insert',
TNoOptional
>;
}
: never;

const exampleTable = pgTable('something', {
id: uuid('id').primaryKey(),
name: text('name').array(),
});

export function createInsertSchema<
TTable extends Table,
TRefine extends Refine<TTable, 'insert'> = Refine<TTable, 'insert'>,
>(
table: TTable,
/**
* @param refine Refine schema fields
*/
refine?: {
[K in keyof TRefine]: K extends keyof TTable['_']['columns']
? TRefine[K]
: DrizzleTypeError<`Column '${K & string}' does not exist in table '${TTable['_']['name']}'`>;
},
): Type<
BuildInsertSchema<
TTable,
Equal<TRefine, Refine<TTable, 'insert'>> extends true ? {} : TRefine
>
> {
const columns = getTableColumns(table);
const columnEntries = Object.entries(columns);

const map = Object.fromEntries(
columnEntries.map(([key, value]) => columnToArkType(key, value)),
);

return type(map);
}

function columnToArkType(key: string, column: Column) {
// console.info(column);
let mappedKey = key;

if (!column.notNull || column.hasDefault) mappedKey = `${mappedKey}?`;

return [mappedKey, mapToArkType(column)];
}

export function createUpdateSchema() {}

export function mapToArkType(
column: Column,
): keyof PrecompiledDefaults | string {
if (is(column, PgUUID)) return 'uuid';

if (is(column, PgDate) || is(column, PgTimestamp)) return 'Date';

if (column.dataType === 'array')
return mapToArkType((column as PgArray<any, any>).baseColumn);

if (column.dataType === 'string' && column.enumValues)
return `${column.enumValues.map(v => `'${v}'`).join(' | ')}`;

if (is(column, PgVarchar) && column.length)
return `${column.dataType}<=${column.length}`;

return column.dataType as keyof PrecompiledDefaults;
}
Loading
Loading