1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Server: Moved CLI commands to separate files

This commit is contained in:
Laurent Cozic 2021-10-27 19:29:54 +01:00
parent 5c1cef8476
commit dca13b3a68
11 changed files with 22450 additions and 25783 deletions

File diff suppressed because it is too large Load Diff

View File

@ -52,7 +52,7 @@
"sqlite3": "^4.1.0",
"stripe": "^8.150.0",
"uuid": "^8.3.2",
"yargs": "^14.0.0",
"yargs": "^17.2.1",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
@ -65,7 +65,7 @@
"@types/markdown-it": "^12.0.0",
"@types/mustache": "^0.8.32",
"@types/nodemailer": "^6.4.1",
"@types/yargs": "^13.0.2",
"@types/yargs": "^17.0.4",
"@types/zxcvbn": "^4.4.1",
"gulp": "^4.0.2",
"jest": "^26.6.3",

View File

@ -3,11 +3,9 @@ require('source-map-support').install();
import * as Koa from 'koa';
import * as fs from 'fs-extra';
import { argv as yargsArgv } from 'yargs';
import Logger, { LoggerWrapper, TargetType } from '@joplin/lib/Logger';
import config, { initConfig, runningInDocker, EnvVariables } from './config';
import { createDb, dropDb } from './tools/dbTools';
import { dropTables, connectDb, disconnectDb, migrateLatest, waitForConnection, sqliteDefaultDir, migrateList, migrateUp, migrateDown } from './db';
import { migrateLatest, waitForConnection, sqliteDefaultDir } from './db';
import { AppContext, Env, KoaNext } from './utils/types';
import FsDriverNode from '@joplin/lib/fs-driver-node';
import routeHandler from './middleware/routeHandler';
@ -19,35 +17,21 @@ import startServices from './utils/startServices';
import { credentialFile } from './utils/testing/testUtils';
import apiVersionHandler from './middleware/apiVersionHandler';
import clickJackingHandler from './middleware/clickJackingHandler';
import deleteOldChanges from './commands/deleteOldChanges';
import newModelFactory from './models/factory';
import deleteOldChanges90 from './commands/deleteOldChanges90';
import setupCommands from './utils/setupCommands';
interface Argv {
env?: Env;
migrateLatest?: boolean;
migrateUp?: boolean;
migrateDown?: boolean;
migrateList?: boolean;
dropDb?: boolean;
pidfile?: string;
dropTables?: boolean;
createDb?: boolean;
envFile?: string;
deleteOldChanges?: boolean;
deleteOldChanges90?: boolean;
}
const argv: Argv = yargsArgv as any;
const nodeSqlite = require('sqlite3');
const cors = require('@koa/cors');
const nodeEnvFile = require('node-env-file');
const { shimInit } = require('@joplin/lib/shim-init-node.js');
shimInit({ nodeSqlite });
const env: Env = argv.env as Env || Env.Prod;
const defaultEnvVariables: Record<Env, EnvVariables> = {
dev: {
// To test with the Postgres database, uncomment DB_CLIENT below and
@ -99,6 +83,11 @@ async function getEnvFilePath(env: Env, argv: any): Promise<string> {
}
async function main() {
const { selectedCommand, argv: yargsArgv } = await setupCommands();
const argv: Argv = yargsArgv as any;
const env: Env = argv.env as Env || Env.Prod;
const envFilePath = await getEnvFilePath(env, argv);
if (envFilePath) nodeEnvFile(envFilePath);
@ -222,42 +211,26 @@ async function main() {
let runCommandAndExitApp = true;
if (argv.migrateLatest) {
const db = await connectDb(config().database);
await migrateLatest(db);
await disconnectDb(db);
} else if (argv.migrateUp) {
const db = await connectDb(config().database);
await migrateUp(db);
await disconnectDb(db);
} else if (argv.migrateDown) {
const db = await connectDb(config().database);
await migrateDown(db);
await disconnectDb(db);
} else if (argv.migrateList) {
const db = await connectDb(config().database);
console.info(await migrateList(db));
} else if (argv.dropDb) {
await dropDb(config().database, { ignoreIfNotExists: true });
} else if (argv.dropTables) {
const db = await connectDb(config().database);
await dropTables(db);
await disconnectDb(db);
} else if (argv.createDb) {
await createDb(config().database);
} else if (argv.deleteOldChanges || argv.deleteOldChanges90) {
// Eventually all commands should be started in a more generic way. All
// should go under /commands, and they will receive a context object
// with an intialized models property.
//
// Also should use yargs command system.
const connectionCheck = await waitForConnection(config().database);
const models = newModelFactory(connectionCheck.connection, config());
if (selectedCommand) {
const commandArgv = {
...argv,
_: (argv as any)._.slice(),
};
commandArgv._.splice(0, 1);
if (argv.deleteOldChanges90) {
await deleteOldChanges90({ models });
if (selectedCommand.commandName() === 'db') {
await selectedCommand.run(commandArgv, {
db: null,
models: null,
});
} else {
await deleteOldChanges({ models });
const connectionCheck = await waitForConnection(config().database);
const models = newModelFactory(connectionCheck.connection, config());
await selectedCommand.run(commandArgv, {
db: connectionCheck.connection,
models,
});
}
} else {
runCommandAndExitApp = false;

View File

@ -0,0 +1,36 @@
import { Options, PositionalOptions } from 'yargs';
import { DbConnection } from '../db';
import { Models } from '../models/factory';
export interface RunContext {
db: DbConnection;
models: Models;
}
export default abstract class BaseCommand {
public commandName(): string {
const splitted = this.command().split(' ');
if (!splitted.length) throw new Error(`Invalid command: ${this.command()}`);
return splitted[0];
}
public command(): string {
throw new Error('Not implemented');
}
public description(): string {
throw new Error('Not implemented');
}
public positionals(): Record<string, PositionalOptions> {
return {};
}
public options(): Record<string, Options> {
return {};
}
public abstract run(argv: any, context: RunContext): Promise<void>;
}

View File

@ -0,0 +1,57 @@
import { PositionalOptions } from 'yargs';
import config from '../config';
import { connectDb, disconnectDb, dropTables } from '../db';
import BaseCommand from './BaseCommand';
import { createDb } from '../tools/dbTools';
enum ArgvCommand {
DropTables = 'dropTables',
Create = 'create',
}
interface Argv {
command: ArgvCommand;
}
export default class DbCommand extends BaseCommand {
public command() {
return 'db <command>';
}
public description() {
return 'execute a database command';
}
public positionals(): Record<string, PositionalOptions> {
return {
'command': {
description: 'command to execute',
choices: [
ArgvCommand.Create,
ArgvCommand.DropTables,
],
},
};
}
public async run(argv: Argv): Promise<void> {
const commands: Record<ArgvCommand, Function> = {
create: async () => {
await createDb(config().database);
},
dropTables: async () => {
const db = await connectDb(config().database);
await dropTables(db);
await disconnectDb(db);
},
};
if (!commands[argv.command]) throw new Error(`Invalid command: ${argv.command}`);
await commands[argv.command]();
}
}

View File

@ -0,0 +1,32 @@
import { Options } from 'yargs';
import { Day } from '../utils/time';
import BaseCommand, { RunContext } from './BaseCommand';
interface Argv {
ttl: number;
}
export default class DeleteOldChangesCommand extends BaseCommand {
public command() {
return 'deleteOldChanges';
}
public description() {
return 'deletes old changes';
}
public options(): Record<string, Options> {
return {
'ttl': {
type: 'number',
description: 'TTL in days',
},
};
}
public async run(argv: Argv, runContext: RunContext): Promise<void> {
await runContext.models.change().deleteOldChanges(argv.ttl ? argv.ttl * Day : null);
}
}

View File

@ -0,0 +1,80 @@
import { PositionalOptions, Options } from 'yargs';
import Logger from '@joplin/lib/Logger';
import { disconnectDb, migrateDown, migrateLatest, migrateList, migrateUnlock, migrateUp } from '../db';
import BaseCommand, { RunContext } from './BaseCommand';
const logger = Logger.create('MigrateCommand');
enum ArgvCommand {
Up = 'up',
Down = 'down',
Latest = 'latest',
List = 'list',
Unlock = 'unlock',
}
interface Argv {
command: ArgvCommand;
}
export default class MigrateCommand extends BaseCommand {
public command() {
return 'migrate <command>';
}
public description() {
return 'execute a database migration';
}
public positionals(): Record<string, PositionalOptions> {
return {
'command': {
description: 'command to execute',
choices: [
ArgvCommand.Up,
ArgvCommand.Down,
ArgvCommand.Latest,
ArgvCommand.List,
ArgvCommand.Unlock,
],
},
};
}
public options(): Record<string, Options> {
return {
'disable-transactions': {
type: 'boolean',
},
};
}
public async run(argv: Argv, runContext: RunContext): Promise<void> {
const commands: Record<ArgvCommand, Function> = {
up: async () => {
await migrateUp(runContext.db);
},
down: async () => {
await migrateDown(runContext.db);
},
latest: async () => {
await migrateLatest(runContext.db);
},
list: async () => {
const s = (await migrateList(runContext.db)) as string;
s.split('\n').forEach(l => logger.info(l));
},
unlock: async () => {
await migrateUnlock(runContext.db);
},
};
if (!commands[argv.command]) throw new Error(`Invalid command: ${argv.command}`);
await commands[argv.command]();
await disconnectDb(runContext.db);
}
}

View File

@ -1,5 +0,0 @@
import { CommandContext } from '../utils/types';
export default async function(ctx: CommandContext) {
await ctx.models.change().deleteOldChanges();
}

View File

@ -1,6 +0,0 @@
import { Day } from '../utils/time';
import { CommandContext } from '../utils/types';
export default async function(ctx: CommandContext) {
await ctx.models.change().deleteOldChanges(90 * Day);
}

View File

@ -195,24 +195,31 @@ export async function disconnectDb(db: DbConnection) {
await db.destroy();
}
export async function migrateLatest(db: DbConnection) {
export async function migrateLatest(db: DbConnection, disableTransactions = false) {
await db.migrate.latest({
directory: migrationDir,
disableTransactions,
});
}
export async function migrateUp(db: DbConnection) {
export async function migrateUp(db: DbConnection, disableTransactions = false) {
await db.migrate.up({
directory: migrationDir,
disableTransactions,
});
}
export async function migrateDown(db: DbConnection) {
export async function migrateDown(db: DbConnection, disableTransactions = false) {
await db.migrate.down({
directory: migrationDir,
disableTransactions,
});
}
export async function migrateUnlock(db: DbConnection) {
await db.migrate.forceFreeMigrationsLock();
}
export async function migrateList(db: DbConnection, asString: boolean = true) {
const migrations: any = await db.migrate.list({
directory: migrationDir,

View File

@ -0,0 +1,77 @@
import yargs = require('yargs');
import BaseCommand from '../commands/BaseCommand';
import DbCommand from '../commands/DbCommand';
import DeleteOldChangesCommand from '../commands/DeleteOldChangesCommand';
import MigrateCommand from '../commands/MigrateCommand';
export interface Commands {
commands: BaseCommand[];
argv: typeof yargs.argv;
selectedCommand: BaseCommand;
yargs: typeof yargs;
}
export default async function setupCommands(): Promise<Commands> {
const commands: BaseCommand[] = [
new MigrateCommand(),
new DbCommand(),
new DeleteOldChangesCommand(),
];
for (const cmd of commands) {
yargs.command(cmd.command(), cmd.description(), (yargs) => {
const positionals = cmd.positionals ? cmd.positionals() : {};
for (const [name, options] of Object.entries(positionals)) {
yargs.positional(name, options ? options : {});
}
const commandOptions = cmd.options() ? cmd.options() : {};
for (const [name, options] of Object.entries(commandOptions)) {
yargs.options(name, options);
}
});
}
// yargs.option('env', {
// default: 'prod',
// type: 'string',
// choices: ['dev', 'prod'],
// hidden: true,
// });
// yargs.option('stack-trace', {
// default: '1',
// type: 'boolean',
// hidden: true,
// });
// yargs.option('db-config-filename', {
// type: 'string',
// hidden: true,
// });
yargs.help();
const argv = await yargs.argv;
const cmdName = argv._ && argv._.length ? argv._[0] : null;
let selectedCommand = null;
for (const cmd of commands) {
if (cmd.commandName() === cmdName) selectedCommand = cmd;
}
if (cmdName && !selectedCommand) {
yargs.showHelp();
console.info('');
throw new Error(`Invalid command: ${cmdName}`);
}
return {
commands,
argv,
selectedCommand,
yargs,
};
}