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 19, 2019
1 parent 5bcb626 commit dbe5efc
Show file tree
Hide file tree
Showing 23 changed files with 562 additions and 13 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
}

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

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

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

if (args._.length === 0) {
yargs.showHelp();
}
58 changes: 58 additions & 0 deletions lib/cli/CLIHelper.ts
@@ -0,0 +1,58 @@
import yargs, { Argv } from 'yargs';
import { ClearCacheCommand } from './ClearCacheCommand';
import { GenerateEntitiesCommand } from './GenerateEntitiesCommand';
import { MikroORM } from '../MikroORM';
import { Configuration, Utils } from '../utils';
import { CreateSchemaCommand } from './CreateSchemaCommand';
import { UpdateSchemaCommand } from './UpdateSchemaCommand';
import { DropSchemaCommand } from './DropSchemaCommand';

export class CLIHelper {

static getConfiguration(): Configuration {
return new Configuration(require(Utils.normalizePath(process.env.MIKRO_ORM_CLI || './cli-config')));
}

static async getORM(): Promise<MikroORM> {
const options = CLIHelper.getConfiguration();
return MikroORM.init(options);
}

static configure() {
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();
}

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;
}

}
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 = 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);
}

}
11 changes: 10 additions & 1 deletion package.json
Expand Up @@ -39,6 +39,9 @@
"coveralls": "cat ./coverage/lcov.info | coveralls",
"lint": "tslint -p ."
},
"bin": {
"mikro-orm": "./dist/cli.js"
},
"jest": {
"transform": {
"^.+\\.tsx?$": "ts-jest"
Expand All @@ -53,6 +56,9 @@
"collectCoverage": false,
"collectCoverageFrom": [
"lib/**/*.ts"
],
"coveragePathIgnorePatterns": [
"lib/cli.ts"
]
},
"commitlint": {
Expand All @@ -79,14 +85,17 @@
"pinVersions": false
},
"dependencies": {
"chalk": "^2.4.2",
"clone": "^2.1.0",
"fast-deep-equal": "^2.0.0",
"fs-extra": "^8.0.0",
"globby": "^10.0.0",
"knex": "^0.19.0",
"ts-morph": "^3.0.0",
"typescript": "^3.5.0",
"uuid": "^3.3.2"
"uuid": "^3.3.2",
"yargonaut": "^1.1.4",
"yargs": "^13.3.0"
},
"peerDependencies": {
"mongodb": "^3.2.0",
Expand Down
1 change: 1 addition & 0 deletions tests/FileCacheAdapter.test.ts
Expand Up @@ -14,6 +14,7 @@ describe('FileCacheAdapter', () => {
await new Promise(resolve => setTimeout(resolve, 10));
writeFileSync(origin, '321');
await expect(cache.get('cache-test-handle')).resolves.toBeNull();
await cache.clear();
});

});
1 change: 1 addition & 0 deletions tests/NullCacheAdapter.test.ts
Expand Up @@ -8,6 +8,7 @@ describe('NullCacheAdapter', () => {
const cache = new NullCacheAdapter();
await cache.set('cache-test-handle', 123, origin);
await expect(cache.get('cache-test-handle')).resolves.toBeNull();
await cache.clear();
});

});
13 changes: 13 additions & 0 deletions tests/cli-config.ts
@@ -0,0 +1,13 @@
import { BASE_DIR } from './bootstrap';
import { JavaScriptMetadataProvider } from '../lib';
import { SqliteDriver } from '../lib/drivers/SqliteDriver';

const { BaseEntity4, Test3 } = require('./entities-js');

export = {
entities: [Test3, BaseEntity4],
dbName: './mikro_orm_test.db',
baseDir: BASE_DIR,
driver: SqliteDriver,
metadataProvider: JavaScriptMetadataProvider,
};

0 comments on commit dbe5efc

Please sign in to comment.