Skip to content

Commit

Permalink
Merge pull request #45 from kontent-ai/add_timestamp
Browse files Browse the repository at this point in the history
Add timestamp
  • Loading branch information
winklertomas authored Mar 30, 2023
2 parents d98f2a8 + 1d2ddc1 commit ff5ee97
Show file tree
Hide file tree
Showing 15 changed files with 266 additions and 45 deletions.
18 changes: 10 additions & 8 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,22 @@ jobs:
publish:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [14.x]

steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.nvmrc'
registry-url: 'https://registry.npmjs.org'
- run: npm install
- run: npm run lint
- run: npm run build
- run: npm publish
- run: npm publish
if: ${{!github.event.release.prerelease}}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_API_KEY }}
- run: npm publish --tag prerelease
if: ${{github.event.release.prerelease}}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_API_KEY }}

9 changes: 3 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@ on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.nvmrc'
- run: npm i
- run: npm run lint
- run: npm run build
Expand Down
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lts/*
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ 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.ai Management SDK](https://github.com/kontent-ai/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 (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
* The migration template contains an `order` property that is used to run a batch of migrations (range or all) in the specified order. Order can be one of the two types - `number` or `date`.
* Ordering by `number` has a higher priority. 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
* Ordering by `date` has a lower priority. To add date ordering use the switch option `-d`. The CLI will generate a new file which name consists of the date in UTC and the name you have specified. Moreover, the property `order` inside the file will be set to the Date accordingly.
* Executing all migrations will firstly migrate migrations with orders specified by number and only then migrations with order specified by date.
* By specifying range you can migrate either number-ordered migrations or date-numbered migrations. They can't be combined.

```typescript
// Example migration template
Expand All @@ -113,6 +117,8 @@ The supported commands are divided into groups according to their target, at thi
```

* `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`).
* By adding `--range` you need to add value in form of `number:number` in case of number ordering or in the format of `Tyyyy-mm-dd-hh-mm-ss:yyyy-mm-dd-hh-mm-ss` in case of date order.
> When using the range with dates, only the year value is mandatory and all other values are optional. It is fine to have a range specified like `T2023-01:2023-02`. It will take all migrations created in January of 2023. Notice the T at the beginning of the Date range. It helps to separate date ordering from number order.
* 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 get additional information logged by HttpService into the console (option `--log-http-service-errors-to-console`).
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kontent-ai/cli",
"version": "0.6.0",
"version": "0.7.0",
"description": "Command line interface tool that can be used for generating and runningKontent.ai migration scripts",
"main": "./lib/index.js",
"types": "./lib/types/index.d.ts",
Expand Down Expand Up @@ -46,7 +46,7 @@
"@kontent-ai/backup-manager": "4.0.1",
"chalk": "^4.1.2",
"dotenv": "^16.0.1",
"yargs": "^17.5.1"
"yargs": "^17.7.0"
},
"peerDependencies": {
"@kontent-ai/management-sdk": "^3.1.0"
Expand All @@ -56,7 +56,7 @@
"@babel/preset-env": "~7.18.10",
"@babel/preset-typescript": "~7.18.6",
"@types/jest": "~28.1.7",
"@types/node": "~18.7.6",
"@types/node": "~18.14.2",
"@types/yargs": "~17.0.11",
"@typescript-eslint/eslint-plugin": "^5.33.1",
"@typescript-eslint/parser": "^5.33.1",
Expand All @@ -68,6 +68,6 @@
"lint-staged": "^13.0.3",
"prettier": "^2.7.1",
"ts-jest": "^28.0.8",
"typescript": "~4.7.4"
"typescript": "~4.9.5"
}
}
10 changes: 8 additions & 2 deletions src/cmds/migration/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,22 @@ const addMigrationCommand: yargs.CommandModule = {
type: 'string',
default: 'javascript',
},
'timestamp-order': {
alias: 'd',
describe: 'Let order of the migrations be determined by the time it was created.',
type: 'boolean',
default: false,
},
})
.demandOption(['name', 'template-type']),
.demandOption(['name', 'template-type', 'timestamp-order']),
handler: (argv: any) => {
if (!['javascript', 'typescript'].includes(argv.templateType)) {
console.error(chalk.redBright(`Unexpected template type ${argv.templateType} allowed is [typescript, javascript]`));
process.exit(1);
}

const templateType = argv.templateType === 'javascript' ? TemplateType.Javascript : TemplateType.TypeScript;
createMigration(argv.name, templateType);
createMigration(argv.name, templateType, argv.timestampOrder);
},
};

Expand Down
73 changes: 66 additions & 7 deletions src/cmds/migration/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ const runMigrationCommand: yargs.CommandModule = {

if (!args.all) {
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".'));
if (!getRange(args.range) && !getRangeDate(args.range)) {
throw new Error(chalk.red('The range has to be a string of a format "number:number" or "Tyyyy-mm-dd-hh-mm-ss:yyyy-mm-dd-hh-mm-ss" where the first value (to the left to ":") is less or equal to the second, eg.: "2:5".'));
}
} else if (args.name) {
if (!isAllowedExtension(args.name)) {
Expand Down Expand Up @@ -111,7 +111,7 @@ const runMigrationCommand: yargs.CommandModule = {
let apiKey = argv.apiKey;
const migrationName = argv.name;
const runAll = argv.all;
const runRange = argv.range && exports.getRange(argv.range);
const runRange = argv.range && (exports.getRange(argv.range) || getRangeDate(argv.range));
const logHttpServiceErrorsToConsole = argv.logHttpServiceErrorsToConsole;
const continueOnError = argv.continueOnError;
let migrationsResults: number = 0;
Expand Down Expand Up @@ -152,7 +152,7 @@ const runMigrationCommand: yargs.CommandModule = {
console.log('No migrations to run.');
}

const sortedMigrationsToRun = migrationsToRun.sort((migrationPrev, migrationNext) => migrationPrev.module.order - migrationNext.module.order);
const sortedMigrationsToRun = migrationsToRun.sort(orderComparator);
let executedMigrationsCount = 0;
for (const migration of sortedMigrationsToRun) {
const migrationResult = await runMigration(migration, apiClient, projectId);
Expand Down Expand Up @@ -183,7 +183,7 @@ const runMigrationCommand: yargs.CommandModule = {
},
};

export const getRange = (range: string): IRange | null => {
export const getRange = (range: string): IRange<number> | null => {
const match = range.match(/^([0-9]+):([0-9]+)$/);
if (!match) {
return null;
Expand All @@ -199,6 +199,28 @@ export const getRange = (range: string): IRange | null => {
: null;
};

export const getRangeDate = (range: string): IRange<Date> | null => {
// format is Tyyyy-mm-dd-hh-mm-ss:yyyy-mm-dd-hh-mm-ss
const match = range.match(/^T(?<from_date>\d{4}((-\d{2}){0,2}))(?:-(?<from_time>(\d{2}-){0,2}\d{2}))?:(?<to_date>\d{4}((-\d{2}){0,2}))(?:-(?<to_time>(\d{2}-){0,2}\d{2}))?$/);
if (!match) {
return null;
}

const from = new Date(formatDate(match.groups?.from_date ?? '', match.groups?.from_time ?? ''));
const to = new Date(formatDate(match.groups?.to_date ?? '', match.groups?.to_time ?? ''));

if (isNaN(from.getTime()) || isNaN(to.getTime())) {
return null;
}

return from.getTime() <= to.getTime()
? {
from,
to,
}
: null;
};

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

Expand All @@ -210,10 +232,20 @@ const checkForDuplicates = (migrationsToRun: IMigration[]): void => {
}
};

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

for (const migration of migrationsToRun) {
if (isIRangeDate(range)) {
for (const migration of migrationsToRun.filter((x) => x.module.order instanceof Date)) {
if ((migration.module.order as Date).getTime() >= range.from.getTime() && (migration.module.order as Date).getTime() <= range.to.getTime()) {
migrations.push(migration);
}
}

return migrations.filter(String);
}

for (const migration of migrationsToRun.filter((x) => typeof x.module.order === 'number')) {
if (migration.module.order >= range.from && migration.module.order <= range.to) {
migrations.push(migration);
}
Expand Down Expand Up @@ -248,5 +280,32 @@ const skipExecutedMigrations = (migrations: IMigration[], projectId: string): IM
return result;
};

const orderComparator = (migrationPrev: IMigration, migrationNext: IMigration) => {
if (typeof migrationPrev.module.order === 'number' && typeof migrationNext.module.order === 'number') {
return migrationPrev.module.order - migrationNext.module.order;
}

if (migrationPrev.module.order instanceof Date && migrationNext.module.order instanceof Date) {
return migrationPrev.module.order.getTime() - migrationNext.module.order.getTime();
}

return typeof migrationPrev.module.order === 'number' ? -1 : 1;
};

const formatDate = (date: string, time: string) => {
if (time === '') {
time = '00:00';
}
if (time.length === 2) {
time = time + ':00';
} else {
time = time.replaceAll('-', ':');
}

return `${date}T${time}Z`;
};

const isIRangeDate = (x: IRange<number | Date>): x is IRange<Date> => x.from instanceof Date && x.to instanceof Date;

// yargs needs exported command in exports object
Object.assign(exports, runMigrationCommand);
6 changes: 3 additions & 3 deletions src/models/range.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface IRange {
from: number;
to: number;
export interface IRange<T extends number | Date> {
from: T;
to: T;
}
2 changes: 1 addition & 1 deletion src/models/status.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export interface IMigrationStatus {
readonly name: string;
readonly success: boolean;
readonly order: number;
readonly order: number | Date;
readonly time: Date;
}

Expand Down
62 changes: 62 additions & 0 deletions src/tests/cmds/run/getRangeDate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { getRangeDate } from '../../../cmds/migration/run';

describe('test getRange', () => {
const correctRanges = [
{
range: 'T2023:2024',
expected: {
from: new Date(Date.UTC(2023, 0)),
to: new Date(Date.UTC(2024, 0)),
},
},
{
range: 'T2023-03:2024',
expected: {
from: new Date(Date.UTC(2023, 2)),
to: new Date(Date.UTC(2024, 0)),
},
},
{
range: 'T2023-03:2024-05-23',
expected: {
from: new Date(Date.UTC(2023, 2)),
to: new Date(Date.UTC(2024, 4, 23)),
},
},
{
range: 'T2023-03-05-09-05-20:2024-05-23',
expected: {
from: new Date(Date.UTC(2023, 2, 5, 9, 5, 20)),
to: new Date(Date.UTC(2024, 4, 23)),
},
},
{
range: 'T2023-03-05-09-05-20:2023-03-05-09-05-21',
expected: {
from: new Date(Date.UTC(2023, 2, 5, 9, 5, 20)),
to: new Date(Date.UTC(2023, 2, 5, 9, 5, 21)),
},
},
{
range: 'T2023-03-05-09-05-20:2023-03-05-09-05-20',
expected: {
from: new Date(Date.UTC(2023, 2, 5, 9, 5, 20)),
to: new Date(Date.UTC(2023, 2, 5, 9, 5, 20)),
},
},
];

test.each(correctRanges)('test $range to return correct object', ({ range, expected }) => {
const result = getRangeDate(range);

expect(result).toStrictEqual(expected);
});

const malformedRanges = ['T', '2023', 'T2023', 'T2023:', 'T2023:05:2024', 'T2023-13:2024', 'T2023-12:2023-12:02', 'T2023-1:2024', 'T2023-01-02-03-04-05:2023-01-02-03-04-04'];

test.each(malformedRanges)('test %s to be null', (range) => {
const result = getRangeDate(range);

expect(result).toBeNull();
});
});
Loading

0 comments on commit ff5ee97

Please sign in to comment.