diff --git a/README.md b/README.md index cafbb3a..4d73005 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ The supported commands are divided into groups according to their target, at thi * The file is stored in the `Migrations` directory within the root of your repository. * Add your migration script in the body of the `run` function using the [Kontent Management SDK](https://github.com/Kentico/kontent-management-sdk-js) that was injected via the `apiClient` parameter. * To choose between JavaScript and TypeScript when generating the script file, use the `--template-type` option, such as `--template-type "javascript"`. - * The migration template contains an `order` property that is used to run a batch of migrations in the specified order. + * The migration template contains an `order` property that is used to run a batch of migrations (range or all) in the specified order. The `order` must be a unique, positive integer or zero. There may be gaps between migrations, for example, the following sequence is perfectly fine 0,3,4,5,10 ```typescript // Example migration template @@ -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 `), 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 `), or runs multiple migration scripts in the order specified in the migration files (options `--all` or `--range`). * You can execute a migration against a specific project (options `--project --api-key `) or environment stored in the local configuration file (option `--environment `). * 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. diff --git a/package.json b/package.json index d9c0a45..e6c5e25 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kentico/kontent-cli", - "version": "0.3.0", + "version": "0.5.0", "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", diff --git a/src/cmds/migration/run.ts b/src/cmds/migration/run.ts index 270bf11..100d590 100644 --- a/src/cmds/migration/run.ts +++ b/src/cmds/migration/run.ts @@ -1,11 +1,12 @@ 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'; import { loadMigrationsExecutionStatus } from '../../utils/statusManager'; import { IMigration } from '../../models/migration'; +import { IRange } from '../../models/range'; const runMigrationCommand: yargs.CommandModule = { command: 'run', @@ -38,6 +39,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.', @@ -58,6 +64,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) => { @@ -66,7 +74,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.`)); } @@ -76,7 +88,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.')); } } @@ -99,6 +111,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; @@ -119,10 +132,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'); @@ -165,6 +183,22 @@ const runMigrationCommand: yargs.CommandModule = { }, }; +export const getRange = (range: string): IRange | 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; +}; + const checkForDuplicates = (migrationsToRun: IMigration[]): void => { const duplicateMigrationsOrder = getDuplicates(migrationsToRun, (migration) => migration.module.order); @@ -176,6 +210,29 @@ const checkForDuplicates = (migrationsToRun: IMigration[]): void => { } }; +const getMigrationsByRange = (migrationsToRun: IMigration[], range: IRange): IMigration[] => { + const migrations: IMigration[] = []; + + for (const migration of migrationsToRun) { + if (migration.module.order >= range.from && migration.module.order <= range.to) { + 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[] = []; diff --git a/src/index.ts b/src/index.ts index 5ef7499..ec05770 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ const createMigrationTool = (): number => { .example('kontent', 'migration run --name migration01 --project-id --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 --api-key ') .example('kontent', 'backup --action backup --environment ') diff --git a/src/models/range.ts b/src/models/range.ts new file mode 100644 index 0000000..a0d5a83 --- /dev/null +++ b/src/models/range.ts @@ -0,0 +1,4 @@ +export interface IRange { + from: number; + to: number; +} diff --git a/src/tests/invalidOrderMigration.test.ts b/src/tests/invalidOrderMigration.test.ts new file mode 100644 index 0000000..ca72048 --- /dev/null +++ b/src/tests/invalidOrderMigration.test.ts @@ -0,0 +1,72 @@ +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', () => { + const migrations = [ + { + module: { + order: 1, + name: 'test', + }, + }, + { + module: { + order: 2, + name: 'test', + }, + }, + { + module: { + order: 6, + name: 'test', + }, + }, + { + module: { + order: 7, + name: 'test', + }, + }, + ]; + + const result = getMigrationsWithInvalidOrder(migrations); + + expect(result.length).toBe(0); + }); +}); diff --git a/src/utils/migrationUtils.ts b/src/utils/migrationUtils.ts index 8e02787..4e86f48 100644 --- a/src/utils/migrationUtils.ts +++ b/src/utils/migrationUtils.ts @@ -139,6 +139,18 @@ export const getDuplicates = (array: T[], key: (obj: T) => number return duplicates; }; +export const getMigrationsWithInvalidOrder = (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 => { const migrationPath = getMigrationFilepath(migrationFile); @@ -158,9 +170,7 @@ export const loadMigrationFiles = async (): Promise => { 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); diff --git a/tsconfig.json b/tsconfig.json index ffcca13..c009de1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "target": "es6", "module": "commonjs", "lib": [ - "esnext" + "esnext", + "DOM" ], "sourceMap": true, "allowJs": true,