Skip to content

Commit 0e142f2

Browse files
committed
zod generation
1 parent 5cbdcbd commit 0e142f2

File tree

5 files changed

+1449
-70
lines changed

5 files changed

+1449
-70
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"camelCase": true,
3+
"dialect": "postgres",
4+
"runtimeEnums":"pascal-case",
5+
"outFile": "src/kyselyTypes.ts",
6+
"url": "env(SUPABASE_DB_URL)",
7+
"envFile": ".env.local",
8+
"includePattern": "public.*"
9+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { Config, TypeScriptSerializer } from 'kysely-codegen';
2+
import * as fs from 'fs';
3+
4+
const kyselyConfigData = fs.readFileSync('.kysely-codegenrc.json')
5+
const kyselyConfig = JSON.parse(kyselyConfigData) as Config;
6+
7+
// From jlarmstrngiv, https://github.com/RobinBlomberg/kysely-codegen/issues/86#issuecomment-2545060194
8+
9+
import { generate as generateZod } from "ts-to-zod";
10+
// import prettier, { Options as PrettierOptions } from "prettier";
11+
12+
const defaultBanner = `/**
13+
* This file was generated by kysely-codegen and ts-to-zod
14+
* Manual edits will be overwritten.
15+
*/
16+
17+
`;
18+
19+
export type GenerateOptions = {
20+
banner?: string;
21+
footer?: string;
22+
// prettierOptions?: PrettierOptions;
23+
};
24+
25+
export const generateFromKysely = (
26+
src: string,
27+
{
28+
banner = defaultBanner,
29+
footer = "",
30+
// prettierOptions = {},
31+
}: GenerateOptions = {},
32+
): string => {
33+
const hasPostgresInterval = src.includes("IPostgresInterval");
34+
35+
// multiline replacement regex https://stackoverflow.com/a/45981809
36+
37+
// remove comment
38+
src = src.replace(
39+
/\/\*\*.* This file was generated by kysely-codegen.*?\*\//s,
40+
"",
41+
);
42+
// remove unneeded imports
43+
src = src.replaceAll(/import.*?;/gs, "");
44+
if (hasPostgresInterval) {
45+
// ts-to-zod is unable to parse IPostgresInterval from import
46+
// reference node_modules/postgres-interval/index.d.ts
47+
src =
48+
`export interface IPostgresInterval {
49+
years: number;
50+
months: number;
51+
days: number;
52+
hours: number;
53+
minutes: number;
54+
seconds: number;
55+
milliseconds: number;
56+
// toPostgres(): string;
57+
// toISO(): string;
58+
// toISOString(): string;
59+
// toISOStringShort(): string;
60+
}` +
61+
"\n" +
62+
src;
63+
}
64+
// remove Generated type
65+
src = src.replace(/export type Generated.*?;/s, "");
66+
src = src.replaceAll(
67+
/: Generated<(.*?)>/g,
68+
(match) => "?: " + match.slice(": Generated<".length, -">".length),
69+
);
70+
// remove array types
71+
src = src.replace(/export type ArrayType<T>.*?;/s, "");
72+
src = src.replace(/export type ArrayTypeImpl<T>.*?;/s, "");
73+
src = src.replaceAll(
74+
/ArrayType<(.*?)>/g,
75+
(match) => match.slice("ArrayType<".length, -">".length) + "[]",
76+
);
77+
// remove json column type
78+
src = src.replaceAll(/JSONColumnType<(.*?)>/g, (match) =>
79+
match.slice("JSONColumnType<".length, -">".length),
80+
);
81+
// remove DB export
82+
src = src.replace(/export interface DB {.*?}/s, "");
83+
84+
// remove and simplify ColumnType
85+
const columnTypeRegex = /export type (.*?) = ColumnType<(.*?)>;/g;
86+
const matches = [...(src.matchAll(columnTypeRegex) ?? [])];
87+
for (const match of matches) {
88+
const original = match[0];
89+
const typeName = match[1];
90+
const types = match[2];
91+
const reducedTypes = [...new Set((types||'').split(/ \| |, /))];
92+
src = src.replace(
93+
original,
94+
`export type ${typeName} = ${reducedTypes.join(" | ")};`,
95+
);
96+
}
97+
98+
// zod programmatic api https://github.com/fabien0102/ts-to-zod/blob/main/src/core/generate.test.ts
99+
const { getZodSchemasFile, errors } = generateZod({
100+
sourceText: src,
101+
skipParseJSDoc: true,
102+
});
103+
104+
// console log and throw errors, if any
105+
if (errors.length > 0) {
106+
for (const error of errors) {
107+
console.error(error);
108+
}
109+
throw new Error(`ts-to-zod generate failed`);
110+
}
111+
112+
// generate zod types
113+
let schemas = getZodSchemasFile("./database-types.ts");
114+
// find enums names
115+
const enumNames = [...schemas.matchAll(/\bz\.enum\(([^)]+)\)/gs)].map(([match, p1])=>p1) || [];
116+
const schemaNames = [...schemas.matchAll(/\bz\.ZodSchema\<([^>]+)\>/gs)].map(([match, p1])=>p1) || [];
117+
const srcImport = `import {${(enumNames.concat(schemaNames)).join(', ')}} from "./kyselyTypes";`
118+
// apply enums
119+
schemas = schemas.replaceAll(/\bz\.enum\b/gs, "z.nativeEnum");
120+
// remove unneeded imports
121+
schemas = schemas.replaceAll(/import.*?;/gs, "");
122+
// add back zod import
123+
schemas = `import { z } from "zod";\nimport { Insertable } from "kysely";\n${srcImport}\n\n${schemas}`;
124+
// Insertable
125+
schemas = schemas.replaceAll(/\bz\.ZodSchema\<([^>]+)\>/gs, (match, p1) =>
126+
(p1.includes('Json'))?match:`z.ZodSchema<Insertable<${p1}>>`
127+
);
128+
// remove comment
129+
schemas = schemas.replace("// Generated by ts-to-zod", "");
130+
// concatenate types and schemas
131+
schemas = banner + schemas + footer;
132+
// format result
133+
// schemas = await prettier.format(schemas, {
134+
// ...prettierOptions,
135+
// parser: "typescript",
136+
// });
137+
138+
return schemas;
139+
}
140+
141+
142+
const config: Config = {
143+
...kyselyConfig,
144+
outFile: "src/zodTypes.ts",
145+
serializer: {
146+
serializeFile: (metadata, dialect, options) => {
147+
const upstream = new TypeScriptSerializer({
148+
runtimeEnums: kyselyConfig.runtimeEnums
149+
});
150+
let input = upstream.serializeFile(metadata, dialect, options);
151+
return generateFromKysely(input);
152+
}
153+
}
154+
};
155+
156+
export default config;

packages/database/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
"@supabase/functions-js": "catalog:",
4747
"@supabase/supabase-js": "catalog:",
4848
"kysely": "^0.28.8",
49-
"pg": "^8.16.3"
49+
"pg": "^8.16.3",
50+
"zod": "^4.1.12"
5051
},
5152
"devDependencies": {
5253
"@cucumber/cucumber": "^12.1.0",
@@ -61,6 +62,7 @@
6162
"prettier-plugin-gherkin": "^3.1.2",
6263
"supabase": "^2.53.6",
6364
"ts-node-maintained": "^10.9.5",
65+
"ts-to-zod": "^5.0.1",
6466
"tsx": "4.20.6",
6567
"typescript": "5.9.2",
6668
"vercel": "48.6.0"
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { DB } from '../kyselyTypes';
2+
import { Pool } from 'pg'
3+
import { Kysely, PostgresDialect } from 'kysely'
4+
5+
let client: Kysely<DB>|null|false = null;
6+
7+
export const getClient = (): Kysely<DB>|false => {
8+
if (client === null) {
9+
if (!process.env.SUPABASE_URL) {
10+
client = false;
11+
} else {
12+
try {
13+
const url = new URL(process.env.SUPABASE_URL);
14+
const dialect = new PostgresDialect({
15+
pool: new Pool({
16+
database: url.pathname,
17+
host: url.host,
18+
user: url.username,
19+
password: url.password,
20+
port: parseInt(url.port),
21+
max: 10,
22+
})
23+
})
24+
client = new Kysely<DB>({
25+
dialect,
26+
});
27+
} catch {
28+
client = false;
29+
}
30+
}
31+
}
32+
return client;
33+
}

0 commit comments

Comments
 (0)