import type { Kysely, TableMetadata as KyselyTableMetadata } from "kysely" export type TableMetadata = KyselyTableMetadata export interface EnumMetadata { name: string values: string[] } export abstract class KyselyTypegenDialect { public abstract readonly database: Kysely public abstract readonly scalars: Record public async getTables(): Promise { const tables = await this.database.introspection.getTables() return tables.sort((a, b) => { return a.name.localeCompare(b.name) }) } public getTablesTypegen(tables: TableMetadata[], enums: EnumMetadata[]): string[] { const enumNames = new Set( enums.map((enumMetadata) => { return enumMetadata.name }), ) const result: string[] = [] for (const table of tables) { const interfaceName = table.name result.push(`export interface ${interfaceName} {`) const columns = [...table.columns].sort((a, b) => { return a.name.localeCompare(b.name) }) for (const column of columns) { const isEnum = enumNames.has(column.dataType) const baseType = isEnum ? column.dataType : (this.scalars[column.dataType] ?? "unknown") let columnType = column.isNullable ? `${baseType} | null` : baseType if (column.hasDefaultValue || column.isAutoIncrementing) { columnType = `Generated<${columnType}>` } result.push(` ${column.name}: ${columnType}`) } result.push("}", "") } return result } protected abstract getEnumsMap(): Promise> public async getEnums(): Promise { const enumsMap = await this.getEnumsMap() const enums: EnumMetadata[] = [] for (const [name, values] of enumsMap) { enums.push({ name, values }) } return enums.sort((a, b) => { return a.name.localeCompare(b.name) }) } public getEnumsTypegen(enums: EnumMetadata[]): string[] { const result: string[] = [] for (const enumMetadata of enums) { const enumName = enumMetadata.name result.push( `export type ${enumName} = ${enumMetadata.values .map((value) => { return `"${value}"` }) .join(" | ")}`, "", ) } return result } public abstract typegen(): Promise<{ lines: string[] enums: EnumMetadata[] tables: TableMetadata[] }> } export class KyselyTypegenPostgresDialect extends KyselyTypegenDialect { public database: KyselyTypegenDialect["database"] // These types have been found through experimentation in Adminer and in the 'pg' source code. public readonly scalars: Record = { bit: "string", bool: "boolean", box: "string", bpchar: "string", bytea: "Buffer", cidr: "string", date: "Timestamp", float4: "number", float8: "number", inet: "string", int2: "number", int4: "number", int8: "Int8", json: "Json", jsonb: "Json", line: "string", lseg: "string", macaddr: "string", money: "string", numeric: "Numeric", oid: "number", path: "string", polygon: "string", text: "string", time: "string", timestamp: "Timestamp", timestamptz: "Timestamp", timetz: "string", tsquery: "string", tsvector: "string", uuid: "string", varbit: "string", varchar: "string", xml: "string", } public constructor(input: { database: KyselyTypegenDialect["database"] }) { super() this.database = input.database } protected async getEnumsMap(): Promise> { const rows = await this.database .withoutPlugins() .selectFrom("pg_type as type") .innerJoin("pg_enum as enum", "type.oid", "enum.enumtypid") .innerJoin("pg_catalog.pg_namespace as namespace", "namespace.oid", "type.typnamespace") .select(["type.typname as enumName", "enum.enumlabel as enumValue"]) .execute() const enums = new Map() for (const row of rows) { const data = row as { enumName: string; enumValue: string } enums.set( data.enumName, [...(enums.get(data.enumName) ?? []), data.enumValue].sort((a, b) => { return a.localeCompare(b) }), ) } return enums } /** * Generate TypeScript types based on the database schema, including tables and enums. */ public async typegen(): Promise<{ lines: string[] enums: EnumMetadata[] tables: TableMetadata[] }> { const [tables, enums] = await Promise.all([this.getTables(), this.getEnums()]) const lines: string[] = [ `// This file was automatically generated by \`kysely-typegen\`.`, "// Do not edit this file manually.", "", 'import type { ColumnType } from "kysely"', "", "export type Generated = T extends ColumnType ? ColumnType : ColumnType", "", "export type Timestamp = ColumnType", "", "export type Numeric = ColumnType", "", "export type Int8 = ColumnType", "", "export type Json = JsonValue", "", "export type JsonArray = JsonValue[]", "", "export interface JsonObject {", " [x: string]: JsonValue | undefined", "}", "", "export type JsonPrimitive = boolean | number | string | null", "", "export type JsonValue = JsonArray | JsonObject | JsonPrimitive", "", ] lines.push(...this.getEnumsTypegen(enums)) lines.push(...this.getTablesTypegen(tables, enums)) lines.push("export interface DB {") for (const table of tables) { lines.push(` ${table.name}: ${table.name}`) } lines.push("}") return { lines, enums, tables } } }