From ff16be6787252e5d1a0285a2c560046705d5c45a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20LUDWIG?= Date: Fri, 2 Oct 2026 16:51:15 +0200 Subject: [PATCH] feat: generate JSDoc from column comments --- README.md | 13 ++++++++++++ .../__snapshots__/mysql.test.ts.snapshot | 7 +++++++ .../__snapshots__/postgres.test.ts.snapshot | 14 +++++++++++++ src/_test/mysql.test.ts | 12 ++++++++--- src/_test/postgres.test.ts | 10 +++++++++ src/index.ts | 21 +++++++++++++++++++ 6 files changed, 74 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e401f24..04470b6 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/_test/__snapshots__/mysql.test.ts.snapshot b/src/_test/__snapshots__/mysql.test.ts.snapshot index e1976c0..ff854f4 100644 --- a/src/_test/__snapshots__/mysql.test.ts.snapshot +++ b/src/_test/__snapshots__/mysql.test.ts.snapshot @@ -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", " currency: Generated<\\"EUR\\" | \\"GBP\\" | \\"USD\\">", " id: Generated", + " /** 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", + " /** Contact email address. */", " email: string | null", " id: Generated", " isActive: Generated", diff --git a/src/_test/__snapshots__/postgres.test.ts.snapshot b/src/_test/__snapshots__/postgres.test.ts.snapshot index d331c35..0a6581d 100644 --- a/src/_test/__snapshots__/postgres.test.ts.snapshot +++ b/src/_test/__snapshots__/postgres.test.ts.snapshot @@ -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", " currency: Generated", " id: Generated", + " /** Free text, may contain *\\\\/ characters. */", " note: string | null", " status: Generated", " userId: string", @@ -90,6 +96,7 @@ exports[`typegen PostgreSQL > generate types matching snapshot (kysely-postgres- "", "export interface Users {", " createdAt: Generated", + " /** Contact email address. */", " email: string | null", " id: Generated", " isActive: Generated", @@ -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", " currency: Generated", " id: Generated", + " /** Free text, may contain *\\\\/ characters. */", " note: string | null", " status: Generated", " userId: string", @@ -201,6 +214,7 @@ exports[`typegen PostgreSQL > generate types matching snapshot (pg) 1`] = ` "", "export interface Users {", " createdAt: Generated", + " /** Contact email address. */", " email: string | null", " id: Generated", " isActive: Generated", diff --git a/src/_test/mysql.test.ts b/src/_test/mysql.test.ts index 63f82df..810242f 100644 --- a/src/_test/mysql.test.ts +++ b/src/_test/mysql.test.ts @@ -117,7 +117,9 @@ const createSchema = async (database: Kysely): Promise => { .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): Promise => { 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`) }) diff --git a/src/_test/postgres.test.ts b/src/_test/postgres.test.ts index 8b89d0b..e32310a 100644 --- a/src/_test/postgres.test.ts +++ b/src/_test/postgres.test.ts @@ -195,6 +195,16 @@ const createSchema = async (database: Kysely): Promise => { 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", () => { diff --git a/src/index.ts b/src/index.ts index 77b41f8..ba3f559 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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("}", "")