4 Commits
Author SHA1 Message Date
theoludwig 63a44d30f1 docs: add PGlite support
Kysely's built-in PGliteDialect uses the same introspector as PostgreSQL, so KyselyTypegenPostgresDialect works with it as-is.
2026-10-09 21:19:26 +02:00
theoludwig ff16be6787 feat: generate JSDoc from column comments 2026-10-02 16:51:15 +02:00
theoludwig 3849b14615 build(deps): update latest 2026-10-02 16:47:08 +02:00
theoludwig f956a31c92 build(deps): update latest to fix tmp
https://github.com/advisories/GHSA-ph9p-34f9-6g65
2026-06-09 16:02:17 +02:00
11 changed files with 1815 additions and 1232 deletions

No files matched your search

+3 -3
View File
@@ -15,12 +15,12 @@ jobs:
TZ: "Europe/Paris"
DO_NOT_TRACK: "1"
steps:
- uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2
- uses: "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1" # v7.0.1
- name: "Setup Node.js"
uses: "actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e" # v6.4.0
uses: "actions/setup-node@820762786026740c76f36085b0efc47a31fe5020" # v7.0.0
with:
node-version: "24.15.0"
node-version: "24.16.0"
cache: "npm"
- run: "node --version"
+3 -3
View File
@@ -17,15 +17,15 @@ jobs:
pull-requests: "write"
id-token: "write"
steps:
- uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2
- uses: "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1" # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: "Setup Node.js"
uses: "actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e" # v6.4.0
uses: "actions/setup-node@820762786026740c76f36085b0efc47a31fe5020" # v7.0.0
with:
node-version: "24.15.0"
node-version: "24.16.0"
registry-url: "https://registry.npmjs.org"
package-manager-cache: false
+38 -1
View File
@@ -16,7 +16,7 @@ Why `kysely-typegen` if there is already `kysely-codegen`? Comparison:
| **Dependencies** | 35 total | 0 (no runtime dependencies) |
| **Type** | CLI | Library/Programmatic Usage |
| **Code Size/Maintainability** | Heavy | Lightweight/Simple and straightforward (string manipulation instead of complex AST) |
| **Database Support** | PostgreSQL, MySQL, SQLite, MSSQL, LibSQL | PostgreSQL, MySQL, SQLite (**can be easily extended to more**) |
| **Database Support** | PostgreSQL, MySQL, SQLite, MSSQL, LibSQL | PostgreSQL, PGlite, MySQL, SQLite (**can be easily extended to more**) |
`kysely-typegen` is a **library** (not a CLI), which means you are in control of where and how to run it, and is designed to be **extensible**, easy to add support for more database dialects.
@@ -79,6 +79,30 @@ export const database = new Kysely<DB>({ dialect })
export const databaseTypegen = new KyselyTypegenPostgresDialect({ database })
```
#### PGlite
[PGlite](https://pglite.dev/) is PostgreSQL compiled to WebAssembly that runs in-process. Use Kysely's built-in [`PGliteDialect`](https://kysely-org.github.io/kysely-apidoc/classes/PGliteDialect.html) with `KyselyTypegenPostgresDialect`: PGlite is introspected exactly like PostgreSQL. Because `kysely-typegen` is a library, it can introspect the PGlite instance your application already uses, which a CLI cannot do.
```sh
npm install @electric-sql/pglite
```
```ts
// database.ts
import { PGlite } from "@electric-sql/pglite"
import { Kysely, PGliteDialect } from "kysely"
import { KyselyTypegenPostgresDialect } from "kysely-typegen/postgres"
import type { DB } from "./codegen.ts"
const dialect = new PGliteDialect({
pglite: new PGlite(process.env["DATABASE_PATH"] ?? "./pgdata"),
})
export const database = new Kysely<DB>({ dialect })
export const databaseTypegen = new KyselyTypegenPostgresDialect({ database })
```
#### MySQL
```sh
@@ -168,6 +192,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.
+1528 -1195
View File
File diff suppressed because it is too large. Load diff
+17 -16
View File
@@ -41,23 +41,24 @@
"release": "semantic-release"
},
"devDependencies": {
"@testcontainers/mysql": "12.0.0",
"@testcontainers/postgresql": "12.0.0",
"@types/better-sqlite3": "7.6.13",
"@types/node": "25.9.1",
"@types/pg": "8.20.0",
"better-sqlite3": "12.10.0",
"kysely": "0.29.2",
"kysely-postgres-js": "3.0.0",
"mysql2": "3.22.3",
"oxfmt": "0.51.0",
"oxlint": "1.66.0",
"oxlint-tsgolint": "0.23.0",
"pg": "8.21.0",
"@electric-sql/pglite": "0.5.8",
"@testcontainers/mysql": "12.2.0",
"@testcontainers/postgresql": "12.2.0",
"@types/better-sqlite3": "9.6.0",
"@types/node": "26.6.4",
"@types/pg": "8.23.1",
"better-sqlite3": "13.0.3",
"kysely": "0.29.6",
"kysely-postgres-js": "5.0.1",
"mysql2": "3.24.5",
"oxfmt": "0.71.0",
"oxlint": "1.86.0",
"oxlint-tsgolint": "7.0.2003",
"pg": "8.23.1",
"postgres": "3.4.9",
"semantic-release": "25.0.3",
"tsdown": "0.22.0",
"typescript": "6.0.3"
"semantic-release": "25.0.9",
"tsdown": "0.23.0",
"typescript": "7.0.2"
},
"peerDependencies": {
"kysely": ">=0.29.0"
@@ -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>",
@@ -1,3 +1,121 @@
exports[`typegen PGlite > generate types matching snapshot 1`] = `
{
"lines": [
"// 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 type Currency = \\"EUR\\" | \\"GBP\\" | \\"USD\\"",
"",
"export type OrderStatus = \\"cancelled\\" | \\"paid\\" | \\"pending\\" | \\"shipped\\"",
"",
"export type UserRole = \\"admin\\" | \\"guest\\" | \\"member\\"",
"",
"export interface AllTypes {",
" colBit: string",
" colBool: boolean",
" colBoolDefault: Generated<boolean>",
" colBoolNullable: boolean | null",
" colBox: string",
" colBpchar: string",
" colBytea: Buffer",
" colCidr: string",
" colDate: Timestamp",
" colFloat4: number",
" colFloat8: number",
" colInet: string",
" colInt2: number",
" colInt4: number",
" colInt8: Int8",
" colJson: Json",
" colJsonb: Json",
" colJsonbDefault: Generated<Json>",
" colLine: string",
" colLseg: string",
" colMacaddr: string",
" colMoney: string",
" colNumeric: Numeric",
" colOid: number",
" colPath: string",
" colPoint: unknown",
" colPolygon: string",
" colText: string",
" colTextNullable: string | null",
" colTime: string",
" colTimestamp: Timestamp",
" colTimestampDefault: Generated<Timestamp>",
" colTimestamptz: Timestamp",
" colTimetz: string",
" colTsquery: string",
" colTsvector: string",
" colUuid: string",
" colVarbit: string",
" colVarchar: string",
" colXml: string",
" createdAt: Generated<Timestamp>",
" id: Generated<string>",
" updatedAt: Timestamp | null",
"}",
"",
"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",
"}",
"",
"export interface Users {",
" createdAt: Generated<Timestamp>",
" /** Contact email address. */",
" email: string | null",
" id: Generated<string>",
" isActive: Generated<boolean>",
" role: Generated<UserRole>",
" username: string",
"}",
"",
"export interface DB {",
" AllTypes: AllTypes",
" Orders: Orders",
" Users: Users",
"}"
],
"tablesCount": 3,
"enumsCount": 3,
"inlineEnumsCount": 0
}
`;
exports[`typegen PostgreSQL > generate types matching snapshot (kysely-postgres-js) 1`] = `
{
"lines": [
@@ -79,10 +197,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 +214,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 +315,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 +332,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`)
})
+45 -1
View File
@@ -1,6 +1,7 @@
import { PGlite } from "@electric-sql/pglite"
import type { StartedPostgreSqlContainer } from "@testcontainers/postgresql"
import { PostgreSqlContainer } from "@testcontainers/postgresql"
import { Kysely, PostgresDialect, sql } from "kysely"
import { Kysely, PGliteDialect, PostgresDialect, sql } from "kysely"
import { PostgresJSDialect } from "kysely-postgres-js"
import { after, before, describe, it } from "node:test"
import pg from "pg"
@@ -195,6 +196,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", () => {
@@ -266,3 +277,36 @@ describe("typegen PostgreSQL", () => {
})
})
})
describe("typegen PGlite", () => {
let database: Kysely<any>
before(async () => {
database = new Kysely<any>({
dialect: new PGliteDialect({
pglite: new PGlite(),
}),
})
await createSchema(database)
})
after(async () => {
await database.destroy()
})
it("generate types matching snapshot", async (testContext) => {
// Arrange - Given
const databaseTypegen = new KyselyTypegenPostgresDialect({ database })
// Act - When
const result = await databaseTypegen.typegen()
// Assert - Then
testContext.assert.snapshot({
lines: result.lines,
tablesCount: result.tables.length,
enumsCount: result.enums.length,
inlineEnumsCount: result.inlineEnums.size,
})
})
})
+31 -8
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("}", "")
@@ -146,14 +167,16 @@ export abstract class KyselyTypegenDialect {
tables: TableMetadata[]
}> {
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, inlineEnums))
lines.push("export interface DB {")
for (const table of tables) {
lines.push(` ${table.name}: ${table.name}`)
}
lines.push("}")
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 }
}
}
+2 -2
View File
@@ -52,12 +52,12 @@ export class KyselyTypegenMySQLDialect extends KyselyTypegenDialect {
])
.where("columns.TABLE_SCHEMA", "=", sql`database()`)
.where("columns.DATA_TYPE", "in", ["enum", "set"])
.$castTo<{ tableName: string; columnName: string; columnType: string }>()
.execute()
const inline = new Map<string, string[]>()
for (const row of rows) {
const data = row as { tableName: string; columnName: string; columnType: string }
inline.set(`${data.tableName}.${data.columnName}`, parseMysqlEnumColumnType(data.columnType))
inline.set(`${row.tableName}.${row.columnName}`, parseMysqlEnumColumnType(row.columnType))
}
return { named: [], inline }
}