mirror of
https://github.com/theoludwig/kysely-typegen.git
synced 2026-10-16 19:16:47 +02:00
183 lines
5.4 KiB
TypeScript
183 lines
5.4 KiB
TypeScript
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<string, string[]>
|
|
}
|
|
|
|
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> = T extends ColumnType<infer S, infer I, infer U> ? ColumnType<S, I | undefined, U> : ColumnType<T, T | undefined, T>",
|
|
"",
|
|
"export type Timestamp = ColumnType<Date, Date | string, Date | string>",
|
|
"",
|
|
"export type Numeric = ColumnType<string, number | string, number | string>",
|
|
"",
|
|
"export type Int8 = ColumnType<string, bigint | number | string, bigint | number | string>",
|
|
"",
|
|
"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<any>
|
|
public abstract readonly scalars: Record<string, string>
|
|
|
|
public constructor(input: { database: Kysely<any> }) {
|
|
this.database = input.database
|
|
}
|
|
|
|
public async getTables(): Promise<TableMetadata[]> {
|
|
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<string, string>,
|
|
inlineEnums: Map<string, string>,
|
|
): 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, string>,
|
|
): string[] {
|
|
const scalars: Record<string, string> = { ...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<IntrospectedEnums> {
|
|
return { named: [], inline: new Map() }
|
|
}
|
|
|
|
public async getEnums(): Promise<{ enums: EnumMetadata[]; inlineEnums: Map<string, string> }> {
|
|
const { named, inline } = await this.introspectEnums()
|
|
const enums = [...named].sort((a, b) => {
|
|
return a.name.localeCompare(b.name)
|
|
})
|
|
const inlineEnums = new Map<string, string>()
|
|
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<string, string>
|
|
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 }
|
|
}
|
|
}
|