feat: generate JSDoc from column comments

This commit is contained in:
theoludwig committed 2026-10-02 16:51:15 +02:00
1 parent 3849b14615
commit ff16be6787
6 files changed
+74 -3

No files matched your search

+13
View File
@@ -168,6 +168,19 @@ const rows = await database.selectFrom("User").selectAll().execute()
Fully type-safe queries derived from your actual database schema.
### Column comments
Column comments (`COMMENT ON COLUMN` in PostgreSQL, `COMMENT '...'` in MySQL) are emitted as JSDoc above the column, so they show up in your editor on hover:
```ts
export interface Users {
/** Contact email address. */
email: string | null
}
```
SQLite has no column comments, so none are generated.
## Extending to other database dialects
`kysely-typegen` ships with `KyselyTypegenPostgresDialect`, `KyselyTypegenMySQLDialect`, and `KyselyTypegenSQLiteDialect`, but you can add support for any database by extending the abstract `KyselyTypegenDialect` class.
@@ -62,10 +62,16 @@ exports[`typegen MySQL > generate types matching snapshot 1`] = `
"}",
"",
"export interface Orders {",
" /**",
" * Total amount in cents.",
" *",
" * Excludes taxes.",
" */",
" amountCents: number",
" createdAt: Generated<Timestamp>",
" currency: Generated<\\"EUR\\" | \\"GBP\\" | \\"USD\\">",
" id: Generated<Int8>",
" /** Free text, may contain *\\\\/ characters. */",
" note: string | null",
" status: Generated<\\"cancelled\\" | \\"paid\\" | \\"pending\\" | \\"shipped\\">",
" userId: number",
@@ -73,6 +79,7 @@ exports[`typegen MySQL > generate types matching snapshot 1`] = `
"",
"export interface Users {",
" createdAt: Generated<Timestamp>",
" /** Contact email address. */",
" email: string | null",
" id: Generated<number>",
" isActive: Generated<number>",
@@ -79,10 +79,16 @@ exports[`typegen PostgreSQL > generate types matching snapshot (kysely-postgres-
"}",
"",
"export interface Orders {",
" /**",
" * Total amount in cents.",
" *",
" * Excludes taxes.",
" */",
" amountCents: number",
" createdAt: Generated<Timestamp>",
" currency: Generated<Currency>",
" id: Generated<Int8>",
" /** Free text, may contain *\\\\/ characters. */",
" note: string | null",
" status: Generated<OrderStatus>",
" userId: string",
@@ -90,6 +96,7 @@ exports[`typegen PostgreSQL > generate types matching snapshot (kysely-postgres-
"",
"export interface Users {",
" createdAt: Generated<Timestamp>",
" /** Contact email address. */",
" email: string | null",
" id: Generated<string>",
" isActive: Generated<boolean>",
@@ -190,10 +197,16 @@ exports[`typegen PostgreSQL > generate types matching snapshot (pg) 1`] = `
"}",
"",
"export interface Orders {",
" /**",
" * Total amount in cents.",
" *",
" * Excludes taxes.",
" */",
" amountCents: number",
" createdAt: Generated<Timestamp>",
" currency: Generated<Currency>",
" id: Generated<Int8>",
" /** Free text, may contain *\\\\/ characters. */",
" note: string | null",
" status: Generated<OrderStatus>",
" userId: string",
@@ -201,6 +214,7 @@ exports[`typegen PostgreSQL > generate types matching snapshot (pg) 1`] = `
"",
"export interface Users {",
" createdAt: Generated<Timestamp>",
" /** Contact email address. */",
" email: string | null",
" id: Generated<string>",
" isActive: Generated<boolean>",
+9 -3
View File
@@ -117,7 +117,9 @@ const createSchema = async (database: Kysely<any>): Promise<void> => {
.addColumn("username", "varchar(50)", (column) => {
return column.notNull().unique()
})
.addColumn("email", "text")
.addColumn("email", "text", (column) => {
return column.modifyEnd(sql`comment ${sql.lit("Contact email address.")}`)
})
.addColumn("role", sql`enum('admin','member','guest')`, (column) => {
return column.notNull().defaultTo("member")
})
@@ -144,9 +146,13 @@ const createSchema = async (database: Kysely<any>): Promise<void> => {
return column.notNull().defaultTo("EUR")
})
.addColumn("amountCents", "integer", (column) => {
return column.notNull()
return column
.notNull()
.modifyEnd(sql`comment ${sql.lit("Total amount in cents.\n\nExcludes taxes.")}`)
})
.addColumn("note", "text", (column) => {
return column.modifyEnd(sql`comment ${sql.lit("Free text, may contain */ characters.")}`)
})
.addColumn("note", "text")
.addColumn("createdAt", "timestamp", (column) => {
return column.notNull().defaultTo(sql`current_timestamp`)
})
+10
View File
@@ -195,6 +195,16 @@ const createSchema = async (database: Kysely<any>): Promise<void> => {
return column.notNull().defaultTo(sql`now()`)
})
.execute()
await sql`comment on column ${sql.ref("Users.email")} is ${sql.lit("Contact email address.")}`.execute(
database,
)
await sql`comment on column ${sql.ref("Orders.amountCents")} is ${sql.lit("Total amount in cents.\n\nExcludes taxes.")}`.execute(
database,
)
await sql`comment on column ${sql.ref("Orders.note")} is ${sql.lit("Free text, may contain */ characters.")}`.execute(
database,
)
}
describe("typegen PostgreSQL", () => {
+21
View File
@@ -73,6 +73,24 @@ export abstract class KyselyTypegenDialect {
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[],
@@ -94,6 +112,9 @@ export abstract class KyselyTypegenDialect {
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("}", "")