mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
refactor(cli): support next tag
This commit is contained in:
parent
a19a522894
commit
5eb822fee5
5 changed files with 124 additions and 65 deletions
|
@ -1,8 +1,9 @@
|
||||||
import { createMockPool } from 'slonik';
|
import { createMockPool } from 'slonik';
|
||||||
|
|
||||||
import * as queries from '../../queries/logto-config';
|
import * as functions from '.';
|
||||||
import { QueryType } from '../../test-utilities';
|
import * as queries from '../../../queries/logto-config';
|
||||||
import * as functions from './alteration';
|
import { QueryType } from '../../../test-utilities';
|
||||||
|
import { chooseAlterationsByVersion } from './version';
|
||||||
|
|
||||||
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
|
const mockQuery: jest.MockedFunction<QueryType> = jest.fn();
|
||||||
|
|
||||||
|
@ -55,27 +56,28 @@ describe('chooseAlterationsByVersion()', () => {
|
||||||
'1.0.0-1663923776-c.js',
|
'1.0.0-1663923776-c.js',
|
||||||
'1.0.1-1663923777-c.js',
|
'1.0.1-1663923777-c.js',
|
||||||
'1.2.0-1663923778-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 }))
|
].map((filename) => ({ filename, path: '/alterations/' + filename }))
|
||||||
);
|
);
|
||||||
|
|
||||||
it('exits with code 1 when no alteration file available', async () => {
|
it('exits with code 1 when no alteration file available', async () => {
|
||||||
jest.spyOn(process, 'exit').mockImplementation(mockExit);
|
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();
|
mockExit.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('chooses correct alteration files', async () => {
|
it('chooses correct alteration files', async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
expect(functions.chooseAlterationsByVersion(files, 'v1.0.0')).resolves.toEqual(
|
expect(chooseAlterationsByVersion(files, 'v1.0.0')).resolves.toEqual(files.slice(0, 7)),
|
||||||
files.slice(0, 7)
|
expect(chooseAlterationsByVersion(files, 'v1.0.0-beta.10')).resolves.toEqual(
|
||||||
),
|
|
||||||
expect(functions.chooseAlterationsByVersion(files, 'v1.0.0-beta.10')).resolves.toEqual(
|
|
||||||
files.slice(0, 3)
|
files.slice(0, 3)
|
||||||
),
|
),
|
||||||
expect(functions.chooseAlterationsByVersion(files, 'v1.1.0')).resolves.toEqual(
|
expect(chooseAlterationsByVersion(files, 'v1.1.0')).resolves.toEqual(files.slice(0, 8)),
|
||||||
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)),
|
||||||
expect(functions.chooseAlterationsByVersion(files, 'v1.2.0')).resolves.toEqual(files),
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -2,20 +2,21 @@ import path from 'path';
|
||||||
|
|
||||||
import { AlterationScript } from '@logto/schemas/lib/types/alteration';
|
import { AlterationScript } from '@logto/schemas/lib/types/alteration';
|
||||||
import { findPackage } from '@logto/shared';
|
import { findPackage } from '@logto/shared';
|
||||||
import { conditional, conditionalString } from '@silverhand/essentials';
|
import { conditionalString } from '@silverhand/essentials';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { copy, existsSync, remove, readdir } from 'fs-extra';
|
import { copy, existsSync, remove, readdir } from 'fs-extra';
|
||||||
import inquirer from 'inquirer';
|
import { SemVer } from 'semver';
|
||||||
import { SemVer, compare, eq, gt } from 'semver';
|
|
||||||
import { DatabasePool } from 'slonik';
|
import { DatabasePool } from 'slonik';
|
||||||
import { CommandModule } from 'yargs';
|
import { CommandModule } from 'yargs';
|
||||||
|
|
||||||
import { createPoolFromConfig } from '../../database';
|
import { createPoolFromConfig } from '../../../database';
|
||||||
import {
|
import {
|
||||||
getCurrentDatabaseAlterationTimestamp,
|
getCurrentDatabaseAlterationTimestamp,
|
||||||
updateDatabaseTimestamp,
|
updateDatabaseTimestamp,
|
||||||
} from '../../queries/logto-config';
|
} from '../../../queries/logto-config';
|
||||||
import { getPathInModule, log } from '../../utilities';
|
import { getPathInModule, log } from '../../../utilities';
|
||||||
|
import { AlterationFile } from './type';
|
||||||
|
import { chooseAlterationsByVersion } from './version';
|
||||||
|
|
||||||
const alterationFilenameRegex = /-(\d+)-?.*\.js$/;
|
const alterationFilenameRegex = /-(\d+)-?.*\.js$/;
|
||||||
|
|
||||||
|
@ -43,8 +44,6 @@ const importAlterationScript = async (filePath: string): Promise<AlterationScrip
|
||||||
return module.default as AlterationScript;
|
return module.default as AlterationScript;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AlterationFile = { path: string; filename: string };
|
|
||||||
|
|
||||||
export const getAlterationFiles = async (): Promise<AlterationFile[]> => {
|
export const getAlterationFiles = async (): Promise<AlterationFile[]> => {
|
||||||
const alterationDirectory = getPathInModule('@logto/schemas', 'alterations');
|
const alterationDirectory = getPathInModule('@logto/schemas', 'alterations');
|
||||||
|
|
||||||
|
@ -126,51 +125,6 @@ const deployAlteration = async (
|
||||||
log.info(`Run alteration ${filename} succeeded`);
|
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<unknown, { action: string; target?: string }> = {
|
const alteration: CommandModule<unknown, { action: string; target?: string }> = {
|
||||||
command: ['alteration <action> [target]', 'alt', 'alter'],
|
command: ['alteration <action> [target]', 'alt', 'alter'],
|
||||||
describe: 'Perform database alteration',
|
describe: 'Perform database alteration',
|
1
packages/cli/src/commands/database/alteration/type.ts
Normal file
1
packages/cli/src/commands/database/alteration/type.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export type AlterationFile = { path: string; filename: string };
|
75
packages/cli/src/commands/database/alteration/version.ts
Normal file
75
packages/cli/src/commands/database/alteration/version.ts
Normal file
|
@ -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);
|
||||||
|
});
|
||||||
|
};
|
|
@ -152,3 +152,30 @@ export const getCliConfigWithPrompt = async ({
|
||||||
|
|
||||||
return input;
|
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<T>(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue