mirror of
https://github.com/theoludwig/kysely-typegen.git
synced 2026-05-22 16:23:25 +02:00
192 lines
5.8 KiB
TypeScript
192 lines
5.8 KiB
TypeScript
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<any>
|
|
public abstract readonly scalars: Record<string, string>
|
|
|
|
public async getTables(): Promise<TableMetadata[]> {
|
|
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<Map<string, string[]>>
|
|
public async getEnums(): Promise<EnumMetadata[]> {
|
|
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<string, string> = {
|
|
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<Map<string, string[]>> {
|
|
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<string, string[]>()
|
|
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> = 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",
|
|
"",
|
|
]
|
|
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 }
|
|
}
|
|
}
|