Skip to content

Type-safe seed file generation #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>` is used. if `prisma-kysely` is installed, you can leave out the `#MyDatabaseTypeName` part, it will default to `<path>#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`.
Expand Down
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
103 changes: 87 additions & 16 deletions src/commands/seed/make.mts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
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'
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 { getTemplateExtension } from '../../utils/get-template-extension.mjs'
import { getKyselyCodegenInstalledVersion } from '../../utils/version.mjs'

const args = {
...CommonArgs,
Expand All @@ -29,11 +34,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)

Expand All @@ -45,30 +50,96 @@ 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 databaseInterfaceConfig = await resolveDatabaseInterfaceConfig(
args,
seeds.databaseInterface,
)
consola.debug('Database interface config:', databaseInterfaceConfig)

if (!databaseInterfaceConfig) {
consola.debug('using non-type-safe seed template')

await copyFile(
join(__dirname, 'templates/seed-template.ts'),
destinationFilePath,
)

return printSuccess(destinationFilePath)
}

consola.debug('using type-safe seed template')

const templatePath = join(
__dirname,
`templates/seed-template.${templateExtension}`,
const templateFile = await readFile(
join(__dirname, 'templates/seed-type-safe-template.ts'),
{ encoding: 'utf8' },
)
consola.debug('Template file:', templateFile)

consola.debug('Template path:', templatePath)
const databaseInterfaceName = databaseInterfaceConfig.name || 'DB'

await copyFile(templatePath, filePath)
const populatedTemplateFile = templateFile
.replace(
/<import>/,
`import type ${
databaseInterfaceConfig.isDefaultExport
? databaseInterfaceName
: `{ ${databaseInterfaceName} }`
} from '${databaseInterfaceConfig.path}'`,
)
.replace(/<name>/, databaseInterfaceName)
consola.debug('Populated template file: ', populatedTemplateFile)

consola.success(`Created seed file at ${filePath}`)
await writeFile(destinationFilePath, populatedTemplateFile)

printSuccess(destinationFilePath)
},
} satisfies CommandDef<typeof args>

function printSuccess(destinationFilePath: string): void {
consola.success(`Created seed file at ${destinationFilePath}`)
}

async function resolveDatabaseInterfaceConfig(
args: HasCWD,
databaseInterface: DatabaseInterface | undefined,
): Promise<DatabaseInterfaceConfig | null> {
if (databaseInterface === 'off') {
return null
}

if (typeof databaseInterface === 'object') {
return databaseInterface
}

if (await getKyselyCodegenInstalledVersion(args)) {
return {
isDefaultExport: false,
name: 'DB',
path: 'kysely-codegen',
}
}

// 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)
45 changes: 44 additions & 1 deletion src/config/kysely-ctl-config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,10 @@ type MigratorlessMigrationsConfig = MigrationsBaseConfig &
}
)

type SeederfulSeedsConfig = Pick<SeedsBaseConfig, 'getSeedPrefix'> & {
type SeederfulSeedsConfig = Pick<
SeedsBaseConfig,
'databaseInterface' | 'getSeedPrefix'
> & {
allowJS?: never
provider?: never
seeder: Seeder
Expand Down Expand Up @@ -186,5 +189,45 @@ export type MigrationsBaseConfig = Omit<MigratorProps, 'db' | 'provider'> & {
}

export type SeedsBaseConfig = Omit<SeederProps, 'db' | 'provider'> & {
/**
* Generate type-safe seed files that rely on an existing database interface.
*
* Default is `'auto'`.
*
* When `'auto'`:
*
* - 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<any>`.
*
* When `'off'`, it will fallback to `Kysely<any>`.
*
* When a config object is passed, it will use the specified database interface path and name.
*/
databaseInterface?: 'auto' | 'off' | DatabaseInterfaceConfig
getSeedPrefix?(): string | Promise<string>
}

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
}
7 changes: 7 additions & 0 deletions src/templates/seed-type-safe-template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Kysely } from 'kysely'
<import>

export async function seed(db: Kysely<<name>>): Promise<void> {
// seed code goes here...
// note: this function is mandatory. you must implement this function.
}
33 changes: 29 additions & 4 deletions src/utils/version.mts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,41 @@ 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<string | null> {
return await getInstalledVersionFromConsumerPackageJSON(args, 'kysely')
}

export async function getKyselyCodegenInstalledVersion(
args: HasCWD,
): Promise<string | null> {
return await getInstalledVersionFromConsumerPackageJSON(
args,
'kysely-codegen',
)
}

export async function getPrismaKyselyInstalledVersion(
args: HasCWD,
): Promise<string | null> {
return await getInstalledVersionFromConsumerPackageJSON(args, 'prisma-kysely')
}

export async function getKanelKyselyInstalledVersion(
args: HasCWD,
): Promise<string | null> {
return await getInstalledVersionFromConsumerPackageJSON(args, 'kanel-kysely')
}

async function getInstalledVersionFromConsumerPackageJSON(
args: HasCWD,
name: string,
): Promise<string | null> {
try {
const pkgJSON = await getConsumerPackageJSON(args)

return getVersionFromPackageJSON('kysely', pkgJSON)
return getVersionFromPackageJSON(name, pkgJSON)
} catch (err) {
return null
}
Expand Down
Loading