From 5eb822fee5698af43b3bc9e2ad3dd3570e2c14b1 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Wed, 12 Oct 2022 23:07:57 +0800 Subject: [PATCH] refactor(cli): support next tag --- .../index.test.ts} | 26 ++++--- .../{alteration.ts => alteration/index.ts} | 60 ++------------- .../src/commands/database/alteration/type.ts | 1 + .../commands/database/alteration/version.ts | 75 +++++++++++++++++++ packages/cli/src/utilities.ts | 27 +++++++ 5 files changed, 124 insertions(+), 65 deletions(-) rename packages/cli/src/commands/database/{alteration.test.ts => alteration/index.test.ts} (71%) rename packages/cli/src/commands/database/{alteration.ts => alteration/index.ts} (74%) create mode 100644 packages/cli/src/commands/database/alteration/type.ts create mode 100644 packages/cli/src/commands/database/alteration/version.ts diff --git a/packages/cli/src/commands/database/alteration.test.ts b/packages/cli/src/commands/database/alteration/index.test.ts similarity index 71% rename from packages/cli/src/commands/database/alteration.test.ts rename to packages/cli/src/commands/database/alteration/index.test.ts index 0428700fa..c5f40dc83 100644 --- a/packages/cli/src/commands/database/alteration.test.ts +++ b/packages/cli/src/commands/database/alteration/index.test.ts @@ -1,8 +1,9 @@ import { createMockPool } from 'slonik'; -import * as queries from '../../queries/logto-config'; -import { QueryType } from '../../test-utilities'; -import * as functions from './alteration'; +import * as functions from '.'; +import * as queries from '../../../queries/logto-config'; +import { QueryType } from '../../../test-utilities'; +import { chooseAlterationsByVersion } from './version'; const mockQuery: jest.MockedFunction = jest.fn(); @@ -55,27 +56,28 @@ describe('chooseAlterationsByVersion()', () => { '1.0.0-1663923776-c.js', '1.0.1-1663923777-c.js', '1.2.0-1663923778-c.js', + 'next-1663923778-c.js', + 'next-1663923779-c.js', + 'next-1663923780-c.js', + 'next1-1663923781-c.js', ].map((filename) => ({ filename, path: '/alterations/' + filename })) ); it('exits with code 1 when no alteration file available', async () => { jest.spyOn(process, 'exit').mockImplementation(mockExit); - await expect(functions.chooseAlterationsByVersion([], 'v1.0.0')).rejects.toThrow('1'); + await expect(chooseAlterationsByVersion([], 'v1.0.0')).rejects.toThrow('1'); mockExit.mockRestore(); }); it('chooses correct alteration files', async () => { await Promise.all([ - expect(functions.chooseAlterationsByVersion(files, 'v1.0.0')).resolves.toEqual( - files.slice(0, 7) - ), - expect(functions.chooseAlterationsByVersion(files, 'v1.0.0-beta.10')).resolves.toEqual( + expect(chooseAlterationsByVersion(files, 'v1.0.0')).resolves.toEqual(files.slice(0, 7)), + expect(chooseAlterationsByVersion(files, 'v1.0.0-beta.10')).resolves.toEqual( files.slice(0, 3) ), - expect(functions.chooseAlterationsByVersion(files, 'v1.1.0')).resolves.toEqual( - files.slice(0, 8) - ), - expect(functions.chooseAlterationsByVersion(files, 'v1.2.0')).resolves.toEqual(files), + expect(chooseAlterationsByVersion(files, 'v1.1.0')).resolves.toEqual(files.slice(0, 8)), + expect(chooseAlterationsByVersion(files, 'v1.2.0')).resolves.toEqual(files.slice(0, 9)), + expect(chooseAlterationsByVersion(files, 'next')).resolves.toEqual(files.slice(0, 11)), ]); }); }); diff --git a/packages/cli/src/commands/database/alteration.ts b/packages/cli/src/commands/database/alteration/index.ts similarity index 74% rename from packages/cli/src/commands/database/alteration.ts rename to packages/cli/src/commands/database/alteration/index.ts index 5602131f7..9052532b9 100644 --- a/packages/cli/src/commands/database/alteration.ts +++ b/packages/cli/src/commands/database/alteration/index.ts @@ -2,20 +2,21 @@ import path from 'path'; import { AlterationScript } from '@logto/schemas/lib/types/alteration'; import { findPackage } from '@logto/shared'; -import { conditional, conditionalString } from '@silverhand/essentials'; +import { 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 { SemVer } from 'semver'; import { DatabasePool } from 'slonik'; import { CommandModule } from 'yargs'; -import { createPoolFromConfig } from '../../database'; +import { createPoolFromConfig } from '../../../database'; import { getCurrentDatabaseAlterationTimestamp, updateDatabaseTimestamp, -} from '../../queries/logto-config'; -import { getPathInModule, log } from '../../utilities'; +} from '../../../queries/logto-config'; +import { getPathInModule, log } from '../../../utilities'; +import { AlterationFile } from './type'; +import { chooseAlterationsByVersion } from './version'; const alterationFilenameRegex = /-(\d+)-?.*\.js$/; @@ -43,8 +44,6 @@ const importAlterationScript = async (filePath: string): Promise => { const alterationDirectory = getPathInModule('@logto/schemas', 'alterations'); @@ -126,51 +125,6 @@ const deployAlteration = async ( log.info(`Run alteration ${filename} succeeded`); }; -const latestTag = 'latest'; - -export const chooseAlterationsByVersion = async ( - alterations: readonly 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 alteration script to deploy'); - } - - 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) => ({ - name: semVersion.version, - value: semVersion, - })), - }, - { - version: conditional(initialVersion && new SemVer(initialVersion)), - } - ); - - log.info(`Deploy target ${chalk.green(targetVersion.version)}`); - - 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', diff --git a/packages/cli/src/commands/database/alteration/type.ts b/packages/cli/src/commands/database/alteration/type.ts new file mode 100644 index 000000000..dc94e658e --- /dev/null +++ b/packages/cli/src/commands/database/alteration/type.ts @@ -0,0 +1 @@ +export type AlterationFile = { path: string; filename: string }; diff --git a/packages/cli/src/commands/database/alteration/version.ts b/packages/cli/src/commands/database/alteration/version.ts new file mode 100644 index 000000000..355474a16 --- /dev/null +++ b/packages/cli/src/commands/database/alteration/version.ts @@ -0,0 +1,75 @@ +import { conditional } from '@silverhand/essentials'; +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import { SemVer, compare, eq, gt } from 'semver'; + +import { findLastIndex, log } from '../../../utilities'; +import { AlterationFile } from './type'; + +const getVersionFromFilename = (filename: string) => { + try { + return new SemVer(filename.split('-')[0]?.replaceAll('_', '-') ?? 'unknown'); + } catch {} +}; + +const latestTag = 'latest'; +const nextTag = 'next'; + +export const chooseAlterationsByVersion = async ( + alterations: readonly AlterationFile[], + initialVersion?: string +) => { + if (initialVersion === nextTag) { + const endIndex = findLastIndex( + alterations, + ({ filename }) => + filename.startsWith(nextTag + '-') || Boolean(getVersionFromFilename(filename)) + ); + + if (endIndex === -1) { + log.error('No alteration script to deploy'); + } + + log.info(`Deploy target ${chalk.green(nextTag)}`); + + return alterations.slice(0, endIndex); + } + + 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 alteration script to deploy'); + } + + 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) => ({ + name: semVersion.version, + value: semVersion, + })), + }, + { + version: conditional(initialVersion && new SemVer(initialVersion)), + } + ); + + log.info(`Deploy target ${chalk.green(targetVersion.version)}`); + + return alterations.filter(({ filename }) => { + const version = getVersionFromFilename(filename); + + return version && !gt(version, targetVersion); + }); +}; diff --git a/packages/cli/src/utilities.ts b/packages/cli/src/utilities.ts index c2d7d42f9..62b14efb7 100644 --- a/packages/cli/src/utilities.ts +++ b/packages/cli/src/utilities.ts @@ -152,3 +152,30 @@ export const getCliConfigWithPrompt = async ({ return input; }; + +// https://stackoverflow.com/a/53187807/12514940 +/** + * Returns the index of the last element in the array where predicate is true, and -1 + * otherwise. + * @param array The source array to search in + * @param predicate find calls predicate once for each element of the array, in descending + * order, until it finds one where predicate returns true. If such an element is found, + * findLastIndex immediately returns that element index. Otherwise, findLastIndex returns -1. + */ +export function findLastIndex( + array: readonly T[], + predicate: (value: T, index: number, object: readonly T[]) => boolean +): number { + // eslint-disable-next-line @silverhand/fp/no-let + let { length } = array; + + // eslint-disable-next-line @silverhand/fp/no-mutation + while (length--) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (predicate(array[length]!, length, array)) { + return length; + } + } + + return -1; +}