1
0
mirror of https://github.com/theoludwig/kysely-typegen.git synced 2026-05-22 16:23:25 +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:
2026-05-22 15:46:21 +02:00
parent c025a63c8c
commit e9adb364e0
10 changed files with 642 additions and 297 deletions
+47 -48
View File
@@ -10,13 +10,13 @@ Thank you [kysely-codegen](https://npmx.dev/package/kysely-codegen) for inspirat
Why `kysely-typegen` if there is already `kysely-codegen`? Comparison:
| | `kysely-codegen@0.20.0` | `kysely-typegen` |
| ----------------------------- | ---------------------------------------- | ------------------------------------------------------------ |
| **Install Size** | 6.8 MB | 5 kB |
| **Dependencies** | 35 total | 0 (no runtime dependencies) |
| **Type** | CLI | Library/Programmatic Usage |
| **Code Size/Maintainability** | Heavy | Less than 200 LOC/Simple and straightforward |
| **Database Support** | PostgreSQL, MySQL, SQLite, MSSQL, LibSQL | PostgreSQL (but can **easily be extended to more dialects**) |
| | `kysely-codegen@0.20.0` | `kysely-typegen` |
| ----------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------- |
| **Install Size** | 6.8 MB | 5 kB |
| **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 (**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.
@@ -40,48 +40,48 @@ Peer dependencies:
npm install kysely
```
Kysely dialect of your choice, for example: [kysely-postgres-js](https://github.com/kysely-org/kysely-postgres-js)
```sh
npm install kysely-postgres-js postgres
```
Plus a Kysely dialect driver for your database (see [Setup Kysely database](#setup-kysely-database) below).
## Usage
### Setup Kysely database
Create your Kysely database instance (example with [kysely-postgres-js](https://github.com/kysely-org/kysely-postgres-js)):
Create your Kysely database instance and export a `databaseTypegen` for the script in the next section. The rest of the guide is dialect-agnostic: only this file changes per database.
#### PostgreSQL
`KyselyTypegenPostgresDialect` works with any Kysely PostgreSQL dialect, including the built-in [`PostgresDialect`](https://kysely.dev/docs/dialects/postgres) (using [`pg`](https://npmx.dev/package/pg)) and [`PostgresJSDialect`](https://npmx.dev/package/kysely-postgres-js) (using [`postgres`](https://npmx.dev/package/postgres)). The example below uses `kysely-postgres-js`.
```sh
npm install kysely-postgres-js postgres
```
```ts
// database.ts
import { Kysely } from "kysely"
import { PostgresJSDialect } from "kysely-postgres-js"
import { KyselyTypegenPostgresDialect } from "kysely-typegen/postgres"
import postgres from "postgres"
import type { DB } from "./codegen.ts"
export const DATABASE_USER = process.env["DATABASE_USER"] ?? "user"
export const DATABASE_PASSWORD = process.env["DATABASE_PASSWORD"] ?? "password"
export const DATABASE_NAME = process.env["DATABASE_NAME"] ?? "database"
export const DATABASE_HOST = process.env["DATABASE_HOST"] ?? "localhost"
export const DATABASE_PORT = Number.parseInt(process.env["DATABASE_PORT"] ?? "5432", 10)
const dialect = new PostgresJSDialect({
postgres: postgres({
database: DATABASE_NAME,
host: DATABASE_HOST,
user: DATABASE_USER,
password: DATABASE_PASSWORD,
port: DATABASE_PORT,
database: process.env["DATABASE_NAME"] ?? "database",
host: process.env["DATABASE_HOST"] ?? "localhost",
user: process.env["DATABASE_USER"] ?? "user",
password: process.env["DATABASE_PASSWORD"] ?? "password",
port: Number.parseInt(process.env["DATABASE_PORT"] ?? "5432", 10),
}),
})
export const database = new Kysely<DB>({ dialect })
export const databaseTypegen = new KyselyTypegenPostgresDialect({ database })
```
### Generate the type definitions
Create a script that uses `kysely-typegen` to introspect your database and write the generated types to a file:
Create a script that uses `databaseTypegen` to introspect your database and write the generated types to a file. This script is the same regardless of the underlying database:
```ts
// scripts/typegen.ts
@@ -90,11 +90,8 @@ Create a script that uses `kysely-typegen` to introspect your database and write
import fs from "node:fs"
import path from "node:path"
import { KyselyTypegenPostgresDialect } from "kysely-typegen"
import { database, databaseTypegen } from "./database.ts"
import { database } from "./database.ts"
const databaseTypegen = new KyselyTypegenPostgresDialect({ database })
const result = await databaseTypegen.typegen()
const codegenContent = result.lines.join("\n")
const codegenPath = path.join(process.cwd(), "codegen.ts")
@@ -125,40 +122,42 @@ Fully type-safe queries derived from your actual database schema.
`kysely-typegen` ships with `KyselyTypegenPostgresDialect`, but you can add support for any database by extending the abstract `KyselyTypegenDialect` class.
Only two things are required:
Only one thing is required:
- `scalars`: a `Record<string, string>` mapping the database column types to TypeScript types.
- `getEnumsMap()`: a method returning a `Map<string, string[]>` of enum names to their values (return an empty `Map` if your database doesn't support enums).
```ts
import { KyselyTypegenDialect } from "kysely-typegen"
import type { Kysely } from "kysely"
export class KyselyTypegenMySQLDialect extends KyselyTypegenDialect {
public database: KyselyTypegenDialect["database"]
public readonly scalars: Record<string, string> = {
bigint: "number",
export class KyselyTypegenMSSQLDialect extends KyselyTypegenDialect {
public override readonly scalars: Record<string, string> = {
bigint: "Int8",
bit: "boolean",
char: "string",
datetime: "Timestamp",
decimal: "Numeric",
int: "number",
json: "Json",
nvarchar: "string",
smallint: "number",
text: "string",
tinyint: "number",
varbinary: "Buffer",
varchar: "string",
// ...
}
}
```
public constructor(input: { database: Kysely<any> }) {
super()
this.database = input.database
}
If your database supports enums, override the optional `introspectEnums()` hook, which returns two maps:
protected async getEnumsMap(): Promise<Map<string, string[]>> {
// Query your database's information schema for enum types.
// Key: enum name, Value: array of enum values.
return new Map()
}
- `named`: enum name → values (emitted as `export type Name = "a" | "b"`).
- `inline`: `${tableName}.${columnName}` → values (emitted inline at the column site, for databases where enums are anonymous per-column).
```ts
import type { IntrospectedEnums } from "kysely-typegen"
protected override async introspectEnums(): Promise<IntrospectedEnums> {
// Query your database's information schema...
return { named: [], inline: new Map() }
}
```
+164
View File
@@ -11,11 +11,13 @@
"devDependencies": {
"@testcontainers/postgresql": "12.0.0",
"@types/node": "25.9.1",
"@types/pg": "8.20.0",
"kysely": "0.29.2",
"kysely-postgres-js": "3.0.0",
"oxfmt": "0.51.0",
"oxlint": "1.66.0",
"oxlint-tsgolint": "0.23.0",
"pg": "8.21.0",
"postgres": "3.4.9",
"semantic-release": "25.0.3",
"tsdown": "0.22.0",
@@ -2328,6 +2330,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/pg": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz",
"integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
"pg-types": "^2.2.0"
}
},
"node_modules/@types/ssh2": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz",
@@ -7599,6 +7613,113 @@
"dev": true,
"license": "MIT"
},
"node_modules/pg": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz",
"integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.13.0",
"pg-pool": "^3.14.0",
"pg-protocol": "^1.14.0",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
"engines": {
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.4.0"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
"integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz",
"integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==",
"dev": true,
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
"integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz",
"integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==",
"dev": true,
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"dev": true,
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/pgpass/node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -7657,6 +7778,49 @@
"url": "https://github.com/sponsors/porsager"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/pretty-ms": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz",
+3
View File
@@ -22,6 +22,7 @@
"types": "./dist/index.d.mts",
"exports": {
".": "./dist/index.mjs",
"./postgres": "./dist/postgres.mjs",
"./package.json": "./package.json"
},
"publishConfig": {
@@ -40,11 +41,13 @@
"devDependencies": {
"@testcontainers/postgresql": "12.0.0",
"@types/node": "25.9.1",
"@types/pg": "8.20.0",
"kysely": "0.29.2",
"kysely-postgres-js": "3.0.0",
"oxfmt": "0.51.0",
"oxlint": "1.66.0",
"oxlint-tsgolint": "0.23.0",
"pg": "8.21.0",
"postgres": "3.4.9",
"semantic-release": "25.0.3",
"tsdown": "0.22.0",
@@ -1,109 +0,0 @@
exports[`typegen > 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 {",
" amountCents: number",
" createdAt: Generated<Timestamp>",
" currency: Generated<Currency>",
" id: Generated<Int8>",
" note: string | null",
" status: Generated<OrderStatus>",
" userId: string",
"}",
"",
"export interface Users {",
" createdAt: Generated<Timestamp>",
" 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
}
`;
@@ -0,0 +1,221 @@
exports[`typegen PostgreSQL > generate types matching snapshot (kysely-postgres-js) 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 {",
" amountCents: number",
" createdAt: Generated<Timestamp>",
" currency: Generated<Currency>",
" id: Generated<Int8>",
" note: string | null",
" status: Generated<OrderStatus>",
" userId: string",
"}",
"",
"export interface Users {",
" createdAt: Generated<Timestamp>",
" 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 (pg) 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 {",
" amountCents: number",
" createdAt: Generated<Timestamp>",
" currency: Generated<Currency>",
" id: Generated<Int8>",
" note: string | null",
" status: Generated<OrderStatus>",
" userId: string",
"}",
"",
"export interface Users {",
" createdAt: Generated<Timestamp>",
" 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
}
`;
+11
View File
@@ -0,0 +1,11 @@
import path from "node:path"
import { snapshot } from "node:test"
snapshot.setResolveSnapshotPath((testFilePath) => {
if (testFilePath == null) {
throw new Error('"testFilePath" is null.')
}
const dir = path.dirname(testFilePath)
const base = path.basename(testFilePath)
return path.join(dir, "__snapshots__", `${base}.snapshot`)
})
@@ -1,20 +1,13 @@
import type { StartedPostgreSqlContainer } from "@testcontainers/postgresql"
import { PostgreSqlContainer } from "@testcontainers/postgresql"
import { Kysely, sql } from "kysely"
import { Kysely, PostgresDialect, sql } from "kysely"
import { PostgresJSDialect } from "kysely-postgres-js"
import path from "node:path"
import { after, before, describe, it, snapshot } from "node:test"
import { after, before, describe, it } from "node:test"
import pg from "pg"
import postgres from "postgres"
import { KyselyTypegenPostgresDialect } from "../index.ts"
snapshot.setResolveSnapshotPath((testFilePath) => {
if (testFilePath == null) {
throw new Error('"testFilePath" is null.')
}
const dir = path.dirname(testFilePath)
const base = path.basename(testFilePath)
return path.join(dir, "__snapshots__", `${base}.snapshot`)
})
import { KyselyTypegenPostgresDialect } from "../postgres.ts"
import "./_setup.ts"
const POSTGRES_IMAGE =
"docker.io/postgres:18.4@sha256:f7ce845ee6873dd84be93c9828fe0d1fab0f9707dc9ac569694657398b290bce"
@@ -204,7 +197,7 @@ const createSchema = async (database: Kysely<any>): Promise<void> => {
.execute()
}
describe("typegen", () => {
describe("typegen PostgreSQL", () => {
let container: StartedPostgreSqlContainer
let database: Kysely<any>
@@ -229,7 +222,7 @@ describe("typegen", () => {
await container.stop()
})
it("generate types matching snapshot", async (testContext) => {
it("generate types matching snapshot (kysely-postgres-js)", async (testContext) => {
// Arrange - Given
const databaseTypegen = new KyselyTypegenPostgresDialect({ database })
@@ -241,6 +234,35 @@ describe("typegen", () => {
lines: result.lines,
tablesCount: result.tables.length,
enumsCount: result.enums.length,
inlineEnumsCount: result.inlineEnums.size,
})
})
it("generate types matching snapshot (pg)", async (testContext) => {
// Arrange - Given
const databasePg = new Kysely<any>({
dialect: new PostgresDialect({
pool: new pg.Pool({
database: container.getDatabase(),
host: container.getHost(),
port: container.getPort(),
user: container.getUsername(),
password: container.getPassword(),
}),
}),
})
const databaseTypegen = new KyselyTypegenPostgresDialect({ database: databasePg })
// Act - When
const result = await databaseTypegen.typegen()
await databasePg.destroy()
// Assert - Then
testContext.assert.snapshot({
lines: result.lines,
tablesCount: result.tables.length,
enumsCount: result.enums.length,
inlineEnumsCount: result.inlineEnums.size,
})
})
})
+94 -125
View File
@@ -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 }
}
}
+65
View File
@@ -0,0 +1,65 @@
import type { EnumMetadata, IntrospectedEnums } from "./index.ts"
import { KyselyTypegenDialect } from "./index.ts"
export class KyselyTypegenPostgresDialect extends KyselyTypegenDialect {
// These types have been found through experimentation in Adminer and in the 'pg' source code.
public override 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",
}
protected override async introspectEnums(): Promise<IntrospectedEnums> {
const rows = (await this.database
.withoutPlugins()
.selectFrom("pg_type as type")
.innerJoin("pg_enum as enum", "type.oid", "enum.enumtypid")
.select(["type.typname as name", "enum.enumlabel as value"])
.execute()) as Array<{ name: string; value: string }>
const grouped = new Map<string, string[]>()
for (const { name, value } of rows) {
const existing = grouped.get(name)
if (existing == null) {
grouped.set(name, [value])
} else {
existing.push(value)
}
}
const named: EnumMetadata[] = []
for (const [name, values] of grouped) {
named.push({ name, values })
}
return { named, inline: new Map() }
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { defineConfig } from "tsdown"
export default defineConfig({
entry: "./src/index.ts",
entry: ["./src/index.ts", "./src/postgres.ts"],
dts: true,
exports: true,
})