1+ import map from 'lodash/map' ;
12import find from 'lodash/find' ;
23import isEmpty from 'lodash/isEmpty' ;
34import { join , resolve } from 'path' ;
4- import { existsSync , writeFileSync } from 'fs' ;
5+ import { existsSync , readFileSync , writeFileSync } from 'fs' ;
6+
57import { ux } from '@contentstack/cli-utilities' ;
68
79import {
@@ -19,9 +21,11 @@ import {
1921 ReferenceFieldDataType ,
2022 ContentTypeSchemaType ,
2123 GlobalFieldSchemaTypes ,
24+ ExtensionOrAppFieldDataType ,
2225} from '../types' ;
2326import auditConfig from '../config' ;
2427import { $t , auditFixMsg , auditMsg , commonMsg } from '../messages' ;
28+ import { MarketplaceAppsInstallationData } from '../types/extension' ;
2529
2630/* The `ContentType` class is responsible for scanning content types, looking for references, and
2731generating a report in JSON and CSV formats. */
@@ -33,6 +37,7 @@ export default class ContentType {
3337 public folderPath : string ;
3438 public currentUid ! : string ;
3539 public currentTitle ! : string ;
40+ public extensions : string [ ] = [ ] ;
3641 public inMemoryFix : boolean = false ;
3742 public gfSchema : ContentTypeStruct [ ] ;
3843 public ctSchema : ContentTypeStruct [ ] ;
@@ -67,6 +72,8 @@ export default class ContentType {
6772
6873 this . schema = this . moduleName === 'content-types' ? this . ctSchema : this . gfSchema ;
6974
75+ await this . prerequisiteData ( ) ;
76+
7077 for ( const schema of this . schema ?? [ ] ) {
7178 this . currentUid = schema . uid ;
7279 this . currentTitle = schema . title ;
@@ -96,6 +103,35 @@ export default class ContentType {
96103 return this . missingRefs ;
97104 }
98105
106+ /**
107+ * @method prerequisiteData
108+ * The `prerequisiteData` function reads and parses JSON files to retrieve extension and marketplace
109+ * app data, and stores them in the `extensions` array.
110+ */
111+ async prerequisiteData ( ) {
112+ const extensionPath = resolve ( this . config . basePath , 'extensions' , 'extensions.json' ) ;
113+ const marketplacePath = resolve ( this . config . basePath , 'marketplace_apps' , 'marketplace_apps.json' ) ;
114+
115+ if ( existsSync ( extensionPath ) ) {
116+ try {
117+ this . extensions = Object . keys ( JSON . parse ( readFileSync ( extensionPath , 'utf8' ) ) ) ;
118+ } catch ( error ) { }
119+ }
120+
121+ if ( existsSync ( marketplacePath ) ) {
122+ try {
123+ const marketplaceApps : MarketplaceAppsInstallationData [ ] = JSON . parse ( readFileSync ( marketplacePath , 'utf8' ) ) ;
124+
125+ for ( const app of marketplaceApps ) {
126+ const metaData = map ( map ( app ?. ui_location ?. locations , 'meta' ) . flat ( ) , 'extension_uid' ) . filter (
127+ ( val ) => val ,
128+ ) as string [ ] ;
129+ this . extensions . push ( ...metaData ) ;
130+ }
131+ } catch ( error ) { }
132+ }
133+ }
134+
99135 /**
100136 * The function checks if it can write the fix content to a file and if so, it writes the content as
101137 * JSON to the specified file path.
@@ -157,10 +193,16 @@ export default class ContentType {
157193 ) ;
158194 break ;
159195 case 'json' :
160- if ( child . field_metadata . extension ) {
161- if ( ! fixTypes . includes ( 'json:custom-field ' ) ) continue ;
196+ if ( 'extension' in child . field_metadata && child . field_metadata . extension ) {
197+ if ( ! fixTypes . includes ( 'json:extension ' ) ) continue ;
162198 // NOTE Custom field type
163- } else if ( child . field_metadata . allow_json_rte ) {
199+ this . missingRefs [ this . currentUid ] . push (
200+ ...this . validateExtensionAndAppField (
201+ [ ...tree , { uid : child . uid , name : child . display_name } ] ,
202+ child as ExtensionOrAppFieldDataType ,
203+ ) ,
204+ ) ;
205+ } else if ( 'allow_json_rte' in child . field_metadata && child . field_metadata . allow_json_rte ) {
164206 if ( ! fixTypes . includes ( 'json:rte' ) ) continue ;
165207 // NOTE JSON RTE field type
166208 this . missingRefs [ this . currentUid ] . push (
@@ -199,6 +241,45 @@ export default class ContentType {
199241 return this . validateReferenceToValues ( tree , field ) ;
200242 }
201243
244+ /**
245+ * The function `validateExtensionAndAppsField` checks if a given field has a valid extension or app
246+ * reference and returns any missing references.
247+ * @param {Record<string, unknown>[] } tree - An array of objects representing a tree structure.
248+ * @param {ExtensionOrAppFieldDataType } field - The `field` parameter is of type `ExtensionOrAppFieldDataType`.
249+ * @returns The function `validateExtensionAndAppsField` returns an array of `RefErrorReturnType`
250+ * objects.
251+ */
252+ validateExtensionAndAppField (
253+ tree : Record < string , unknown > [ ] ,
254+ field : ExtensionOrAppFieldDataType ,
255+ ) : RefErrorReturnType [ ] {
256+ if ( this . fix ) return [ ] ;
257+
258+ const missingRefs = [ ] ;
259+ let { uid, extension_uid, display_name, data_type } = field ;
260+
261+ if ( ! this . extensions . includes ( extension_uid ) ) {
262+ missingRefs . push ( { uid, extension_uid, type : 'Extension or Apps' } as any ) ;
263+ }
264+
265+ return missingRefs . length
266+ ? [
267+ {
268+ tree,
269+ data_type,
270+ missingRefs,
271+ display_name,
272+ ct_uid : this . currentUid ,
273+ name : this . currentTitle ,
274+ treeStr : tree
275+ . map ( ( { name } ) => name )
276+ . filter ( ( val ) => val )
277+ . join ( ' ➜ ' ) ,
278+ } ,
279+ ]
280+ : [ ] ;
281+ }
282+
202283 /**
203284 * The function "validateGlobalField" asynchronously validates a global field by looking for a
204285 * reference in a tree data structure.
@@ -298,7 +379,9 @@ export default class ContentType {
298379
299380 for ( const reference of reference_to ?? [ ] ) {
300381 // NOTE Can skip specific references keys (Ex, system defined keys can be skipped)
301- if ( this . config . skipRefs . includes ( reference ) ) continue ;
382+ if ( this . config . skipRefs . includes ( reference ) ) {
383+ continue ;
384+ }
302385
303386 const refExist = find ( this . ctSchema , { uid : reference } ) ;
304387
@@ -350,14 +433,13 @@ export default class ContentType {
350433 case 'json' :
351434 case 'reference' :
352435 if ( data_type === 'json' ) {
353- if ( field . field_metadata . extension ) {
436+ if ( 'extension' in field . field_metadata && field . field_metadata . extension ) {
354437 // NOTE Custom field type
355- if ( ! fixTypes . includes ( 'json:custom-field ' ) ) return field ;
438+ if ( ! fixTypes . includes ( 'json:extension ' ) ) return field ;
356439
357440 // NOTE Fix logic
358-
359- return field ;
360- } else if ( field . field_metadata . allow_json_rte ) {
441+ return this . fixMissingExtensionOrApp ( tree , field as ExtensionOrAppFieldDataType ) ;
442+ } else if ( 'allow_json_rte' in field . field_metadata && field . field_metadata . allow_json_rte ) {
361443 if ( ! fixTypes . includes ( 'json:rte' ) ) return field ;
362444
363445 return this . fixMissingReferences ( tree , field as JsonRTEFieldDataType ) ;
@@ -456,10 +538,10 @@ export default class ContentType {
456538 const refErrorObj = {
457539 tree,
458540 display_name,
459- fixStatus : 'Fixed' ,
460- missingRefs : [ reference_to ] ,
461541 ct_uid : this . currentUid ,
462542 name : this . currentTitle ,
543+ missingRefs : [ reference_to ] ,
544+ fixStatus : this . fix ? 'Fixed' : undefined ,
463545 treeStr : tree . map ( ( { name } ) => name ) . join ( ' ➜ ' ) ,
464546 } ;
465547
@@ -499,6 +581,41 @@ export default class ContentType {
499581 . filter ( ( val ) => val ) as ModularBlockType [ ] ;
500582 }
501583
584+ /**
585+ * The function checks for missing extension or app references in a given tree and fixes them if the
586+ * fix flag is enabled.
587+ * @param {Record<string, unknown>[] } tree - An array of objects representing a tree structure.
588+ * @param {ExtensionOrAppFieldDataType } field - The `field` parameter is of type
589+ * `ExtensionOrAppFieldDataType`.
590+ * @returns If the `fix` flag is true and there are missing references (`missingRefs` is not empty),
591+ * then `null` is returned. Otherwise, the `field` parameter is returned.
592+ */
593+ fixMissingExtensionOrApp ( tree : Record < string , unknown > [ ] , field : ExtensionOrAppFieldDataType ) {
594+ const missingRefs : string [ ] = [ ] ;
595+ const { uid, extension_uid, data_type, display_name } = field ;
596+
597+ if ( ! this . extensions . includes ( extension_uid ) ) {
598+ missingRefs . push ( { uid, extension_uid, type : 'Extension or Apps' } as any ) ;
599+ }
600+
601+ if ( this . fix && ! isEmpty ( missingRefs ) ) {
602+ this . missingRefs [ this . currentUid ] . push ( {
603+ tree,
604+ data_type,
605+ missingRefs,
606+ display_name,
607+ fixStatus : 'Fixed' ,
608+ ct_uid : this . currentUid ,
609+ name : this . currentTitle ,
610+ treeStr : tree . map ( ( { name } ) => name ) . join ( ' ➜ ' ) ,
611+ } ) ;
612+
613+ return null
614+ }
615+
616+ return field ;
617+ }
618+
502619 /**
503620 * The function `fixMissingReferences` checks for missing references in a given tree and field, and
504621 * attempts to fix them by removing the missing references from the field's `reference_to` array.
@@ -515,7 +632,9 @@ export default class ContentType {
515632
516633 for ( const reference of reference_to ?? [ ] ) {
517634 // NOTE Can skip specific references keys (Ex, system defined keys can be skipped)
518- if ( this . config . skipRefs . includes ( reference ) ) continue ;
635+ if ( this . config . skipRefs . includes ( reference ) ) {
636+ continue ;
637+ }
519638
520639 const refExist = find ( this . ctSchema , { uid : reference } ) ;
521640
@@ -557,6 +676,7 @@ export default class ContentType {
557676 */
558677 fixGroupField ( tree : Record < string , unknown > [ ] , field : GroupFieldDataType ) {
559678 const { data_type, display_name } = field ;
679+
560680 field . schema = this . runFixOnSchema ( tree , field . schema as ContentTypeSchemaType [ ] ) ;
561681
562682 if ( isEmpty ( field . schema ) ) {
0 commit comments