Skip to content

Commit c947bde

Browse files
committed
feat: Adding initial validators
1 parent da05c8d commit c947bde

File tree

5 files changed

+431
-0
lines changed

5 files changed

+431
-0
lines changed

packages/arktype-drizzle/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# ArkType Drizzle Validators
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "@jhecht/arktype-drizzle",
3+
"version": "0.0.1",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"build": "echo 'Add build script here'",
8+
"test": "vitest run",
9+
"test:watch": "vitest --coverage",
10+
"lint": "echo 'Add lint script here'"
11+
},
12+
"peerDependencies": {
13+
"arktype": "^2.0.4",
14+
"drizzle-orm": "0.32.1"
15+
},
16+
"devDependencies": {
17+
"@jhecht/eslint-plugin": "workspace:*",
18+
"@jhecht/typescript-config": "workspace:*",
19+
"@vitest/coverage-v8": "^1.2.2",
20+
"arktype": "^2.0.4",
21+
"vitest": "^1.1.3"
22+
}
23+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { createInsertSchema, mapToArkType } from './main.js';
3+
import {
4+
pgTable,
5+
text,
6+
uuid,
7+
integer,
8+
date,
9+
timestamp,
10+
smallint,
11+
bigint,
12+
smallserial,
13+
bigserial,
14+
varchar,
15+
} from 'drizzle-orm/pg-core';
16+
17+
describe('mapToArkType', () => {
18+
const intTable = pgTable('integers', {
19+
base: integer('base'),
20+
smallInt: smallint('small_int'),
21+
bigInt: bigint('big_int', { mode: 'number' }),
22+
reallyBigInt: bigint('really_big_int', { mode: 'bigint' }),
23+
smallSerial: smallserial('small_serial'),
24+
bigSerial: bigserial('big_serial', { mode: 'number' }),
25+
reallyBigSerial: bigserial('really_big_serial', { mode: 'bigint' }),
26+
});
27+
28+
const textTable = pgTable('texts', {
29+
text: text('text'),
30+
textWithEnum: text('text_with_enums', { enum: ['a', 'b'] }),
31+
varchar: varchar('varchar_column'),
32+
varcharWithLimit: varchar('varchar_with_limit', { length: 10 }),
33+
});
34+
35+
it('Works with integer columns', () => {
36+
expect(mapToArkType(intTable.base)).toBe('number');
37+
expect(mapToArkType(intTable.smallInt)).toBe('number');
38+
expect(mapToArkType(intTable.bigInt)).toBe('number');
39+
expect(mapToArkType(intTable.reallyBigInt)).toBe('bigint');
40+
expect(mapToArkType(intTable.smallSerial)).toBe('number');
41+
expect(mapToArkType(intTable.bigSerial)).toBe('number');
42+
expect(mapToArkType(intTable.reallyBigSerial)).toBe('bigint');
43+
});
44+
45+
it('Works with string/text columns', () => {
46+
expect(mapToArkType(textTable.text)).toBe('string');
47+
expect(mapToArkType(textTable.textWithEnum)).toBe("'a' | 'b'");
48+
expect(mapToArkType(textTable.varchar)).toBe('string');
49+
expect(mapToArkType(textTable.varcharWithLimit)).toBe('string<=10');
50+
});
51+
});
52+
53+
describe('createInsertSchema', () => {
54+
const exampleWithArrayColumn = pgTable('example', {
55+
id: uuid('id').primaryKey(),
56+
things: integer('things').array(),
57+
something: integer('something').notNull(),
58+
date: date('date', { mode: 'string' }),
59+
});
60+
61+
const exampleSimple = pgTable('users', {
62+
id: uuid('id').primaryKey(),
63+
name: text('name').notNull(),
64+
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(),
65+
varchar: varchar('varchar', { length: 100 }),
66+
});
67+
68+
describe('createInsertSchema', () => {
69+
it('Works with simple types', () => {
70+
const insertSchema = createInsertSchema(exampleSimple);
71+
const uid = crypto.randomUUID();
72+
const d = new Date();
73+
const output = insertSchema({
74+
id: uid,
75+
name: 'Testing',
76+
createdAt: d,
77+
varchar: 'a',
78+
});
79+
// No problems
80+
expect(output.problems).toBeUndefined();
81+
82+
// data is valid
83+
expect(output.data).toStrictEqual({
84+
id: uid,
85+
name: 'Testing',
86+
createdAt: d,
87+
varchar: 'a',
88+
});
89+
});
90+
});
91+
});
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { type, type Type } from 'arktype';
2+
import type {
3+
Assume,
4+
Column,
5+
DrizzleTypeError,
6+
Equal,
7+
Simplify,
8+
Table,
9+
} from 'drizzle-orm';
10+
import { getTableColumns, is } from 'drizzle-orm';
11+
import {
12+
PgArray,
13+
PgDate,
14+
PgTimestamp,
15+
PgUUID,
16+
PgVarchar,
17+
pgTable,
18+
text,
19+
uuid,
20+
} from 'drizzle-orm/pg-core';
21+
22+
const literalSchema = type('string | number | boolean | null');
23+
24+
type Literal = typeof literalSchema.infer;
25+
26+
type Json = Literal | { [key: string]: Json };
27+
28+
type MapInsertColumnsToArk<
29+
TColumn extends Column,
30+
TType extends Type<any>,
31+
> = TColumn['_']['notNull'] extends false
32+
? Type<Partial<TType>>
33+
: TColumn['_']['hasDefault'] extends true
34+
? Type<Partial<TType>>
35+
: TType;
36+
37+
type MapSelectColumnToArk<
38+
TColumn extends Column,
39+
TType extends Type<any>,
40+
> = TColumn['_']['notNull'] extends false ? Type<Partial<TType>> : TType;
41+
42+
type MapColumnToArk<
43+
TColumn extends Column,
44+
TType extends Type<any>,
45+
TMode extends 'insert' | 'select',
46+
> = TMode extends 'insert'
47+
? MapInsertColumnsToArk<TColumn, TType>
48+
: MapSelectColumnToArk<TColumn, TType>;
49+
50+
type MaybeOptional<
51+
TColumn extends Column,
52+
TType extends Type<any>,
53+
TMode extends 'insert' | 'select',
54+
TNoOptional extends boolean,
55+
> = TNoOptional extends true ? TType : MapColumnToArk<TColumn, TType, TMode>;
56+
57+
let a = arrayOf('number | string');
58+
59+
type GetArkType<TColumn extends Column> =
60+
TColumn['_']['dataType'] extends infer TDataType
61+
? TDataType extends 'custom'
62+
? PrecompiledDefaults['any']
63+
: TDataType extends 'json'
64+
? Type<Json>
65+
: TColumn extends { enumValues: [string, ...string[]] }
66+
? Equal<TColumn['enumValues'], [string, ...string[]]> extends true
67+
? PrecompiledDefaults['string']
68+
: Type<TColumn['enumValues']>
69+
: TDataType extends 'array'
70+
? GetArkType<
71+
Assume<TColumn['_'], { baseColumn: Column }>['baseColumn']
72+
>[]
73+
: TDataType extends 'bigint'
74+
? PrecompiledDefaults['bigint']
75+
: TDataType extends 'number'
76+
? PrecompiledDefaults['number']
77+
: TDataType extends 'string'
78+
? PrecompiledDefaults['string']
79+
: TDataType extends 'boolean'
80+
? PrecompiledDefaults['boolean']
81+
: TDataType extends 'date'
82+
? PrecompiledDefaults['Date']
83+
: PrecompiledDefaults['any']
84+
: never;
85+
86+
type ValueOrUpdater<T, TUpdaterArg> = T | ((arg: TUpdaterArg) => T);
87+
88+
type UnwrapValueOrUpdater<T> =
89+
T extends ValueOrUpdater<infer U, any> ? U : never;
90+
91+
type Refine<TTable extends Table, TMode extends 'select' | 'insert'> = {
92+
[K in keyof TTable['_']['columns']]?: ValueOrUpdater<
93+
PrecompiledDefaults['any'],
94+
TMode extends 'select'
95+
? BuildSelectSchema<TTable, {}, true>
96+
: BuildInsertSchema<TTable, {}, true>
97+
>;
98+
};
99+
100+
type BuildSelectSchema<
101+
TTable extends Table,
102+
TRefine extends Refine<TTable, 'select'>,
103+
TNoOptional extends boolean = false,
104+
> = Simplify<{
105+
[K in keyof TTable['_']['columns']]: MaybeOptional<
106+
TTable['_']['columns'][K],
107+
K extends keyof TRefine
108+
? Assume<UnwrapValueOrUpdater<TRefine[K]>, PrecompiledDefaults['any']>
109+
: GetArkType<TTable['_']['columns'][K]>,
110+
'select',
111+
TNoOptional
112+
>;
113+
}>;
114+
115+
export type BuildInsertSchema<
116+
TTable extends Table,
117+
TRefine extends Refine<TTable, 'insert'> | {},
118+
TNoOptional extends boolean = false,
119+
> = TTable['_']['columns'] extends infer TColumns extends Record<
120+
string,
121+
Column<any>
122+
>
123+
? {
124+
[K in keyof TColumns & string]: MaybeOptional<
125+
TColumns[K],
126+
K extends keyof TRefine
127+
? Assume<UnwrapValueOrUpdater<TRefine[K]>, PrecompiledDefaults['any']>
128+
: GetArkType<TColumns[K]>,
129+
'insert',
130+
TNoOptional
131+
>;
132+
}
133+
: never;
134+
135+
const exampleTable = pgTable('something', {
136+
id: uuid('id').primaryKey(),
137+
name: text('name').array(),
138+
});
139+
140+
export function createInsertSchema<
141+
TTable extends Table,
142+
TRefine extends Refine<TTable, 'insert'> = Refine<TTable, 'insert'>,
143+
>(
144+
table: TTable,
145+
/**
146+
* @param refine Refine schema fields
147+
*/
148+
refine?: {
149+
[K in keyof TRefine]: K extends keyof TTable['_']['columns']
150+
? TRefine[K]
151+
: DrizzleTypeError<`Column '${K & string}' does not exist in table '${TTable['_']['name']}'`>;
152+
},
153+
): Type<
154+
BuildInsertSchema<
155+
TTable,
156+
Equal<TRefine, Refine<TTable, 'insert'>> extends true ? {} : TRefine
157+
>
158+
> {
159+
const columns = getTableColumns(table);
160+
const columnEntries = Object.entries(columns);
161+
162+
const map = Object.fromEntries(
163+
columnEntries.map(([key, value]) => columnToArkType(key, value)),
164+
);
165+
166+
return type(map);
167+
}
168+
169+
function columnToArkType(key: string, column: Column) {
170+
// console.info(column);
171+
let mappedKey = key;
172+
173+
if (!column.notNull || column.hasDefault) mappedKey = `${mappedKey}?`;
174+
175+
return [mappedKey, mapToArkType(column)];
176+
}
177+
178+
export function createUpdateSchema() {}
179+
180+
export function mapToArkType(
181+
column: Column,
182+
): keyof PrecompiledDefaults | string {
183+
if (is(column, PgUUID)) return 'uuid';
184+
185+
if (is(column, PgDate) || is(column, PgTimestamp)) return 'Date';
186+
187+
if (column.dataType === 'array')
188+
return mapToArkType((column as PgArray<any, any>).baseColumn);
189+
190+
if (column.dataType === 'string' && column.enumValues)
191+
return `${column.enumValues.map(v => `'${v}'`).join(' | ')}`;
192+
193+
if (is(column, PgVarchar) && column.length)
194+
return `${column.dataType}<=${column.length}`;
195+
196+
return column.dataType as keyof PrecompiledDefaults;
197+
}

0 commit comments

Comments
 (0)