Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CS-43976 - added audit for workflow #1306

Merged
merged 19 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading