diff --git a/src/_test/__snapshots__/index.test.ts.snapshot b/src/_test/__snapshots__/index.test.ts.snapshot new file mode 100644 index 0000000..fa81d10 --- /dev/null +++ b/src/_test/__snapshots__/index.test.ts.snapshot @@ -0,0 +1,608 @@ +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 extends ColumnType ? ColumnType : ColumnType", + "", + "export type Timestamp = ColumnType", + "", + "export type Numeric = ColumnType", + "", + "export type Int8 = ColumnType", + "", + "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", + " 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", + " 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", + " colTimestamptz: Timestamp", + " colTimetz: string", + " colTsquery: string", + " colTsvector: string", + " colUuid: string", + " colVarbit: string", + " colVarchar: string", + " colXml: string", + " createdAt: Generated", + " id: Generated", + " updatedAt: Timestamp | null", + "}", + "", + "export interface Orders {", + " amountCents: number", + " createdAt: Generated", + " currency: Generated", + " id: Generated", + " note: string | null", + " status: Generated", + " userId: string", + "}", + "", + "export interface Users {", + " createdAt: Generated", + " email: string | null", + " id: Generated", + " isActive: Generated", + " role: Generated", + " username: string", + "}", + "", + "export interface DB {", + " AllTypes: AllTypes", + " Orders: Orders", + " Users: Users", + "}" + ], + "enums": [ + { + "name": "Currency", + "values": [ + "EUR", + "GBP", + "USD" + ] + }, + { + "name": "OrderStatus", + "values": [ + "cancelled", + "paid", + "pending", + "shipped" + ] + }, + { + "name": "UserRole", + "values": [ + "admin", + "guest", + "member" + ] + } + ], + "tables": [ + { + "columns": [ + { + "dataType": "uuid", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": true, + "isAutoIncrementing": false, + "isNullable": false, + "name": "id" + }, + { + "dataType": "bool", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colBool" + }, + { + "dataType": "bool", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": true, + "name": "colBoolNullable" + }, + { + "dataType": "bool", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": true, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colBoolDefault" + }, + { + "dataType": "int2", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colInt2" + }, + { + "dataType": "int4", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colInt4" + }, + { + "dataType": "int8", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colInt8" + }, + { + "dataType": "float4", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colFloat4" + }, + { + "dataType": "float8", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colFloat8" + }, + { + "dataType": "numeric", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colNumeric" + }, + { + "dataType": "money", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colMoney" + }, + { + "dataType": "text", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colText" + }, + { + "dataType": "varchar", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colVarchar" + }, + { + "dataType": "bpchar", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colBpchar" + }, + { + "dataType": "bytea", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colBytea" + }, + { + "dataType": "date", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colDate" + }, + { + "dataType": "time", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colTime" + }, + { + "dataType": "timetz", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colTimetz" + }, + { + "dataType": "timestamp", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colTimestamp" + }, + { + "dataType": "timestamptz", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colTimestamptz" + }, + { + "dataType": "json", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colJson" + }, + { + "dataType": "jsonb", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colJsonb" + }, + { + "dataType": "uuid", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colUuid" + }, + { + "dataType": "inet", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colInet" + }, + { + "dataType": "cidr", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colCidr" + }, + { + "dataType": "macaddr", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colMacaddr" + }, + { + "dataType": "bit", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colBit" + }, + { + "dataType": "varbit", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colVarbit" + }, + { + "dataType": "xml", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colXml" + }, + { + "dataType": "tsvector", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colTsvector" + }, + { + "dataType": "tsquery", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colTsquery" + }, + { + "dataType": "point", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colPoint" + }, + { + "dataType": "line", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colLine" + }, + { + "dataType": "lseg", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colLseg" + }, + { + "dataType": "box", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colBox" + }, + { + "dataType": "path", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colPath" + }, + { + "dataType": "polygon", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colPolygon" + }, + { + "dataType": "oid", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colOid" + }, + { + "dataType": "text", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": true, + "name": "colTextNullable" + }, + { + "dataType": "jsonb", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": true, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colJsonbDefault" + }, + { + "dataType": "timestamp", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": true, + "isAutoIncrementing": false, + "isNullable": false, + "name": "colTimestampDefault" + }, + { + "dataType": "timestamptz", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": true, + "isAutoIncrementing": false, + "isNullable": false, + "name": "createdAt" + }, + { + "dataType": "timestamptz", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": true, + "name": "updatedAt" + } + ], + "isForeign": false, + "isView": false, + "name": "AllTypes", + "schema": "public" + }, + { + "columns": [ + { + "dataType": "int8", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": true, + "isAutoIncrementing": true, + "isNullable": false, + "name": "id" + }, + { + "dataType": "uuid", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "userId" + }, + { + "dataType": "OrderStatus", + "dataTypeSchema": "public", + "hasDefaultValue": true, + "isAutoIncrementing": false, + "isNullable": false, + "name": "status" + }, + { + "dataType": "Currency", + "dataTypeSchema": "public", + "hasDefaultValue": true, + "isAutoIncrementing": false, + "isNullable": false, + "name": "currency" + }, + { + "dataType": "int4", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "amountCents" + }, + { + "dataType": "text", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": true, + "name": "note" + }, + { + "dataType": "timestamptz", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": true, + "isAutoIncrementing": false, + "isNullable": false, + "name": "createdAt" + } + ], + "isForeign": false, + "isView": false, + "name": "Orders", + "schema": "public" + }, + { + "columns": [ + { + "dataType": "uuid", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": true, + "isAutoIncrementing": false, + "isNullable": false, + "name": "id" + }, + { + "dataType": "varchar", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": false, + "name": "username" + }, + { + "dataType": "text", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": false, + "isAutoIncrementing": false, + "isNullable": true, + "name": "email" + }, + { + "dataType": "UserRole", + "dataTypeSchema": "public", + "hasDefaultValue": true, + "isAutoIncrementing": false, + "isNullable": false, + "name": "role" + }, + { + "dataType": "bool", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": true, + "isAutoIncrementing": false, + "isNullable": false, + "name": "isActive" + }, + { + "dataType": "timestamptz", + "dataTypeSchema": "pg_catalog", + "hasDefaultValue": true, + "isAutoIncrementing": false, + "isNullable": false, + "name": "createdAt" + } + ], + "isForeign": false, + "isView": false, + "name": "Users", + "schema": "public" + } + ] +} +`; diff --git a/src/_test/index.test.ts b/src/_test/index.test.ts index aa246fb..735995f 100644 --- a/src/_test/index.test.ts +++ b/src/_test/index.test.ts @@ -1,17 +1,242 @@ -import assert from "node:assert/strict" -import { describe, it } from "node:test" -import { fn } from "../index.ts" +import type { StartedPostgreSqlContainer } from "@testcontainers/postgresql" +import { PostgreSqlContainer } from "@testcontainers/postgresql" +import { Kysely, sql } from "kysely" +import { PostgresJSDialect } from "kysely-postgres-js" +import path from "node:path" +import { after, before, describe, it, snapshot } from "node:test" +import postgres from "postgres" +import { KyselyTypegenPostgresDialect } from "../index.ts" -describe("index", () => { - it('should return "Hello, tsdown!"', () => { +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`) +}) + +const POSTGRES_IMAGE = + "docker.io/postgres:18.4@sha256:f7ce845ee6873dd84be93c9828fe0d1fab0f9707dc9ac569694657398b290bce" + +const createSchema = async (database: Kysely): Promise => { + await database.schema.createType("Currency").asEnum(["EUR", "USD", "GBP"]).execute() + await database.schema.createType("UserRole").asEnum(["admin", "member", "guest"]).execute() + await database.schema + .createType("OrderStatus") + .asEnum(["pending", "paid", "shipped", "cancelled"]) + .execute() + + await database.schema + .createTable("AllTypes") + .addColumn("id", "uuid", (column) => { + return column + .notNull() + .primaryKey() + .defaultTo(sql`gen_random_uuid()`) + }) + .addColumn("colBool", "boolean", (column) => { + return column.notNull() + }) + .addColumn("colBoolNullable", "boolean") + .addColumn("colBoolDefault", "boolean", (column) => { + return column.notNull().defaultTo(false) + }) + .addColumn("colInt2", "smallint", (column) => { + return column.notNull() + }) + .addColumn("colInt4", "integer", (column) => { + return column.notNull() + }) + .addColumn("colInt8", "bigint", (column) => { + return column.notNull() + }) + .addColumn("colFloat4", "real", (column) => { + return column.notNull() + }) + .addColumn("colFloat8", "double precision", (column) => { + return column.notNull() + }) + .addColumn("colNumeric", sql`numeric(12, 2)`, (column) => { + return column.notNull() + }) + .addColumn("colMoney", sql`money`, (column) => { + return column.notNull() + }) + .addColumn("colText", "text", (column) => { + return column.notNull() + }) + .addColumn("colVarchar", "varchar(255)", (column) => { + return column.notNull() + }) + .addColumn("colBpchar", sql`char(10)`, (column) => { + return column.notNull() + }) + .addColumn("colBytea", "bytea", (column) => { + return column.notNull() + }) + .addColumn("colDate", "date", (column) => { + return column.notNull() + }) + .addColumn("colTime", "time", (column) => { + return column.notNull() + }) + .addColumn("colTimetz", sql`timetz`, (column) => { + return column.notNull() + }) + .addColumn("colTimestamp", "timestamp", (column) => { + return column.notNull() + }) + .addColumn("colTimestamptz", "timestamptz", (column) => { + return column.notNull() + }) + .addColumn("colJson", "json", (column) => { + return column.notNull() + }) + .addColumn("colJsonb", "jsonb", (column) => { + return column.notNull() + }) + .addColumn("colUuid", "uuid", (column) => { + return column.notNull() + }) + .addColumn("colInet", sql`inet`, (column) => { + return column.notNull() + }) + .addColumn("colCidr", sql`cidr`, (column) => { + return column.notNull() + }) + .addColumn("colMacaddr", sql`macaddr`, (column) => { + return column.notNull() + }) + .addColumn("colBit", sql`bit(8)`, (column) => { + return column.notNull() + }) + .addColumn("colVarbit", sql`varbit(16)`, (column) => { + return column.notNull() + }) + .addColumn("colXml", sql`xml`, (column) => { + return column.notNull() + }) + .addColumn("colTsvector", sql`tsvector`, (column) => { + return column.notNull() + }) + .addColumn("colTsquery", sql`tsquery`, (column) => { + return column.notNull() + }) + .addColumn("colPoint", sql`point`, (column) => { + return column.notNull() + }) + .addColumn("colLine", sql`line`, (column) => { + return column.notNull() + }) + .addColumn("colLseg", sql`lseg`, (column) => { + return column.notNull() + }) + .addColumn("colBox", sql`box`, (column) => { + return column.notNull() + }) + .addColumn("colPath", sql`path`, (column) => { + return column.notNull() + }) + .addColumn("colPolygon", sql`polygon`, (column) => { + return column.notNull() + }) + .addColumn("colOid", sql`oid`, (column) => { + return column.notNull() + }) + .addColumn("colTextNullable", "text") + .addColumn("colJsonbDefault", "jsonb", (column) => { + return column.notNull().defaultTo(sql`'{}'::jsonb`) + }) + .addColumn("colTimestampDefault", "timestamp", (column) => { + return column.notNull().defaultTo(sql`now()`) + }) + .addColumn("createdAt", "timestamptz", (column) => { + return column.notNull().defaultTo(sql`now()`) + }) + .addColumn("updatedAt", "timestamptz") + .execute() + + await database.schema + .createTable("Users") + .addColumn("id", "uuid", (column) => { + return column + .notNull() + .primaryKey() + .defaultTo(sql`gen_random_uuid()`) + }) + .addColumn("username", "varchar(50)", (column) => { + return column.notNull().unique() + }) + .addColumn("email", "text") + .addColumn("role", sql`"UserRole"`, (column) => { + return column.notNull().defaultTo("member") + }) + .addColumn("isActive", "boolean", (column) => { + return column.notNull().defaultTo(true) + }) + .addColumn("createdAt", "timestamptz", (column) => { + return column.notNull().defaultTo(sql`now()`) + }) + .execute() + + await database.schema + .createTable("Orders") + .addColumn("id", "bigserial", (column) => { + return column.notNull().primaryKey() + }) + .addColumn("userId", "uuid", (column) => { + return column.notNull().references("Users.id") + }) + .addColumn("status", sql`"OrderStatus"`, (column) => { + return column.notNull().defaultTo("pending") + }) + .addColumn("currency", sql`"Currency"`, (column) => { + return column.notNull().defaultTo("EUR") + }) + .addColumn("amountCents", "integer", (column) => { + return column.notNull() + }) + .addColumn("note", "text") + .addColumn("createdAt", "timestamptz", (column) => { + return column.notNull().defaultTo(sql`now()`) + }) + .execute() +} + +describe("typegen", () => { + let container: StartedPostgreSqlContainer + let database: Kysely + + before(async () => { + container = await new PostgreSqlContainer(POSTGRES_IMAGE).start() + database = new Kysely({ + dialect: new PostgresJSDialect({ + postgres: postgres({ + database: container.getDatabase(), + host: container.getHost(), + port: container.getPort(), + user: container.getUsername(), + password: container.getPassword(), + }), + }), + }) + await createSchema(database) + }) + + after(async () => { + await database.destroy() + await container.stop() + }) + + it("generate types matching snapshot", async (testContext) => { // Arrange - Given - const input = "tsdown" + const databaseTypegen = new KyselyTypegenPostgresDialect({ database }) // Act - When - const output = fn(input) + const result = await databaseTypegen.typegen() // Assert - Then - const expected = "Hello, tsdown!" - assert.strictEqual(output, expected) + testContext.assert.snapshot(result) }) }) diff --git a/src/index.ts b/src/index.ts index fae4df4..8721ca0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,191 @@ -export const fn = (input: string): string => { - return `Hello, ${input}!` +import type { Kysely, TableMetadata as KyselyTableMetadata } from "kysely" + +export type TableMetadata = KyselyTableMetadata +export interface EnumMetadata { + name: string + values: string[] +} + +export abstract class KyselyTypegenDialect { + public abstract readonly database: Kysely + public abstract readonly scalars: Record + + public async getTables(): Promise { + 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 + }), + ) + const result: string[] = [] + for (const table of tables) { + const interfaceName = table.name + result.push(`export interface ${interfaceName} {`) + 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") + let columnType = column.isNullable ? `${baseType} | null` : baseType + if (column.hasDefaultValue || column.isAutoIncrementing) { + columnType = `Generated<${columnType}>` + } + result.push(` ${column.name}: ${columnType}`) + } + result.push("}", "") + } + return result + } + + protected abstract getEnumsMap(): Promise> + public async getEnums(): Promise { + const enumsMap = await this.getEnumsMap() + const enums: EnumMetadata[] = [] + for (const [name, values] of enumsMap) { + enums.push({ name, values }) + } + return enums.sort((a, b) => { + return a.name.localeCompare(b.name) + }) + } + public getEnumsTypegen(enums: EnumMetadata[]): string[] { + const result: string[] = [] + for (const enumMetadata of enums) { + const enumName = enumMetadata.name + result.push( + `export type ${enumName} = ${enumMetadata.values + .map((value) => { + return `"${value}"` + }) + .join(" | ")}`, + "", + ) + } + return result + } + + public abstract typegen(): Promise<{ + lines: string[] + enums: EnumMetadata[] + tables: TableMetadata[] + }> +} +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 = { + 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> { + 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() + for (const row of rows) { + const data = row as { enumName: string; enumValue: string } + enums.set( + data.enumName, + [...(enums.get(data.enumName) ?? []), data.enumValue].sort((a, b) => { + return a.localeCompare(b) + }), + ) + } + return enums + } + + /** + * Generate TypeScript types based on the database schema, including tables and enums. + */ + public async typegen(): Promise<{ + lines: string[] + enums: EnumMetadata[] + 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 extends ColumnType ? ColumnType : ColumnType", + "", + "export type Timestamp = ColumnType", + "", + "export type Numeric = ColumnType", + "", + "export type Int8 = ColumnType", + "", + "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", + "", + ] + lines.push(...this.getEnumsTypegen(enums)) + lines.push(...this.getTablesTypegen(tables, enums)) + lines.push("export interface DB {") + for (const table of tables) { + lines.push(` ${table.name}: ${table.name}`) + } + lines.push("}") + return { lines, enums, tables } + } }