diff --git a/packages/contentstack-audit/src/audit-base-command.ts b/packages/contentstack-audit/src/audit-base-command.ts index e2f19b6aa8..5b057b9616 100644 --- a/packages/contentstack-audit/src/audit-base-command.ts +++ b/packages/contentstack-audit/src/audit-base-command.ts @@ -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, Extensions } from './modules'; import { CommandNames, ContentTypeStruct, OutputColumn, RefErrorReturnType } from './types'; export abstract class AuditBaseCommand extends BaseCommand { @@ -42,15 +42,21 @@ export abstract class AuditBaseCommand extends BaseCommand }[]) { + if (!this.sharedConfig.showTerminalOutput || this.flags['external-config']?.noTerminalOutput) { + return; + } + + this.log(''); // Adding a new line + + for (const { module, missingRefs } of allMissingRefs) { + if (isEmpty(missingRefs)) { + continue; + } + + print([{ bold: true, color: 'cyan', message: ` ${module}` }]); + + const tableValues = Object.values(missingRefs).flat(); + + const tableKeys = Object.keys(missingRefs[0]); + const arrayOfObjects = tableKeys.map((key) => { + if (['title', 'name', 'uid', 'content_types', 'fixStatus'].includes(key)) { + return { + [key]: { + minWidth: 7, + header: key, + get: (row: Record) => { + if(key==='fixStatus') { + return chalk.green(typeof row[key] === 'object' ? JSON.stringify(row[key]) : row[key]); + } 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]); + } + }, + }, + }; + } + return {}; + }); + + const mergedObject = Object.assign({}, ...arrayOfObjects); + + ux.table(tableValues, mergedObject, { ...this.flags }); + this.log(''); // Adding a 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. @@ -311,13 +372,14 @@ export abstract class AuditBaseCommand extends BaseCommand[] = []; - for (const issue of missingRefs) { let row: Record = {}; 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') { diff --git a/packages/contentstack-audit/src/config/index.ts b/packages/contentstack-audit/src/config/index.ts index 92028f9c40..0020553fde 100644 --- a/packages/contentstack-audit/src/config/index.ts +++ b/packages/contentstack-audit/src/config/index.ts @@ -2,7 +2,7 @@ const config = { showTerminalOutput: true, skipRefs: ['sys_assets'], skipFieldTypes: ['taxonomy', 'group'], - modules: ['content-types', 'global-fields', 'entries'], + modules: ['content-types', 'global-fields', 'entries', 'extensions'], 'fix-fields': ['reference', 'global_field', 'json:rte', 'json:extension', 'blocks', 'group'], moduleConfig: { 'content-types': { @@ -25,6 +25,11 @@ const config = { dirName: 'locales', fileName: 'locales.json', }, + extensions: { + name: 'extensions', + dirName: 'extensions', + fileName: 'extensions.json', + }, }, entries: { systemKeys: [ diff --git a/packages/contentstack-audit/src/messages/index.ts b/packages/contentstack-audit/src/messages/index.ts index a607a5d0fb..6ae890e4c0 100644 --- a/packages/contentstack-audit/src/messages/index.ts +++ b/packages/contentstack-audit/src/messages/index.ts @@ -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.?', + 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?`, }; const auditMsg = { @@ -28,6 +30,7 @@ const auditMsg = { FINAL_REPORT_PATH: "Reports ready. Please find the reports at '{path}'.", SCAN_CT_SUCCESS_MSG: "Successfully completed the scanning of {module} '{title}'.", 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', }; diff --git a/packages/contentstack-audit/src/modules/extensions.ts b/packages/contentstack-audit/src/modules/extensions.ts new file mode 100644 index 0000000000..4b07481417 --- /dev/null +++ b/packages/contentstack-audit/src/modules/extensions.ts @@ -0,0 +1,120 @@ +import path, { join, resolve } from 'path'; +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { cloneDeep } from 'lodash'; +import { LogFn, ConfigType, ContentTypeStruct, CtConstructorParam, ModuleConstructorParam, Extension } 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 Extensions { + public log: LogFn; + protected fix: boolean; + public fileName: any; + public config: ConfigType; + public folderPath: string; + public extensionsSchema: Extension[]; + public ctSchema: ContentTypeStruct[]; + public moduleName: keyof typeof auditConfig.moduleConfig; + public ctUidSet: Set; + public missingCtInExtensions: Extension[]; + public missingCts: Set; + public extensionsPath: string; + + constructor({ + log, + fix, + config, + moduleName, + ctSchema, + }: ModuleConstructorParam & Pick) { + this.log = log; + this.config = config; + this.fix = fix ?? false; + this.ctSchema = ctSchema; + this.extensionsSchema = []; + this.moduleName = moduleName ?? 'extensions'; + this.fileName = config.moduleConfig[this.moduleName].fileName; + this.folderPath = resolve(config.basePath, config.moduleConfig[this.moduleName].dirName); + this.ctUidSet = new Set(['$all']); + this.missingCtInExtensions = []; + this.missingCts = new Set(); + this.extensionsPath = ''; + } + + 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.extensionsPath = path.join(this.folderPath, this.fileName); + + this.extensionsSchema = existsSync(this.extensionsPath) + ? values(JSON.parse(readFileSync(this.extensionsPath, 'utf-8')) as Extension[]) + : []; + this.ctSchema.map((ct) => this.ctUidSet.add(ct.uid)); + for (const ext of this.extensionsSchema) { + const { title, uid, scope } = ext; + const ctNotPresent = scope?.content_types.filter((ct) => !this.ctUidSet.has(ct)); + + if (ctNotPresent?.length && ext.scope) { + ext.content_types = ctNotPresent; + ctNotPresent.forEach((ct) => this.missingCts.add(ct)); + this.missingCtInExtensions.push(cloneDeep(ext)); + } + + this.log( + $t(auditMsg.SCAN_EXT_SUCCESS_MSG, { + title, + module: this.config.moduleConfig[this.moduleName].name, + uid, + }), + 'info', + ); + } + + if (this.fix && this.missingCtInExtensions.length) { + await this.fixExtensionsScope(cloneDeep(this.missingCtInExtensions)); + this.missingCtInExtensions.forEach((ext) => (ext.fixStatus = 'Fixed')); + return this.missingCtInExtensions + } + return this.missingCtInExtensions; + } + + async fixExtensionsScope(missingCtInExtensions: Extension[]) { + let newExtensionSchema: Record = existsSync(this.extensionsPath) + ? JSON.parse(readFileSync(this.extensionsPath, 'utf8')) + : {}; + for (const ext of missingCtInExtensions) { + const { uid, title } = ext; + const fixedCts = ext?.scope?.content_types.filter((ct) => !this.missingCts.has(ct)); + if (fixedCts?.length && newExtensionSchema[uid]?.scope) { + newExtensionSchema[uid].scope.content_types = fixedCts; + } else { + this.log($t(commonMsg.EXTENSION_FIX_WARN, { title: title, uid }), { color: 'yellow' }); + const shouldDelete = this.config.flags.yes || (await ux.confirm(commonMsg.EXTENSION_FIX_CONFIRMATION)); + if (shouldDelete) { + delete newExtensionSchema[uid]; + } + } + } + await this.writeFixContent(newExtensionSchema); + } + + async writeFixContent(fixedExtensions: Record) { + if ( + this.fix || + (this.config.flags['copy-dir'] || + this.config.flags['external-config']?.skipConfirm || + (await ux.confirm(commonMsg.FIX_CONFIRMATION))) + ) { + writeFileSync( + join(this.folderPath, this.config.moduleConfig[this.moduleName].fileName), + JSON.stringify(fixedExtensions), + ); + } + } +} diff --git a/packages/contentstack-audit/src/modules/index.ts b/packages/contentstack-audit/src/modules/index.ts index 81d42bc7b5..e0b1e5ac9c 100644 --- a/packages/contentstack-audit/src/modules/index.ts +++ b/packages/contentstack-audit/src/modules/index.ts @@ -1,5 +1,5 @@ -import Entries from "./entries" -import GlobalField from "./global-fields" -import ContentType from "./content-types" - -export { Entries, GlobalField, ContentType } \ No newline at end of file +import Entries from './entries'; +import GlobalField from './global-fields'; +import ContentType from './content-types'; +import Extensions from './extensions'; +export { Entries, GlobalField, ContentType, Extensions }; diff --git a/packages/contentstack-audit/src/types/content-types.ts b/packages/contentstack-audit/src/types/content-types.ts index ebd8f32ac8..8f4d614eef 100644 --- a/packages/contentstack-audit/src/types/content-types.ts +++ b/packages/contentstack-audit/src/types/content-types.ts @@ -48,6 +48,9 @@ type RefErrorReturnType = { missingRefs: string[]; display_name: string; tree: Record[]; + uid?: string; + content_types?: string[]; + title?: string; }; // NOTE Type 1 @@ -113,6 +116,9 @@ enum OutputColumn { 'Field type' = 'data_type', 'Missing references' = 'missingRefs', Path = 'treeStr', + title = 'title', + 'uid' = 'uid', + 'missingCts' = 'content_types', } export { diff --git a/packages/contentstack-audit/src/types/extensions.ts b/packages/contentstack-audit/src/types/extensions.ts new file mode 100644 index 0000000000..91393bd3dd --- /dev/null +++ b/packages/contentstack-audit/src/types/extensions.ts @@ -0,0 +1,24 @@ +export interface Extension { + stackHeaders: { + api_key: string; + }; + urlPath: string; + uid: string; + created_at: string; + updated_at: string; + created_by: string; + updated_by: string; + tags?: []; + _version: number; + title: string; + config: {}; + type: 'field'; + data_type: string; + multiple: boolean; + srcdoc?: string; + scope: { + content_types: string[]; + }; + content_types?: string[]; + fixStatus?: string; +} diff --git a/packages/contentstack-audit/src/types/index.ts b/packages/contentstack-audit/src/types/index.ts index 9c78f7486e..a3d24f2525 100644 --- a/packages/contentstack-audit/src/types/index.ts +++ b/packages/contentstack-audit/src/types/index.ts @@ -2,3 +2,4 @@ export * from './utils'; export * from './common'; export * from './entries'; export * from './content-types'; +export * from './extensions'; \ No newline at end of file diff --git a/packages/contentstack-audit/test/unit/audit-base-command.test.ts b/packages/contentstack-audit/test/unit/audit-base-command.test.ts index 81301e8997..f85899cb47 100644 --- a/packages/contentstack-audit/test/unit/audit-base-command.test.ts +++ b/packages/contentstack-audit/test/unit/audit-base-command.test.ts @@ -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 } from '../../src/modules'; +import { ContentType, Entries, GlobalField, Extensions } from '../../src/modules'; import { FileTransportInstance } from 'winston/lib/winston/transports'; import { $t, auditMsg } from '../../src/messages'; @@ -41,6 +41,8 @@ describe('AuditBaseCommand class', () => { .stub(Entries.prototype, 'run', () => ({ entry_1: {} })) .stub(ContentType.prototype, 'run', () => ({ ct_1: {} })) .stub(GlobalField.prototype, 'run', () => ({ gf_1: {} })) + .stub(Extensions.prototype, 'run', () => ({ ext_1: {} })) + .stub(AuditBaseCommand.prototype, 'showOutputOnScreenWorkflowsAndExtension', () => {}) .stub(fs, 'createWriteStream', () => new PassThrough()) .it('should show audit report path', async (ctx) => { await AuditCMD.run(['--data-dir', resolve(__dirname, 'mock', 'contents')]); @@ -61,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(Extensions.prototype, 'run', () => ({ ext_1: {} })) .stub(fs, 'createWriteStream', () => new PassThrough()) .it('should print info of no ref found', async (ctx) => { await AuditCMD.run([]); @@ -78,6 +81,7 @@ describe('AuditBaseCommand class', () => { .stub(ux, 'table', (...args: any) => { args[1].missingRefs.get({ missingRefs: ['gf_0'] }); }) + .stub(AuditBaseCommand.prototype, 'showOutputOnScreenWorkflowsAndExtension', () => {}) .stub(ux.action, 'stop', () => {}) .stub(ux.action, 'start', () => {}) .stub(Entries.prototype, 'run', () => ({ @@ -91,6 +95,7 @@ describe('AuditBaseCommand class', () => { })) .stub(ContentType.prototype, 'run', () => ({ ct_1: {} })) .stub(GlobalField.prototype, 'run', () => ({ gf_1: {} })) + .stub(Extensions.prototype, 'run', () => ({ ext_1: {} })) .stub(fs, 'createBackUp', () => {}) .stub(fs, 'createWriteStream', () => new PassThrough()) .stub(AuditBaseCommand.prototype, 'createBackUp', () => {}) diff --git a/packages/contentstack-audit/test/unit/mock/contents/extensions/allCts/extensions/extensions.json b/packages/contentstack-audit/test/unit/mock/contents/extensions/allCts/extensions/extensions.json new file mode 100644 index 0000000000..f9d142a39a --- /dev/null +++ b/packages/contentstack-audit/test/unit/mock/contents/extensions/allCts/extensions/extensions.json @@ -0,0 +1,44 @@ +{ + "ext1": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext1", + "uid": "ext1", + "created_at": "2024-02-22T09:45:48.681Z", + "updated_at": "2024-02-22T09:45:48.681Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Progress Bar", + "config": {}, + "type": "field", + "data_type": "number", + "multiple": false, + "scope": { + "content_types": ["$all"] + } + }, + "ext2": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext2", + "uid": "ext2", + "created_at": "2024-02-22T09:45:11.284Z", + "updated_at": "2024-02-22T09:45:11.284Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Color Picker", + "config": {}, + "type": "field", + "data_type": "text", + "multiple": false, + "scope": { + "content_types": ["$all"] + } + } +} diff --git a/packages/contentstack-audit/test/unit/mock/contents/extensions/ctSchema.json b/packages/contentstack-audit/test/unit/mock/contents/extensions/ctSchema.json new file mode 100644 index 0000000000..12c2324e02 --- /dev/null +++ b/packages/contentstack-audit/test/unit/mock/contents/extensions/ctSchema.json @@ -0,0 +1,303 @@ +[ + { + "title": "ct2", + "uid": "ct2", + "schema": [ + { + "data_type": "text", + "display_name": "Title", + "field_metadata": { "_default": true, "version": 3 }, + "mandatory": true, + "uid": "title", + "unique": true, + "multiple": false, + "non_localizable": false + } + ], + "description": "", + "options": { "is_page": false, "singleton": true, "sub_title": [], "title": "title" } + }, + { + "title": "ct3", + "uid": "ct3", + "schema": [ + { + "data_type": "text", + "display_name": "Title", + "field_metadata": { "_default": true, "version": 3 }, + "mandatory": true, + "uid": "title", + "unique": true, + "multiple": false, + "non_localizable": false + } + ], + "description": "", + "options": { "is_page": false, "singleton": true, "sub_title": [], "title": "title" } + }, + { + "title": "ct4", + "uid": "ct4", + "schema": [ + { + "data_type": "text", + "display_name": "Title", + "field_metadata": { "_default": true, "version": 3 }, + "mandatory": true, + "uid": "title", + "unique": true, + "multiple": false, + "non_localizable": false + } + ], + "description": "", + "options": { "is_page": false, "singleton": true, "sub_title": [], "title": "title" } + }, + { + "title": "ct5", + "uid": "ct5", + "schema": [ + { + "data_type": "text", + "display_name": "Title", + "field_metadata": { "_default": true, "version": 3 }, + "mandatory": true, + "uid": "title", + "unique": true, + "multiple": false, + "non_localizable": false + } + ], + "description": "", + "options": { "is_page": false, "singleton": true, "sub_title": [], "title": "title" } + }, + { + "title": "mpa", + "uid": "mpa", + "schema": [ + { + "data_type": "text", + "display_name": "Title", + "field_metadata": { "_default": true, "version": 3 }, + "mandatory": true, + "uid": "title", + "unique": true, + "multiple": false, + "non_localizable": false + }, + { + "display_name": "Custom", + "extension_uid": "blt1ea8d9d672fe8449", + "field_metadata": { "extension": true, "ref_multiple_content_types": true, "ref_multiple": true }, + "uid": "custom", + "mandatory": false, + "non_localizable": false, + "unique": false, + "config": {}, + "reference_to": ["mpa"], + "data_type": "reference", + "multiple": false + } + ], + "description": "", + "options": { "is_page": false, "singleton": false, "sub_title": [], "title": "title" } + }, + { + "title": "ct1", + "uid": "ct1", + "schema": [ + { + "data_type": "text", + "display_name": "Title", + "field_metadata": { "_default": true, "version": 3 }, + "mandatory": true, + "uid": "title", + "unique": true, + "multiple": false, + "non_localizable": false + }, + { + "data_type": "reference", + "display_name": "Reference", + "reference_to": ["ct1"], + "field_metadata": { "ref_multiple": false, "ref_multiple_content_types": true }, + "uid": "reference", + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false + }, + { + "data_type": "group", + "display_name": "Group", + "field_metadata": { "description": "", "instruction": "" }, + "schema": [ + { + "data_type": "text", + "display_name": "Rich Text Editor", + "uid": "rich_text_editor", + "field_metadata": { + "allow_rich_text": true, + "description": "", + "multiline": false, + "rich_text_type": "custom", + "options": [], + "version": 3 + }, + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false + }, + { + "data_type": "json", + "display_name": "JSON Rich Text Editor", + "uid": "json_rte", + "field_metadata": { + "allow_json_rte": true, + "embed_entry": true, + "description": "", + "default_value": "", + "multiline": false, + "rich_text_type": "advanced", + "options": [], + "ref_multiple_content_types": true + }, + "format": "", + "error_messages": { "format": "" }, + "reference_to": ["sys_assets", "ct3"], + "multiple": false, + "non_localizable": false, + "unique": false, + "mandatory": false + }, + { + "data_type": "group", + "display_name": "Group", + "field_metadata": { "description": "", "instruction": "" }, + "schema": [ + { + "data_type": "reference", + "display_name": "Reference", + "reference_to": ["ct3"], + "field_metadata": { "ref_multiple": false, "ref_multiple_content_types": true }, + "uid": "reference", + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false + } + ], + "uid": "group", + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false + }, + { + "data_type": "global_field", + "display_name": "Global", + "reference_to": "gf1", + "field_metadata": { "description": "" }, + "uid": "global_field", + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "schema": [ + { + "data_type": "reference", + "display_name": "Reference", + "reference_to": ["ct1"], + "field_metadata": { "ref_multiple": false, "ref_multiple_content_types": true }, + "uid": "reference", + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false, + "indexed": false, + "inbuilt_model": false + } + ] + }, + { + "data_type": "reference", + "display_name": "Reference", + "reference_to": ["ct2"], + "field_metadata": { "ref_multiple": false, "ref_multiple_content_types": true }, + "uid": "reference", + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false + } + ], + "uid": "group", + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false + } + ], + "description": "", + "options": { "is_page": false, "singleton": true, "sub_title": [], "title": "title" } + }, + { + "title": "json", + "uid": "json", + "schema": [ + { + "data_type": "text", + "display_name": "Title", + "field_metadata": { "_default": true, "version": 3 }, + "mandatory": true, + "uid": "title", + "unique": true, + "multiple": false, + "non_localizable": false + }, + { + "data_type": "json", + "display_name": "JSON Rich Text Editor", + "uid": "json_rte", + "field_metadata": { + "allow_json_rte": true, + "embed_entry": true, + "description": "", + "default_value": "", + "multiline": false, + "rich_text_type": "advanced", + "options": ["widget"], + "ref_multiple_content_types": true + }, + "format": "", + "error_messages": { "format": "" }, + "reference_to": ["json", "mpa", "ct5", "ct4", "sys_assets"], + "multiple": false, + "non_localizable": false, + "unique": false, + "mandatory": false, + "plugins": [ + "blt314473f8f7e13956", + "bltc4143ac9b693fbdc", + "blt0f35138b9c17384e", + "bltcb085fe6edbb3407", + "bltc674482e12b1eb30" + ] + }, + { + "data_type": "reference", + "display_name": "Reference", + "reference_to": ["mpa", "json", "ct5", "ct4"], + "field_metadata": { "ref_multiple": false, "ref_multiple_content_types": true }, + "uid": "reference", + "mandatory": false, + "multiple": false, + "non_localizable": false, + "unique": false + } + ], + "description": "", + "options": { "is_page": false, "singleton": true, "sub_title": [], "title": "title" } + } +] diff --git a/packages/contentstack-audit/test/unit/mock/contents/extensions/extensions.json b/packages/contentstack-audit/test/unit/mock/contents/extensions/extensions.json new file mode 100644 index 0000000000..54405cb969 --- /dev/null +++ b/packages/contentstack-audit/test/unit/mock/contents/extensions/extensions.json @@ -0,0 +1,248 @@ +{ + "missingCts": { + "ext1": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext1", + "uid": "ext1", + "created_at": "2024-02-22T09:45:48.681Z", + "updated_at": "2024-02-22T09:45:48.681Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Progress Bar", + "config": {}, + "type": "field", + "data_type": "number", + "multiple": false, + "scope": { + "content_types": ["ct6"] + } + }, + "ext2": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext2", + "uid": "ext2", + "created_at": "2024-02-22T09:45:11.284Z", + "updated_at": "2024-02-22T09:45:11.284Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Color Picker", + "config": {}, + "type": "field", + "data_type": "text", + "multiple": false, + "scope": { + "content_types": ["ct8"] + } + }, + "ext5": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext5", + "uid": "ext5", + "created_at": "2024-02-22T09:44:27.030Z", + "updated_at": "2024-02-22T09:44:27.030Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Text Intelligence", + "config": { + "token": "your_token_here" + }, + "type": "widget", + "scope": { + "content_types": ["ct4", "ct3", "ct2", "ct1"] + } + }, + "ext6": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext6", + "uid": "ext6", + "created_at": "2024-02-22T09:44:01.784Z", + "updated_at": "2024-02-22T09:44:01.784Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Ace Editor", + "config": {}, + "type": "field", + "data_type": "reference", + "multiple": true + }, + "ext7": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext7", + "uid": "ext7", + "created_at": "2024-02-22T09:43:35.589Z", + "updated_at": "2024-02-22T09:43:35.589Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Gatsby Preview", + "config": { + "siteUrl": "your_site_url" + }, + "type": "widget", + "scope": { + "content_types": ["ct3", "ct5"] + } + } + }, + "allCts": { + "ext1": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext1", + "uid": "ext1", + "created_at": "2024-02-22T09:45:48.681Z", + "updated_at": "2024-02-22T09:45:48.681Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Progress Bar", + "config": {}, + "type": "field", + "data_type": "number", + "multiple": false, + "scope": { + "content_types": ["$all"] + } + }, + "ext2": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext2", + "uid": "ext2", + "created_at": "2024-02-22T09:45:11.284Z", + "updated_at": "2024-02-22T09:45:11.284Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Color Picker", + "config": {}, + "type": "field", + "data_type": "text", + "multiple": false, + "scope": { + "content_types": ["$all"] + } + } + }, + "noMissingCts": { + "ext1": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext1", + "uid": "ext1", + "created_at": "2024-02-22T09:45:48.681Z", + "updated_at": "2024-02-22T09:45:48.681Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Progress Bar", + "config": {}, + "type": "field", + "data_type": "number", + "multiple": false + }, + "ext2": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext2", + "uid": "ext2", + "created_at": "2024-02-22T09:45:11.284Z", + "updated_at": "2024-02-22T09:45:11.284Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Color Picker", + "config": {}, + "type": "field", + "data_type": "text", + "multiple": false + }, + "ext5": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext5", + "uid": "ext5", + "created_at": "2024-02-22T09:44:27.030Z", + "updated_at": "2024-02-22T09:44:27.030Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Text Intelligence", + "config": { + "token": "your_token_here" + }, + "type": "widget", + "scope": { + "content_types": ["ct4", "ct3", "ct2", "ct1"] + } + }, + "ext6": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext6", + "uid": "ext6", + "created_at": "2024-02-22T09:44:01.784Z", + "updated_at": "2024-02-22T09:44:01.784Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Ace Editor", + "config": {}, + "type": "field", + "data_type": "reference", + "multiple": true + }, + "ext7": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext7", + "uid": "ext7", + "created_at": "2024-02-22T09:43:35.589Z", + "updated_at": "2024-02-22T09:43:35.589Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Gatsby Preview", + "config": { + "siteUrl": "your_site_url" + }, + "type": "widget", + "scope": { + "content_types": ["ct3", "ct5"] + } + } + } +} diff --git a/packages/contentstack-audit/test/unit/mock/contents/extensions/invalidExtensions/extensions/extensions.json b/packages/contentstack-audit/test/unit/mock/contents/extensions/invalidExtensions/extensions/extensions.json new file mode 100644 index 0000000000..dad7460f85 --- /dev/null +++ b/packages/contentstack-audit/test/unit/mock/contents/extensions/invalidExtensions/extensions/extensions.json @@ -0,0 +1,104 @@ +{ + "ext1": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext1", + "uid": "ext1", + "created_at": "2024-02-22T09:45:48.681Z", + "updated_at": "2024-02-22T09:45:48.681Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Progress Bar", + "config": {}, + "type": "field", + "data_type": "number", + "multiple": false, + "scope": { + "content_types": ["ct6"] + } + }, + "ext2": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext2", + "uid": "ext2", + "created_at": "2024-02-22T09:45:11.284Z", + "updated_at": "2024-02-22T09:45:11.284Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Color Picker", + "config": {}, + "type": "field", + "data_type": "text", + "multiple": false, + "scope": { + "content_types": ["ct8"] + } + }, + "ext5": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext5", + "uid": "ext5", + "created_at": "2024-02-22T09:44:27.030Z", + "updated_at": "2024-02-22T09:44:27.030Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Text Intelligence", + "config": { + "token": "your_token_here" + }, + "type": "widget", + "scope": { + "content_types": ["ct4", "ct3", "ct2", "ct1", "ct6"] + } + }, + "ext6": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext6", + "uid": "ext6", + "created_at": "2024-02-22T09:44:01.784Z", + "updated_at": "2024-02-22T09:44:01.784Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Ace Editor", + "config": {}, + "type": "field", + "data_type": "reference", + "multiple": true + }, + "ext7": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext7", + "uid": "ext7", + "created_at": "2024-02-22T09:43:35.589Z", + "updated_at": "2024-02-22T09:43:35.589Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Gatsby Preview", + "config": { + "siteUrl": "your_site_url" + }, + "type": "widget", + "scope": { + "content_types": ["ct3", "ct5"] + } + } +} diff --git a/packages/contentstack-audit/test/unit/mock/contents/extensions/validExtensions/extensions.json b/packages/contentstack-audit/test/unit/mock/contents/extensions/validExtensions/extensions.json new file mode 100644 index 0000000000..c72bd53885 --- /dev/null +++ b/packages/contentstack-audit/test/unit/mock/contents/extensions/validExtensions/extensions.json @@ -0,0 +1,98 @@ +{ + "ext1": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext1", + "uid": "ext1", + "created_at": "2024-02-22T09:45:48.681Z", + "updated_at": "2024-02-22T09:45:48.681Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Progress Bar", + "config": {}, + "type": "field", + "data_type": "number", + "multiple": false + }, + "ext2": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext2", + "uid": "ext2", + "created_at": "2024-02-22T09:45:11.284Z", + "updated_at": "2024-02-22T09:45:11.284Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Color Picker", + "config": {}, + "type": "field", + "data_type": "text", + "multiple": false + }, + "ext5": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext5", + "uid": "ext5", + "created_at": "2024-02-22T09:44:27.030Z", + "updated_at": "2024-02-22T09:44:27.030Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Text Intelligence", + "config": { + "token": "your_token_here" + }, + "type": "widget", + "scope": { + "content_types": ["ct4", "ct3", "ct2", "ct1"] + } + }, + "ext6": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext6", + "uid": "ext6", + "created_at": "2024-02-22T09:44:01.784Z", + "updated_at": "2024-02-22T09:44:01.784Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Ace Editor", + "config": {}, + "type": "field", + "data_type": "reference", + "multiple": true + }, + "ext7": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext7", + "uid": "ext7", + "created_at": "2024-02-22T09:43:35.589Z", + "updated_at": "2024-02-22T09:43:35.589Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Gatsby Preview", + "config": { + "siteUrl": "your_site_url" + }, + "type": "widget", + "scope": { + "content_types": ["ct3", "ct5"] + } + } +} diff --git a/packages/contentstack-audit/test/unit/modules/extensions.test.ts b/packages/contentstack-audit/test/unit/modules/extensions.test.ts new file mode 100644 index 0000000000..fc279d6d08 --- /dev/null +++ b/packages/contentstack-audit/test/unit/modules/extensions.test.ts @@ -0,0 +1,384 @@ +import { resolve } from 'path'; +import { fancy } from 'fancy-test'; +import { expect } from '@oclif/test'; +import cloneDeep from 'lodash/cloneDeep'; +import { ux } from '@contentstack/cli-utilities'; + +import config from '../../../src/config'; +import { Extensions } from '../../../src/modules'; +import { $t, auditMsg } from '../../../src/messages'; +import sinon from 'sinon'; +import { Extension } from '../../../src/types'; + +const fixedSchema = [ + { + stackHeaders: { + api_key: 'apiKey', + }, + urlPath: '/extensions/ext1', + uid: 'ext1', + created_at: '2024-02-22T09:45:48.681Z', + updated_at: '2024-02-22T09:45:48.681Z', + created_by: 'u1', + updated_by: 'u1', + tags: [], + _version: 1, + title: 'Progress Bar', + config: {}, + type: 'field', + data_type: 'number', + "fixStatus": "Fixed", + content_types: ['ct6'], + multiple: false, + scope: { + content_types: ['ct6'], + } + }, + { + stackHeaders: { + api_key: 'apiKey', + }, + urlPath: '/extensions/ext2', + uid: 'ext2', + created_at: '2024-02-22T09:45:11.284Z', + updated_at: '2024-02-22T09:45:11.284Z', + created_by: 'u1', + updated_by: 'u1', + tags: [], + _version: 1, + title: 'Color Picker', + config: {}, + type: 'field', + data_type: 'text', + "fixStatus": "Fixed", + multiple: false, + content_types: ['ct8'], + scope: { + content_types: ['ct8'], + } + }, + { + stackHeaders: { + api_key: 'apiKey', + }, + urlPath: '/extensions/ext5', + uid: 'ext5', + created_at: '2024-02-22T09:44:27.030Z', + updated_at: '2024-02-22T09:44:27.030Z', + created_by: 'u1', + "fixStatus": "Fixed", + updated_by: 'u1', + tags: [], + _version: 1, + title: 'Text Intelligence', + config: { + token: 'your_token_here', + }, + content_types: ['ct6'], + type: 'widget', + scope: { + content_types: ["ct4", "ct3", "ct2", "ct1",'ct6'], + }, + }, +] +describe('Extensions scope containing content_types uids', () => { + describe('run method with invalid path for extensions', () => { + const ext = new Extensions({ + log: () => {}, + moduleName: 'extensions', + ctSchema: cloneDeep(require('./../mock/contents/extensions/ctSchema.json')), + config: Object.assign(config, { basePath: resolve(__dirname, '..', 'mock', 'workflows'), flags: {} }), + }); + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(ux, 'confirm', async () => true) + .it('Should Validate the base path for workflows', async () => { + try { + await ext.run(); + } catch (error: any) { + expect(error).to.be.instanceOf(Error); + expect(error.message).to.eql($t(auditMsg.NOT_VALID_PATH, { path: ext.folderPath })); + } + }); + }); + describe('run method with valid path for extensions containing extensions with missing content types', () => { + const ext = new Extensions({ + log: () => {}, + moduleName: 'extensions', + ctSchema: cloneDeep(require('./../mock/contents/extensions/ctSchema.json')), + config: Object.assign(config, { + basePath: resolve(`./test/unit/mock/contents/extensions/invalidExtensions/`), + flags: {}, + }), + }); + fancy + .stdout({ print: process.env.PRINT === 'true' || true }) + .stub(ux, 'confirm', async () => true) + .it( + 'should expect missing Cts to the ones that are not present in the schema, and MissingCts in extension equal to the extensions which has Cts that are not present', + async () => { + await ext.run(); + expect(ext.missingCtInExtensions).eql([ + { + stackHeaders: { + api_key: 'apiKey', + }, + urlPath: '/extensions/ext1', + uid: 'ext1', + created_at: '2024-02-22T09:45:48.681Z', + updated_at: '2024-02-22T09:45:48.681Z', + created_by: 'u1', + updated_by: 'u1', + tags: [], + _version: 1, + title: 'Progress Bar', + config: {}, + type: 'field', + data_type: 'number', + content_types: ['ct6'], + multiple: false, + scope: { + content_types: ['ct6'], + }, + }, + { + stackHeaders: { + api_key: 'apiKey', + }, + urlPath: '/extensions/ext2', + uid: 'ext2', + created_at: '2024-02-22T09:45:11.284Z', + updated_at: '2024-02-22T09:45:11.284Z', + created_by: 'u1', + updated_by: 'u1', + tags: [], + _version: 1, + title: 'Color Picker', + config: {}, + type: 'field', + data_type: 'text', + multiple: false, + content_types: ['ct8'], + scope: { + content_types: ['ct8'], + }, + }, + { + stackHeaders: { + api_key: 'apiKey', + }, + urlPath: '/extensions/ext5', + uid: 'ext5', + created_at: '2024-02-22T09:44:27.030Z', + updated_at: '2024-02-22T09:44:27.030Z', + created_by: 'u1', + updated_by: 'u1', + tags: [], + _version: 1, + title: 'Text Intelligence', + config: { + token: 'your_token_here', + }, + content_types: ['ct6'], + type: 'widget', + scope: { + content_types: ["ct4", "ct3", "ct2", "ct1", "ct6"], + }, + }, + ]); + expect(ext.missingCts).eql(new Set(['ct6', 'ct8'])); + }, + ); + }); + describe('run method with valid path for extensions containing extensions with no missing content types and ct set to $all', () => { + const ext = new Extensions({ + log: () => {}, + moduleName: 'extensions', + ctSchema: cloneDeep(require('./../mock/contents/extensions/ctSchema.json')), + config: Object.assign(config, { + basePath: resolve(`./test/unit/mock/contents/extensions/allCts/`), + flags: {}, + }), + }); + fancy + .stdout({ print: process.env.PRINT === 'true' || true }) + .stub(ux, 'confirm', async () => true) + .it( + 'should expect missingRefs equal to empty array, expect entire workflow schema and empty missingCts', + async () => { + expect(ext.missingCtInExtensions).eql([]); + expect(ext.missingCts).eql(new Set([])); + }, + ); + }); + describe('run method with valid path for extensions containing extensions with no missing content types and ct set content types that are present', () => { + const ext = new Extensions({ + log: () => {}, + moduleName: 'extensions', + ctSchema: cloneDeep(require('./../mock/contents/extensions/ctSchema.json')), + config: Object.assign(config, { + basePath: resolve(`./test/unit/mock/contents/extensions/validExtensions/`), + flags: {}, + }), + }); + fancy + .stdout({ print: process.env.PRINT === 'true' || true }) + .stub(ux, 'confirm', async () => true) + .it( + 'should expect missingRefs equal to empty array, expect entire workflow schema and empty missingCts', + async () => { + await ext.run(); + expect(ext.missingCtInExtensions).eql([]); + expect(ext.missingCts).eql(new Set([])); + }, + ); + }); + describe('fixSchema method with valid path for extensions containing extensions with missing content types', () => { + const ext = new (class Class extends Extensions { + public fixedExtensions!: Record; + constructor() { + super({ + log: () => {}, + moduleName: 'extensions', + ctSchema: cloneDeep(require('./../mock/contents/extensions/ctSchema.json')), + config: Object.assign(config, { + basePath: resolve(`./test/unit/mock/contents/extensions/invalidExtensions/`), + flags: {}, + }), + fix: true, + }); + } + async writeFixContent(fixedExtensions: Record) { + this.fixedExtensions = fixedExtensions; + } + })(); + + fancy + .stdout({ print: process.env.PRINT === 'true' || true }) + .stub(ux, 'confirm', async () => true) + .stub(ext, 'writeFileSync', () => {}) + .it( + 'missingCts in extension to extensionSchema containing, extensions with fixed scope, missing Cts to the Cts that are not present in Ct Schema, And the fixed extensions that would be overwritten in the file', + async () => { + const missingRefs = await ext.run(); + expect(ext.missingCtInExtensions).eql(fixedSchema); + expect(missingRefs).eql(fixedSchema); + expect(ext.missingCts).eql(new Set(['ct6', 'ct8'])); + expect(ext.fixedExtensions).eql({ + "ext5": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext5", + "uid": "ext5", + "created_at": "2024-02-22T09:44:27.030Z", + "updated_at": "2024-02-22T09:44:27.030Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Text Intelligence", + "config": { + "token": "your_token_here" + }, + "type": "widget", + "scope": { + "content_types": ["ct4", "ct3", "ct2", "ct1"] + } + }, + "ext6": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext6", + "uid": "ext6", + "created_at": "2024-02-22T09:44:01.784Z", + "updated_at": "2024-02-22T09:44:01.784Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Ace Editor", + "config": {}, + "type": "field", + "data_type": "reference", + "multiple": true + }, + "ext7": { + "stackHeaders": { + "api_key": "apiKey" + }, + "urlPath": "/extensions/ext7", + "uid": "ext7", + "created_at": "2024-02-22T09:43:35.589Z", + "updated_at": "2024-02-22T09:43:35.589Z", + "created_by": "u1", + "updated_by": "u1", + "tags": [], + "_version": 1, + "title": "Gatsby Preview", + "config": { + "siteUrl": "your_site_url" + }, + "type": "widget", + "scope": { + "content_types": ["ct3", "ct5"] + } + } + } + ); + }, + ); + }); + describe('fixSchema method with valid path for extensions containing extensions with missing content types checking the fixed content', () => { + const ext = new Extensions({ + log: () => {}, + moduleName: 'extensions', + ctSchema: cloneDeep(require('./../mock/contents/extensions/ctSchema.json')), + config: Object.assign(config, { + basePath: resolve(`./test/unit/mock/contents/extensions/invalidExtensions/`), + flags: {}, + }), + fix: true, + }); + fancy + .stdout({ print: process.env.PRINT === 'true' || true }) + .stub(ux, 'confirm', async () => true) + .stub(ext, 'writeFixContent', async () => {}) + .stub(ext, 'writeFileSync', () => {}) + .it( + 'missingCts in extension to extensionSchema containing, extensions with fixed scope, missing Cts to the Cts that are not present in Ct Schema, Not overwriting to the file', + async () => { + const missingRefs = await ext.run(); + expect(ext.missingCtInExtensions).eql(fixedSchema); + expect(missingRefs).eql(fixedSchema); + expect(ext.missingCts).eql(new Set(['ct6', 'ct8'])); + }, + ); + }); + describe('fixSchema method with valid path for extensions containing extensions with no missing content types and ct set to $all', () => { + const ext = new Extensions({ + log: () => {}, + moduleName: 'extensions', + ctSchema: cloneDeep(require('./../mock/contents/extensions/ctSchema.json')), + config: Object.assign(config, { + basePath: resolve(`./test/unit/mock/contents/extensions/allCts/`), + flags: {}, + }), + fix: true, + }); + fancy + .stdout({ print: process.env.PRINT === 'true' || true }) + .stub(ux, 'confirm', async () => true) + .stub(ext, 'writeFileSync', () => {}) + .it( + 'should expect missingRefs equal to empty array, expect entire workflow schema and empty missingCts', + async () => { + expect(ext.missingCtInExtensions).eql([]); + expect(ext.missingCts).eql(new Set([])); + const fixExt = sinon.spy(ext, 'fixExtensionsScope'); + expect(fixExt.callCount).to.be.equals(0); + }, + ); + }); +}); diff --git a/packages/contentstack-import/src/import/module-importer.ts b/packages/contentstack-import/src/import/module-importer.ts index 599e41447f..9db609b62b 100755 --- a/packages/contentstack-import/src/import/module-importer.ts +++ b/packages/contentstack-import/src/import/module-importer.ts @@ -132,7 +132,7 @@ class ModuleImporter { args.push('--modules', this.importConfig.moduleName); } else if (this.importConfig.modules.types.length) { this.importConfig.modules.types - .filter((val) => ['content-types', 'global-fields', 'entries'].includes(val)) + .filter((val) => ['content-types', 'global-fields', 'entries', 'extensions'].includes(val)) .forEach((val) => { args.push('--modules', val); });