From 94518289fab360a59588741670ed5d5c227adea3 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Wed, 19 Jun 2024 22:34:03 +0300 Subject: [PATCH 1/4] type-safe seed file generation --- src/commands/seed/make.mts | 67 ++++++++++++++++++------ src/config/kysely-ctl-config.mts | 8 +++ src/templates/seed-type-safe-template.ts | 7 +++ src/utils/version.mts | 27 ++++++++-- 4 files changed, 88 insertions(+), 21 deletions(-) create mode 100644 src/templates/seed-type-safe-template.ts diff --git a/src/commands/seed/make.mts b/src/commands/seed/make.mts index f177bc8..7d6e1ae 100644 --- a/src/commands/seed/make.mts +++ b/src/commands/seed/make.mts @@ -1,4 +1,4 @@ -import { copyFile, mkdir } from 'node:fs/promises' +import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises' import type { ArgsDef, CommandDef } from 'citty' import { consola } from 'consola' import { join } from 'pathe' @@ -6,7 +6,10 @@ import { CommonArgs } from '../../arguments/common.mjs' import { ExtensionArg, assertExtension } from '../../arguments/extension.mjs' import { getConfigOrFail } from '../../config/get-config.mjs' import { createSubcommand } from '../../utils/create-subcommand.mjs' -import { getTemplateExtension } from '../../utils/get-template-extension.mjs' +import { + getKyselyCodegenInstalledVersion, + getPrismaKyselyInstalledVersion, +} from '../../utils/version.mjs' const args = { ...CommonArgs, @@ -29,11 +32,11 @@ const BaseMakeCommand = { consola.debug(context, []) - const config = await getConfigOrFail(args) + assertExtension(extension) - assertExtension(extension, config, 'seeds') + const { seeds, ...config } = await getConfigOrFail(args) - const seedsFolderPath = join(config.cwd, config.seeds.seedFolder) + const seedsFolderPath = join(config.cwd, seeds.seedFolder) consola.debug('Seeds folder path:', seedsFolderPath) @@ -45,28 +48,58 @@ const BaseMakeCommand = { consola.debug('Seeds folder created') } - const filename = `${await config.seeds.getSeedPrefix()}${ + const destinationFilename = `${await seeds.getSeedPrefix()}${ args.seed_name }.${extension}` - consola.debug('Filename:', filename) + consola.debug('Destination filename:', destinationFilename) - const filePath = join(seedsFolderPath, filename) + const destinationFilePath = join(seedsFolderPath, destinationFilename) - consola.debug('File path:', filePath) + consola.debug('File path:', destinationFilePath) - const templateExtension = await getTemplateExtension(extension) + const databaseInterfacePath = + seeds.databaseInterfacePath || + ((await getKyselyCodegenInstalledVersion(args)) + ? 'kysely-codegen' + : undefined) - const templatePath = join( - __dirname, - `templates/seed-template.${templateExtension}`, - ) + consola.debug('Database interface path:', databaseInterfacePath) + + if (!databaseInterfacePath) { + await copyFile( + join(__dirname, 'templates/seed-template.ts'), + destinationFilePath, + ) + } else { + const templateFile = await readFile( + join(__dirname, 'templates/seed-type-safe-template.ts'), + { encoding: 'utf8' }, + ) + + consola.debug('templateFile', templateFile) + + const [ + databaseInterfaceFilePath, + databaseInterfaceName = databaseInterfaceFilePath === + 'kysely-codegen' || (await getPrismaKyselyInstalledVersion(args)) + ? 'DB' + : 'Database', + ] = databaseInterfacePath.split('#') - consola.debug('Template path:', templatePath) + consola.debug('Database interface file path: ', databaseInterfaceFilePath) + consola.debug('Database interface name: ', databaseInterfaceName) - await copyFile(templatePath, filePath) + const populatedTemplateFile = templateFile + .replace(//g, databaseInterfaceName) + .replace(//g, databaseInterfaceFilePath) + + consola.debug('Populated template file: ', populatedTemplateFile) + + await writeFile(destinationFilePath, populatedTemplateFile) + } - consola.success(`Created seed file at ${filePath}`) + consola.success(`Created seed file at ${destinationFilePath}`) }, } satisfies CommandDef diff --git a/src/config/kysely-ctl-config.mts b/src/config/kysely-ctl-config.mts index a398687..b1400da 100644 --- a/src/config/kysely-ctl-config.mts +++ b/src/config/kysely-ctl-config.mts @@ -186,5 +186,13 @@ export type MigrationsBaseConfig = Omit & { } export type SeedsBaseConfig = Omit & { + /** + * `Database` interface relative-to-seed-folder path, e.g. `kysely-codegen`, `../path/to/database#MyDatabaseTypeName`. + * + * Default is `kysely-codegen` if it is installed, otherwise `Kysely`. + * + * If `prisma-kysely` is installed, you can leave out the `#MyDatabaseTypeName` part, it will default to `#DB`. + */ + databaseInterfacePath?: string getSeedPrefix?(): string | Promise } diff --git a/src/templates/seed-type-safe-template.ts b/src/templates/seed-type-safe-template.ts new file mode 100644 index 0000000..6866b66 --- /dev/null +++ b/src/templates/seed-type-safe-template.ts @@ -0,0 +1,7 @@ +import type { } from '' +import type { Kysely } from 'kysely' + +export async function seed(db: Kysely<>): Promise { + // seed code goes here... + // note: this function is mandatory. you must implement this function. +} diff --git a/src/utils/version.mts b/src/utils/version.mts index d4abc74..6d494fc 100644 --- a/src/utils/version.mts +++ b/src/utils/version.mts @@ -6,16 +6,35 @@ import type { HasCWD } from '../config/get-cwd.mjs' import { getPackageManager } from './package-manager.mjs' import { getCTLPackageJSON, getConsumerPackageJSON } from './pkg-json.mjs' -/** - * Returns the version of the Kysely package. - */ export async function getKyselyInstalledVersion( args: HasCWD, +): Promise { + return await getInstalledVersionFromConsumerPackageJSON(args, 'kysely') +} + +export async function getKyselyCodegenInstalledVersion( + args: HasCWD, +): Promise { + return await getInstalledVersionFromConsumerPackageJSON( + args, + 'kysely-codegen', + ) +} + +export async function getPrismaKyselyInstalledVersion( + args: HasCWD, +): Promise { + return await getInstalledVersionFromConsumerPackageJSON(args, 'prisma-kysely') +} + +async function getInstalledVersionFromConsumerPackageJSON( + args: HasCWD, + name: string, ): Promise { try { const pkgJSON = await getConsumerPackageJSON(args) - return getVersionFromPackageJSON('kysely', pkgJSON) + return getVersionFromPackageJSON(name, pkgJSON) } catch (err) { return null } From d1c1dde9562d1b3b359c08651efd34ab5073a902 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Wed, 19 Jun 2024 22:38:59 +0300 Subject: [PATCH 2/4] .. --- biome.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/biome.json b/biome.json index e4982ae..29a3a19 100644 --- a/biome.json +++ b/biome.json @@ -1,7 +1,8 @@ { "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "files": { - "include": ["src", "tests"] + "include": ["src", "tests"], + "ignore": ["src/templates/seed-type-safe-template.ts"] }, "javascript": { "formatter": { From 2a246c5e00e411408471c9d22d9d8a11af00fe30 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Sat, 29 Jun 2024 23:41:20 +0300 Subject: [PATCH 3/4] update README.md. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4d000ff..d217444 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ export default defineConfig({ plugins, // optional. `Kysely` plugins list. default is `[]`. seeds: { // optional. allowJS, // optional. controls whether `.js`, `.cjs` or `.mjs` seeds are allowed. default is `false`. + databaseInterfacePath, // optional. database interface relative-to-seed-folder path, e.g. `kysely-codegen`, `../path/to/database#MyDatabaseTypeName`. default is `kysely-codegen` if it is installed, otherwise `Kysely` is used. if `prisma-kysely` is installed, you can leave out the `#MyDatabaseTypeName` part, it will default to `#DB`. getSeedPrefix, // optional. a function that returns a seed prefix. affects `seed make` command. default is `() => ${Date.now()}_`. provider, // optional. a seed provider instance. default is `kysely-ctl`'s `FileSeedProvider`. seeder, // optional. a seeder instance. default is `kysely-ctl`'s `Seeder`. From 4bd7345605204f5bcf8ded501ed8978c13d40b88 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Thu, 4 Jul 2024 03:12:39 +0300 Subject: [PATCH 4/4] auto|off|config. --- src/commands/seed/make.mts | 106 +++++++++++++++-------- src/config/kysely-ctl-config.mts | 45 ++++++++-- src/templates/seed-type-safe-template.ts | 4 +- src/utils/version.mts | 6 ++ 4 files changed, 120 insertions(+), 41 deletions(-) diff --git a/src/commands/seed/make.mts b/src/commands/seed/make.mts index 7d6e1ae..b2f94bc 100644 --- a/src/commands/seed/make.mts +++ b/src/commands/seed/make.mts @@ -5,11 +5,13 @@ import { join } from 'pathe' import { CommonArgs } from '../../arguments/common.mjs' import { ExtensionArg, assertExtension } from '../../arguments/extension.mjs' import { getConfigOrFail } from '../../config/get-config.mjs' +import type { HasCWD } from '../../config/get-cwd.mjs' +import type { + DatabaseInterface, + DatabaseInterfaceConfig, +} from '../../config/kysely-ctl-config.mjs' import { createSubcommand } from '../../utils/create-subcommand.mjs' -import { - getKyselyCodegenInstalledVersion, - getPrismaKyselyInstalledVersion, -} from '../../utils/version.mjs' +import { getKyselyCodegenInstalledVersion } from '../../utils/version.mjs' const args = { ...CommonArgs, @@ -58,50 +60,86 @@ const BaseMakeCommand = { consola.debug('File path:', destinationFilePath) - const databaseInterfacePath = - seeds.databaseInterfacePath || - ((await getKyselyCodegenInstalledVersion(args)) - ? 'kysely-codegen' - : undefined) + const databaseInterfaceConfig = await resolveDatabaseInterfaceConfig( + args, + seeds.databaseInterface, + ) + consola.debug('Database interface config:', databaseInterfaceConfig) - consola.debug('Database interface path:', databaseInterfacePath) + if (!databaseInterfaceConfig) { + consola.debug('using non-type-safe seed template') - if (!databaseInterfacePath) { await copyFile( join(__dirname, 'templates/seed-template.ts'), destinationFilePath, ) - } else { - const templateFile = await readFile( - join(__dirname, 'templates/seed-type-safe-template.ts'), - { encoding: 'utf8' }, - ) - consola.debug('templateFile', templateFile) + return printSuccess(destinationFilePath) + } - const [ - databaseInterfaceFilePath, - databaseInterfaceName = databaseInterfaceFilePath === - 'kysely-codegen' || (await getPrismaKyselyInstalledVersion(args)) - ? 'DB' - : 'Database', - ] = databaseInterfacePath.split('#') + consola.debug('using type-safe seed template') - consola.debug('Database interface file path: ', databaseInterfaceFilePath) - consola.debug('Database interface name: ', databaseInterfaceName) + const templateFile = await readFile( + join(__dirname, 'templates/seed-type-safe-template.ts'), + { encoding: 'utf8' }, + ) + consola.debug('Template file:', templateFile) + + const databaseInterfaceName = databaseInterfaceConfig.name || 'DB' + + const populatedTemplateFile = templateFile + .replace( + //, + `import type ${ + databaseInterfaceConfig.isDefaultExport + ? databaseInterfaceName + : `{ ${databaseInterfaceName} }` + } from '${databaseInterfaceConfig.path}'`, + ) + .replace(//, databaseInterfaceName) + consola.debug('Populated template file: ', populatedTemplateFile) - const populatedTemplateFile = templateFile - .replace(//g, databaseInterfaceName) - .replace(//g, databaseInterfaceFilePath) + await writeFile(destinationFilePath, populatedTemplateFile) - consola.debug('Populated template file: ', populatedTemplateFile) + printSuccess(destinationFilePath) + }, +} satisfies CommandDef - await writeFile(destinationFilePath, populatedTemplateFile) +function printSuccess(destinationFilePath: string): void { + consola.success(`Created seed file at ${destinationFilePath}`) +} + +async function resolveDatabaseInterfaceConfig( + args: HasCWD, + databaseInterface: DatabaseInterface | undefined, +): Promise { + if (databaseInterface === 'off') { + return null + } + + if (typeof databaseInterface === 'object') { + return databaseInterface + } + + if (await getKyselyCodegenInstalledVersion(args)) { + return { + isDefaultExport: false, + name: 'DB', + path: 'kysely-codegen', } + } - consola.success(`Created seed file at ${destinationFilePath}`) - }, -} satisfies CommandDef + // if (await getPrismaKyselyInstalledVersion(config)) { + // TODO: generates by default to ./prisma/generated/types.ts#DB + // but configurable at the kysely generator config level located in ./prisma/schema.prisma + // } + + // if (await getKanelKyselyInstalledVersion(config)) { + // TODO: generates by default to + // } + + return null +} export const MakeCommand = createSubcommand('make', BaseMakeCommand) export const LegacyMakeCommand = createSubcommand('seed:make', BaseMakeCommand) diff --git a/src/config/kysely-ctl-config.mts b/src/config/kysely-ctl-config.mts index b1400da..4009e03 100644 --- a/src/config/kysely-ctl-config.mts +++ b/src/config/kysely-ctl-config.mts @@ -134,7 +134,10 @@ type MigratorlessMigrationsConfig = MigrationsBaseConfig & } ) -type SeederfulSeedsConfig = Pick & { +type SeederfulSeedsConfig = Pick< + SeedsBaseConfig, + 'databaseInterface' | 'getSeedPrefix' +> & { allowJS?: never provider?: never seeder: Seeder @@ -187,12 +190,44 @@ export type MigrationsBaseConfig = Omit & { export type SeedsBaseConfig = Omit & { /** - * `Database` interface relative-to-seed-folder path, e.g. `kysely-codegen`, `../path/to/database#MyDatabaseTypeName`. + * Generate type-safe seed files that rely on an existing database interface. + * + * Default is `'auto'`. + * + * When `'auto'`: * - * Default is `kysely-codegen` if it is installed, otherwise `Kysely`. + * - When `kysely-codegen` is installed, it will use `import type { DB } from 'kysely-codegen'`. + * - **SOON** When `prisma-kysely` is installed, it will try to find the right path and use `import type { DB } from 'path/to/types'`. + * - **SOON** When `kanel-kysely` is installed, it will try to find the right path and use `import type Database from 'path/to/Database'`. + * - Otherwise, it will fallback to `Kysely`. * - * If `prisma-kysely` is installed, you can leave out the `#MyDatabaseTypeName` part, it will default to `#DB`. + * When `'off'`, it will fallback to `Kysely`. + * + * When a config object is passed, it will use the specified database interface path and name. */ - databaseInterfacePath?: string + databaseInterface?: 'auto' | 'off' | DatabaseInterfaceConfig getSeedPrefix?(): string | Promise } + +export type DatabaseInterface = 'auto' | 'off' | DatabaseInterfaceConfig + +export interface DatabaseInterfaceConfig { + /** + * Whether the database interface is the default export. + * + * Default is `false`. + */ + isDefaultExport?: boolean + + /** + * Name of the database interface. + * + * Default is `'DB'`. + */ + name?: string + + /** + * Path to the database interface, relative to the seed folder. + */ + path: string +} diff --git a/src/templates/seed-type-safe-template.ts b/src/templates/seed-type-safe-template.ts index 6866b66..dfeb401 100644 --- a/src/templates/seed-type-safe-template.ts +++ b/src/templates/seed-type-safe-template.ts @@ -1,7 +1,7 @@ -import type { } from '' import type { Kysely } from 'kysely' + -export async function seed(db: Kysely<>): Promise { +export async function seed(db: Kysely<>): Promise { // seed code goes here... // note: this function is mandatory. you must implement this function. } diff --git a/src/utils/version.mts b/src/utils/version.mts index 6d494fc..3d61b7e 100644 --- a/src/utils/version.mts +++ b/src/utils/version.mts @@ -27,6 +27,12 @@ export async function getPrismaKyselyInstalledVersion( return await getInstalledVersionFromConsumerPackageJSON(args, 'prisma-kysely') } +export async function getKanelKyselyInstalledVersion( + args: HasCWD, +): Promise { + return await getInstalledVersionFromConsumerPackageJSON(args, 'kanel-kysely') +} + async function getInstalledVersionFromConsumerPackageJSON( args: HasCWD, name: string,