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
9 changes: 9 additions & 0 deletions packages/database/.kysely-codegenrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"camelCase": true,
"dialect": "postgres",
"runtimeEnums":"pascal-case",
"outFile": "src/kyselyTypes.ts",
"url": "env(SUPABASE_DB_URL)",
"envFile": ".env.local",
"includePattern": "public.*"
}
156 changes: 156 additions & 0 deletions packages/database/.zod-codegenrc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { Config, TypeScriptSerializer } from 'kysely-codegen';
import * as fs from 'fs';

const kyselyConfigData = fs.readFileSync('.kysely-codegenrc.json')
const kyselyConfig = JSON.parse(kyselyConfigData) as Config;

// From jlarmstrngiv, https://github.com/RobinBlomberg/kysely-codegen/issues/86#issuecomment-2545060194

import { generate as generateZod } from "ts-to-zod";
// import prettier, { Options as PrettierOptions } from "prettier";

const defaultBanner = `/**
* This file was generated by kysely-codegen and ts-to-zod
* Manual edits will be overwritten.
*/

`;

export type GenerateOptions = {
banner?: string;
footer?: string;
// prettierOptions?: PrettierOptions;
};

export const generateFromKysely = (
src: string,
{
banner = defaultBanner,
footer = "",
// prettierOptions = {},
}: GenerateOptions = {},
): string => {
const hasPostgresInterval = src.includes("IPostgresInterval");

// multiline replacement regex https://stackoverflow.com/a/45981809

// remove comment
src = src.replace(
/\/\*\*.* This file was generated by kysely-codegen.*?\*\//s,
"",
);
// remove unneeded imports
src = src.replaceAll(/import.*?;/gs, "");
if (hasPostgresInterval) {
// ts-to-zod is unable to parse IPostgresInterval from import
// reference node_modules/postgres-interval/index.d.ts
src =
`export interface IPostgresInterval {
years: number;
months: number;
days: number;
hours: number;
minutes: number;
seconds: number;
milliseconds: number;
// toPostgres(): string;
// toISO(): string;
// toISOString(): string;
// toISOStringShort(): string;
}` +
"\n" +
src;
}
// remove Generated type
src = src.replace(/export type Generated.*?;/s, "");
src = src.replaceAll(
/: Generated<(.*?)>/g,
(match) => "?: " + match.slice(": Generated<".length, -">".length),
);
// remove array types
src = src.replace(/export type ArrayType<T>.*?;/s, "");
src = src.replace(/export type ArrayTypeImpl<T>.*?;/s, "");
src = src.replaceAll(
/ArrayType<(.*?)>/g,
(match) => match.slice("ArrayType<".length, -">".length) + "[]",
);
// remove json column type
src = src.replaceAll(/JSONColumnType<(.*?)>/g, (match) =>
match.slice("JSONColumnType<".length, -">".length),
);
// remove DB export
src = src.replace(/export interface DB {.*?}/s, "");

// remove and simplify ColumnType
const columnTypeRegex = /export type (.*?) = ColumnType<(.*?)>;/g;
const matches = [...(src.matchAll(columnTypeRegex) ?? [])];
for (const match of matches) {
const original = match[0];
const typeName = match[1];
const types = match[2];
const reducedTypes = [...new Set((types||'').split(/ \| |, /))];
src = src.replace(
original,
`export type ${typeName} = ${reducedTypes.join(" | ")};`,
);
}

// zod programmatic api https://github.com/fabien0102/ts-to-zod/blob/main/src/core/generate.test.ts
const { getZodSchemasFile, errors } = generateZod({
sourceText: src,
skipParseJSDoc: true,
});

// console log and throw errors, if any
if (errors.length > 0) {
for (const error of errors) {
console.error(error);
}
throw new Error(`ts-to-zod generate failed`);
}

// generate zod types
let schemas = getZodSchemasFile("./database-types.ts");
// find enums names
const enumNames = [...schemas.matchAll(/\bz\.enum\(([^)]+)\)/gs)].map(([match, p1])=>p1) || [];
const schemaNames = [...schemas.matchAll(/\bz\.ZodSchema\<([^>]+)\>/gs)].map(([match, p1])=>p1) || [];
const srcImport = `import {${(enumNames.concat(schemaNames)).join(', ')}} from "./kyselyTypes";`
// apply enums
schemas = schemas.replaceAll(/\bz\.enum\b/gs, "z.nativeEnum");
// remove unneeded imports
schemas = schemas.replaceAll(/import.*?;/gs, "");
// add back zod import
schemas = `import { z } from "zod";\nimport { Insertable } from "kysely";\n${srcImport}\n\n${schemas}`;
// Insertable
schemas = schemas.replaceAll(/\bz\.ZodSchema\<([^>]+)\>/gs, (match, p1) =>
(p1.includes('Json'))?match:`z.ZodSchema<Insertable<${p1}>>`
);
// remove comment
schemas = schemas.replace("// Generated by ts-to-zod", "");
// concatenate types and schemas
schemas = banner + schemas + footer;
// format result
// schemas = await prettier.format(schemas, {
// ...prettierOptions,
// parser: "typescript",
// });

return schemas;
}


const config: Config = {
...kyselyConfig,
outFile: "src/zodTypes.ts",
serializer: {
serializeFile: (metadata, dialect, options) => {
const upstream = new TypeScriptSerializer({
runtimeEnums: kyselyConfig.runtimeEnums
});
let input = upstream.serializeFile(metadata, dialect, options);
return generateFromKysely(input);
}
}
};

export default config;
10 changes: 8 additions & 2 deletions packages/database/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"migrate": "tsx scripts/migrate.ts",
"test": "pnpm run build && cucumber-js",
"genenv": "tsx scripts/createEnv.mts",
"gentypes": "tsx scripts/genTypes.ts",
"gentypes": "tsx scripts/genTypes.ts && kysely-codegen && kysely-codegen --config-file .zod-codegenrc.ts",
"dbdiff": "supabase stop && supabase db diff",
"dbdiff:save": "supabase stop && supabase db diff -f",
"deploy": "tsx scripts/deploy.ts",
Expand All @@ -44,19 +44,25 @@
"@repo/utils": "workspace:*",
"@supabase/auth-js": "catalog:",
"@supabase/functions-js": "catalog:",
"@supabase/supabase-js": "catalog:"
"@supabase/supabase-js": "catalog:",
"kysely": "^0.28.8",
"pg": "^8.16.3",
"zod": "^4.1.12"
},
"devDependencies": {
"@cucumber/cucumber": "^12.1.0",
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/node": "^20",
"@types/pg": "^8.15.5",
"@vercel/sdk": "^1.16.0",
"dotenv": "^16.6.1",
"eslint": "catalog:",
"kysely-codegen": "^0.19.0",
"prettier-plugin-gherkin": "^3.1.2",
"supabase": "^2.53.6",
"ts-node-maintained": "^10.9.5",
"ts-to-zod": "^5.0.1",
"tsx": "4.20.6",
"typescript": "5.9.2",
"vercel": "48.6.0"
Expand Down
Loading