Skip to content
This repository has been archived by the owner on Feb 27, 2024. It is now read-only.

Commit

Permalink
feat!: Adds support for backing up & restoring workflows as well as k…
Browse files Browse the repository at this point in the history
…eeping the workflow status of imported language variants. Because of this the now deprecated "workflowSteps" are removed in favor of dedicated "workflows". Adds new configuration option 'preserveWorkflow'.
  • Loading branch information
Enngage committed Jul 13, 2022
1 parent 2652531 commit 2465f64
Show file tree
Hide file tree
Showing 16 changed files with 355 additions and 146 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Install package globally:
| skipValidation | Skips validation endpoint during project export
| force | If enabled, project will we exported / restored even if there are data inconsistencies. Enabled by default. |
| baseUrl | Custom base URL for Management API calls. |
| enablePublish | Indicates if language variants published on the source project are also published on target. Enabled by default |
| preserveWorkflow | Indicates language variant workflow information should be preserved |
| exportFilter | Can be used to export only selected data types. Expects CSV of types. For example `contentType,language` will cause backup manager to export only content types & language data. List of data types can be found below. |

### Data types
Expand All @@ -39,7 +39,7 @@ Install package globally:
* language
* assetFolder
* binaryFile
* workflowSteps (only export)
* workflows

### Execution

Expand Down Expand Up @@ -165,7 +165,7 @@ const run = async () => {
languageVariant: item => true, // all language variants will be imported
taxonomy: item => true,// all taxonomies will be imported
},
enablePublish: true, // when enables, previously published language variants will be published after restore (does not affect unpublished variants)
preserveWorkflow: true, // when enabled, language variants will preserve their workflow information
projectId: 'targetProjectId',
apiKey: 'targetProjectId',
enableLog: true, // shows progress of immport in console
Expand Down
72 changes: 46 additions & 26 deletions lib/clean/clean.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AssetFolderModels, ManagementClient } from '@kentico/kontent-management';
import { HttpService } from '@kentico/kontent-core';

import { ItemType } from '../core';
import { defaultWorkflowCodename, handleError, ItemType } from '../core';
import { ICleanConfig, ICleanResult } from './clean.models';

export class CleanService {
Expand All @@ -10,7 +11,10 @@ export class CleanService {
this.client = new ManagementClient({
apiKey: config.apiKey,
projectId: config.projectId,
baseUrl: config.baseUrl
baseUrl: config.baseUrl,
httpService: new HttpService({
logErrorsToConsole: false
})
});
}

Expand All @@ -22,6 +26,7 @@ export class CleanService {
await this.cleanTaxonomiesAsync();
await this.cleanAssetsAsync();
await this.cleanAssetFoldersAsync();
await this.cleanWorkflowsAsync();

return {
metadata: {
Expand All @@ -30,11 +35,30 @@ export class CleanService {
}
};
} catch (err) {
console.log(err);
throw err;
}
}

public async cleanWorkflowsAsync(): Promise<void> {
const workflows = (await this.client.listWorkflows().toPromise()).data;

for (const workflow of workflows) {
// default workflow cannot be deleted
if (workflow.codename.toLowerCase() === defaultWorkflowCodename.toLowerCase()) {
continue;
}

await this.client
.deleteWorkflow()
.byWorkflowId(workflow.id)
.toPromise()
.then((response) => {
this.processItem(workflow.name, 'workflow', workflow);
})
.catch((error) => this.handleCleanError(error));
}
}

public async cleanTaxonomiesAsync(): Promise<void> {
const taxonomies = (await this.client.listTaxonomies().toPromise()).data.items;

Expand All @@ -43,10 +67,10 @@ export class CleanService {
.deleteTaxonomy()
.byTaxonomyId(taxonomy.id)
.toPromise()
.then(response => {
.then((response) => {
this.processItem(taxonomy.name, 'taxonomy', taxonomy);
})
.catch(error => this.handleCleanError(error));
.catch((error) => this.handleCleanError(error));
}
}

Expand All @@ -58,10 +82,10 @@ export class CleanService {
.deleteContentTypeSnippet()
.byTypeId(contentTypeSnippet.id)
.toPromise()
.then(response => {
.then((response) => {
this.processItem(contentTypeSnippet.name, 'contentTypeSnippet', contentTypeSnippet);
})
.catch(error => this.handleCleanError(error));
.catch((error) => this.handleCleanError(error));
}
}

Expand All @@ -73,10 +97,10 @@ export class CleanService {
.deleteContentType()
.byTypeId(contentType.id)
.toPromise()
.then(response => {
.then((response) => {
this.processItem(contentType.name, 'contentType', contentType);
})
.catch(error => this.handleCleanError(error));
.catch((error) => this.handleCleanError(error));
}
}

Expand All @@ -88,10 +112,10 @@ export class CleanService {
.deleteAsset()
.byAssetId(asset.id)
.toPromise()
.then(m => {
.then((m) => {
this.processItem(asset.fileName, 'asset', asset);
})
.catch(error => this.handleCleanError(error));
.catch((error) => this.handleCleanError(error));
}
}

Expand All @@ -102,8 +126,8 @@ export class CleanService {
await this.client
.modifyAssetFolders()
.withData(
assetFolders.map(m => {
return <AssetFolderModels.IModifyAssetFoldersData> {
assetFolders.map((m) => {
return <AssetFolderModels.IModifyAssetFoldersData>{
op: 'remove',
reference: {
id: m.id
Expand All @@ -112,12 +136,12 @@ export class CleanService {
})
)
.toPromise()
.then(response => {
.then((response) => {
for (const folder of assetFolders) {
this.processItem(folder.name, 'assetFolder', folder);
}
})
.catch(error => this.handleCleanError(error));
.catch((error) => this.handleCleanError(error));
}
}

Expand All @@ -129,20 +153,16 @@ export class CleanService {
.deleteContentItem()
.byItemId(contentItem.id)
.toPromise()
.then(response => {
.then((response) => {
this.processItem(contentItem.name, 'contentItem', contentItem);
})
.catch(error => this.handleCleanError(error));
.catch((error) => this.handleCleanError(error));
}
}

public async cleanLanguageVariantsAsync(contentItemId: string): Promise<void> {
const languageVariants = (
await this.client
.listLanguageVariantsOfItem()
.byItemId(contentItemId)
.toPromise()
).data.items;
const languageVariants = (await this.client.listLanguageVariantsOfItem().byItemId(contentItemId).toPromise())
.data.items;

for (const languageVariant of languageVariants) {
const languageId = languageVariant.language.id;
Expand All @@ -157,15 +177,15 @@ export class CleanService {
.byItemId(itemId)
.byLanguageId(languageId)
.toPromise()
.then(response => {
.then((response) => {
this.processItem(itemId, 'languageVariant', languageVariant);
})
.catch(error => this.handleCleanError(error));
.catch((error) => this.handleCleanError(error));
}
}

private handleCleanError(error: any): void {
console.log(error);
handleError(error);
}

private processItem(title: string, type: ItemType, data: any): void {
Expand Down
3 changes: 3 additions & 0 deletions lib/core/core-properties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const defaultWorkflowCodename: string = 'Default';
export const defaultObjectId: string = '00000000-0000-0000-0000-000000000000';
export const codenameOfReservedDefaultWorkflowSteps: string[] = ['Draft', 'Published', 'Archived'];
12 changes: 8 additions & 4 deletions lib/core/core.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import {
LanguageVariantContracts,
ContentItemContracts,
AssetFolderModels,
AssetFolderContracts
AssetFolderContracts,
WorkflowContracts,
WorkflowModels
} from '@kentico/kontent-management';

export interface ICliFileConfig {
Expand All @@ -23,7 +25,7 @@ export interface ICliFileConfig {
action: CliAction;
zipFilename: string;
enableLog: boolean;
enablePublish: boolean;
preserveWorkflow: boolean;
force: boolean;
baseUrl?: string;
exportFilter?: ItemType[];
Expand All @@ -40,9 +42,9 @@ export type ItemType =
| 'language'
| 'asset'
| 'assetFolder'
| 'workflowStep'
| 'collection'
| 'webhook'
| 'workflow'
| 'binaryFile';

export type ActionType = ItemType | 'publish' | 'changeWorkflowStep';
Expand All @@ -55,6 +57,7 @@ export type ValidImportModel =
| ContentItemModels.ContentItem
| LanguageModels.LanguageModel
| AssetModels.Asset
| WorkflowModels.Workflow
| AssetFolderModels.AssetFolder;

export type ValidImportContract =
Expand All @@ -66,6 +69,7 @@ export type ValidImportContract =
| AssetContracts.IAssetModelContract
| LanguageVariantContracts.ILanguageVariantModelContract
| LanguageContracts.ILanguageModelContract
| WorkflowContracts.IWorkflowContract
| AssetFolderContracts.IAssetFolderContract;

export interface IProcessedItem {
Expand Down Expand Up @@ -103,7 +107,7 @@ export interface IPackageDataOverview {
languagesCount: number;
assetsCount: number;
assetFoldersCount: number;
workflowStepsCount: number;
workflowsCount: number;
webhooksCount: number;
collectionsCount: number;
}
24 changes: 18 additions & 6 deletions lib/core/global-helper.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import { SharedModels } from '@kentico/kontent-management';

export function getFilenameWithoutExtension(filename: string): string {
if (!filename) {
throw Error(`Invalid filename`);
}

if (!filename.includes('.')) {
return filename
};
return filename;
}

return filename
.split('.')
.slice(0, -1)
.join('.');
return filename.split('.').slice(0, -1).join('.');
}

export function handleError(error: any | SharedModels.ContentManagementBaseKontentError): void {
let result = error;
if (error instanceof SharedModels.ContentManagementBaseKontentError) {
result = {
Message: `Failed to import data with error: ${error.message}`,
ErrorCode: error.errorCode,
RequestId: error.requestId,
ValidationErrors: `${error.validationErrors.map((m) => m.message).join(', ')}`
};
}
throw result;
}
3 changes: 2 additions & 1 deletion lib/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './core.models';
export * from './translation-helper';
export * from './id-translate-helper';
export * from './global-helper';
export * from './global-helper';
export * from './core-properties';
17 changes: 12 additions & 5 deletions lib/core/translation-helper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { defaultObjectId } from './core-properties';
import { IIdCodenameTranslationResult } from './core.models';

export class TranslationHelper {
Expand Down Expand Up @@ -30,12 +31,13 @@ export class TranslationHelper {
public replaceIdReferencesWithCodenames(
data: any,
allData: any,
storedCodenames: IIdCodenameTranslationResult
storedCodenames: IIdCodenameTranslationResult,
codenameForDefaultId?: string
): void {
if (data) {
if (Array.isArray(data)) {
for (const arrayItem of data) {
this.replaceIdReferencesWithCodenames(arrayItem, allData, storedCodenames);
this.replaceIdReferencesWithCodenames(arrayItem, allData, storedCodenames, codenameForDefaultId);
}
} else {
for (const key of Object.keys(data)) {
Expand All @@ -45,9 +47,14 @@ export class TranslationHelper {
const codename = (data as any).codename;

if (!codename) {
// replace id with codename
const foundCodename = this.tryFindCodenameForId(id, allData, storedCodenames);
let foundCodename: string | undefined;
if (id.toLowerCase() === defaultObjectId.toLowerCase() && codenameForDefaultId) {
foundCodename = codenameForDefaultId;
} else {
foundCodename = this.tryFindCodenameForId(id, allData, storedCodenames);
}

// replace id with codename
if (foundCodename) {
// remove id prop
delete data.id;
Expand All @@ -59,7 +66,7 @@ export class TranslationHelper {
}

if (typeof val === 'object' && val !== null) {
this.replaceIdReferencesWithCodenames(val, allData, storedCodenames);
this.replaceIdReferencesWithCodenames(val, allData, storedCodenames, codenameForDefaultId);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/export/export.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface IExportConfig {
}

export interface IExportData {
workflowSteps: WorkflowContracts.IWorkflowStepContract[];
workflows: WorkflowContracts.IWorkflowContract[];
taxonomies: TaxonomyContracts.ITaxonomyContract[];
contentTypeSnippets: ContentTypeSnippetContracts.IContentTypeSnippetContract[];
contentTypes: ContentTypeContracts.IContentTypeContract[];
Expand Down
Loading

0 comments on commit 2465f64

Please sign in to comment.