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 5 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
89 changes: 79 additions & 10 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 } from './modules';
import { Entries, GlobalField, ContentType, 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,15 +42,22 @@ 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 } = await this.scanAndFix();
const { missingCtRefs, missingGfRefs, missingEntryRefs, missingCtRefsInWorkflow } = await this.scanAndFix();

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

if (!isEmpty(missingCtRefs) || !isEmpty(missingGfRefs) || !isEmpty(missingEntryRefs)) {
this.showOutputOnScreenWorkflowsAndExtension([{ module: 'Workflows', missingRefs: missingCtRefsInWorkflow }]);

if (
!isEmpty(missingCtRefs) ||
!isEmpty(missingGfRefs) ||
!isEmpty(missingEntryRefs) ||
isEmpty(missingCtRefsInWorkflow)
) {
if (this.currentCommand === 'cm:stacks:audit') {
this.log(this.$t(auditMsg.FINAL_REPORT_PATH, { path: this.sharedConfig.reportPath }), 'warn');
} else {
Expand All @@ -70,7 +77,12 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
}
}

return !isEmpty(missingCtRefs) || !isEmpty(missingGfRefs) || !isEmpty(missingEntryRefs);
return (
!isEmpty(missingCtRefs) ||
!isEmpty(missingGfRefs) ||
!isEmpty(missingEntryRefs) ||
!isEmpty(missingCtRefsInWorkflow)
);
}

/**
Expand All @@ -81,7 +93,7 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
*/
async scanAndFix() {
let { ctSchema, gfSchema } = this.getCtAndGfSchema();
let missingCtRefs, missingGfRefs, missingEntryRefs;
let missingCtRefs, missingGfRefs, missingEntryRefs, missingCtRefsInWorkflow;
for (const module of this.sharedConfig.flags.modules || this.sharedConfig.modules) {
ux.action.start(this.$t(this.messages.AUDIT_START_SPINNER, { module }));

Expand All @@ -107,12 +119,22 @@ 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;
}

ux.action.stop();
}

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

/**
Expand Down Expand Up @@ -241,6 +263,52 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
}
}

// Make it generic it takes the column header as param
showOutputOnScreenWorkflowsAndExtension(allMissingRefs: { module: string; missingRefs?: Record<string, any> }[]) {
if (this.sharedConfig.showTerminalOutput && !this.flags['external-config']?.noTerminalOutput) {
this.log(''); // NOTE adding new line
for (const { module, missingRefs } of allMissingRefs) {
if (!isEmpty(missingRefs)) {
print([
{
bold: true,
color: 'cyan',
message: ` ${module}`,
},
]);
const tableValues = Object.values(missingRefs).flat();
ux.table(
tableValues,
{
name: {
minWidth: 7,
header: 'Title',
},
uid: {
minWidth: 12,
header: 'Workflow Uid',
},
content_types: {
minWidth: 7,
header: 'Missing Content Types',
get: (row) => {
return chalk.red(
typeof row.content_types === 'object' ? JSON.stringify(row.content_types) : row.content_types,
);
},
},
...(tableValues[0]?.fixStatus ? this.fixStatus : {}),
},
{
...this.flags,
},
);
this.log(''); // NOTE adding new line
}
}
}
}

/**
* The function prepares a report by writing a JSON file and a CSV file with a list of missing
* references for a given module.
Expand Down Expand Up @@ -299,8 +367,10 @@ export abstract class AuditBaseCommand extends BaseCommand<typeof AuditBaseComma
let row: Record<string, string | string[]> = {};

for (const column of columns) {
row[column] = issue[OutputColumn[column]];
row[column] = typeof row[column] === 'object' ? JSON.stringify(row[column]) : row[column];
if (Object.keys(issue).includes(OutputColumn[column])) {
row[column] = issue[OutputColumn[column]] as string;
row[column] = typeof row[column] === 'object' ? JSON.stringify(row[column]) : row[column];
}
}

if (this.currentCommand === 'cm:stacks:audit:fix') {
Expand All @@ -309,7 +379,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'],
modules: ['content-types', 'global-fields', 'entries'],
'fix-fields': ['reference', 'global_field', 'json:rte', 'json:extension', 'blocks', 'group'],
modules: ['content-types', 'global-fields', 'entries', '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',
},
},
entries: {
systemKeys: [
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 @@ -12,6 +12,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: `Workflow {uid} named '{name}' will be removed.`,
WORKFLOW_FIX_CONFIRMATION: 'Would you like to overwrite existing file.?',
};

const auditMsg = {
Expand All @@ -26,6 +28,7 @@ const auditMsg = {
SCAN_CT_SUCCESS_MSG: "Successfully completed the scanning of {module} '{title}'.",
SCAN_ENTRY_SUCCESS_MSG: "Successfully completed the scanning of {module} ({local}) '{title}'.",
AUDIT_CMD_DESCRIPTION: 'Perform audits and find possible errors in the exported Contentstack data',
SCAN_WF_SUCCESS_MSG: "Successfully completed the scanning of {module} '{name}'.",
};

const auditFixMsg = {
Expand All @@ -35,6 +38,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"

export { Entries, GlobalField, ContentType }
export { Entries, GlobalField, ContentType, Workflows }
141 changes: 141 additions & 0 deletions packages/contentstack-audit/src/modules/workflows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
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 = [];
this.ctSchema.forEach((ct) => this.ctUidSet.add(ct.uid));

this.workflowSchema.forEach((workflow: Workflow) => {
let ctNotPresent: string[] = [];
workflow.content_types.forEach((ct) => {
if (!this.ctUidSet.has(ct)) {
ctNotPresent.push(ct);
this.missingCts.add(ct);
}
});
if (ctNotPresent.length) {
workflow.content_types = ctNotPresent;
this.missingCtInWorkflows.push(cloneDeep(workflow));
}
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();
}
return this.missingCtInWorkflows;
}

async fixWorkflowSchema() {
for (let i in this.workflowSchema) {
aman19K marked this conversation as resolved.
Show resolved Hide resolved
this.workflowSchema[i].content_types = this.workflowSchema[i].content_types.filter((ct) => {
!this.missingCts.has(ct);
});
}
let newWorkflowSchema: Record<string, Workflow> = existsSync(this.workflowPath)
? JSON.parse(readFileSync(this.workflowPath, 'utf8'))
: {};
if (Object.keys(newWorkflowSchema).length !== 0) {
for (let i in this.workflowSchema) {
let fixedCts = this.workflowSchema[i].content_types.filter((ct) => {
!this.missingCts.has(ct);
});
if (fixedCts.length) {
newWorkflowSchema[this.workflowSchema[i].uid].content_types = fixedCts;
} else {
this.log(
$t(commonMsg.WORKFLOW_FIX_WARN, {
name: this.workflowSchema[i].name,
uid: this.workflowSchema[i].uid,
}),
{ color: 'yellow' },
);
if (this.config.flags.yes || (await ux.confirm(commonMsg.WORKFLOW_FIX_CONFIRMATION))) {
delete newWorkflowSchema[this.workflowSchema[i].uid];
}
}
}
}

this.writeFixContent(newWorkflowSchema);
}

async writeFixContent(newWorkflowSchema: Record<string, Workflow>) {
let canWrite = true;

if (this.fix) {
if (!this.config.flags['copy-dir'] && !this.config.flags['external-config']?.skipConfirm) {
canWrite = this.config.flags.yes ?? (await ux.confirm(commonMsg.FIX_CONFIRMATION));
}
if (canWrite) {
writeFileSync(
join(this.folderPath, this.config.moduleConfig[this.moduleName].fileName),
JSON.stringify(newWorkflowSchema),
);
}
}
}
}
4 changes: 4 additions & 0 deletions packages/contentstack-audit/src/types/content-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ type RefErrorReturnType = {
missingRefs: string[];
display_name: string;
tree: Record<string, unknown>[];
uid?: string;
content_types?: string[];
};

// NOTE Type 1
Expand Down Expand Up @@ -113,6 +115,8 @@ enum OutputColumn {
'Field type' = 'data_type',
'Missing references' = 'missingRefs',
Path = 'treeStr',
'uid' = 'uid',
'missingCts' = 'content_types',
}

export {
Expand Down
Loading
Loading