From 926e69fd69b3692cbdc491316a97d53ddceb37d8 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Wed, 12 Oct 2022 19:07:35 +0800 Subject: [PATCH] refactor(cli): inquire for version before alteration --- .../cli/src/commands/database/alteration.ts | 98 +++++++++++++++---- packages/cli/src/commands/install/index.ts | 25 +++-- packages/cli/src/index.ts | 6 +- 3 files changed, 98 insertions(+), 31 deletions(-) diff --git a/packages/cli/src/commands/database/alteration.ts b/packages/cli/src/commands/database/alteration.ts index 437822e9f..26edc27b3 100644 --- a/packages/cli/src/commands/database/alteration.ts +++ b/packages/cli/src/commands/database/alteration.ts @@ -2,9 +2,11 @@ import path from 'path'; import { AlterationScript } from '@logto/schemas/lib/types/alteration'; import { findPackage } from '@logto/shared'; -import { conditionalString } from '@silverhand/essentials'; +import { conditional, conditionalString } from '@silverhand/essentials'; import chalk from 'chalk'; import { copy, existsSync, remove, readdir } from 'fs-extra'; +import inquirer from 'inquirer'; +import { SemVer, compare, eq, gt } from 'semver'; import { DatabasePool } from 'slonik'; import { CommandModule } from 'yargs'; @@ -15,18 +17,24 @@ import { } from '../../queries/logto-config'; import { getPathInModule, log } from '../../utilities'; -const alterationFileNameRegex = /-(\d+)-?.*\.js$/; +const alterationFilenameRegex = /-(\d+)-?.*\.js$/; -const getTimestampFromFileName = (fileName: string) => { - const match = alterationFileNameRegex.exec(fileName); +const getTimestampFromFilename = (filename: string) => { + const match = alterationFilenameRegex.exec(filename); if (!match?.[1]) { - throw new Error(`Can not get timestamp: ${fileName}`); + throw new Error(`Can not get timestamp: ${filename}`); } return Number(match[1]); }; +const getVersionFromFilename = (filename: string) => { + try { + return new SemVer(filename.split('-')[0]?.replaceAll('_', '-') ?? 'unknown'); + } catch {} +}; + const importAlterationScript = async (filePath: string): Promise => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const module = await import(filePath); @@ -67,11 +75,11 @@ export const getAlterationFiles = async (): Promise => { await copy(alterationDirectory, localAlterationDirectory); const directory = await readdir(localAlterationDirectory); - const files = directory.filter((file) => alterationFileNameRegex.test(file)); + const files = directory.filter((file) => alterationFilenameRegex.test(file)); return files .slice() - .sort((file1, file2) => getTimestampFromFileName(file1) - getTimestampFromFileName(file2)) + .sort((file1, file2) => getTimestampFromFilename(file1) - getTimestampFromFilename(file2)) .map((filename) => ({ path: path.join(localAlterationDirectory, filename), filename })); }; @@ -83,14 +91,14 @@ export const getLatestAlterationTimestamp = async () => { return 0; } - return getTimestampFromFileName(lastFile.filename); + return getTimestampFromFilename(lastFile.filename); }; export const getUndeployedAlterations = async (pool: DatabasePool) => { const databaseTimestamp = await getCurrentDatabaseAlterationTimestamp(pool); const files = await getAlterationFiles(); - return files.filter(({ filename }) => getTimestampFromFileName(filename) > databaseTimestamp); + return files.filter(({ filename }) => getTimestampFromFilename(filename) > 0); }; const deployAlteration = async ( @@ -102,7 +110,7 @@ const deployAlteration = async ( try { await pool.transaction(async (connection) => { await up(connection); - await updateDatabaseTimestamp(connection, getTimestampFromFileName(filename)); + await updateDatabaseTimestamp(connection, getTimestampFromFilename(filename)); }); } catch (error: unknown) { console.error(error); @@ -118,22 +126,74 @@ const deployAlteration = async ( log.info(`Run alteration ${filename} succeeded`); }; -const alteration: CommandModule = { - command: ['alteration ', 'alt', 'alter'], +const latestTag = 'latest'; + +// TODO: add tests +export const chooseAlterationsByVersion = async ( + alterations: AlterationFile[], + initialVersion?: string +) => { + const versions = alterations + .map(({ filename }) => getVersionFromFilename(filename)) + .filter((version): version is SemVer => version instanceof SemVer) + // Cannot use `Set` to deduplicate since it's a class + .filter((version, index, self) => index === self.findIndex((another) => eq(version, another))) + .slice() + .sort((i, j) => compare(j, i)); + + if (!versions[0]) { + log.error('No deployable alteration found'); + } + + const { version: targetVersion } = + initialVersion === latestTag + ? { version: versions[0] } + : await inquirer.prompt<{ version: SemVer }>( + { + type: 'list', + message: 'Choose the alteration target version', + name: 'version', + choices: versions.map((semVersion, index) => ({ + name: semVersion.version + conditionalString(!index && ` (${latestTag})`), + value: semVersion, + })), + }, + { + version: conditional(initialVersion && new SemVer(initialVersion)), + } + ); + + return alterations.filter(({ filename }) => { + const version = getVersionFromFilename(filename); + + return version && !gt(version, targetVersion); + }); +}; + +const alteration: CommandModule = { + command: ['alteration [target]', 'alt', 'alter'], describe: 'Perform database alteration', builder: (yargs) => - yargs.positional('action', { - describe: 'The action to perform, now it only accepts `deploy`', - type: 'string', - demandOption: true, - }), - handler: async ({ action }) => { + yargs + .positional('action', { + describe: 'The action to perform, now it only accepts `deploy`', + type: 'string', + demandOption: true, + }) + .positional('target', { + describe: 'The target Logto version for alteration', + type: 'string', + }), + handler: async ({ action, target }) => { if (action !== 'deploy') { log.error('Unsupported action'); } const pool = await createPoolFromConfig(); - const alterations = await getUndeployedAlterations(pool); + const alterations = await chooseAlterationsByVersion( + await getUndeployedAlterations(pool), + target + ); log.info( `Found ${alterations.length} alteration${conditionalString( diff --git a/packages/cli/src/commands/install/index.ts b/packages/cli/src/commands/install/index.ts index 42aec162f..1844309dd 100644 --- a/packages/cli/src/commands/install/index.ts +++ b/packages/cli/src/commands/install/index.ts @@ -63,30 +63,37 @@ const installLogto = async ({ path, skipSeed, officialConnectors }: InstallArgs) logFinale(instancePath); }; -const install: CommandModule = { +const install: CommandModule< + unknown, + { + p?: string; + ss: boolean; + oc?: boolean; + } +> = { command: ['init', 'i', 'install'], describe: 'Download and run the latest Logto release', builder: (yargs) => yargs.options({ - path: { - alias: 'p', + p: { + alias: 'path', describe: 'Path of Logto, must be a non-existing path', type: 'string', }, - skipSeed: { - alias: 'ss', + ss: { + alias: 'skip-seed', describe: 'Skip Logto database seeding', type: 'boolean', default: false, }, - officialConnectors: { - alias: 'oc', + oc: { + alias: 'official-connectors', describe: 'Add official connectors after downloading Logto', type: 'boolean', }, }), - handler: async ({ path, skipSeed, officialConnectors }) => { - await installLogto({ path, skipSeed, officialConnectors }); + handler: async ({ p, ss, oc }) => { + await installLogto({ path: p, skipSeed: ss, officialConnectors: oc }); }, }; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index ff8ce851c..e1ad79612 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -14,12 +14,12 @@ void yargs(hideBin(process.argv)) describe: 'The path to your `.env` file', type: 'string', }) - .option('databaseUrl', { - alias: ['db-url'], + .option('db', { + alias: ['db-url', 'database-url'], describe: 'The Postgres URL to Logto database', type: 'string', }) - .middleware(({ env, databaseUrl }) => { + .middleware(({ env, db: databaseUrl }) => { dotenv.config({ path: env }); const initialDatabaseUrl = databaseUrl ?? process.env[ConfigKey.DatabaseUrl];