mirror of
https://github.com/theoludwig/kysely-typegen.git
synced 2026-10-16 19:16:47 +02:00
refactor!: move KyselyTypegenPostgresDialect export to /postgres
To prepare for additional dialects and keep the default entry tiny,
`KyselyTypegenPostgresDialect` is now exported from a dedicated
`kysely-typegen/postgres` subpath instead of the package root.
The `KyselyTypegenDialect` base class is also reworked to support
dialects where enums are declared per-column rather
than as a named top-level type. The abstract `getEnumsMap()` method
is replaced by an optional `introspectEnums()` hook returning both
`named` and `inline` enums.
BREAKING CHANGE: `KyselyTypegenPostgresDialect` is no longer exported
from `kysely-typegen`. Import it from `kysely-typegen/postgres`.
Before:
```ts
import { KyselyTypegenPostgresDialect } from "kysely-typegen"
```
After:
```ts
import { KyselyTypegenPostgresDialect } from "kysely-typegen/postgres"
```
BREAKING CHANGE: custom dialects extending `KyselyTypegenDialect`
must replace `protected getEnumsMap()` with `protected introspectEnums()`,
which returns `{ named, inline }` instead of a flat `Map<string, string[]>`.
Before:
```ts
protected async getEnumsMap(): Promise<Map<string, string[]>> {
return new Map([["Role", ["admin", "member"]]])
}
```
After:
```ts
protected override async introspectEnums(): Promise<IntrospectedEnums> {
return {
named: [{ name: "Role", values: ["admin", "member"] }],
inline: new Map(),
}
}
```
This commit is contained in:
10 files changed
+642
-297
No files matched your search
+94
-125
@@ -1,37 +1,90 @@
|
||||
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 abstract readonly database: Kysely<any>
|
||||
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)
|
||||
})
|
||||
}
|
||||
public getTablesTypegen(tables: TableMetadata[], enums: EnumMetadata[]): string[] {
|
||||
const enumNames = new Set(
|
||||
enums.map((enumMetadata) => {
|
||||
return enumMetadata.name
|
||||
}),
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
return scalars[column.dataType] ?? "unknown"
|
||||
}
|
||||
|
||||
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) {
|
||||
const interfaceName = table.name
|
||||
result.push(`export interface ${interfaceName} {`)
|
||||
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 isEnum = enumNames.has(column.dataType)
|
||||
const baseType = isEnum ? column.dataType : (this.scalars[column.dataType] ?? "unknown")
|
||||
const baseType = this.resolveColumnType(table, column, scalars, inlineEnums)
|
||||
let columnType = column.isNullable ? `${baseType} | null` : baseType
|
||||
if (column.hasDefaultValue || column.isAutoIncrementing) {
|
||||
columnType = `Generated<${columnType}>`
|
||||
@@ -43,30 +96,38 @@ export abstract class KyselyTypegenDialect {
|
||||
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) => {
|
||||
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) {
|
||||
const enumName = enumMetadata.name
|
||||
result.push(
|
||||
`export type ${enumName} = ${enumMetadata.values
|
||||
.sort((a, b) => {
|
||||
return a.localeCompare(b)
|
||||
})
|
||||
.map((value) => {
|
||||
return `"${value}"`
|
||||
})
|
||||
.join(" | ")}`,
|
||||
`export type ${enumMetadata.name} = ${this.formatEnumUnion(enumMetadata.values)}`,
|
||||
"",
|
||||
)
|
||||
}
|
||||
@@ -76,110 +137,18 @@ export abstract class KyselyTypegenDialect {
|
||||
public async typegen(): Promise<{
|
||||
lines: string[]
|
||||
enums: EnumMetadata[]
|
||||
inlineEnums: Map<string, string>
|
||||
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",
|
||||
"",
|
||||
]
|
||||
const [tables, { enums, inlineEnums }] = await Promise.all([this.getTables(), this.getEnums()])
|
||||
const lines: string[] = [...PREAMBLE_LINES]
|
||||
lines.push(...this.getEnumsTypegen(enums))
|
||||
lines.push(...this.getTablesTypegen(tables, enums))
|
||||
lines.push(...this.getTablesTypegen(tables, enums, inlineEnums))
|
||||
lines.push("export interface DB {")
|
||||
for (const table of tables) {
|
||||
lines.push(` ${table.name}: ${table.name}`)
|
||||
}
|
||||
lines.push("}")
|
||||
return { lines, enums, tables }
|
||||
}
|
||||
}
|
||||
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 }
|
||||
const existing = enums.get(data.enumName)
|
||||
if (existing == null) {
|
||||
enums.set(data.enumName, [data.enumValue])
|
||||
} else {
|
||||
existing.push(data.enumValue)
|
||||
}
|
||||
}
|
||||
return enums
|
||||
return { lines, enums, inlineEnums, tables }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user