From d617f20e2527d91b9aa21767d38b70a6dc24bc8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Kir=C3=A1=C4=BE?= <54802833+IvanKiral@users.noreply.github.com> Date: Thu, 16 Mar 2023 08:29:03 +0100 Subject: [PATCH 01/14] Implement importing the function from the statusImpl if it exists, --- src/cmds/status.ts | 24 +++++++++++ src/tests/statusManager.test.ts | 12 +++--- src/utils/migrationUtils.ts | 4 +- src/utils/status/statusPlugin.ts | 19 +++++++++ src/utils/statusManager.ts | 71 ++++++++++++++++++++++++++++---- 5 files changed, 113 insertions(+), 17 deletions(-) create mode 100644 src/cmds/status.ts create mode 100644 src/utils/status/statusPlugin.ts diff --git a/src/cmds/status.ts b/src/cmds/status.ts new file mode 100644 index 0000000..83598a1 --- /dev/null +++ b/src/cmds/status.ts @@ -0,0 +1,24 @@ +import yargs from 'yargs'; +import { createStatusImplementationFile } from '../utils/statusManager'; + +const statusCommand: yargs.CommandModule = { + command: 'status', + describe: 'Status commands', + builder: (yargs: any) => + yargs + .options({ + implementation: { + alias: 'i', + describe: 'Implement your own save/read functions to deal with status.json.', + type: 'boolean', + }, + }) + .demandOption(['implementation']), + handler: (argv: any) => { + if (argv.implementation) { + createStatusImplementationFile(); + } + }, +}; + +Object.assign(exports, statusCommand); diff --git a/src/tests/statusManager.test.ts b/src/tests/statusManager.test.ts index 5e656c1..89f689e 100644 --- a/src/tests/statusManager.test.ts +++ b/src/tests/statusManager.test.ts @@ -15,10 +15,10 @@ describe('Status manager', () => { jest.spyOn(Date, 'now').mockImplementation(() => 1575939660000); }); - it('Project has success status in status manager file', () => { + it('Project has success status in status manager file', async () => { const projectId = 'project1'; const migrationName = 'migration1'; - markAsCompleted(projectId, migrationName, 1); + await markAsCompleted(projectId, migrationName, 1); const statusFile = readStatusFile(); const status = statusFile[projectId][0]; @@ -26,21 +26,21 @@ describe('Status manager', () => { expect(status).toMatchSnapshot(); }); - it('Not executed migration is not present in status file', () => { + it('Not executed migration is not present in status file', async () => { const project1Id = 'project1'; const project2Id = 'project2'; const migration1Name = 'migration1'; - markAsCompleted(project1Id, migration1Name, 1); + await markAsCompleted(project1Id, migration1Name, 1); const projectMigrationStatus = wasSuccessfullyExecuted(migration1Name, project2Id); expect(projectMigrationStatus).toBe(false); }); - it('Executed migration is present in status file', () => { + it('Executed migration is present in status file', async () => { const project2Id = 'project2'; const migration2Name = 'migration2'; - markAsCompleted(project2Id, migration2Name, 1); + await markAsCompleted(project2Id, migration2Name, 1); const projectMigrationStatus = wasSuccessfullyExecuted(project2Id, migration2Name); diff --git a/src/utils/migrationUtils.ts b/src/utils/migrationUtils.ts index 673cafa..07a13ff 100644 --- a/src/utils/migrationUtils.ts +++ b/src/utils/migrationUtils.ts @@ -47,8 +47,8 @@ export const runMigration = async (migration: IMigration, client: ManagementClie let isSuccess = true; try { - await migration.module.run(client).then(() => { - markAsCompleted(projectId, migration.name, migration.module.order); + await migration.module.run(client).then(async () => { + await markAsCompleted(projectId, migration.name, migration.module.order); }); } catch (e) { console.error(chalk.redBright('An error occurred while running migration:'), chalk.yellowBright(migration.name), chalk.redBright('see the output from running the script.')); diff --git a/src/utils/status/statusPlugin.ts b/src/utils/status/statusPlugin.ts new file mode 100644 index 0000000..8b46033 --- /dev/null +++ b/src/utils/status/statusPlugin.ts @@ -0,0 +1,19 @@ +import { IStatus } from '../../models/status'; + +interface StatusPlugin { + saveStatusToFile: (data: string) => void; + readStatusFromFile: () => IStatus; +} + +export const loadStatusPlugin = async (path: string): Promise => { + const pluginModule = await import(path); + + if (!('saveStatusToFile' in pluginModule && typeof pluginModule.saveStatusToFile === 'function') || !('readStatusFromFile' in pluginModule && typeof pluginModule.readStatusFromFile === 'function')) { + throw new Error('Invalid plugin: does not implement saveStatusToFile or readStatusFromFile functions'); + } + + return { + saveStatusToFile: pluginModule.saveStatusToFile, + readStatusFromFile: pluginModule.readStatusFromFile, + }; +}; diff --git a/src/utils/statusManager.ts b/src/utils/statusManager.ts index 7926509..07bbce2 100644 --- a/src/utils/statusManager.ts +++ b/src/utils/statusManager.ts @@ -1,12 +1,15 @@ import { IMigrationStatus, IStatus } from '../models/status'; -import { readFileSync, writeFileSync } from 'fs'; +import { readFileSync, writeFileSync, mkdirSync } from 'fs'; import { fileExists } from './fileUtils'; import * as path from 'path'; +import { loadStatusPlugin } from './status/statusPlugin'; const migrationStatusFilename = 'status.json'; +const statusDirectoryName = 'status'; +const statusImplementationFilename = 'statusImpl.ts'; let status: IStatus = {}; -const updateMigrationStatus = (projectId: string, migrationStatus: IMigrationStatus) => { +const updateMigrationStatus = async (projectId: string, migrationStatus: IMigrationStatus) => { let projectMigrationsHistory = status[projectId]; if (projectMigrationsHistory === undefined) { @@ -20,10 +23,10 @@ const updateMigrationStatus = (projectId: string, migrationStatus: IMigrationSta projectMigrationsHistory.push(migrationStatus); } - saveStatusFile(); + await saveStatusFile(); }; -export const markAsCompleted = (projectId: string, name: string, order: number | Date) => { +export const markAsCompleted = async (projectId: string, name: string, order: number | Date) => { const migrationStatus = { name, order, @@ -31,12 +34,23 @@ export const markAsCompleted = (projectId: string, name: string, order: number | time: new Date(Date.now()), }; - updateMigrationStatus(projectId, migrationStatus); + await updateMigrationStatus(projectId, migrationStatus); }; -const saveStatusFile = () => { +const saveStatusFile = async () => { const statusJSON = JSON.stringify(status, null, 2); + if (fileExists(getStatusImplementationFilePath())) { + try { + const file = await loadStatusPlugin(getStatusImplementationFilePath().slice(0, -3) + '.js'); + file.saveStatusToFile(statusJSON); + } catch (e) { + console.log(`Could not load the plugin due to ${e}. Fallbacking to status.json`); + saveStatusToFile(statusJSON); + } finally { + return; + } + } saveStatusToFile(statusJSON); }; @@ -65,14 +79,18 @@ export const loadMigrationsExecutionStatus = () => { } try { - const projectsMigrationStatuses = readFileSync(statusFilepath).toString(); - - status = JSON.parse(projectsMigrationStatuses); + status = readFromStatus(); } catch (error) { console.warn(`Status JSON file is invalid because of ${error instanceof Error ? error.message : 'unknown error.'}. Continuing with empty status.`); } }; +const readFromStatus = (): IStatus => { + const projectsMigrationStatuses = readFileSync(getStatusFilepath()).toString(); + + return JSON.parse(projectsMigrationStatuses); +}; + const saveStatusToFile = (data: string): void => { const statusFilepath = getStatusFilepath(); @@ -83,3 +101,38 @@ const saveStatusToFile = (data: string): void => { console.error(`Status file save failed, because of ${error instanceof Error ? error.message : 'unknown error.'}`); } }; + +const getStatusImplementationFilePath = () => path.join(process.cwd(), statusDirectoryName, statusImplementationFilename); + +const getStatusImplementationDirectoryPath = () => path.join(process.cwd(), statusDirectoryName); + +export const createStatusImplementationFile = () => { + const statusImplementationPath = getStatusImplementationFilePath(); + + if (fileExists(statusImplementationPath)) { + console.error(`File ${statusImplementationPath} already exists`); + } + + try { + if (!fileExists(getStatusImplementationDirectoryPath())) { + mkdirSync(getStatusImplementationDirectoryPath()); + } + + writeFileSync(statusImplementationPath, statusImplementationTemplate, { flag: 'w' }); + console.log(`Status.ts file was created see ${statusImplementationPath}`); + } catch (error) { + console.error(`Status.ts file creation failed, because of ${error instanceof Error ? error.message : 'unknown error.'}`); + } +}; + +const statusImplementationTemplate = ` +import type {IStatus} from "@kontent-ai/cli"; + +export const saveStatusToFile = (data: string) => { + +} + +export const readStatusFromFile = (): IStatus => { + +} +`; From 8e734cfde07f4864f61ec8c367e2f643b11d5057 Mon Sep 17 00:00:00 2001 From: Ivan Kiral Date: Thu, 30 Mar 2023 11:30:40 +0200 Subject: [PATCH 02/14] Add load status from plugin and refactor the plugin strucuture --- src/cmds/migration/run.ts | 2 +- src/index.ts | 2 ++ src/utils/status/statusPlugin.ts | 12 +++++----- src/utils/statusManager.ts | 38 +++++++++++++++++++++----------- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/cmds/migration/run.ts b/src/cmds/migration/run.ts index dd71e80..a6e416f 100644 --- a/src/cmds/migration/run.ts +++ b/src/cmds/migration/run.ts @@ -130,7 +130,7 @@ const runMigrationCommand: yargs.CommandModule = { logHttpServiceErrorsToConsole, }); - loadMigrationsExecutionStatus(); + await loadMigrationsExecutionStatus(); if (runAll || runRange) { let migrationsToRun = await loadMigrationFiles(); diff --git a/src/index.ts b/src/index.ts index 1749f68..65b273d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,8 @@ const createMigrationTool = (): number => { .example('kontent', 'backup --action restore --name backup_file --project-id --api-key ') .example('kontent', 'backup --action restore --name backup_file --environment --preserve-workflow false') .example('kontent', 'backup --action clean --project-id --api-key ') + + .example('kontent', 'status -i') .strict().argv; return 0; diff --git a/src/utils/status/statusPlugin.ts b/src/utils/status/statusPlugin.ts index 8b46033..2ff0a47 100644 --- a/src/utils/status/statusPlugin.ts +++ b/src/utils/status/statusPlugin.ts @@ -1,19 +1,19 @@ import { IStatus } from '../../models/status'; interface StatusPlugin { - saveStatusToFile: (data: string) => void; - readStatusFromFile: () => IStatus; + saveStatus: (data: string) => Promise; + readStatus: () => Promise; } export const loadStatusPlugin = async (path: string): Promise => { const pluginModule = await import(path); - if (!('saveStatusToFile' in pluginModule && typeof pluginModule.saveStatusToFile === 'function') || !('readStatusFromFile' in pluginModule && typeof pluginModule.readStatusFromFile === 'function')) { - throw new Error('Invalid plugin: does not implement saveStatusToFile or readStatusFromFile functions'); + if (!('saveStatus' in pluginModule && typeof pluginModule.saveStatus === 'function') || !('readStatus' in pluginModule && typeof pluginModule.readStatus === 'function')) { + throw new Error('Invalid plugin: does not implement saveStatus or readStatus functions'); } return { - saveStatusToFile: pluginModule.saveStatusToFile, - readStatusFromFile: pluginModule.readStatusFromFile, + saveStatus: pluginModule.saveStatus, + readStatus: pluginModule.readStatus, }; }; diff --git a/src/utils/statusManager.ts b/src/utils/statusManager.ts index 07bbce2..cb7bd4c 100644 --- a/src/utils/statusManager.ts +++ b/src/utils/statusManager.ts @@ -43,7 +43,7 @@ const saveStatusFile = async () => { if (fileExists(getStatusImplementationFilePath())) { try { const file = await loadStatusPlugin(getStatusImplementationFilePath().slice(0, -3) + '.js'); - file.saveStatusToFile(statusJSON); + await file.saveStatus(statusJSON); } catch (e) { console.log(`Could not load the plugin due to ${e}. Fallbacking to status.json`); saveStatusToFile(statusJSON); @@ -72,23 +72,35 @@ const getStatusFilepath = (): string => { return path.join(process.cwd(), migrationStatusFilename); }; -export const loadMigrationsExecutionStatus = () => { +export const loadMigrationsExecutionStatus = async () => { + if (fileExists(getStatusImplementationFilePath())) { + try { + const file = await loadStatusPlugin(getStatusImplementationFilePath().slice(0, -3) + '.js'); + status = await file.readStatus(); + } catch (e) { + console.log(`Could not load the plugin due to ${e}. Fallbacking to status.json`); + status = readFromStatus(); + } finally { + return; + } + } + status = readFromStatus(); +}; + +const readFromStatus = (): IStatus => { const statusFilepath = getStatusFilepath(); if (!fileExists(statusFilepath)) { - return; + return {}; } try { - status = readFromStatus(); + const projectsMigrationStatuses = readFileSync(getStatusFilepath()).toString(); + + return JSON.parse(projectsMigrationStatuses); } catch (error) { console.warn(`Status JSON file is invalid because of ${error instanceof Error ? error.message : 'unknown error.'}. Continuing with empty status.`); - } -}; - -const readFromStatus = (): IStatus => { - const projectsMigrationStatuses = readFileSync(getStatusFilepath()).toString(); - - return JSON.parse(projectsMigrationStatuses); + return {}; + } }; const saveStatusToFile = (data: string): void => { @@ -128,11 +140,11 @@ export const createStatusImplementationFile = () => { const statusImplementationTemplate = ` import type {IStatus} from "@kontent-ai/cli"; -export const saveStatusToFile = (data: string) => { +export const saveStatus = async (data: string) => { } -export const readStatusFromFile = (): IStatus => { +export const readStatus = async (): IStatus => { } `; From 34aba661e0cab8c812ee080c38a41ae2ff1ce1d3 Mon Sep 17 00:00:00 2001 From: Ivan Kiral Date: Thu, 30 Mar 2023 11:31:05 +0200 Subject: [PATCH 03/14] Add test for loading plugin --- src/tests/statusPlugin/statusPlugin.test.ts | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/tests/statusPlugin/statusPlugin.test.ts diff --git a/src/tests/statusPlugin/statusPlugin.test.ts b/src/tests/statusPlugin/statusPlugin.test.ts new file mode 100644 index 0000000..d395b04 --- /dev/null +++ b/src/tests/statusPlugin/statusPlugin.test.ts @@ -0,0 +1,29 @@ +import { loadStatusPlugin } from "../../utils/status/statusPlugin"; + +describe("status plugin tests", () => { + + const saveStatus = () => {}; + const readStatus = () => ({}); + + jest.mock('plugin', () => ({ + saveStatus, + readStatus + }), {virtual:true}); + + jest.mock('malformedPlugin', () => ({ + save: saveStatus, + read: readStatus + }), {virtual:true}); + + it("test correct plugin", async () => { + const functions = await loadStatusPlugin('plugin'); + + expect(functions.saveStatus).toEqual(saveStatus); + expect(functions.readStatus).toEqual(readStatus); + }) + + it("test malformed plugin", async () => { + expect (loadStatusPlugin('malformedPlugin')).rejects.toThrow(); + }) + +}) \ No newline at end of file From 2d64177d9e343f2df598afaf4af2fe792ffa616f Mon Sep 17 00:00:00 2001 From: Ivan Kiral Date: Fri, 31 Mar 2023 08:54:16 +0200 Subject: [PATCH 04/14] Fix test --- src/tests/cmds/run/runMigration.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/cmds/run/runMigration.test.ts b/src/tests/cmds/run/runMigration.test.ts index 1a53a9c..f27073f 100644 --- a/src/tests/cmds/run/runMigration.test.ts +++ b/src/tests/cmds/run/runMigration.test.ts @@ -35,8 +35,8 @@ const migrations: IMigration[] = [ }, ]; -jest.spyOn(statusManager, 'markAsCompleted').mockImplementation(() => {}); -jest.spyOn(statusManager, 'loadMigrationsExecutionStatus').mockImplementation(() => {}); +jest.spyOn(statusManager, 'markAsCompleted').mockImplementation(async () => {}); +jest.spyOn(statusManager, 'loadMigrationsExecutionStatus').mockImplementation(async () => {}); jest.spyOn(migrationUtils, 'loadMigrationFiles').mockReturnValue( new Promise((resolve) => { resolve(migrations); From 8ffda802cacff87d650bb213c317dcb4e0962e59 Mon Sep 17 00:00:00 2001 From: Ivan Kiral Date: Fri, 31 Mar 2023 09:05:36 +0200 Subject: [PATCH 05/14] Fix lint errors --- src/tests/statusPlugin/statusPlugin.test.ts | 44 ++++++++++++--------- src/utils/statusManager.ts | 2 +- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/tests/statusPlugin/statusPlugin.test.ts b/src/tests/statusPlugin/statusPlugin.test.ts index d395b04..c7a3fd7 100644 --- a/src/tests/statusPlugin/statusPlugin.test.ts +++ b/src/tests/statusPlugin/statusPlugin.test.ts @@ -1,29 +1,35 @@ -import { loadStatusPlugin } from "../../utils/status/statusPlugin"; - -describe("status plugin tests", () => { +import { loadStatusPlugin } from '../../utils/status/statusPlugin'; +describe('status plugin tests', () => { const saveStatus = () => {}; const readStatus = () => ({}); - jest.mock('plugin', () => ({ - saveStatus, - readStatus - }), {virtual:true}); + jest.mock( + 'plugin', + () => ({ + saveStatus, + readStatus, + }), + { virtual: true } + ); + + jest.mock( + 'malformedPlugin', + () => ({ + save: saveStatus, + read: readStatus, + }), + { virtual: true } + ); - jest.mock('malformedPlugin', () => ({ - save: saveStatus, - read: readStatus - }), {virtual:true}); - - it("test correct plugin", async () => { + it('test correct plugin', async () => { const functions = await loadStatusPlugin('plugin'); expect(functions.saveStatus).toEqual(saveStatus); expect(functions.readStatus).toEqual(readStatus); - }) - - it("test malformed plugin", async () => { - expect (loadStatusPlugin('malformedPlugin')).rejects.toThrow(); - }) + }); -}) \ No newline at end of file + it('test malformed plugin', async () => { + expect(loadStatusPlugin('malformedPlugin')).rejects.toThrow(); + }); +}); diff --git a/src/utils/statusManager.ts b/src/utils/statusManager.ts index cb7bd4c..94d6b36 100644 --- a/src/utils/statusManager.ts +++ b/src/utils/statusManager.ts @@ -100,7 +100,7 @@ const readFromStatus = (): IStatus => { } catch (error) { console.warn(`Status JSON file is invalid because of ${error instanceof Error ? error.message : 'unknown error.'}. Continuing with empty status.`); return {}; - } + } }; const saveStatusToFile = (data: string): void => { From e865ac8c436777ac326dc89635cde6099bee0efb Mon Sep 17 00:00:00 2001 From: Ivan Kiral Date: Fri, 31 Mar 2023 10:05:57 +0200 Subject: [PATCH 06/14] Fix circular dependency --- src/utils/fileUtils.ts | 10 +--------- src/utils/migrationUtils.ts | 12 +++++++++--- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/utils/fileUtils.ts b/src/utils/fileUtils.ts index 8a951dc..867374a 100644 --- a/src/utils/fileUtils.ts +++ b/src/utils/fileUtils.ts @@ -1,14 +1,6 @@ -import fs, { Dirent, PathLike } from 'fs'; -import { getMigrationDirectory } from './migrationUtils'; +import fs, { PathLike } from 'fs'; import * as path from 'path'; -export const listFiles = (fileExtension: string): Dirent[] => { - return fs - .readdirSync(getMigrationDirectory(), { withFileTypes: true }) - .filter((f) => f.isFile()) - .filter((f) => f.name.endsWith(fileExtension)); -}; - export const fileExists = (filePath: PathLike): boolean => { return fs.existsSync(filePath); }; diff --git a/src/utils/migrationUtils.ts b/src/utils/migrationUtils.ts index 07a13ff..a30cbeb 100644 --- a/src/utils/migrationUtils.ts +++ b/src/utils/migrationUtils.ts @@ -1,14 +1,20 @@ import { ManagementClient, SharedModels } from '@kontent-ai/management-sdk'; import chalk from 'chalk'; import path from 'path'; -import fs from 'fs'; -import { listFiles } from './fileUtils'; +import fs, { Dirent } from 'fs'; import { TemplateType } from '../models/templateType'; import { MigrationModule } from '../types'; import { IMigration } from '../models/migration'; import { markAsCompleted, wasSuccessfullyExecuted } from './statusManager'; import { formatDateForFileName } from './dateUtils'; +const listMigrationFiles = (fileExtension: string): Dirent[] => { + return fs + .readdirSync(getMigrationDirectory(), { withFileTypes: true }) + .filter((f) => f.isFile()) + .filter((f) => f.name.endsWith(fileExtension)); +}; + export const getMigrationDirectory = (): string => { const migrationDirectory = 'Migrations'; return path.join(process.cwd(), migrationDirectory); @@ -182,7 +188,7 @@ export const loadModule = async (migrationFile: string): Promise => { const migrations: IMigration[] = []; - const files = listFiles('.js'); + const files = listMigrationFiles('.js'); for (const file of files) { migrations.push({ name: file.name, module: await loadModule(file.name) }); From 90d8bbf80002c499bc8a2aea74063d40c8dbd11d Mon Sep 17 00:00:00 2001 From: Ivan Kiral Date: Fri, 31 Mar 2023 10:18:00 +0200 Subject: [PATCH 07/14] Add test for testing loadMigrationsExecutionStatus --- src/tests/statusManager.test.ts | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/tests/statusManager.test.ts b/src/tests/statusManager.test.ts index 89f689e..ede9428 100644 --- a/src/tests/statusManager.test.ts +++ b/src/tests/statusManager.test.ts @@ -1,12 +1,14 @@ -import { markAsCompleted, wasSuccessfullyExecuted } from '../utils/statusManager'; -import { readFileSync } from 'fs'; +import * as statusManager from '../utils/statusManager'; +import * as statusPlugin from '../utils/status/statusPlugin'; +import * as fileUtils from '../utils/fileUtils'; +import * as fs from 'fs'; import * as path from 'path'; import { IStatus } from '../models/status'; const readStatusFile = (): IStatus => { const statusFilepath = path.join(process.cwd(), 'status.json'); - const fileContent = readFileSync(statusFilepath).toString(); + const fileContent = fs.readFileSync(statusFilepath).toString(); return JSON.parse(fileContent); }; @@ -18,7 +20,7 @@ describe('Status manager', () => { it('Project has success status in status manager file', async () => { const projectId = 'project1'; const migrationName = 'migration1'; - await markAsCompleted(projectId, migrationName, 1); + await statusManager.markAsCompleted(projectId, migrationName, 1); const statusFile = readStatusFile(); const status = statusFile[projectId][0]; @@ -30,9 +32,9 @@ describe('Status manager', () => { const project1Id = 'project1'; const project2Id = 'project2'; const migration1Name = 'migration1'; - await markAsCompleted(project1Id, migration1Name, 1); + await statusManager.markAsCompleted(project1Id, migration1Name, 1); - const projectMigrationStatus = wasSuccessfullyExecuted(migration1Name, project2Id); + const projectMigrationStatus = statusManager.wasSuccessfullyExecuted(migration1Name, project2Id); expect(projectMigrationStatus).toBe(false); }); @@ -40,10 +42,25 @@ describe('Status manager', () => { it('Executed migration is present in status file', async () => { const project2Id = 'project2'; const migration2Name = 'migration2'; - await markAsCompleted(project2Id, migration2Name, 1); + await statusManager.markAsCompleted(project2Id, migration2Name, 1); - const projectMigrationStatus = wasSuccessfullyExecuted(project2Id, migration2Name); + const projectMigrationStatus = statusManager.wasSuccessfullyExecuted(project2Id, migration2Name); expect(projectMigrationStatus).toBe(false); }); + + it('loadMigrationsExecutionStatus to be called with plugins', async () => { + jest.spyOn(fileUtils, 'fileExists').mockReturnValue(true); + + const readStatusMocked = jest.fn().mockResolvedValue({}); + + jest.spyOn(statusPlugin, 'loadStatusPlugin').mockResolvedValue({ + readStatus: readStatusMocked, + saveStatus: jest.fn().mockImplementation(() => Promise.resolve()), + }); + + await statusManager.loadMigrationsExecutionStatus(); + + expect(readStatusMocked).toHaveBeenCalled(); + }); }); From 0b5ff7d34958cbf6f159491a445efa4c3fdc0e76 Mon Sep 17 00:00:00 2001 From: Ivan Kiral Date: Mon, 3 Apr 2023 09:06:40 +0200 Subject: [PATCH 08/14] change version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 777509b..33c5d27 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kontent-ai/cli", - "version": "0.7.0", + "version": "0.7.1", "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", From 2bdbb1e4d2195e4ba8e068eaa18558d0c9f72527 Mon Sep 17 00:00:00 2001 From: Ivan Kiral Date: Mon, 3 Apr 2023 11:28:36 +0200 Subject: [PATCH 09/14] Add test for saveStatus from plugin to be called --- src/tests/statusManager.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/tests/statusManager.test.ts b/src/tests/statusManager.test.ts index ede9428..11535cb 100644 --- a/src/tests/statusManager.test.ts +++ b/src/tests/statusManager.test.ts @@ -63,4 +63,19 @@ describe('Status manager', () => { expect(readStatusMocked).toHaveBeenCalled(); }); + + it('MarkAsCompleted to be called with plugins', async () => { + jest.spyOn(fileUtils, 'fileExists').mockReturnValue(true); + + const saveStatusMocked = jest.fn().mockImplementation(() => Promise.resolve()); + + jest.spyOn(statusPlugin, 'loadStatusPlugin').mockResolvedValue({ + readStatus: jest.fn().mockResolvedValue({}), + saveStatus: saveStatusMocked, + }); + + await statusManager.markAsCompleted('', 'testMigration', 1); + + expect(saveStatusMocked).toHaveBeenCalled(); + }); }); From 5b084147e64a7acb135e170c27f8947d651d57ab Mon Sep 17 00:00:00 2001 From: Ivan Kiral Date: Tue, 4 Apr 2023 10:44:50 +0200 Subject: [PATCH 10/14] Fix to not fallback when the plugin can not be load due to error. --- src/utils/statusManager.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/utils/statusManager.ts b/src/utils/statusManager.ts index 94d6b36..6460427 100644 --- a/src/utils/statusManager.ts +++ b/src/utils/statusManager.ts @@ -46,7 +46,6 @@ const saveStatusFile = async () => { await file.saveStatus(statusJSON); } catch (e) { console.log(`Could not load the plugin due to ${e}. Fallbacking to status.json`); - saveStatusToFile(statusJSON); } finally { return; } @@ -78,8 +77,7 @@ export const loadMigrationsExecutionStatus = async () => { const file = await loadStatusPlugin(getStatusImplementationFilePath().slice(0, -3) + '.js'); status = await file.readStatus(); } catch (e) { - console.log(`Could not load the plugin due to ${e}. Fallbacking to status.json`); - status = readFromStatus(); + console.log(`Could not load the plugin due to ${e}`); } finally { return; } From 25dd051c15c254391cc8e61063297b93fc6407f5 Mon Sep 17 00:00:00 2001 From: Ivan Kiral Date: Wed, 5 Apr 2023 10:34:38 +0200 Subject: [PATCH 11/14] Add description to readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 4c8a15c..7c95e53 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,9 @@ The supported commands are divided into groups according to their target, at thi * `backup --action [backup|restore|clean]` - This command enables you to use [Kontent.ai backup manager](https://github.com/kontent-ai/backup-manager-js) * The purpose of this tool is to backup & restore [Kontent.ai projects](https://kontent.ai/). This project uses CM API to both get & restore data. +* `status --implementation` - This command creates in your working directory a new directory with a file `statusImpl.ts`. Having this file created, CLI will take your custom implementation of saving/reading the migrations status. + > 1. Don't forget to transpile the `statusImpl.ts` into `statusImpl.js` into the Javascript as it won't work otherwise! + ### Debugging By default, we do not provide any additional logs from the HttpService. If you require these logs, you can change this behavior by using (option `--log-http-service-errors-to-console`). From 2527e54952f574c2fcd3aa212065c4f15365b8fe Mon Sep 17 00:00:00 2001 From: Ivan Kiral Date: Wed, 5 Apr 2023 10:35:15 +0200 Subject: [PATCH 12/14] Update to load plugin before the migrations run --- src/cmds/migration/run.ts | 11 +++++++---- src/tests/statusManager.test.ts | 21 +++++---------------- src/utils/migrationUtils.ts | 5 +++-- src/utils/status/statusPlugin.ts | 2 +- src/utils/statusManager.ts | 32 ++++++++++++++++---------------- 5 files changed, 32 insertions(+), 39 deletions(-) diff --git a/src/cmds/migration/run.ts b/src/cmds/migration/run.ts index a6e416f..9f78cc1 100644 --- a/src/cmds/migration/run.ts +++ b/src/cmds/migration/run.ts @@ -4,9 +4,10 @@ import { getDuplicates, getSuccessfullyExecutedMigrations, getMigrationFilepath, import { fileExists, getFileWithExtension, isAllowedExtension } from '../../utils/fileUtils'; import { environmentConfigExists, getEnvironmentsConfig } from '../../utils/environmentUtils'; import { createManagementClient } from '../../managementClientFactory'; -import { loadMigrationsExecutionStatus } from '../../utils/statusManager'; +import { getStatusImplementationFilePath, loadMigrationsExecutionStatus } from '../../utils/statusManager'; import { IMigration } from '../../models/migration'; import { IRange } from '../../models/range'; +import { loadStatusPlugin } from '../../utils/status/statusPlugin'; const runMigrationCommand: yargs.CommandModule = { command: 'run', @@ -124,13 +125,15 @@ const runMigrationCommand: yargs.CommandModule = { apiKey = environments[argv.environment].apiKey || argv.apiKey; } + const plugin = fileExists(getStatusImplementationFilePath()) ? await loadStatusPlugin(getStatusImplementationFilePath().slice(0, -3) + '.js') : undefined; + const apiClient = createManagementClient({ projectId, apiKey, logHttpServiceErrorsToConsole, }); - await loadMigrationsExecutionStatus(); + await loadMigrationsExecutionStatus(plugin?.readStatus ?? null); if (runAll || runRange) { let migrationsToRun = await loadMigrationFiles(); @@ -155,7 +158,7 @@ const runMigrationCommand: yargs.CommandModule = { const sortedMigrationsToRun = migrationsToRun.sort(orderComparator); let executedMigrationsCount = 0; for (const migration of sortedMigrationsToRun) { - const migrationResult = await runMigration(migration, apiClient, projectId); + const migrationResult = await runMigration(migration, apiClient, projectId, plugin?.saveStatus ?? null); if (migrationResult > 0) { if (!continueOnError) { @@ -176,7 +179,7 @@ const runMigrationCommand: yargs.CommandModule = { module: migrationModule, }; - migrationsResults = await runMigration(migration, apiClient, projectId); + migrationsResults = await runMigration(migration, apiClient, projectId, plugin?.saveStatus ?? null); } process.exit(migrationsResults); diff --git a/src/tests/statusManager.test.ts b/src/tests/statusManager.test.ts index 11535cb..d98ea80 100644 --- a/src/tests/statusManager.test.ts +++ b/src/tests/statusManager.test.ts @@ -1,5 +1,4 @@ import * as statusManager from '../utils/statusManager'; -import * as statusPlugin from '../utils/status/statusPlugin'; import * as fileUtils from '../utils/fileUtils'; import * as fs from 'fs'; import * as path from 'path'; @@ -20,7 +19,7 @@ describe('Status manager', () => { it('Project has success status in status manager file', async () => { const projectId = 'project1'; const migrationName = 'migration1'; - await statusManager.markAsCompleted(projectId, migrationName, 1); + await statusManager.markAsCompleted(projectId, migrationName, 1, null); const statusFile = readStatusFile(); const status = statusFile[projectId][0]; @@ -32,7 +31,7 @@ describe('Status manager', () => { const project1Id = 'project1'; const project2Id = 'project2'; const migration1Name = 'migration1'; - await statusManager.markAsCompleted(project1Id, migration1Name, 1); + await statusManager.markAsCompleted(project1Id, migration1Name, 1, null); const projectMigrationStatus = statusManager.wasSuccessfullyExecuted(migration1Name, project2Id); @@ -42,7 +41,7 @@ describe('Status manager', () => { it('Executed migration is present in status file', async () => { const project2Id = 'project2'; const migration2Name = 'migration2'; - await statusManager.markAsCompleted(project2Id, migration2Name, 1); + await statusManager.markAsCompleted(project2Id, migration2Name, 1, null); const projectMigrationStatus = statusManager.wasSuccessfullyExecuted(project2Id, migration2Name); @@ -54,12 +53,7 @@ describe('Status manager', () => { const readStatusMocked = jest.fn().mockResolvedValue({}); - jest.spyOn(statusPlugin, 'loadStatusPlugin').mockResolvedValue({ - readStatus: readStatusMocked, - saveStatus: jest.fn().mockImplementation(() => Promise.resolve()), - }); - - await statusManager.loadMigrationsExecutionStatus(); + await statusManager.loadMigrationsExecutionStatus(readStatusMocked); expect(readStatusMocked).toHaveBeenCalled(); }); @@ -69,12 +63,7 @@ describe('Status manager', () => { const saveStatusMocked = jest.fn().mockImplementation(() => Promise.resolve()); - jest.spyOn(statusPlugin, 'loadStatusPlugin').mockResolvedValue({ - readStatus: jest.fn().mockResolvedValue({}), - saveStatus: saveStatusMocked, - }); - - await statusManager.markAsCompleted('', 'testMigration', 1); + await statusManager.markAsCompleted('', 'testMigration', 1, saveStatusMocked); expect(saveStatusMocked).toHaveBeenCalled(); }); diff --git a/src/utils/migrationUtils.ts b/src/utils/migrationUtils.ts index a30cbeb..e7c49df 100644 --- a/src/utils/migrationUtils.ts +++ b/src/utils/migrationUtils.ts @@ -7,6 +7,7 @@ import { MigrationModule } from '../types'; import { IMigration } from '../models/migration'; import { markAsCompleted, wasSuccessfullyExecuted } from './statusManager'; import { formatDateForFileName } from './dateUtils'; +import { StatusPlugin } from './status/statusPlugin'; const listMigrationFiles = (fileExtension: string): Dirent[] => { return fs @@ -47,14 +48,14 @@ export const saveMigrationFile = (migrationName: string, migrationData: string, return migrationFilepath; }; -export const runMigration = async (migration: IMigration, client: ManagementClient, projectId: string): Promise => { +export const runMigration = async (migration: IMigration, client: ManagementClient, projectId: string, saveStatusFromPlugin: StatusPlugin['saveStatus'] | null): Promise => { console.log(`Running the ${migration.name} migration.`); let isSuccess = true; try { await migration.module.run(client).then(async () => { - await markAsCompleted(projectId, migration.name, migration.module.order); + await markAsCompleted(projectId, migration.name, migration.module.order, saveStatusFromPlugin); }); } catch (e) { console.error(chalk.redBright('An error occurred while running migration:'), chalk.yellowBright(migration.name), chalk.redBright('see the output from running the script.')); diff --git a/src/utils/status/statusPlugin.ts b/src/utils/status/statusPlugin.ts index 2ff0a47..42037cc 100644 --- a/src/utils/status/statusPlugin.ts +++ b/src/utils/status/statusPlugin.ts @@ -1,6 +1,6 @@ import { IStatus } from '../../models/status'; -interface StatusPlugin { +export interface StatusPlugin { saveStatus: (data: string) => Promise; readStatus: () => Promise; } diff --git a/src/utils/statusManager.ts b/src/utils/statusManager.ts index 6460427..2bdb53a 100644 --- a/src/utils/statusManager.ts +++ b/src/utils/statusManager.ts @@ -2,14 +2,14 @@ import { IMigrationStatus, IStatus } from '../models/status'; import { readFileSync, writeFileSync, mkdirSync } from 'fs'; import { fileExists } from './fileUtils'; import * as path from 'path'; -import { loadStatusPlugin } from './status/statusPlugin'; +import { type StatusPlugin } from './status/statusPlugin'; const migrationStatusFilename = 'status.json'; const statusDirectoryName = 'status'; const statusImplementationFilename = 'statusImpl.ts'; let status: IStatus = {}; -const updateMigrationStatus = async (projectId: string, migrationStatus: IMigrationStatus) => { +const updateMigrationStatus = async (projectId: string, migrationStatus: IMigrationStatus, saveStatusFromPlugin: StatusPlugin['saveStatus'] | null) => { let projectMigrationsHistory = status[projectId]; if (projectMigrationsHistory === undefined) { @@ -23,10 +23,10 @@ const updateMigrationStatus = async (projectId: string, migrationStatus: IMigrat projectMigrationsHistory.push(migrationStatus); } - await saveStatusFile(); + await saveStatusFile(saveStatusFromPlugin); }; -export const markAsCompleted = async (projectId: string, name: string, order: number | Date) => { +export const markAsCompleted = async (projectId: string, name: string, order: number | Date, saveStatusFromPlugin: StatusPlugin['saveStatus'] | null) => { const migrationStatus = { name, order, @@ -34,22 +34,22 @@ export const markAsCompleted = async (projectId: string, name: string, order: nu time: new Date(Date.now()), }; - await updateMigrationStatus(projectId, migrationStatus); + await updateMigrationStatus(projectId, migrationStatus, saveStatusFromPlugin); }; -const saveStatusFile = async () => { +const saveStatusFile = async (saveStatusFromPlugin: StatusPlugin['saveStatus'] | null) => { const statusJSON = JSON.stringify(status, null, 2); - if (fileExists(getStatusImplementationFilePath())) { + if (saveStatusFromPlugin) { try { - const file = await loadStatusPlugin(getStatusImplementationFilePath().slice(0, -3) + '.js'); - await file.saveStatus(statusJSON); + await saveStatusFromPlugin(statusJSON); } catch (e) { - console.log(`Could not load the plugin due to ${e}. Fallbacking to status.json`); + console.error(`The error ${e} occured when using saveStatus function from plugin.`); } finally { return; } } + saveStatusToFile(statusJSON); }; @@ -71,17 +71,17 @@ const getStatusFilepath = (): string => { return path.join(process.cwd(), migrationStatusFilename); }; -export const loadMigrationsExecutionStatus = async () => { - if (fileExists(getStatusImplementationFilePath())) { +export const loadMigrationsExecutionStatus = async (readStatusFromPlugin: StatusPlugin['readStatus'] | null) => { + if (readStatusFromPlugin) { try { - const file = await loadStatusPlugin(getStatusImplementationFilePath().slice(0, -3) + '.js'); - status = await file.readStatus(); + status = await readStatusFromPlugin(); } catch (e) { - console.log(`Could not load the plugin due to ${e}`); + console.error(`The error ${e} occured when using readStatus function from plugin.`); } finally { return; } } + status = readFromStatus(); }; @@ -112,7 +112,7 @@ const saveStatusToFile = (data: string): void => { } }; -const getStatusImplementationFilePath = () => path.join(process.cwd(), statusDirectoryName, statusImplementationFilename); +export const getStatusImplementationFilePath = () => path.join(process.cwd(), statusDirectoryName, statusImplementationFilename); const getStatusImplementationDirectoryPath = () => path.join(process.cwd(), statusDirectoryName); From 54691fbb76fb505f6e8062c9db27a4d9ba801b2d Mon Sep 17 00:00:00 2001 From: Ivan Kiral Date: Fri, 14 Apr 2023 09:13:54 +0200 Subject: [PATCH 13/14] Change the name of implementation to plugins and remove the creating plugins file command update lint --- src/cmds/migration/run.ts | 4 ++-- src/cmds/status.ts | 24 ------------------- src/index.ts | 2 -- src/types/index.ts | 4 ++++ src/utils/status/statusPlugin.ts | 6 ++--- src/utils/statusManager.ts | 40 +++----------------------------- 6 files changed, 12 insertions(+), 68 deletions(-) delete mode 100644 src/cmds/status.ts diff --git a/src/cmds/migration/run.ts b/src/cmds/migration/run.ts index 9f78cc1..289e1cc 100644 --- a/src/cmds/migration/run.ts +++ b/src/cmds/migration/run.ts @@ -4,7 +4,7 @@ import { getDuplicates, getSuccessfullyExecutedMigrations, getMigrationFilepath, import { fileExists, getFileWithExtension, isAllowedExtension } from '../../utils/fileUtils'; import { environmentConfigExists, getEnvironmentsConfig } from '../../utils/environmentUtils'; import { createManagementClient } from '../../managementClientFactory'; -import { getStatusImplementationFilePath, loadMigrationsExecutionStatus } from '../../utils/statusManager'; +import { getPluginsFilePath, loadMigrationsExecutionStatus } from '../../utils/statusManager'; import { IMigration } from '../../models/migration'; import { IRange } from '../../models/range'; import { loadStatusPlugin } from '../../utils/status/statusPlugin'; @@ -125,7 +125,7 @@ const runMigrationCommand: yargs.CommandModule = { apiKey = environments[argv.environment].apiKey || argv.apiKey; } - const plugin = fileExists(getStatusImplementationFilePath()) ? await loadStatusPlugin(getStatusImplementationFilePath().slice(0, -3) + '.js') : undefined; + const plugin = fileExists(getPluginsFilePath()) ? await loadStatusPlugin(getPluginsFilePath().slice(0, -3) + '.js') : undefined; const apiClient = createManagementClient({ projectId, diff --git a/src/cmds/status.ts b/src/cmds/status.ts deleted file mode 100644 index 83598a1..0000000 --- a/src/cmds/status.ts +++ /dev/null @@ -1,24 +0,0 @@ -import yargs from 'yargs'; -import { createStatusImplementationFile } from '../utils/statusManager'; - -const statusCommand: yargs.CommandModule = { - command: 'status', - describe: 'Status commands', - builder: (yargs: any) => - yargs - .options({ - implementation: { - alias: 'i', - describe: 'Implement your own save/read functions to deal with status.json.', - type: 'boolean', - }, - }) - .demandOption(['implementation']), - handler: (argv: any) => { - if (argv.implementation) { - createStatusImplementationFile(); - } - }, -}; - -Object.assign(exports, statusCommand); diff --git a/src/index.ts b/src/index.ts index 65b273d..1749f68 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,8 +23,6 @@ const createMigrationTool = (): number => { .example('kontent', 'backup --action restore --name backup_file --project-id --api-key ') .example('kontent', 'backup --action restore --name backup_file --environment --preserve-workflow false') .example('kontent', 'backup --action clean --project-id --api-key ') - - .example('kontent', 'status -i') .strict().argv; return 0; diff --git a/src/types/index.ts b/src/types/index.ts index 19c7d39..042c118 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,10 @@ import { ManagementClient } from '@kontent-ai/management-sdk'; +import { IStatus } from '../models/status'; export declare interface MigrationModule { readonly order: number | Date; run(apiClient: ManagementClient): Promise; } + +export type SaveStatusType = (data: string) => Promise; +export type ReadStatusType = () => Promise; diff --git a/src/utils/status/statusPlugin.ts b/src/utils/status/statusPlugin.ts index 42037cc..05cb148 100644 --- a/src/utils/status/statusPlugin.ts +++ b/src/utils/status/statusPlugin.ts @@ -1,8 +1,8 @@ -import { IStatus } from '../../models/status'; +import { ReadStatusType, SaveStatusType } from '../../types'; export interface StatusPlugin { - saveStatus: (data: string) => Promise; - readStatus: () => Promise; + saveStatus: SaveStatusType; + readStatus: ReadStatusType; } export const loadStatusPlugin = async (path: string): Promise => { diff --git a/src/utils/statusManager.ts b/src/utils/statusManager.ts index 2bdb53a..8096e9b 100644 --- a/src/utils/statusManager.ts +++ b/src/utils/statusManager.ts @@ -1,12 +1,11 @@ import { IMigrationStatus, IStatus } from '../models/status'; -import { readFileSync, writeFileSync, mkdirSync } from 'fs'; +import { readFileSync, writeFileSync } from 'fs'; import { fileExists } from './fileUtils'; import * as path from 'path'; import { type StatusPlugin } from './status/statusPlugin'; const migrationStatusFilename = 'status.json'; -const statusDirectoryName = 'status'; -const statusImplementationFilename = 'statusImpl.ts'; +const pluginsFilename = 'plugins.js'; let status: IStatus = {}; const updateMigrationStatus = async (projectId: string, migrationStatus: IMigrationStatus, saveStatusFromPlugin: StatusPlugin['saveStatus'] | null) => { @@ -112,37 +111,4 @@ const saveStatusToFile = (data: string): void => { } }; -export const getStatusImplementationFilePath = () => path.join(process.cwd(), statusDirectoryName, statusImplementationFilename); - -const getStatusImplementationDirectoryPath = () => path.join(process.cwd(), statusDirectoryName); - -export const createStatusImplementationFile = () => { - const statusImplementationPath = getStatusImplementationFilePath(); - - if (fileExists(statusImplementationPath)) { - console.error(`File ${statusImplementationPath} already exists`); - } - - try { - if (!fileExists(getStatusImplementationDirectoryPath())) { - mkdirSync(getStatusImplementationDirectoryPath()); - } - - writeFileSync(statusImplementationPath, statusImplementationTemplate, { flag: 'w' }); - console.log(`Status.ts file was created see ${statusImplementationPath}`); - } catch (error) { - console.error(`Status.ts file creation failed, because of ${error instanceof Error ? error.message : 'unknown error.'}`); - } -}; - -const statusImplementationTemplate = ` -import type {IStatus} from "@kontent-ai/cli"; - -export const saveStatus = async (data: string) => { - -} - -export const readStatus = async (): IStatus => { - -} -`; +export const getPluginsFilePath = () => path.join(process.cwd(), pluginsFilename); From a99b07b1d0ac227d50a265cb6ee0a491cd42e872 Mon Sep 17 00:00:00 2001 From: Ivan Kiral Date: Fri, 14 Apr 2023 09:14:06 +0200 Subject: [PATCH 14/14] Update readme to show the changes --- README.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7c95e53..e0a35a6 100644 --- a/README.md +++ b/README.md @@ -126,8 +126,30 @@ The supported commands are divided into groups according to their target, at thi * `backup --action [backup|restore|clean]` - This command enables you to use [Kontent.ai backup manager](https://github.com/kontent-ai/backup-manager-js) * The purpose of this tool is to backup & restore [Kontent.ai projects](https://kontent.ai/). This project uses CM API to both get & restore data. -* `status --implementation` - This command creates in your working directory a new directory with a file `statusImpl.ts`. Having this file created, CLI will take your custom implementation of saving/reading the migrations status. - > 1. Don't forget to transpile the `statusImpl.ts` into `statusImpl.js` into the Javascript as it won't work otherwise! +### Custom implementation of reading/saving status of migrations + +You might want to implement your way to store information about migrations status. For instance, you would like to save it into DB such as MongoDB, Firebase, etc,... and not use the default JSON file. Therefore, we provide you with an option to implement functions `readStatus` and `saveStatus`. To do so, create a new file called `plugins.js` at the root of your migrations project, and implement mentioned functions there. To fit into the required declarations, you can use the template below: + +```js +//plugins.js +exports.saveStatus = async (data) => {} + +exports.readStatus = async () => {} +``` +> Note: Both functions must be implemented. + +It is also possible to use Typescript. We have prepared types `SaveStatusType` and `ReadStatusType` to typesafe your functions. To create plugins in Typescript, create a file `plugins.ts` and implement your functions there. We suggest using and implementing the template below: + +```ts +//plugins.ts +import type { ReadStatusType, SaveStatusType } from "@kontent-ai/cli"; + +export const saveStatus: SaveStatusType = async (data: string) => {} + +export const readStatus: ReadStatusType = async () => {} +``` + +> Note: Don't forget to transpile `plugins.ts` into `plugins.js` otherwise your plugins will not work. ### Debugging