mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
refactor(cli): inquire for version before alteration
This commit is contained in:
parent
926394030d
commit
926e69fd69
3 changed files with 98 additions and 31 deletions
|
@ -2,9 +2,11 @@ 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 { conditionalString } from '@silverhand/essentials';
|
import { conditional, 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, compare, eq, gt } from 'semver';
|
||||||
import { DatabasePool } from 'slonik';
|
import { DatabasePool } from 'slonik';
|
||||||
import { CommandModule } from 'yargs';
|
import { CommandModule } from 'yargs';
|
||||||
|
|
||||||
|
@ -15,18 +17,24 @@ import {
|
||||||
} from '../../queries/logto-config';
|
} from '../../queries/logto-config';
|
||||||
import { getPathInModule, log } from '../../utilities';
|
import { getPathInModule, log } from '../../utilities';
|
||||||
|
|
||||||
const alterationFileNameRegex = /-(\d+)-?.*\.js$/;
|
const alterationFilenameRegex = /-(\d+)-?.*\.js$/;
|
||||||
|
|
||||||
const getTimestampFromFileName = (fileName: string) => {
|
const getTimestampFromFilename = (filename: string) => {
|
||||||
const match = alterationFileNameRegex.exec(fileName);
|
const match = alterationFilenameRegex.exec(filename);
|
||||||
|
|
||||||
if (!match?.[1]) {
|
if (!match?.[1]) {
|
||||||
throw new Error(`Can not get timestamp: ${fileName}`);
|
throw new Error(`Can not get timestamp: ${filename}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Number(match[1]);
|
return Number(match[1]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getVersionFromFilename = (filename: string) => {
|
||||||
|
try {
|
||||||
|
return new SemVer(filename.split('-')[0]?.replaceAll('_', '-') ?? 'unknown');
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
const importAlterationScript = async (filePath: string): Promise<AlterationScript> => {
|
const importAlterationScript = async (filePath: string): Promise<AlterationScript> => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
const module = await import(filePath);
|
const module = await import(filePath);
|
||||||
|
@ -67,11 +75,11 @@ export const getAlterationFiles = async (): Promise<AlterationFile[]> => {
|
||||||
await copy(alterationDirectory, localAlterationDirectory);
|
await copy(alterationDirectory, localAlterationDirectory);
|
||||||
|
|
||||||
const directory = await readdir(localAlterationDirectory);
|
const directory = await readdir(localAlterationDirectory);
|
||||||
const files = directory.filter((file) => alterationFileNameRegex.test(file));
|
const files = directory.filter((file) => alterationFilenameRegex.test(file));
|
||||||
|
|
||||||
return files
|
return files
|
||||||
.slice()
|
.slice()
|
||||||
.sort((file1, file2) => getTimestampFromFileName(file1) - getTimestampFromFileName(file2))
|
.sort((file1, file2) => getTimestampFromFilename(file1) - getTimestampFromFilename(file2))
|
||||||
.map((filename) => ({ path: path.join(localAlterationDirectory, filename), filename }));
|
.map((filename) => ({ path: path.join(localAlterationDirectory, filename), filename }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -83,14 +91,14 @@ export const getLatestAlterationTimestamp = async () => {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getTimestampFromFileName(lastFile.filename);
|
return getTimestampFromFilename(lastFile.filename);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUndeployedAlterations = async (pool: DatabasePool) => {
|
export const getUndeployedAlterations = async (pool: DatabasePool) => {
|
||||||
const databaseTimestamp = await getCurrentDatabaseAlterationTimestamp(pool);
|
const databaseTimestamp = await getCurrentDatabaseAlterationTimestamp(pool);
|
||||||
const files = await getAlterationFiles();
|
const files = await getAlterationFiles();
|
||||||
|
|
||||||
return files.filter(({ filename }) => getTimestampFromFileName(filename) > databaseTimestamp);
|
return files.filter(({ filename }) => getTimestampFromFilename(filename) > 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deployAlteration = async (
|
const deployAlteration = async (
|
||||||
|
@ -102,7 +110,7 @@ const deployAlteration = async (
|
||||||
try {
|
try {
|
||||||
await pool.transaction(async (connection) => {
|
await pool.transaction(async (connection) => {
|
||||||
await up(connection);
|
await up(connection);
|
||||||
await updateDatabaseTimestamp(connection, getTimestampFromFileName(filename));
|
await updateDatabaseTimestamp(connection, getTimestampFromFilename(filename));
|
||||||
});
|
});
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -118,22 +126,74 @@ const deployAlteration = async (
|
||||||
log.info(`Run alteration ${filename} succeeded`);
|
log.info(`Run alteration ${filename} succeeded`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const alteration: CommandModule<unknown, { action: string }> = {
|
const latestTag = 'latest';
|
||||||
command: ['alteration <action>', 'alt', 'alter'],
|
|
||||||
|
// 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<unknown, { action: string; target?: string }> = {
|
||||||
|
command: ['alteration <action> [target]', 'alt', 'alter'],
|
||||||
describe: 'Perform database alteration',
|
describe: 'Perform database alteration',
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
yargs.positional('action', {
|
yargs
|
||||||
describe: 'The action to perform, now it only accepts `deploy`',
|
.positional('action', {
|
||||||
type: 'string',
|
describe: 'The action to perform, now it only accepts `deploy`',
|
||||||
demandOption: true,
|
type: 'string',
|
||||||
}),
|
demandOption: true,
|
||||||
handler: async ({ action }) => {
|
})
|
||||||
|
.positional('target', {
|
||||||
|
describe: 'The target Logto version for alteration',
|
||||||
|
type: 'string',
|
||||||
|
}),
|
||||||
|
handler: async ({ action, target }) => {
|
||||||
if (action !== 'deploy') {
|
if (action !== 'deploy') {
|
||||||
log.error('Unsupported action');
|
log.error('Unsupported action');
|
||||||
}
|
}
|
||||||
|
|
||||||
const pool = await createPoolFromConfig();
|
const pool = await createPoolFromConfig();
|
||||||
const alterations = await getUndeployedAlterations(pool);
|
const alterations = await chooseAlterationsByVersion(
|
||||||
|
await getUndeployedAlterations(pool),
|
||||||
|
target
|
||||||
|
);
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
`Found ${alterations.length} alteration${conditionalString(
|
`Found ${alterations.length} alteration${conditionalString(
|
||||||
|
|
|
@ -63,30 +63,37 @@ const installLogto = async ({ path, skipSeed, officialConnectors }: InstallArgs)
|
||||||
logFinale(instancePath);
|
logFinale(instancePath);
|
||||||
};
|
};
|
||||||
|
|
||||||
const install: CommandModule<unknown, InstallArgs> = {
|
const install: CommandModule<
|
||||||
|
unknown,
|
||||||
|
{
|
||||||
|
p?: string;
|
||||||
|
ss: boolean;
|
||||||
|
oc?: boolean;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
command: ['init', 'i', 'install'],
|
command: ['init', 'i', 'install'],
|
||||||
describe: 'Download and run the latest Logto release',
|
describe: 'Download and run the latest Logto release',
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
yargs.options({
|
yargs.options({
|
||||||
path: {
|
p: {
|
||||||
alias: 'p',
|
alias: 'path',
|
||||||
describe: 'Path of Logto, must be a non-existing path',
|
describe: 'Path of Logto, must be a non-existing path',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
skipSeed: {
|
ss: {
|
||||||
alias: 'ss',
|
alias: 'skip-seed',
|
||||||
describe: 'Skip Logto database seeding',
|
describe: 'Skip Logto database seeding',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
officialConnectors: {
|
oc: {
|
||||||
alias: 'oc',
|
alias: 'official-connectors',
|
||||||
describe: 'Add official connectors after downloading Logto',
|
describe: 'Add official connectors after downloading Logto',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
handler: async ({ path, skipSeed, officialConnectors }) => {
|
handler: async ({ p, ss, oc }) => {
|
||||||
await installLogto({ path, skipSeed, officialConnectors });
|
await installLogto({ path: p, skipSeed: ss, officialConnectors: oc });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,12 +14,12 @@ void yargs(hideBin(process.argv))
|
||||||
describe: 'The path to your `.env` file',
|
describe: 'The path to your `.env` file',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
})
|
})
|
||||||
.option('databaseUrl', {
|
.option('db', {
|
||||||
alias: ['db-url'],
|
alias: ['db-url', 'database-url'],
|
||||||
describe: 'The Postgres URL to Logto database',
|
describe: 'The Postgres URL to Logto database',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
})
|
})
|
||||||
.middleware(({ env, databaseUrl }) => {
|
.middleware(({ env, db: databaseUrl }) => {
|
||||||
dotenv.config({ path: env });
|
dotenv.config({ path: env });
|
||||||
|
|
||||||
const initialDatabaseUrl = databaseUrl ?? process.env[ConfigKey.DatabaseUrl];
|
const initialDatabaseUrl = databaseUrl ?? process.env[ConfigKey.DatabaseUrl];
|
||||||
|
|
Loading…
Add table
Reference in a new issue