import type { Kysely, TableMetadata as KyselyTableMetadata } from "kysely" export type TableMetadata = KyselyTableMetadata export type ColumnMetadata = TableMetadata["columns"][number] export interface EnumMetadata { name: string values: string[] } export interface IntrospectedEnums { named: EnumMetadata[] inline: Map } const PREAMBLE_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", "", ] export abstract class KyselyTypegenDialect { public readonly database: Kysely public abstract readonly scalars: Record public constructor(input: { database: Kysely }) { this.database = input.database } public async getTables(): Promise { const tables = await this.database.introspection.getTables() return tables.sort((a, b) => { return a.name.localeCompare(b.name) }) } protected normalizeDataType(dataType: string): string { return dataType } protected resolveColumnType( table: TableMetadata, column: ColumnMetadata, scalars: Record, inlineEnums: Map, ): string { const inlineUnion = inlineEnums.get(`${table.name}.${column.name}`) if (inlineUnion != null) { return inlineUnion } const dataType = this.normalizeDataType(column.dataType) return scalars[dataType] ?? "unknown" } protected formatColumnComment(comment: string): string[] { const trimmedComment = comment.trim() if (trimmedComment.length === 0) { return [] } const lines = trimmedComment.replaceAll("*/", "*\\/").split(/\r?\n/u) if (lines.length === 1) { return [` /** ${lines[0]} */`] } return [ " /**", ...lines.map((line) => { return line.length === 0 ? " *" : ` * ${line}` }), " */", ] } public getTablesTypegen( tables: TableMetadata[], enums: EnumMetadata[], inlineEnums: Map, ): string[] { const scalars: Record = { ...this.scalars } for (const enumMetadata of enums) { scalars[enumMetadata.name] = enumMetadata.name } const result: string[] = [] for (const table of tables) { result.push(`export interface ${table.name} {`) const columns = [...table.columns].sort((a, b) => { return a.name.localeCompare(b.name) }) for (const column of columns) { const baseType = this.resolveColumnType(table, column, scalars, inlineEnums) let columnType = column.isNullable ? `${baseType} | null` : baseType if (column.hasDefaultValue || column.isAutoIncrementing) { columnType = `Generated<${columnType}>` } if (column.comment != null) { result.push(...this.formatColumnComment(column.comment)) } result.push(` ${column.name}: ${columnType}`) } result.push("}", "") } return result } protected async introspectEnums(): Promise { return { named: [], inline: new Map() } } public async getEnums(): Promise<{ enums: EnumMetadata[]; inlineEnums: Map }> { const { named, inline } = await this.introspectEnums() const enums = [...named].sort((a, b) => { return a.name.localeCompare(b.name) }) const inlineEnums = new Map() for (const [key, values] of inline) { inlineEnums.set(key, this.formatEnumUnion(values)) } return { enums, inlineEnums } } protected formatEnumUnion(values: string[]): string { return [...values] .sort((a, b) => { return a.localeCompare(b) }) .map((value) => { return `"${value}"` }) .join(" | ") } public getEnumsTypegen(enums: EnumMetadata[]): string[] { const result: string[] = [] for (const enumMetadata of enums) { result.push( `export type ${enumMetadata.name} = ${this.formatEnumUnion(enumMetadata.values)}`, "", ) } return result } public async typegen(): Promise<{ lines: string[] enums: EnumMetadata[] inlineEnums: Map tables: TableMetadata[] }> { const [tables, { enums, inlineEnums }] = await Promise.all([this.getTables(), this.getEnums()]) const lines: string[] = [ ...PREAMBLE_LINES, ...this.getEnumsTypegen(enums), ...this.getTablesTypegen(tables, enums, inlineEnums), "export interface DB {", ...tables.map((table) => { return ` ${table.name}: ${table.name}` }), "}", ] return { lines, enums, inlineEnums, tables } } }