Skip to content

Commit

Permalink
Merge pull request #1306 from contentstack/feat/CS-43976
Browse files Browse the repository at this point in the history
CS-43976 - added audit for workflow
  • Loading branch information
cs-raj authored Mar 8, 2024
2 parents 2b835fc + 69a1c5d commit a5f8e47
Show file tree
Hide file tree
Showing 21 changed files with 12,818 additions and 10,956 deletions.
23,212 changes: 12,291 additions & 10,921 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/contentstack-audit/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@contentstack/cli-audit",
"version": "1.4.1",
"version": "1.5.0",
"description": "Contentstack audit plugin",
"author": "Contentstack CLI",
"homepage": "https://github.com/contentstack/cli",
Expand Down
33 changes: 21 additions & 12 deletions packages/contentstack-audit/src/audit-base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import config from './config';
import { print } from './util/log';
import { auditMsg } from './messages';
import { BaseCommand } from './base-command';
import { Entries, GlobalField, ContentType, Extensions } from './modules';
import { Entries, GlobalField, ContentType, Extensions, Workflows } from './modules';
import { CommandNames, ContentTypeStruct, OutputColumn, RefErrorReturnType } from './types';

export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseCommand> {
Expand All @@ -31,7 +31,7 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
}

/**
* The `start` function performs an audit on content types, global fields, and entries, and displays
* The `start` function performs an audit on content types, global fields, entries, and workflows and displays
* any missing references.
* @param {string} command - The `command` parameter is a string that represents the current command
* being executed.
Expand All @@ -42,19 +42,21 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
await this.createBackUp();
this.sharedConfig.reportPath = resolve(this.flags['report-path'] || process.cwd(), 'audit-report');

const { missingCtRefs, missingGfRefs, missingEntryRefs, missingCtRefsInExtensions } = await this.scanAndFix();
const { missingCtRefs, missingGfRefs, missingEntryRefs, missingCtRefsInExtensions, missingCtRefsInWorkflow } =
await this.scanAndFix();

this.showOutputOnScreen([
{ module: 'Content types', missingRefs: missingCtRefs },
{ module: 'Global Fields', missingRefs: missingGfRefs },
{ module: 'Entries', missingRefs: missingEntryRefs },
]);
this.showOutputOnScreenWorkflowsAndExtension([{ module: 'Extensions', missingRefs: missingCtRefsInExtensions }]);

this.showOutputOnScreenWorkflowsAndExtension([{ module: 'Workflows', missingRefs: missingCtRefsInWorkflow }]);
if (
!isEmpty(missingCtRefs) ||
!isEmpty(missingGfRefs) ||
!isEmpty(missingEntryRefs) ||
!isEmpty(missingCtRefsInWorkflow) ||
!isEmpty(missingCtRefsInExtensions)
) {
if (this.currentCommand === 'cm:stacks:audit') {
Expand All @@ -80,6 +82,7 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
!isEmpty(missingCtRefs) ||
!isEmpty(missingGfRefs) ||
!isEmpty(missingEntryRefs) ||
!isEmpty(missingCtRefsInWorkflow) ||
!isEmpty(missingCtRefsInExtensions)
);
}
Expand All @@ -92,7 +95,7 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
*/
async scanAndFix() {
let { ctSchema, gfSchema } = this.getCtAndGfSchema();
let missingCtRefs, missingGfRefs, missingEntryRefs, missingCtRefsInExtensions;
let missingCtRefs, missingGfRefs, missingEntryRefs, missingCtRefsInExtensions, missingCtRefsInWorkflow;
for (const module of this.sharedConfig.flags.modules || this.sharedConfig.modules) {
print([
{
Expand Down Expand Up @@ -124,6 +127,16 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
missingEntryRefs = await new Entries(cloneDeep(constructorParam)).run();
await this.prepareReport(module, missingEntryRefs);
break;
case 'workflows':
missingCtRefsInWorkflow = await new Workflows({
ctSchema,
log: this.log,
moduleName: module,
config: this.sharedConfig,
fix: this.currentCommand === 'cm:stacks:audit:fix',
}).run();
await this.prepareReport(module, missingCtRefsInWorkflow);
break;
case 'extensions':
missingCtRefsInExtensions = await new Extensions(cloneDeep(constructorParam)).run();
await this.prepareReport(module, missingCtRefsInExtensions);
Expand All @@ -144,7 +157,7 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
]);
}

return { missingCtRefs, missingGfRefs, missingEntryRefs, missingCtRefsInExtensions };
return { missingCtRefs, missingGfRefs, missingEntryRefs, missingCtRefsInExtensions, missingCtRefsInWorkflow };
}

/**
Expand Down Expand Up @@ -232,7 +245,6 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
},
]);
const tableValues = Object.values(missingRefs).flat();

ux.table(
tableValues,
{
Expand Down Expand Up @@ -278,7 +290,6 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
if (!this.sharedConfig.showTerminalOutput || this.flags['external-config']?.noTerminalOutput) {
return;
}

this.log(''); // Adding a new line

for (const { module, missingRefs } of allMissingRefs) {
Expand All @@ -298,9 +309,9 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
minWidth: 7,
header: key,
get: (row: Record<string, unknown>) => {
if(key==='fixStatus') {
if (key === 'fixStatus') {
return chalk.green(typeof row[key] === 'object' ? JSON.stringify(row[key]) : row[key]);
} else if(key==='content_types') {
} else if (key === 'content_types') {
return chalk.red(typeof row[key] === 'object' ? JSON.stringify(row[key]) : row[key]);
} else {
return chalk.white(typeof row[key] === 'object' ? JSON.stringify(row[key]) : row[key]);
Expand All @@ -311,7 +322,6 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
}
return {};
});

const mergedObject = Object.assign({}, ...arrayOfObjects);

ux.table(tableValues, mergedObject, { ...this.flags });
Expand Down Expand Up @@ -388,7 +398,6 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma

rowData.push(row);
}

csv.write(rowData, { headers: true }).pipe(ws).on('error', reject).on('finish', resolve);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default class Audit extends AuditBaseCommand {

/**
* The `run` function is an asynchronous function that performs an audit on different modules
* (content-types, global-fields, entries) and generates a report.
* (content-types, global-fields, entries, workflows) and generates a report.
*/
async run(): Promise<void> {
try {
Expand Down
9 changes: 7 additions & 2 deletions packages/contentstack-audit/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ const config = {
showTerminalOutput: true,
skipRefs: ['sys_assets'],
skipFieldTypes: ['taxonomy', 'group'],
modules: ['content-types', 'global-fields', 'entries', 'extensions'],
'fix-fields': ['reference', 'global_field', 'json:rte', 'json:extension', 'blocks', 'group'],
modules: ['content-types', 'global-fields', 'entries', 'extensions', 'workflows'],
'fix-fields': ['reference', 'global_field', 'json:rte', 'json:extension', 'blocks', 'group', 'content_types'],
moduleConfig: {
'content-types': {
name: 'content type',
Expand All @@ -25,6 +25,11 @@ const config = {
dirName: 'locales',
fileName: 'locales.json',
},
workflows: {
name: 'workflows',
dirName: 'workflows',
fileName: 'workflows.json',
},
extensions: {
name: 'extensions',
dirName: 'extensions',
Expand Down
4 changes: 4 additions & 0 deletions packages/contentstack-audit/src/messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const commonMsg = {
CONFIG: 'Path of the external config',
DATA_DIR: 'Path where the data is stored',
FIX_CONFIRMATION: 'Would you like to overwrite existing file.?',
WORKFLOW_FIX_WARN: `The workflow associated with UID {uid} and name {name} will be removed.`,
WORKFLOW_FIX_CONFIRMATION: 'Would you like to overwrite existing file?',
EXTENSION_FIX_WARN: `The extension associated with UID {uid} and title '{title}' will be removed.`,
EXTENSION_FIX_CONFIRMATION: `Would you like to overwrite existing file?`,
};
Expand All @@ -32,6 +34,7 @@ const auditMsg = {
SCAN_ENTRY_SUCCESS_MSG: "Successfully completed the scanning of {module} ({local}) '{title}'.",
SCAN_EXT_SUCCESS_MSG: "Successfully completed scanning the {module} titled '{title}' with UID '{uid}'",
AUDIT_CMD_DESCRIPTION: 'Perform audits and find possible errors in the exported Contentstack data',
SCAN_WF_SUCCESS_MSG: 'Successfully removed the workflow with UID {uid} and name {name}.',
};

const auditFixMsg = {
Expand All @@ -41,6 +44,7 @@ const auditFixMsg = {
FIXED_CONTENT_PATH_MAG: 'You can locate the fixed content at {path}.',
EMPTY_FIX_MSG: 'Successfully removed the empty field/block found at {path} from the schema.',
AUDIT_FIX_CMD_DESCRIPTION: 'Perform audits and fix possible errors in the exported Contentstack data.',
WF_FIX_MSG: 'Successfully removed the workflow {uid} named {name}.',
};

const messages: typeof errors &
Expand Down
2 changes: 1 addition & 1 deletion packages/contentstack-audit/src/modules/content-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ export default class ContentType {
treeStr: tree.map(({ name }) => name).join(' ➜ '),
});

return null
return null;
}

return field;
Expand Down
3 changes: 2 additions & 1 deletion packages/contentstack-audit/src/modules/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Entries from './entries';
import GlobalField from './global-fields';
import ContentType from './content-types';
import Workflows from './workflows';
import Extensions from './extensions';
export { Entries, GlobalField, ContentType, Extensions };
export { Entries, GlobalField, ContentType, Workflows, Extensions };
127 changes: 127 additions & 0 deletions packages/contentstack-audit/src/modules/workflows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { join, resolve } from 'path';
import { existsSync, readFileSync, writeFileSync } from 'fs';
import { cloneDeep } from 'lodash';
import { LogFn, ConfigType, ContentTypeStruct, CtConstructorParam, ModuleConstructorParam, Workflow } from '../types';
import { ux } from '@contentstack/cli-utilities';

import auditConfig from '../config';
import { $t, auditMsg, commonMsg } from '../messages';
import { values } from 'lodash';

export default class Workflows {
public log: LogFn;
protected fix: boolean;
public fileName: any;
public config: ConfigType;
public folderPath: string;
public workflowSchema: Workflow[];
public ctSchema: ContentTypeStruct[];
public moduleName: keyof typeof auditConfig.moduleConfig;
public ctUidSet: Set<string>;
public missingCtInWorkflows: Workflow[];
public missingCts: Set<string>;
public workflowPath: string;

constructor({
log,
fix,
config,
moduleName,
ctSchema,
}: ModuleConstructorParam & Pick<CtConstructorParam, 'ctSchema'>) {
this.log = log;
this.config = config;
this.fix = fix ?? false;
this.ctSchema = ctSchema;
this.workflowSchema = [];
this.moduleName = moduleName ?? 'workflows';
this.fileName = config.moduleConfig[this.moduleName].fileName;
this.folderPath = resolve(config.basePath, config.moduleConfig[this.moduleName].dirName);
this.ctUidSet = new Set(['$all']);
this.missingCtInWorkflows = [];
this.missingCts = new Set();
this.workflowPath = '';
}
/**
* Check whether the given path for the workflow exists or not
* If path exist read
* From the ctSchema add all the content type UID into ctUidSet to check whether the content-type is present or not
* @returns Array of object containing the workflow name, uid and content_types that are missing
*/
async run() {
if (!existsSync(this.folderPath)) {
this.log(`Skipping ${this.moduleName} audit`, 'warn');
this.log($t(auditMsg.NOT_VALID_PATH, { path: this.folderPath }), { color: 'yellow' });
return {};
}

this.workflowPath = join(this.folderPath, this.fileName);
this.workflowSchema = existsSync(this.workflowPath) ? values(JSON.parse(readFileSync(this.workflowPath, 'utf8')) as Workflow[]) : [];

this.ctSchema.forEach((ct) => this.ctUidSet.add(ct.uid));

for (const workflow of this.workflowSchema) {
const ctNotPresent = workflow.content_types.filter((ct) => !this.ctUidSet.has(ct));
if (ctNotPresent.length) {
const tempwf = cloneDeep(workflow);
tempwf.content_types = ctNotPresent;
ctNotPresent.forEach((ct) => this.missingCts.add(ct));
this.missingCtInWorkflows.push(tempwf);
}

this.log(
$t(auditMsg.SCAN_WF_SUCCESS_MSG, {
name: workflow.name,
module: this.config.moduleConfig[this.moduleName].name,
}),
'info'
);
}

if (this.fix && this.missingCtInWorkflows.length) {
await this.fixWorkflowSchema();
this.missingCtInWorkflows.forEach((wf) => (wf.fixStatus = 'Fixed'));
}

return this.missingCtInWorkflows;
}

async fixWorkflowSchema() {
const newWorkflowSchema: Record<string, Workflow> = existsSync(this.workflowPath)
? JSON.parse(readFileSync(this.workflowPath, 'utf8'))
: {};

if (Object.keys(newWorkflowSchema).length !== 0) {
for (const workflow of this.workflowSchema) {
const fixedCts = workflow.content_types.filter((ct) => !this.missingCts.has(ct));
if (fixedCts.length) {
newWorkflowSchema[workflow.uid].content_types = fixedCts;
} else {
const { name, uid } = workflow;
const warningMessage = $t(commonMsg.WORKFLOW_FIX_WARN, { name, uid });

this.log(warningMessage, { color: 'yellow' });

if (this.config.flags.yes || (await ux.confirm(commonMsg.WORKFLOW_FIX_CONFIRMATION))) {
delete newWorkflowSchema[workflow.uid];
}
}
}
}

await this.writeFixContent(newWorkflowSchema);
}

async writeFixContent(newWorkflowSchema: Record<string, Workflow>) {
if (
this.fix ||
!(this.config.flags['copy-dir'] || this.config.flags['external-config']?.skipConfirm) &&
(this.config.flags.yes || (await ux.confirm(commonMsg.FIX_CONFIRMATION)))
) {
writeFileSync(
join(this.folderPath, this.config.moduleConfig[this.moduleName].fileName),
JSON.stringify(newWorkflowSchema),
);
}
}
}
3 changes: 2 additions & 1 deletion packages/contentstack-audit/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from './utils';
export * from './common';
export * from './entries';
export * from './content-types';
export * from './extensions';
export * from './workflow';
export * from './extensions';
13 changes: 13 additions & 0 deletions packages/contentstack-audit/src/types/workflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface Workflow {
uid: string;
name: string;
content_types: string[];
org_uid?: string;
api_key?: string;
workflow_stages?: Record<string, unknown>;
admin_users?: any;
enabled?: boolean;
deleted_at?: any;
missingRefs?: any;
fixStatus?:string
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { expect } from '@oclif/test';
import { ux, cliux } from '@contentstack/cli-utilities';

import { AuditBaseCommand } from '../../src/audit-base-command';
import { ContentType, Entries, GlobalField, Extensions } from '../../src/modules';
import { ContentType, Entries, GlobalField, Extensions, Workflows } from '../../src/modules';
import { FileTransportInstance } from 'winston/lib/winston/transports';
import { $t, auditMsg } from '../../src/messages';

Expand Down Expand Up @@ -63,6 +63,7 @@ describe('AuditBaseCommand class', () => {
.stub(Entries.prototype, 'run', () => ({ entry_1: {} }))
.stub(ContentType.prototype, 'run', () => ({ ct_1: {} }))
.stub(GlobalField.prototype, 'run', () => ({ gf_1: {} }))
.stub(Workflows.prototype, 'run', () => ({ wf_1: {} }))
.stub(Extensions.prototype, 'run', () => ({ ext_1: {} }))
.stub(fs, 'createWriteStream', () => new PassThrough())
.it('should print info of no ref found', async (ctx) => {
Expand All @@ -78,6 +79,7 @@ describe('AuditBaseCommand class', () => {
.stub(winston, 'createLogger', () => ({ log: console.log, error: console.error }))
.stub(fs, 'mkdirSync', () => {})
.stub(fs, 'writeFileSync', () => {})
.stub(AuditBaseCommand.prototype, 'showOutputOnScreenWorkflowsAndExtension', () => {})
.stub(ux, 'table', (...args: any) => {
args[1].missingRefs.get({ missingRefs: ['gf_0'] });
})
Expand All @@ -95,6 +97,7 @@ describe('AuditBaseCommand class', () => {
}))
.stub(ContentType.prototype, 'run', () => ({ ct_1: {} }))
.stub(GlobalField.prototype, 'run', () => ({ gf_1: {} }))
.stub(Workflows.prototype, 'run', () => ({ wf_1: {} }))
.stub(Extensions.prototype, 'run', () => ({ ext_1: {} }))
.stub(fs, 'createBackUp', () => {})
.stub(fs, 'createWriteStream', () => new PassThrough())
Expand Down
Loading

0 comments on commit a5f8e47

Please sign in to comment.