Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run a range of migrations #32

Merged
merged 2 commits into from May 12, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -112,7 +112,7 @@ The supported commands are divided into groups according to their target, at thi
export default migration;
```

* `migration run` - Runs a migration script specified by file name (option `--name <file name>`), or runs multiple migration scripts in the order specified in the migration files (option `--all`).
* `migration run` - Runs a migration script specified by file name (option `--name <file name>`), or runs multiple migration scripts in the order specified in the migration files (options `--all` or `--range`).
Simply007 marked this conversation as resolved.
Show resolved Hide resolved
* You can execute a migration against a specific project (options `--project <YOUR_PROJECT_ID> --api-key <YOUR_MANAGEMENT_API_KEY>`) or environment stored in the local configuration file (option `--environment <YOUR_ENVIRONMENT_NAME>`).
* After each run of a migration script, the CLI logs the execution into a status file. This file holds data for the next run to prevent running the same migration script more than once. You can choose to override this behavior, for example for debugging purposes, by using the `--force` parameter.
* You can choose whether you want to keep executing the migration scripts even if one migration script fails (option `--continue-on-error`) or whether you want to run in the debug mode (option `--debug`) and get additional information for certain issues logged into the console.
Expand Down
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "@kentico/kontent-cli",
"version": "0.3.0",
"version": "0.5.0",
Simply007 marked this conversation as resolved.
Show resolved Hide resolved
"description": "Command line interface tool that can be used for generating and running Kontent migration scripts",
"main": "./lib/index.js",
"types": "./lib/types/index.d.ts",
Expand Down
59 changes: 55 additions & 4 deletions src/cmds/migration/run.ts
@@ -1,6 +1,6 @@
import yargs from 'yargs';
import chalk from 'chalk';
import { getDuplicates, getSuccessfullyExecutedMigrations, getMigrationFilepath, loadMigrationFiles, loadModule, runMigration } from '../../utils/migrationUtils';
import { getDuplicates, getSuccessfullyExecutedMigrations, getMigrationFilepath, loadMigrationFiles, loadModule, runMigration, getMigrationsWithInvalidOrder } from '../../utils/migrationUtils';
import { fileExists, getFileWithExtension, isAllowedExtension } from '../../utils/fileUtils';
import { environmentConfigExists, getEnvironmentsConfig } from '../../utils/environmentUtils';
import { createManagementClient } from '../../managementClientFactory';
Expand Down Expand Up @@ -38,6 +38,11 @@ const runMigrationCommand: yargs.CommandModule = {
describe: 'Run all migration scripts in the specified order',
type: 'boolean',
},
range: {
alias: 'r',
describe: 'Run all migration scripts in the specified range, eg.: 3:5 will run migrations with the "order" property set to 3, 4 and 5',
type: 'string',
},
force: {
alias: 'f',
describe: 'Enforces run of already executed scripts.',
Expand All @@ -58,6 +63,8 @@ const runMigrationCommand: yargs.CommandModule = {
},
})
.conflicts('all', 'name')
.conflicts('range', 'name')
.conflicts('all', 'range')
.conflicts('environment', 'api-key')
.conflicts('environment', 'project-id')
.check((args: any) => {
Expand All @@ -66,7 +73,11 @@ const runMigrationCommand: yargs.CommandModule = {
}

if (!args.all) {
if (args.name) {
if (args.range) {
if (!getRange(args.range)) {
throw new Error(chalk.red(`The range has to be a string of a format "number:number" where the first number is less or equal to the second, eg.: "2:5".`));
}
} else if (args.name) {
if (!isAllowedExtension(args.name)) {
throw new Error(chalk.red(`File ${args.name} has not supported extension.`));
}
Expand All @@ -76,7 +87,7 @@ const runMigrationCommand: yargs.CommandModule = {
throw new Error(chalk.red(`Cannot find the specified migration script: ${migrationFilePath}.`));
}
} else {
throw new Error(chalk.red('Either the migration script name or all migration options needs to be specified.'));
throw new Error(chalk.red('Either the migration script name, range or all migration options needs to be specified.'));
}
}

Expand All @@ -99,6 +110,7 @@ const runMigrationCommand: yargs.CommandModule = {
let apiKey = argv.apiKey;
const migrationName = argv.name;
const runAll = argv.all;
const runRange = getRange(argv.range);
const debugMode = argv.debug;
const continueOnError = argv.continueOnError;
let migrationsResults: number = 0;
Expand All @@ -119,10 +131,15 @@ const runMigrationCommand: yargs.CommandModule = {

loadMigrationsExecutionStatus();

if (runAll) {
if (runAll || runRange) {
let migrationsToRun = await loadMigrationFiles();

checkForDuplicates(migrationsToRun);
checkForInvalidOrder(migrationsToRun);

if (runRange) {
migrationsToRun = getMigrationsByRange(migrationsToRun, runRange);
}

if (runForce) {
console.log('Skipping to check already executed migrations');
Expand Down Expand Up @@ -165,6 +182,17 @@ const runMigrationCommand: yargs.CommandModule = {
},
};

export const getRange = (range: string): [number, number] | null => {
const match = range.match(/^([0-9]+):([0-9]+)$/);
if (!match) {
return null;
}
const from = Number(match[1]);
const to = Number(match[2]);

return from <= to ? [from, to] : null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might return

{
  from,
  to
}

object

Just for readability.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've introduced new IRange interface, thanks for the tip

};

const checkForDuplicates = (migrationsToRun: IMigration[]): void => {
const duplicateMigrationsOrder = getDuplicates(migrationsToRun, (migration) => migration.module.order);

Expand All @@ -176,6 +204,29 @@ const checkForDuplicates = (migrationsToRun: IMigration[]): void => {
}
};

const getMigrationsByRange = (migrationsToRun: IMigration[], range: [number, number]): IMigration[] => {
const migrations: IMigration[] = [];

for (const migration of migrationsToRun) {
if (migration.module.order >= range[0] && migration.module.order <= range[1]) {
migrations.push(migration);
}
}

return migrations.filter(String);
};

const checkForInvalidOrder = (migrationsToRun: IMigration[]): void => {
const migrationsWithInvalidOrder: IMigration[] = getMigrationsWithInvalidOrder(migrationsToRun);

if (migrationsWithInvalidOrder.length > 0) {
console.log('Migration order has to be positive integer or zero:');
migrationsWithInvalidOrder.map((migration) => console.error(chalk.red(`Migration: ${migration.name} order: ${migration.module.order}`)));

process.exit(1);
}
};

const skipExecutedMigrations = (migrations: IMigration[], projectId: string): IMigration[] => {
const executedMigrations = getSuccessfullyExecutedMigrations(migrations, projectId);
const result: IMigration[] = [];
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -16,6 +16,7 @@ const createMigrationTool = (): number => {
.example('kontent', 'migration run --name migration01 --project-id <YOUR_PROJECT_ID> --api-key <YOUR_MANAGEMENT_API_KEY>')
.example('kontent', 'migration run --name migration01 --environment DEV --debug true')
.example('kontent', 'migration run --all --environment DEV')
.example('kontent', 'migration run --range 1:4 --environment DEV')

.example('kontent', 'backup --action backup --project-id <YOUR_PROJECT_ID> --api-key <YOUR_MANAGEMENT_API_KEY>')
.example('kontent', 'backup --action backup --environment <YOUR_ENVIRONMENT>')
Expand Down
66 changes: 66 additions & 0 deletions src/tests/invalidOrderMigration.test.ts
@@ -0,0 +1,66 @@
import { getMigrationsWithInvalidOrder } from '../utils/migrationUtils';

describe('Detection of invalid orders', () => {
it('Finds migrations with invalid order', () => {
const migrations = [
{
module: {
order: -1,
},
name: 'test',
},
{
module: {
order: 1.1,
},
name: 'test',
},
{
module: {
order: 2,
},
name: 'test',
},
{
module: {
order: 'aaa',
},
name: 'test',
},
];

const result = getMigrationsWithInvalidOrder(migrations);

expect(result.length).toBe(3);
expect(result[0].module.order).toBe(-1);
expect(result[1].module.order).toBe(1.1);
expect(result[2].module.order).toBe('aaa');
});

it('No invalid order is found', () => {
Simply007 marked this conversation as resolved.
Show resolved Hide resolved
const migrations = [
{
module: {
order: 1,
name: 'test',
},
},
{
module: {
order: 2,
name: 'test',
},
},
{
module: {
order: 3,
name: 'test',
},
},
];

const result = getMigrationsWithInvalidOrder(migrations);

expect(result.length).toBe(0);
});
});
16 changes: 13 additions & 3 deletions src/utils/migrationUtils.ts
Expand Up @@ -139,6 +139,18 @@ export const getDuplicates = <T extends any>(array: T[], key: (obj: T) => number
return duplicates;
};

export const getMigrationsWithInvalidOrder = <T extends any>(array: T[]): T[] => {
const migrationsWithInvalidOrder: T[] = [];

for (const migration of array) {
if (!Number.isInteger(migration.module.order) || Number(migration.module.order) < 0) {
migrationsWithInvalidOrder.push(migration);
}
}

return migrationsWithInvalidOrder;
};

export const loadModule = async (migrationFile: string): Promise<MigrationModule> => {
const migrationPath = getMigrationFilepath(migrationFile);

Expand All @@ -158,9 +170,7 @@ export const loadMigrationFiles = async (): Promise<IMigration[]> => {
const files = listFiles('.js');

for (const file of files) {
const migrationModule = await loadModule(file.name);

migrations.push({ name: file.name, module: migrationModule });
migrations.push({ name: file.name, module: await loadModule(file.name) });
}

return migrations.filter(String);
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Expand Up @@ -3,7 +3,8 @@
"target": "es6",
"module": "commonjs",
"lib": [
"esnext"
"esnext",
"DOM"
Simply007 marked this conversation as resolved.
Show resolved Hide resolved
],
"sourceMap": true,
"allowJs": true,
Expand Down