@jadejr/kysely-pglite
Version:
Kysely dialect for @electric-sql/pglite (temporary fork https://github.com/dnlsandiego/kysely-pglite)
157 lines • 6.76 kB
JavaScript
import { Codegen } from '#codegen.js';
import { DEFAULT_OUT_FILE, pgDataFiles } from '#constants.js';
import { applyLogsPatch } from '#utils/apply-logs-patch.js';
import { createKyselyPGlite } from '#utils/create-kysely.js';
import { createMigrator } from '#utils/create-migrator.js';
import { getDatabaseErrorInfo } from '#utils/get-database-error-info.js';
import { Args, Command, Flags } from '@oclif/core';
import { isError } from '@sindresorhus/is';
import chokidar from 'chokidar';
import consola from 'consola';
import { colorize } from 'consola/utils';
import { lstatSync } from 'fs';
import fs from 'fs-extra';
import { group } from 'radash';
// Patching console.log and console.warn because of noisy logs from @electic/pglite
// https://github.com/electric-sql/pglite/issues/256
applyLogsPatch();
export default class CodegenCommand extends Command {
static args = {
path: Args.string({
required: true,
parse: async (path) => {
const stat = lstatSync(path, { throwIfNoEntry: false });
if (!stat?.isDirectory() && !stat?.isFile()) {
throw new Error(`${path} is an invalid path. It doesn't resolve to a file or directory`);
}
return path;
},
description: 'The path to a file/directory of Kysely migrations or a persisted PGlite database',
}),
};
static description = 'Generate TypeScript types based on Kysely migrations or a persisted PGlite database';
static examples = [
'<%= config.bin %> <%= command.id %> ./src/db/migrations --out-file ./src/db/types.ts',
'<%= config.bin %> <%= command.id %> ./src/db/migrations-file.ts',
'<%= config.bin %> <%= command.id %> ./pgdata',
'<%= config.bin %> <%= command.id %> --watch ./src/db/migrations',
];
static flags = {
watch: Flags.boolean({
char: 'w',
default: false,
description: 'Watches the given path and generates types whenever a change is detected',
}),
outFile: Flags.string({
char: 'o',
aliases: ['out-file'],
description: 'Path to persist the generated types',
default: DEFAULT_OUT_FILE,
}),
camelCase: Flags.boolean({
aliases: ['camel-case'],
description: 'Use the Kysely CamelCasePlugin',
}),
envFile: Flags.directory({
aliases: ['env-file'],
description: 'The path to an environment file to use',
}),
excludePattern: Flags.string({
aliases: ['exclude-pattern'],
description: 'Exclude tables matching the specified glob pattern',
}),
includePattern: Flags.string({
aliases: ['include-pattern'],
description: 'Only include tables matching the specified glob pattern',
}),
logLevel: Flags.string({
aliases: ['log-level'],
description: 'Set the terminal log level',
options: ['debug', 'info', 'warn', 'error', 'silent'],
}),
print: Flags.boolean({
description: 'Print the generated output to the terminal',
char: 'p',
}),
runtimeEnums: Flags.boolean({
aliases: ['runtime-enums'],
description: 'Generate runtime enums instead of string unions',
}),
typeOnlyImports: Flags.boolean({
aliases: ['type-only-imports'],
description: 'Generate TypeScript 3.8+ `import type` syntax',
default: true,
}),
verify: Flags.boolean({
description: 'Verify that the generated types are up-to-date',
default: false,
}),
noDomain: Flags.boolean({
aliases: ['no-domain'],
description: 'Skip generating types for PostgreSQL domains',
default: false,
}),
};
async isDataDir(path) {
const stat = lstatSync(path);
if (!stat.isDirectory()) {
return false;
}
const files = await fs.readdir(path, {
encoding: 'utf-8',
withFileTypes: true,
});
return files.some((f) => pgDataFiles.includes(f.name));
}
async runCodegen() {
const { args, flags: { watch, ...flags }, } = await this.parse(CodegenCommand);
const isDataDir = await this.isDataDir(args.path);
const { db, dialect } = await createKyselyPGlite(isDataDir ? args.path : undefined);
const codegen = new Codegen(dialect);
const migrator = await createMigrator(db, args.path);
if (isDataDir) {
await codegen.generate({ ...flags, db });
}
else {
const { results = [], error } = await migrator.migrateToLatest();
const { Success = [], Error = [] } = group(results, (r) => r.status);
if (Success.length) {
await codegen.generate({ ...flags, db });
}
if (isError(error)) {
const [migration] = Error;
const { message, hint } = getDatabaseErrorInfo(error);
this.error(`${message} in ${migration.migrationName}`, {
suggestions: hint ? [hint] : [],
});
}
}
return db;
}
async run() {
const { args, flags } = await this.parse(CodegenCommand);
consola.start('Introspecting database...');
const db = await this.runCodegen();
const tables = await db.introspection.getTables();
consola.success(`Introspected ${tables.length} tables. Generated types in ${colorize('underline', flags.outFile)}`);
if (flags.watch) {
// https://oclif.io/docs/commands/#avoiding-timeouts
return new Promise(() => {
consola.start(`Watching changes in ${colorize('underline', args.path)}...`);
const watcher = chokidar.watch(args.path, {
ignored: /(^|[\/\\])\../, // ignore dotfiles
ignoreInitial: true,
});
const watcherFn = async (event, path) => {
await this.runCodegen();
consola.info(`${event} File: ${colorize('underline', path)}. Types updated`);
};
watcher
.on('add', async (path) => watcherFn(colorize('blueBright', '[added]'), path))
.on('change', async (path) => watcherFn(colorize('cyan', '[changed]'), path))
.on('unlink', async (path) => watcherFn(colorize('magenta', '[deleted]'), path));
});
}
}
}
//# sourceMappingURL=cli.js.map