From ffe41915d392ca5df599408a79b0ab06752791fd Mon Sep 17 00:00:00 2001 From: Tom Meagher Date: Sat, 28 Oct 2023 18:59:19 -0400 Subject: [PATCH] test: codegen --- package.json | 34 +--- pnpm-lock.yaml | 84 +--------- src/commands/codegen.ts | 41 +---- src/commands/down.ts | 12 +- src/commands/to.ts | 10 +- src/commands/up.ts | 9 +- src/config.ts | 11 +- src/utils/clack.ts | 30 +++- src/utils/codegen/declarations.ts | 65 ++++++++ src/utils/codegen/definitions/postgres.ts | 69 +++++++- src/utils/codegen/getTypes.test.ts | 165 +++++++++++++++++++ src/utils/codegen/getTypes.ts | 187 +++++++++++++--------- src/utils/loadConfig.ts | 5 +- 13 files changed, 470 insertions(+), 252 deletions(-) create mode 100644 src/utils/codegen/getTypes.test.ts diff --git a/package.json b/package.json index 19f81fe..f27de2a 100644 --- a/package.json +++ b/package.json @@ -54,14 +54,8 @@ }, "peerDependencies": { "kysely": ">=0.26.3", - "kysely-codegen": ">=0.11.0", "typescript": ">=5" }, - "peerDependenciesMeta": { - "kysely-codegen": { - "optional": true - } - }, "dependencies": { "@clack/prompts": "^0.7.0", "bundle-require": "^4.0.2", @@ -73,7 +67,8 @@ "find-up": "^6.3.0", "human-id": "^4.1.0", "is-unicode-supported": "^1.3.0", - "picocolors": "^1.0.0" + "picocolors": "^1.0.0", + "std-env": "^3.4.3" }, "devDependencies": { "@biomejs/biome": "1.1.2", @@ -89,7 +84,6 @@ "glob": "^10.3.10", "knip": "^2.29.0", "kysely": "^0.26.3", - "kysely-codegen": "^0.11.0", "mysql2": "^3.6.2", "publint": "^0.2.2", "rimraf": "^4.4.1", @@ -97,31 +91,15 @@ "typescript": "5.2.2", "vitest": "^0.34.5" }, - "contributors": [ - "tmm@awkweb.com" - ], + "contributors": ["tmm@awkweb.com"], "funding": "https://github.com/sponsors/tmm", - "keywords": [ - "kysely", - "cli", - "migrate", - "migrations", - "codegen" - ], + "keywords": ["kysely", "cli", "migrate", "migrations", "codegen"], "packageManager": "pnpm@8.8.0", "simple-git-hooks": { "pre-commit": "pnpm format && pnpm lint:fix" }, "knip": { - "entry": [ - "src/**/*.ts!", - "src/exports/index.ts!" - ], - "ignoreDependencies": [ - "kysely-codegen" - ], - "project": [ - ".scripts/**/*.ts" - ] + "entry": ["src/**/*.ts!", "src/exports/index.ts!"], + "project": [".scripts/**/*.ts"] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e3cc06..86cab74 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ dependencies: picocolors: specifier: ^1.0.0 version: 1.0.0 + std-env: + specifier: ^3.4.3 + version: 3.4.3 devDependencies: '@biomejs/biome': @@ -79,9 +82,6 @@ devDependencies: kysely: specifier: ^0.26.3 version: 0.26.3 - kysely-codegen: - specifier: ^0.11.0 - version: 0.11.0(kysely@0.26.3)(mysql2@3.6.2) mysql2: specifier: ^3.6.2 version: 3.6.2 @@ -1803,11 +1803,6 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true - /diff@3.5.0: - resolution: {integrity: sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==} - engines: {node: '>=0.3.1'} - dev: true - /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1830,6 +1825,7 @@ packages: /dotenv@16.3.1: resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} engines: {node: '>=12'} + dev: false /dotenv@8.6.0: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} @@ -2245,17 +2241,6 @@ packages: get-intrinsic: 1.2.2 dev: true - /git-diff@2.0.6: - resolution: {integrity: sha512-/Iu4prUrydE3Pb3lCBMbcSNIf81tgGt0W1ZwknnyF62t3tHmtiJTRj0f+1ZIhp3+Rh0ktz1pJVoa7ZXUCskivA==} - engines: {node: '>= 4.8.0'} - dependencies: - chalk: 2.4.2 - diff: 3.5.0 - loglevel: 1.8.1 - shelljs: 0.8.5 - shelljs.exec: 1.1.8 - dev: true - /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2499,11 +2484,6 @@ packages: side-channel: 1.0.4 dev: true - /interpret@1.4.0: - resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} - engines: {node: '>= 0.10'} - dev: true - /is-array-buffer@3.0.2: resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} dependencies: @@ -2820,34 +2800,6 @@ packages: - domexception dev: true - /kysely-codegen@0.11.0(kysely@0.26.3)(mysql2@3.6.2): - resolution: {integrity: sha512-8aklzXygjANshk5BoGSQ0BWukKIoPL4/k1iFWyteGUQ/VtB1GlyrELBZv1GglydjLGECSSVDpsOgEXyWQmuksg==} - hasBin: true - peerDependencies: - '@libsql/kysely-libsql': ^0.3.0 - better-sqlite3: '>=7.6.2' - kysely: '>=0.19.12' - mysql2: ^2.3.3 || ^3.0.0 - pg: ^8.8.0 - peerDependenciesMeta: - '@libsql/kysely-libsql': - optional: true - better-sqlite3: - optional: true - mysql2: - optional: true - pg: - optional: true - dependencies: - chalk: 4.1.2 - dotenv: 16.3.1 - git-diff: 2.0.6 - kysely: 0.26.3 - micromatch: 4.0.5 - minimist: 1.2.8 - mysql2: 3.6.2 - dev: true - /kysely@0.26.3: resolution: {integrity: sha512-yWSgGi9bY13b/W06DD2OCDDHQmq1kwTGYlQ4wpZkMOJqMGCstVCFIvxCCVG4KfY1/3G0MhDAcZsip/Lw8/vJWw==} engines: {node: '>=14.0.0'} @@ -2916,11 +2868,6 @@ packages: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} dev: true - /loglevel@1.8.1: - resolution: {integrity: sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==} - engines: {node: '>= 0.6.0'} - dev: true - /long@5.2.3: resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} dev: true @@ -3633,13 +3580,6 @@ packages: util-deprecate: 1.0.2 dev: true - /rechoir@0.6.2: - resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} - engines: {node: '>= 0.10'} - dependencies: - resolve: 1.22.8 - dev: true - /redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -3844,21 +3784,6 @@ packages: resolution: {integrity: sha512-lT297f1WLAdq0A4O+AknIFRP6kkiI3s8C913eJ0XqBxJbZPGWUNkRQk2u8zk4bEAjUJ5i+fSLwB6z1HzeT+DEg==} dev: true - /shelljs.exec@1.1.8: - resolution: {integrity: sha512-vFILCw+lzUtiwBAHV8/Ex8JsFjelFMdhONIsgKNLgTzeRckp2AOYRQtHJE/9LhNvdMmE27AGtzWx0+DHpwIwSw==} - engines: {node: '>= 4.0.0'} - dev: true - - /shelljs@0.8.5: - resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} - engines: {node: '>=4'} - hasBin: true - dependencies: - glob: 7.2.3 - interpret: 1.4.0 - rechoir: 0.6.2 - dev: true - /side-channel@1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: @@ -3987,7 +3912,6 @@ packages: /std-env@3.4.3: resolution: {integrity: sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==} - dev: true /stream-transform@2.1.3: resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==} diff --git a/src/commands/codegen.ts b/src/commands/codegen.ts index 3640ca4..c017840 100644 --- a/src/commands/codegen.ts +++ b/src/commands/codegen.ts @@ -1,11 +1,9 @@ -import { setTimeout as sleep } from 'node:timers/promises' -import { dirname, relative } from 'path' +import { relative } from 'path' import { capitalCase } from 'change-case' import { writeFile } from 'fs/promises' import pc from 'picocolors' -import { spinner } from '@clack/prompts' -import { S_BAR, S_SUCCESS, message } from '../utils/clack.js' +import { S_BAR, S_SUCCESS, message, spinner } from '../utils/clack.js' import { getTypes } from '../utils/codegen/getTypes.js' import { findConfig } from '../utils/findConfig.js' import { loadConfig } from '../utils/loadConfig.js' @@ -27,19 +25,21 @@ export async function codegen(options: CodegenOptions) { if (!config.codegen) throw new Error('`codegen` config required to generate types.') - const s = spinner() - s.start('Generating types') - // so spinner has a chance :) - if (config._spinnerMs) await sleep(config._spinnerMs) + const s = spinner(config._spinnerMs) + await s.start('Generating types') const tables = await db.introspection.getTables() + // TODO: Add support for enums + schemas + // - mysql https://github.com/RobinBlomberg/kysely-codegen/blob/b749a677e6bfd7370559767e57e4c69746898f94/src/dialects/mysql/mysql-introspector.ts#L28-L46 + // - postgres https://github.com/RobinBlomberg/kysely-codegen/blob/b749a677e6bfd7370559767e57e4c69746898f94/src/dialects/postgres/postgres-introspector.ts#L22-L36 const content = getTypes( tables, config.codegen.dialect, config.codegen.definitions, ) await writeFile(config.codegen.out, content) + s.stop('Generated types') if (tables.length) process.stdout.write(`${pc.gray(S_BAR)}\n`) @@ -57,31 +57,6 @@ export async function codegen(options: CodegenOptions) { ) } - const kyselyCodegenOptions = config.codegen['kysely-codegen'] - if (kyselyCodegenOptions) { - const { Cli } = await import('kysely-codegen').catch(() => ({ Cli: null })) - if (!Cli) throw new Error('`kysely-codegen` not installed.') - const defaultOptions = { - camelCase: false, - dialectName: config.codegen.dialect, - envFile: undefined, - excludePattern: undefined, - includePattern: undefined, - logLevel: 0, - outFile: `${dirname(config.codegen.out)}/types-kc.ts`, - print: false, - schema: undefined, - typeOnlyImports: true, - verify: false, - } - const cliOptions = - typeof kyselyCodegenOptions === 'object' - ? { ...defaultOptions, ...kyselyCodegenOptions } - : { ...defaultOptions, url: kyselyCodegenOptions } - const cli = new Cli() - await cli.generate(cliOptions) - } - const codegenRelativeFilePath = relative(process.cwd(), config.codegen.out) return `Created ${pc.green(codegenRelativeFilePath)}` } diff --git a/src/commands/down.ts b/src/commands/down.ts index 9ff2f24..cb6ecd4 100644 --- a/src/commands/down.ts +++ b/src/commands/down.ts @@ -1,9 +1,9 @@ import { existsSync } from 'node:fs' import { mkdir } from 'node:fs/promises' -import { setTimeout as sleep } from 'node:timers/promises' -import { cancel, confirm, isCancel, spinner } from '@clack/prompts' +import { cancel, confirm, isCancel } from '@clack/prompts' import { type MigrationResultSet } from 'kysely' +import { spinner } from '../utils/clack.js' import { findConfig } from '../utils/findConfig.js' import { getAppliedMigrationsCount } from '../utils/getAppliedMigrationsCount.js' import { getMigrator } from '../utils/getMigrator.js' @@ -42,14 +42,12 @@ export async function down(options: DownOptions) { if (!shouldContinue) return 'Applied 0 migrations.' } - const s = spinner() - s.start('Running migrations') - // so spinner has a chance :) - if (config._spinnerMs) await sleep(config._spinnerMs) + const s = spinner(config._spinnerMs) + await s.start('Running migrations') let resultSet: MigrationResultSet if (options.reset) { - // TODO: migrator.migrateTo(NO_MIGRATIONS) throwing when run with linked package + // migrator.migrateTo(NO_MIGRATIONS) throwing when run with linked package so handling manually const migration = migrations[0]! resultSet = await migrator.migrateTo(migration.name) if (!resultSet.error) { diff --git a/src/commands/to.ts b/src/commands/to.ts index 0ea3c51..2646d3e 100644 --- a/src/commands/to.ts +++ b/src/commands/to.ts @@ -1,9 +1,9 @@ import { existsSync } from 'node:fs' import { mkdir } from 'node:fs/promises' -import { setTimeout as sleep } from 'node:timers/promises' -import { cancel, isCancel, select, spinner } from '@clack/prompts' +import { cancel, isCancel, select } from '@clack/prompts' import pc from 'picocolors' +import { spinner } from '../utils/clack.js' import { findConfig } from '../utils/findConfig.js' import { getAppliedMigrationsCount } from '../utils/getAppliedMigrationsCount.js' import { getMigrator } from '../utils/getMigrator.js' @@ -68,10 +68,8 @@ export async function to(options: ToOptions) { } } - const s = spinner() - s.start('Running migrations') - // so spinner has a chance :) - if (config._spinnerMs) await sleep(config._spinnerMs) + const s = spinner(config._spinnerMs) + await s.start('Running migrations') const resultSet = await migrator.migrateTo(migration as string) diff --git a/src/commands/up.ts b/src/commands/up.ts index 1a3c43e..e07cf75 100644 --- a/src/commands/up.ts +++ b/src/commands/up.ts @@ -1,9 +1,8 @@ import { existsSync } from 'node:fs' import { mkdir } from 'node:fs/promises' -import { setTimeout as sleep } from 'node:timers/promises' -import { spinner } from '@clack/prompts' import { type MigrationResultSet } from 'kysely' +import { spinner } from '../utils/clack.js' import { findConfig } from '../utils/findConfig.js' import { getAppliedMigrationsCount } from '../utils/getAppliedMigrationsCount.js' import { getMigrator } from '../utils/getMigrator.js' @@ -31,10 +30,8 @@ export async function up(options: UpOptions) { if (pendingMigrations.length === 0) return 'No pending migrations.' - const s = spinner() - s.start('Running migrations') - // so spinner has a chance :) - if (config._spinnerMs) await sleep(config._spinnerMs) + const s = spinner(config._spinnerMs) + await s.start('Running migrations') let resultSet: MigrationResultSet if (options.latest) resultSet = await migrator.migrateToLatest() diff --git a/src/config.ts b/src/config.ts index 27c68d5..52b1cf5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,7 +5,6 @@ import { Migrator, } from 'kysely' -import { type CliOptions } from 'kysely-codegen' import { type Definitions } from './utils/codegen/types.js' export type Config = { @@ -22,22 +21,14 @@ type Codegen = | { definitions?: Evaluate | undefined dialect: 'mysql' | 'postgres' | 'sqlite' - 'kysely-codegen'?: string | KyselyCodegenOptions | undefined out: string } | { definitions: Evaluate | undefined - dialect?: 'mysql' | 'postgres' | 'sqlite' - 'kysely-codegen'?: string | KyselyCodegenOptions | undefined + dialect?: 'mysql' | 'postgres' | 'sqlite' | undefined out: string } -type KyselyCodegenOptions = Evaluate< - Partial< - Pick - > & { url: string } -> - export function defineConfig( config: | Evaluate diff --git a/src/utils/clack.ts b/src/utils/clack.ts index 3635d95..622716e 100644 --- a/src/utils/clack.ts +++ b/src/utils/clack.ts @@ -1,6 +1,12 @@ -import { type LogMessageOptions } from '@clack/prompts' +import { setTimeout as sleep } from 'node:timers/promises' +import { + type LogMessageOptions, + log, + spinner as clack_spinner, +} from '@clack/prompts' import isUnicodeSupported from 'is-unicode-supported' import pc from 'picocolors' +import { isCI } from 'std-env' // TODO: Import from Clack const unicode = isUnicodeSupported() @@ -27,3 +33,25 @@ export function message(message = '', options: LogMessageOptions = {}) { } process.stdout.write(`${parts.join('\n')}\n`) } + +// TODO: CI check should be handled by Clack +// https://github.com/natemoo-re/clack/pull/169 +export function spinner(ms = 250) { + const s = clack_spinner() + return { + async start(message: string) { + if (isCI) log.info(message) + else { + s.start(message) + // so spinner has a chance :) + if (ms) await sleep(ms) + } + }, + stop(message: string, error: unknown = undefined) { + if (isCI) { + if (error) log.error(message) + else log.success(message) + } else s.stop(message, error ? 1 : 0) + }, + } +} diff --git a/src/utils/codegen/declarations.ts b/src/utils/codegen/declarations.ts index e4e2615..63d592a 100644 --- a/src/utils/codegen/declarations.ts +++ b/src/utils/codegen/declarations.ts @@ -105,3 +105,68 @@ export const jsonPrimitiveTypeAlias = factory.createTypeAliasDeclaration( factory.createKeywordTypeNode(SyntaxKind.StringKeyword), ]), ) + +const columnIdentifier = factory.createIdentifier('c') +const selectIdentifier = factory.createIdentifier('s') +const insertIdentifier = factory.createIdentifier('i') +const updateIdentifier = factory.createIdentifier('u') + +export const unwrapColumnTypeIdentifier = + factory.createIdentifier('UnwrapColumnType') +export const unwrapColumnTypeTypeAlias = factory.createTypeAliasDeclaration( + [factory.createToken(SyntaxKind.ExportKeyword)], + unwrapColumnTypeIdentifier, + [ + factory.createTypeParameterDeclaration( + undefined, + columnIdentifier, + undefined, + undefined, + ), + ], + factory.createConditionalTypeNode( + factory.createTypeReferenceNode(columnIdentifier, undefined), + factory.createTypeReferenceNode(kyselyColumnTypeIdentifier, [ + factory.createInferTypeNode( + factory.createTypeParameterDeclaration( + undefined, + selectIdentifier, + undefined, + undefined, + ), + ), + factory.createInferTypeNode( + factory.createTypeParameterDeclaration( + undefined, + insertIdentifier, + undefined, + undefined, + ), + ), + factory.createInferTypeNode( + factory.createTypeParameterDeclaration( + undefined, + updateIdentifier, + undefined, + undefined, + ), + ), + ]), + factory.createTypeReferenceNode(kyselyColumnTypeIdentifier, [ + factory.createTypeReferenceNode(selectIdentifier, undefined), + factory.createUnionTypeNode([ + factory.createTypeReferenceNode(insertIdentifier, undefined), + factory.createKeywordTypeNode(SyntaxKind.UndefinedKeyword), + ]), + factory.createTypeReferenceNode(updateIdentifier, undefined), + ]), + factory.createTypeReferenceNode(kyselyColumnTypeIdentifier, [ + factory.createTypeReferenceNode(columnIdentifier, undefined), + factory.createUnionTypeNode([ + factory.createTypeReferenceNode(columnIdentifier, undefined), + factory.createKeywordTypeNode(SyntaxKind.UndefinedKeyword), + ]), + factory.createTypeReferenceNode(columnIdentifier, undefined), + ]), + ), +) diff --git a/src/utils/codegen/definitions/postgres.ts b/src/utils/codegen/definitions/postgres.ts index 38ea1b8..87092c8 100644 --- a/src/utils/codegen/definitions/postgres.ts +++ b/src/utils/codegen/definitions/postgres.ts @@ -7,6 +7,7 @@ import { jsonPrimitiveTypeAlias, jsonTypeAlias, jsonValueTypeAlias, + kyselyColumnTypeIdentifier, kyselyColumnTypeImportSpecifier, } from '../declarations.js' import { type DefinitionNode, type Definitions } from '../types.js' @@ -23,33 +24,89 @@ const json = { value: factory.createTypeReferenceNode(jsonIdentifier, undefined), } satisfies DefinitionNode -// TODO: Complete definitions -// bool, circle, date, int8, interval, numeric, point, timestamp, timestamptz +const timestamp = { + imports: { kysely: [kyselyColumnTypeImportSpecifier] }, + declarations: [], + value: factory.createTypeReferenceNode(kyselyColumnTypeIdentifier, [ + factory.createTypeReferenceNode( + factory.createIdentifier('Date'), + undefined, + ), + factory.createUnionTypeNode([ + factory.createTypeReferenceNode( + factory.createIdentifier('Date'), + undefined, + ), + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + ]), + factory.createUnionTypeNode([ + factory.createTypeReferenceNode( + factory.createIdentifier('Date'), + undefined, + ), + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + ]), + ]), +} satisfies DefinitionNode + export const postgresDefinitions = { bit: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + bool: factory.createKeywordTypeNode(SyntaxKind.BooleanKeyword), box: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), bpchar: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), - bytea: factory.createTypeReferenceNode( - factory.createIdentifier('Buffer'), - undefined, - ), + bytea: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), cidr: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + date: timestamp, float4: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), float8: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), inet: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), int2: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), int4: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), + int8: { + imports: { kysely: [kyselyColumnTypeImportSpecifier] }, + declarations: [], + value: factory.createTypeReferenceNode(kyselyColumnTypeIdentifier, [ + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + factory.createUnionTypeNode([ + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), + factory.createKeywordTypeNode(SyntaxKind.BigIntKeyword), + ]), + factory.createUnionTypeNode([ + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), + factory.createKeywordTypeNode(SyntaxKind.BigIntKeyword), + ]), + ]), + }, json, jsonb: json, line: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), lseg: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), macaddr: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), money: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + numeric: { + imports: { kysely: [kyselyColumnTypeImportSpecifier] }, + declarations: [], + value: factory.createTypeReferenceNode(kyselyColumnTypeIdentifier, [ + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + factory.createUnionTypeNode([ + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), + ]), + factory.createUnionTypeNode([ + factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), + ]), + ]), + }, oid: factory.createKeywordTypeNode(SyntaxKind.NumberKeyword), path: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), polygon: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), text: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), time: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), + timestamp, + timestampz: timestamp, tsquery: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), tsvector: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), txid_snapshot: factory.createKeywordTypeNode(SyntaxKind.StringKeyword), diff --git a/src/utils/codegen/getTypes.test.ts b/src/utils/codegen/getTypes.test.ts new file mode 100644 index 0000000..5598252 --- /dev/null +++ b/src/utils/codegen/getTypes.test.ts @@ -0,0 +1,165 @@ +import { + EmitHint, + type PropertySignature, + ScriptTarget, + createPrinter, + createSourceFile, + factory, +} from 'typescript' +import { expect, test } from 'vitest' + +import { mysqlDefinitions } from './definitions/mysql.js' +import { postgresDefinitions } from './definitions/postgres.js' +import { getColumnType, getTypes } from './getTypes.js' + +function printPropertySignature(propertySignature: PropertySignature) { + const node = factory.createTypeAliasDeclaration( + undefined, + factory.createIdentifier('Table'), + undefined, + factory.createTypeLiteralNode([propertySignature]), + ) + const printer = createPrinter() + return printer.printNode( + EmitHint.Unspecified, + node, + createSourceFile('', '', ScriptTarget.Latest), + ) +} + +test('getColumnType > Generated', () => { + const res = getColumnType( + { + name: 'id', + dataType: 'bigint', + hasDefaultValue: false, + isAutoIncrementing: true, + isNullable: false, + }, + mysqlDefinitions, + new Map(), + new Set(), + ) + expect(printPropertySignature(res)).toMatchInlineSnapshot(` + "type Table = { + id: Generated; + };" + `) +}) + +test('getColumnType > Generated > UnwrapColumnType', () => { + const res = getColumnType( + { + name: 'created_at', + dataType: 'timestamp', + hasDefaultValue: true, + isAutoIncrementing: false, + isNullable: false, + }, + postgresDefinitions, + new Map(), + new Set(), + ) + expect(printPropertySignature(res)).toMatchInlineSnapshot(` + "type Table = { + created_at: Generated>>; + };" + `) +}) + +test('getColumnType > nullable', () => { + const res = getColumnType( + { + name: 'foo', + dataType: 'varchar', + hasDefaultValue: false, + isAutoIncrementing: false, + isNullable: true, + }, + mysqlDefinitions, + new Map(), + new Set(), + ) + expect(printPropertySignature(res)).toMatchInlineSnapshot(` + "type Table = { + foo: string | null; + };" + `) +}) + +test('getColumnType > unknown definition', () => { + const res = getColumnType( + { + name: 'foo', + dataType: 'bar', + hasDefaultValue: false, + isAutoIncrementing: false, + isNullable: false, + }, + mysqlDefinitions, + new Map(), + new Set(), + ) + expect(printPropertySignature(res)).toMatchInlineSnapshot(` + "type Table = { + foo: unknown; + };" + `) +}) + +test('getColumnType > Generated', () => { + const res = getTypes( + [ + { + name: 'user', + columns: [ + { + name: 'id', + dataType: 'bigint', + hasDefaultValue: false, + isAutoIncrementing: true, + isNullable: false, + }, + { + name: 'created_at', + dataType: 'datetime', + hasDefaultValue: true, + isAutoIncrementing: false, + isNullable: false, + }, + { + name: 'email', + dataType: 'varchar', + hasDefaultValue: false, + isAutoIncrementing: false, + isNullable: false, + }, + ], + isView: false, + }, + ], + 'mysql', + ) + expect(res).toMatchInlineSnapshot(` + "/** generated by kysely-migrate */ + import { type Generated, type Selectable, type Insertable, type Updateable } from \\"kysely\\"; + + export type User = { + id: Generated; + created_at: Generated; + email: string; + }; + + export type UserSelectable = Selectable; + + export type UserInsertable = Insertable; + + export type UserUpdateable = Updateable; + + export interface DB { + user: User; + } + + " + `) +}) diff --git a/src/utils/codegen/getTypes.ts b/src/utils/codegen/getTypes.ts index 431abf4..e5b8672 100644 --- a/src/utils/codegen/getTypes.ts +++ b/src/utils/codegen/getTypes.ts @@ -1,5 +1,5 @@ import { capitalCase } from 'change-case' -import { type TableMetadata } from 'kysely' +import { type ColumnMetadata, type TableMetadata } from 'kysely' import { EmitHint, type Identifier, @@ -12,9 +12,11 @@ import { createPrinter, createSourceFile, factory, + isTypeReferenceNode, } from 'typescript' import { + kyselyColumnTypeIdentifier, kyselyGeneratedIdentifier, kyselyGeneratedImportSpecifier, kyselyInsertableIdentifier, @@ -23,6 +25,8 @@ import { kyselySelectableImportSpecifier, kyselyUpdateableIdentifier, kyselyUpdateableImportSpecifier, + unwrapColumnTypeIdentifier, + unwrapColumnTypeTypeAlias, } from './declarations.js' import { mysqlDefinitions } from './definitions/mysql.js' import { postgresDefinitions } from './definitions/postgres.js' @@ -44,10 +48,6 @@ export function getTypes( dialect: Dialect | undefined, customDefinitions: Definitions | undefined = {}, ) { - // TODO: Parse enums (https://github.com/RobinBlomberg/kysely-codegen/blob/b749a677e6bfd7370559767e57e4c69746898f94/src/dialects/mysql/mysql-introspector.ts#L28-L46) - // TODO: Tests for different dialects, custom definitions, table metadata, etc. - // TODO: Test out against Postgres https://github.com/OpenPipe/OpenPipe/blob/409b1b536dbfd79f85551f936fd68409c36223e2/app/src/types/kysely-codegen.types.ts - // Get dialect node mapping if (!dialect && !customDefinitions) throw new Error( @@ -68,53 +68,11 @@ export function getTypes( // Create type property for each column const columnProperties = [] for (const column of table.columns) { - // Get type from lookup - let type: TypeNode - if (column.dataType in definitions) { - const definition = definitions[ - column.dataType as keyof typeof definitions - ] as TypeNode | DefinitionNode - if ('value' in definition) { - type = definition.value - for (const [name, imports] of Object.entries(definition.imports)) { - if (importsMap.has(name)) { - const nameImports = importsMap.get(name)! - importsMap.set(name, new Set([...nameImports, ...imports])) - } else importsMap.set(name, new Set(imports)) - } - for (const declaration of definition.declarations) { - typeDeclarations.add(declaration) - } - } else type = definition - } else type = factory.createKeywordTypeNode(SyntaxKind.UnknownKeyword) - - // Create node based on properties (e.g. nullable, default) - let columnTypeNode: TypeNode - if (column.isNullable) - columnTypeNode = factory.createUnionTypeNode([ - type, - factory.createLiteralTypeNode(factory.createNull()), - ]) - else if (column.hasDefaultValue || column.isAutoIncrementing) { - if (importsMap.has('kysely')) { - const kyselyImports = importsMap.get('kysely')! - kyselyImports.add(kyselyGeneratedImportSpecifier) - importsMap.set('kysely', kyselyImports) - } else { - importsMap.set('kysely', new Set([kyselyGeneratedImportSpecifier])) - } - columnTypeNode = factory.createTypeReferenceNode( - kyselyGeneratedIdentifier, - [type], - ) - } else columnTypeNode = type - - // Create property - const columnProperty = factory.createPropertySignature( - undefined, - factory.createIdentifier(column.name), - undefined, - columnTypeNode, + const columnProperty = getColumnType( + column, + definitions, + importsMap, + typeDeclarations, ) columnProperties.push(columnProperty) } @@ -147,31 +105,27 @@ export function getTypes( ]), ) - function createWrapperTypeAlias( - type: 'insertable' | 'selectable' | 'updateable', - ) { - let identifier: Identifier - if (type === 'insertable') identifier = kyselyInsertableIdentifier - else if (type === 'selectable') identifier = kyselySelectableIdentifier - else identifier = kyselyUpdateableIdentifier - return factory.createTypeAliasDeclaration( - [factory.createModifier(SyntaxKind.ExportKeyword)], - factory.createIdentifier(`${tableTypeName}${capitalCase(type)}`), - undefined, - factory.createTypeReferenceNode(identifier, [ - factory.createTypeReferenceNode(tableTypeIdentifier, undefined), - ]), - ) - } - const insertableTypeAlias = createWrapperTypeAlias('insertable') - const selectableTypeAlias = createWrapperTypeAlias('selectable') - const updateableTypeAlias = createWrapperTypeAlias('updateable') + const insertableTypeAlias = createWrapperTypeAlias( + tableTypeName, + tableTypeIdentifier, + 'insertable', + ) + const selectableTypeAlias = createWrapperTypeAlias( + tableTypeName, + tableTypeIdentifier, + 'selectable', + ) + const updateableTypeAlias = createWrapperTypeAlias( + tableTypeName, + tableTypeIdentifier, + 'updateable', + ) nodes.push(selectableTypeAlias, insertableTypeAlias, updateableTypeAlias) // Create table type property for encompassing `DB` type const tableDbTypeParameter = factory.createPropertySignature( undefined, - factory.createIdentifier(table.name), + table.name, undefined, factory.createTypeReferenceNode(tableTypeIdentifier, undefined), ) @@ -220,3 +174,92 @@ export function getTypes( return content } + +export function getColumnType( + column: ColumnMetadata, + definitions: Definitions, + importsMap: Map>, + typeDeclarations: Set, +) { + // Get type from lookup + let type: TypeNode + if (column.dataType in definitions) { + const definition = definitions[ + column.dataType as keyof typeof definitions + ] as TypeNode | DefinitionNode + if ('value' in definition) { + type = definition.value + for (const [name, imports] of Object.entries(definition.imports)) { + if (importsMap.has(name)) { + const nameImports = importsMap.get(name)! + importsMap.set(name, new Set([...nameImports, ...imports])) + } else importsMap.set(name, new Set(imports)) + } + for (const declaration of definition.declarations) { + typeDeclarations.add(declaration) + } + } else type = definition + } else type = factory.createKeywordTypeNode(SyntaxKind.UnknownKeyword) + + // Create node based on properties (e.g. nullable, default) + let columnTypeNode: TypeNode + if (column.isNullable) + columnTypeNode = factory.createUnionTypeNode([ + type, + factory.createLiteralTypeNode(factory.createNull()), + ]) + else if (column.hasDefaultValue || column.isAutoIncrementing) { + if (importsMap.has('kysely')) { + const kyselyImports = importsMap.get('kysely')! + kyselyImports.add(kyselyGeneratedImportSpecifier) + importsMap.set('kysely', kyselyImports) + } else { + importsMap.set('kysely', new Set([kyselyGeneratedImportSpecifier])) + } + + // Unwrap declarations already contained in `ColumnType` + if ( + isTypeReferenceNode(type) && + (type.typeName as Identifier).escapedText === + kyselyColumnTypeIdentifier.escapedText + ) { + if (!typeDeclarations.has(unwrapColumnTypeTypeAlias)) + typeDeclarations.add(unwrapColumnTypeTypeAlias) + columnTypeNode = factory.createTypeReferenceNode( + kyselyGeneratedIdentifier, + [factory.createTypeReferenceNode(unwrapColumnTypeIdentifier, [type])], + ) + } else + columnTypeNode = factory.createTypeReferenceNode( + kyselyGeneratedIdentifier, + [type], + ) + } else columnTypeNode = type + + // Create property + return factory.createPropertySignature( + undefined, + column.name, + undefined, + columnTypeNode, + ) +} + +function createWrapperTypeAlias( + tableTypeName: string, + tableTypeIdentifier: Identifier, + type: 'insertable' | 'selectable' | 'updateable', +) { + let identifier: Identifier + if (type === 'insertable') identifier = kyselyInsertableIdentifier + else if (type === 'selectable') identifier = kyselySelectableIdentifier + else identifier = kyselyUpdateableIdentifier + return factory.createTypeAliasDeclaration( + [factory.createModifier(SyntaxKind.ExportKeyword)], + factory.createIdentifier(`${tableTypeName}${capitalCase(type)}`), + undefined, + factory.createTypeReferenceNode(identifier, [ + factory.createTypeReferenceNode(tableTypeIdentifier, undefined), + ]), + ) +} diff --git a/src/utils/loadConfig.ts b/src/utils/loadConfig.ts index 2a92b14..e6fcf7e 100644 --- a/src/utils/loadConfig.ts +++ b/src/utils/loadConfig.ts @@ -13,7 +13,6 @@ export async function loadConfig( const res = await bundleRequire({ filepath: configPath }) let config = res.mod.default if (config.default) config = config.default - if (typeof config !== 'function') return { _spinnerMs: 250, ...config } - const resolved = await config() - return { _spinnerMs: 250, ...resolved } + if (typeof config !== 'function') return config + return config() }