Skip to content

Commit 71f2ea8

Browse files
author
Tomáš Winkler
authored
Merge pull request #32 from Kentico/Run-a-range-of-migrations
Run a range of migrations
2 parents 8027aa4 + 91541f3 commit 71f2ea8

File tree

8 files changed

+156
-11
lines changed

8 files changed

+156
-11
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ The supported commands are divided into groups according to their target, at thi
9696
* The file is stored in the `Migrations` directory within the root of your repository.
9797
* 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.
9898
* To choose between JavaScript and TypeScript when generating the script file, use the `--template-type` option, such as `--template-type "javascript"`.
99-
* The migration template contains an `order` property that is used to run a batch of migrations in the specified order.
99+
* 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
100100

101101
```typescript
102102
// Example migration template
@@ -112,7 +112,7 @@ The supported commands are divided into groups according to their target, at thi
112112
export default migration;
113113
```
114114

115-
* `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`).
115+
* `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`).
116116
* 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>`).
117117
* 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.
118118
* 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.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@kentico/kontent-cli",
3-
"version": "0.3.0",
3+
"version": "0.5.0",
44
"description": "Command line interface tool that can be used for generating and running Kontent migration scripts",
55
"main": "./lib/index.js",
66
"types": "./lib/types/index.d.ts",

src/cmds/migration/run.ts

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import yargs from 'yargs';
22
import chalk from 'chalk';
3-
import { getDuplicates, getSuccessfullyExecutedMigrations, getMigrationFilepath, loadMigrationFiles, loadModule, runMigration } from '../../utils/migrationUtils';
3+
import { getDuplicates, getSuccessfullyExecutedMigrations, getMigrationFilepath, loadMigrationFiles, loadModule, runMigration, getMigrationsWithInvalidOrder } from '../../utils/migrationUtils';
44
import { fileExists, getFileWithExtension, isAllowedExtension } from '../../utils/fileUtils';
55
import { environmentConfigExists, getEnvironmentsConfig } from '../../utils/environmentUtils';
66
import { createManagementClient } from '../../managementClientFactory';
77
import { loadMigrationsExecutionStatus } from '../../utils/statusManager';
88
import { IMigration } from '../../models/migration';
9+
import { IRange } from '../../models/range';
910

1011
const runMigrationCommand: yargs.CommandModule = {
1112
command: 'run',
@@ -38,6 +39,11 @@ const runMigrationCommand: yargs.CommandModule = {
3839
describe: 'Run all migration scripts in the specified order',
3940
type: 'boolean',
4041
},
42+
range: {
43+
alias: 'r',
44+
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',
45+
type: 'string',
46+
},
4147
force: {
4248
alias: 'f',
4349
describe: 'Enforces run of already executed scripts.',
@@ -58,6 +64,8 @@ const runMigrationCommand: yargs.CommandModule = {
5864
},
5965
})
6066
.conflicts('all', 'name')
67+
.conflicts('range', 'name')
68+
.conflicts('all', 'range')
6169
.conflicts('environment', 'api-key')
6270
.conflicts('environment', 'project-id')
6371
.check((args: any) => {
@@ -66,7 +74,11 @@ const runMigrationCommand: yargs.CommandModule = {
6674
}
6775

6876
if (!args.all) {
69-
if (args.name) {
77+
if (args.range) {
78+
if (!getRange(args.range)) {
79+
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".`));
80+
}
81+
} else if (args.name) {
7082
if (!isAllowedExtension(args.name)) {
7183
throw new Error(chalk.red(`File ${args.name} has not supported extension.`));
7284
}
@@ -76,7 +88,7 @@ const runMigrationCommand: yargs.CommandModule = {
7688
throw new Error(chalk.red(`Cannot find the specified migration script: ${migrationFilePath}.`));
7789
}
7890
} else {
79-
throw new Error(chalk.red('Either the migration script name or all migration options needs to be specified.'));
91+
throw new Error(chalk.red('Either the migration script name, range or all migration options needs to be specified.'));
8092
}
8193
}
8294

@@ -99,6 +111,7 @@ const runMigrationCommand: yargs.CommandModule = {
99111
let apiKey = argv.apiKey;
100112
const migrationName = argv.name;
101113
const runAll = argv.all;
114+
const runRange = getRange(argv.range);
102115
const debugMode = argv.debug;
103116
const continueOnError = argv.continueOnError;
104117
let migrationsResults: number = 0;
@@ -119,10 +132,15 @@ const runMigrationCommand: yargs.CommandModule = {
119132

120133
loadMigrationsExecutionStatus();
121134

122-
if (runAll) {
135+
if (runAll || runRange) {
123136
let migrationsToRun = await loadMigrationFiles();
124137

125138
checkForDuplicates(migrationsToRun);
139+
checkForInvalidOrder(migrationsToRun);
140+
141+
if (runRange) {
142+
migrationsToRun = getMigrationsByRange(migrationsToRun, runRange);
143+
}
126144

127145
if (runForce) {
128146
console.log('Skipping to check already executed migrations');
@@ -165,6 +183,22 @@ const runMigrationCommand: yargs.CommandModule = {
165183
},
166184
};
167185

186+
export const getRange = (range: string): IRange | null => {
187+
const match = range.match(/^([0-9]+):([0-9]+)$/);
188+
if (!match) {
189+
return null;
190+
}
191+
const from = Number(match[1]);
192+
const to = Number(match[2]);
193+
194+
return from <= to
195+
? {
196+
from,
197+
to,
198+
}
199+
: null;
200+
};
201+
168202
const checkForDuplicates = (migrationsToRun: IMigration[]): void => {
169203
const duplicateMigrationsOrder = getDuplicates(migrationsToRun, (migration) => migration.module.order);
170204

@@ -176,6 +210,29 @@ const checkForDuplicates = (migrationsToRun: IMigration[]): void => {
176210
}
177211
};
178212

213+
const getMigrationsByRange = (migrationsToRun: IMigration[], range: IRange): IMigration[] => {
214+
const migrations: IMigration[] = [];
215+
216+
for (const migration of migrationsToRun) {
217+
if (migration.module.order >= range.from && migration.module.order <= range.to) {
218+
migrations.push(migration);
219+
}
220+
}
221+
222+
return migrations.filter(String);
223+
};
224+
225+
const checkForInvalidOrder = (migrationsToRun: IMigration[]): void => {
226+
const migrationsWithInvalidOrder: IMigration[] = getMigrationsWithInvalidOrder(migrationsToRun);
227+
228+
if (migrationsWithInvalidOrder.length > 0) {
229+
console.log('Migration order has to be positive integer or zero:');
230+
migrationsWithInvalidOrder.map((migration) => console.error(chalk.red(`Migration: ${migration.name} order: ${migration.module.order}`)));
231+
232+
process.exit(1);
233+
}
234+
};
235+
179236
const skipExecutedMigrations = (migrations: IMigration[], projectId: string): IMigration[] => {
180237
const executedMigrations = getSuccessfullyExecutedMigrations(migrations, projectId);
181238
const result: IMigration[] = [];

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const createMigrationTool = (): number => {
1616
.example('kontent', 'migration run --name migration01 --project-id <YOUR_PROJECT_ID> --api-key <YOUR_MANAGEMENT_API_KEY>')
1717
.example('kontent', 'migration run --name migration01 --environment DEV --debug true')
1818
.example('kontent', 'migration run --all --environment DEV')
19+
.example('kontent', 'migration run --range 1:4 --environment DEV')
1920

2021
.example('kontent', 'backup --action backup --project-id <YOUR_PROJECT_ID> --api-key <YOUR_MANAGEMENT_API_KEY>')
2122
.example('kontent', 'backup --action backup --environment <YOUR_ENVIRONMENT>')

src/models/range.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface IRange {
2+
from: number;
3+
to: number;
4+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { getMigrationsWithInvalidOrder } from '../utils/migrationUtils';
2+
3+
describe('Detection of invalid orders', () => {
4+
it('Finds migrations with invalid order', () => {
5+
const migrations = [
6+
{
7+
module: {
8+
order: -1,
9+
},
10+
name: 'test',
11+
},
12+
{
13+
module: {
14+
order: 1.1,
15+
},
16+
name: 'test',
17+
},
18+
{
19+
module: {
20+
order: 2,
21+
},
22+
name: 'test',
23+
},
24+
{
25+
module: {
26+
order: 'aaa',
27+
},
28+
name: 'test',
29+
},
30+
];
31+
32+
const result = getMigrationsWithInvalidOrder(migrations);
33+
34+
expect(result.length).toBe(3);
35+
expect(result[0].module.order).toBe(-1);
36+
expect(result[1].module.order).toBe(1.1);
37+
expect(result[2].module.order).toBe('aaa');
38+
});
39+
40+
it('No invalid order is found', () => {
41+
const migrations = [
42+
{
43+
module: {
44+
order: 1,
45+
name: 'test',
46+
},
47+
},
48+
{
49+
module: {
50+
order: 2,
51+
name: 'test',
52+
},
53+
},
54+
{
55+
module: {
56+
order: 6,
57+
name: 'test',
58+
},
59+
},
60+
{
61+
module: {
62+
order: 7,
63+
name: 'test',
64+
},
65+
},
66+
];
67+
68+
const result = getMigrationsWithInvalidOrder(migrations);
69+
70+
expect(result.length).toBe(0);
71+
});
72+
});

src/utils/migrationUtils.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,18 @@ export const getDuplicates = <T extends any>(array: T[], key: (obj: T) => number
139139
return duplicates;
140140
};
141141

142+
export const getMigrationsWithInvalidOrder = <T extends any>(array: T[]): T[] => {
143+
const migrationsWithInvalidOrder: T[] = [];
144+
145+
for (const migration of array) {
146+
if (!Number.isInteger(migration.module.order) || Number(migration.module.order) < 0) {
147+
migrationsWithInvalidOrder.push(migration);
148+
}
149+
}
150+
151+
return migrationsWithInvalidOrder;
152+
};
153+
142154
export const loadModule = async (migrationFile: string): Promise<MigrationModule> => {
143155
const migrationPath = getMigrationFilepath(migrationFile);
144156

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

160172
for (const file of files) {
161-
const migrationModule = await loadModule(file.name);
162-
163-
migrations.push({ name: file.name, module: migrationModule });
173+
migrations.push({ name: file.name, module: await loadModule(file.name) });
164174
}
165175

166176
return migrations.filter(String);

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"target": "es6",
44
"module": "commonjs",
55
"lib": [
6-
"esnext"
6+
"esnext",
7+
"DOM"
78
],
89
"sourceMap": true,
910
"allowJs": true,

0 commit comments

Comments
 (0)