Skip to content

Commit

Permalink
feat(cli): add basic CLI tool (#102)
Browse files Browse the repository at this point in the history
Adds `mikro-orm` npm command that allows to clear cache and work with SchemaGenerator and EntityGenerator.

First one needs to provide `cli-config.[jt]s` file in the root directory that will export the ORM configuration.

Closes #101
  • Loading branch information
B4nan committed Aug 20, 2019
1 parent 64f84c1 commit 87cfe80
Show file tree
Hide file tree
Showing 37 changed files with 722 additions and 57 deletions.
2 changes: 1 addition & 1 deletion lib/MikroORM.ts
Expand Up @@ -13,7 +13,7 @@ export class MikroORM {
private readonly driver: IDatabaseDriver;
private readonly logger: Logger;

static async init(options: Options): Promise<MikroORM> {
static async init(options: Options | Configuration): Promise<MikroORM> {
const orm = new MikroORM(options);
const driver = await orm.connect();

Expand Down
2 changes: 2 additions & 0 deletions lib/cache/CacheAdapter.ts
Expand Up @@ -4,4 +4,6 @@ export interface CacheAdapter {

set(name: string, data: any, origin: string): Promise<void>;

clear(): Promise<void>;

}
12 changes: 11 additions & 1 deletion lib/cache/FileCacheAdapter.ts
@@ -1,4 +1,5 @@
import { ensureDir, pathExists, readJSON, stat, writeJSON } from 'fs-extra';
import globby from 'globby';
import { ensureDir, pathExists, readJSON, stat, unlink, writeJSON } from 'fs-extra';
import { CacheAdapter } from './CacheAdapter';

export class FileCacheAdapter implements CacheAdapter {
Expand Down Expand Up @@ -28,6 +29,15 @@ export class FileCacheAdapter implements CacheAdapter {
await writeJSON(path, { modified, data, origin });
}

async clear(): Promise<void> {
const path = await this.path('*');
const files = await globby(path);

for (const file of files) {
await unlink(file);
}
}

private async path(name: string): Promise<string> {
await ensureDir(this.options.cacheDir);
return `${this.options.cacheDir}/${name}.json`;
Expand Down
4 changes: 4 additions & 0 deletions lib/cache/NullCacheAdapter.ts
Expand Up @@ -10,4 +10,8 @@ export class NullCacheAdapter implements CacheAdapter {
// ignore
}

async clear(): Promise<void> {
// ignore
}

}
18 changes: 18 additions & 0 deletions lib/cli.ts
@@ -0,0 +1,18 @@
#!/usr/bin/env node

require('yargonaut')
.style('blue')
.style('yellow', 'required')
.helpStyle('green')
.errorsStyle('red');

import yargs from 'yargs';
import { CLIHelper } from './cli/CLIHelper';

(async () => {
const args = (await CLIHelper.configure()).parse(process.argv.slice(2)) as { _: string[] };

if (args._.length === 0) {
yargs.showHelp();
}
})();
108 changes: 108 additions & 0 deletions lib/cli/CLIHelper.ts
@@ -0,0 +1,108 @@
import yargs, { Argv } from 'yargs';
import { pathExists } from 'fs-extra';

import { MikroORM } from '../MikroORM';
import { Configuration, Utils } from '../utils';
import { ClearCacheCommand } from './ClearCacheCommand';
import { GenerateEntitiesCommand } from './GenerateEntitiesCommand';
import { CreateSchemaCommand } from './CreateSchemaCommand';
import { UpdateSchemaCommand } from './UpdateSchemaCommand';
import { DropSchemaCommand } from './DropSchemaCommand';

export class CLIHelper {

static async getConfiguration(): Promise<Configuration> {
const paths = await CLIHelper.getConfigPaths();

for (let path of paths) {
path = Utils.normalizePath(path);

if (await pathExists(path)) {
return new Configuration(require(path));
}
}

throw new Error(`cli-config not found in ['${paths.join(`', '`)}']`);
}

static async getORM(): Promise<MikroORM> {
const options = await CLIHelper.getConfiguration();
const settings = await CLIHelper.getSettings();

if (settings.useTsNode) {
options.set('tsNode', true);
}

return MikroORM.init(options);
}

static async configure(): Promise<Argv> {
const settings = await CLIHelper.getSettings();

if (settings.useTsNode) {
require('ts-node').register();
}

return yargs
.scriptName('mikro-orm')
.version(require('../../package.json').version)
.usage('Usage: $0 <command> [options]')
.example('$0 schema:update --run', 'Runs schema synchronization')
.alias('v', 'version')
.alias('h', 'help')
.command(new ClearCacheCommand())
.command(new GenerateEntitiesCommand())
.command(new CreateSchemaCommand())
.command(new DropSchemaCommand())
.command(new UpdateSchemaCommand())
.recommendCommands()
.strict();
}

private static async getSettings(): Promise<Settings> {
if (await pathExists(process.cwd() + '/package.json')) {
const config = require(process.cwd() + '/package.json');
return config['mikro-orm'];
}

return {};
}

static configureSchemaCommand(args: Argv) {
args.option('r', {
alias: 'run',
type: 'boolean',
desc: 'Runs queries',
});
args.option('d', {
alias: 'dump',
type: 'boolean',
desc: 'Dumps all queries to console',
});
args.option('no-fk', {
type: 'boolean',
desc: 'Disable foreign key checks if possible',
default: true,
});

return args;
}

private static async getConfigPaths(): Promise<string[]> {
const paths: string[] = [];

if (await pathExists(process.cwd() + '/package.json')) {
const config = require(process.cwd() + '/package.json');
const settings = config['mikro-orm'] as Settings;
paths.push(...(settings.configPaths || []));
}

return [...paths, process.env.MIKRO_ORM_CLI || './cli-config'];
}

}

export interface Settings {
useTsNode?: boolean;
configPaths?: string[];
}
19 changes: 19 additions & 0 deletions lib/cli/ClearCacheCommand.ts
@@ -0,0 +1,19 @@
import { Arguments, CommandModule } from 'yargs';
import chalk from 'chalk';
import { CLIHelper } from './CLIHelper';

export class ClearCacheCommand implements CommandModule {

command = 'cache:clear';
describe = 'Clear metadata cache';

async handler(args: Arguments) {
const config = await CLIHelper.getConfiguration();
const cache = config.getCacheAdapter();
await cache.clear();

// tslint:disable-next-line:no-console
console.log(chalk.green('Metadata cache was successfully cleared') + '\n');
}

}
38 changes: 38 additions & 0 deletions lib/cli/CreateSchemaCommand.ts
@@ -0,0 +1,38 @@
import yargs, { Arguments, Argv, CommandModule } from 'yargs';
import chalk from 'chalk';
import { CLIHelper } from './CLIHelper';

export type Options = { dump: boolean; run: boolean; noFk: boolean };

export class CreateSchemaCommand<U extends Options = Options> implements CommandModule<{}, U> {

command = 'schema:create';
describe = 'Create database schema based on current metadata';

builder(args: Argv) {
return CLIHelper.configureSchemaCommand(args) as Argv<U>;
}

async handler(args: Arguments<U>) {
if (!args.run && !args.dump) {
yargs.showHelp();
return;
}

const orm = await CLIHelper.getORM();
const generator = orm.getSchemaGenerator();

if (args.dump) {
const dump = await generator.getCreateSchemaSQL(args.noFk);
// tslint:disable-next-line:no-console
console.log(dump);
} else {
await generator.createSchema(args.noFk);
// tslint:disable-next-line:no-console
console.log(chalk.green('Schema successfully created'));
}

await orm.close(true);
}

}
38 changes: 38 additions & 0 deletions lib/cli/DropSchemaCommand.ts
@@ -0,0 +1,38 @@
import yargs, { Arguments, Argv, CommandModule } from 'yargs';
import chalk from 'chalk';
import { CLIHelper } from './CLIHelper';

export type Options = { dump: boolean; run: boolean; noFk: boolean };

export class DropSchemaCommand<U extends Options = Options> implements CommandModule<{}, U> {

command = 'schema:drop';
describe = 'Drop all tables based on current metadata';

builder(args: Argv) {
return CLIHelper.configureSchemaCommand(args) as Argv<U>;
}

async handler(args: Arguments<U>) {
if (!args.run && !args.dump) {
yargs.showHelp();
return;
}

const orm = await CLIHelper.getORM();
const generator = orm.getSchemaGenerator();

if (args.dump) {
const dump = await generator.getDropSchemaSQL(args.noFk);
// tslint:disable-next-line:no-console
console.log(dump);
} else {
await generator.dropSchema(args.noFk);
// tslint:disable-next-line:no-console
console.log(chalk.green('Schema successfully dropped') + '\n');
}

await orm.close(true);
}

}
48 changes: 48 additions & 0 deletions lib/cli/GenerateEntitiesCommand.ts
@@ -0,0 +1,48 @@
import yargs, { Arguments, Argv, CommandModule } from 'yargs';
import { CLIHelper } from './CLIHelper';

export type Options = { dump: boolean; save: boolean; path: string };

export class GenerateEntitiesCommand<U extends Options = Options> implements CommandModule<{}, U> {

command = 'generate-entities';
describe = 'Generate entities based on current database schema';

builder(args: Argv) {
args.option('s', {
alias: 'save',
type: 'boolean',
desc: 'Saves entities to directory defined by --path',
});
args.option('d', {
alias: 'dump',
type: 'boolean',
desc: 'Dumps all entities to console',
});
args.option('p', {
alias: 'path',
type: 'string',
desc: 'Sets path to directory where to save entities',
});

return args as unknown as Argv<U>;
}

async handler(args: Arguments<U>) {
if (!args.save && !args.dump) {
yargs.showHelp();
return;
}

const orm = await CLIHelper.getORM();
const generator = orm.getEntityGenerator();
const dump = await generator.generate({ save: args.save, baseDir: args.path });

if (args.dump) {
process.stdout.write(dump.join('\n\n'));
}

await orm.close(true);
}

}
38 changes: 38 additions & 0 deletions lib/cli/UpdateSchemaCommand.ts
@@ -0,0 +1,38 @@
import yargs, { Arguments, Argv, CommandModule } from 'yargs';
import chalk from 'chalk';
import { CLIHelper } from './CLIHelper';

export type Options = { dump: boolean; run: boolean; noFk: boolean };

export class UpdateSchemaCommand<U extends Options = Options> implements CommandModule<{}, U> {

command = 'schema:update';
describe = 'Update database schema based on current metadata';

builder(args: Argv) {
return CLIHelper.configureSchemaCommand(args) as Argv<U>;
}

async handler(args: Arguments<U>) {
if (!args.run && !args.dump) {
yargs.showHelp();
return;
}

const orm = await CLIHelper.getORM();
const generator = orm.getSchemaGenerator();

if (args.dump) {
const dump = await generator.getUpdateSchemaSQL(args.noFk);
// tslint:disable-next-line:no-console
console.log(dump + '\n');
} else {
await generator.updateSchema(args.noFk);
// tslint:disable-next-line:no-console
console.log(chalk.green('Schema successfully updated') + '\n');
}

await orm.close(true);
}

}
7 changes: 4 additions & 3 deletions lib/connections/AbstractSqlConnection.ts
Expand Up @@ -40,12 +40,13 @@ export abstract class AbstractSqlConnection extends Connection {
});
}

async execute<T = QueryResult | EntityData<IEntity> | EntityData<IEntity>[]>(queryOrKnex: string | QueryBuilder | Raw, params: any[] = [], method: 'all' | 'get' | 'run' = 'all'): Promise<T> {
async execute<T extends QueryResult | EntityData<IEntity> | EntityData<IEntity>[] = EntityData<IEntity>[]>(queryOrKnex: string | QueryBuilder | Raw, params: any[] = [], method: 'all' | 'get' | 'run' = 'all'): Promise<T> {
if (Utils.isObject<QueryBuilder | Raw>(queryOrKnex)) {
return await this.executeKnex(queryOrKnex, method);
}

const res = await this.executeQuery<any>(queryOrKnex, params, () => this.client.raw(queryOrKnex, params));
const sql = this.client.client.positionBindings(queryOrKnex);
const res = await this.executeQuery<any>(sql, () => this.client.raw(queryOrKnex, params));
return this.transformRawResult<T>(res, method);
}

Expand Down Expand Up @@ -74,7 +75,7 @@ export abstract class AbstractSqlConnection extends Connection {
protected async executeKnex(qb: QueryBuilder | Raw, method: 'all' | 'get' | 'run'): Promise<QueryResult | any | any[]> {
const q = qb.toSQL();
const query = q.toNative ? q.toNative() : q;
const res = await this.executeQuery(query.sql, query.bindings, () => qb);
const res = await this.executeQuery(query.sql, () => qb);

return this.transformKnexResult(res, method);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/connections/Connection.ts
Expand Up @@ -60,7 +60,7 @@ export abstract class Connection {
this.metadata = metadata;
}

protected async executeQuery<T>(query: string, params: any[], cb: () => Promise<T>): Promise<T> {
protected async executeQuery<T>(query: string, cb: () => Promise<T>): Promise<T> {
const now = Date.now();
const res = await cb();
this.logQuery(query, Date.now() - now);
Expand Down

0 comments on commit 87cfe80

Please sign in to comment.