diff --git a/packages/contentstack-import/src/import/modules/marketplace-apps.ts b/packages/contentstack-import/src/import/modules/marketplace-apps.ts index cb150583a..e418a4256 100644 --- a/packages/contentstack-import/src/import/modules/marketplace-apps.ts +++ b/packages/contentstack-import/src/import/modules/marketplace-apps.ts @@ -697,46 +697,84 @@ export default class ImportMarketplaceApps extends BaseClass { if (!isEmpty(configuration)) { log.debug(`Updating app configuration for: ${appName}`, this.importConfig.context); - await this.appSdk - .marketplace(this.importConfig.org_uid) - .installation(installation_uid) - .setConfiguration(this.nodeCrypto.decrypt(configuration)) - .then(({ data }: any) => { - if (data?.message) { - log.debug(data, this.importConfig.context); - log.info(formatError(data.message), this.importConfig.context); - } else { - log.success(`${appName} app config updated successfully.!`, this.importConfig.context); - log.debug(`Configuration update successful for: ${appName}`, this.importConfig.context); - } - }) - .catch((error: any) => { - log.debug(error, this.importConfig.context); - log.error(formatError(error), this.importConfig.context); - log.debug(`Configuration update failed for: ${appName}`, this.importConfig.context); - }); + // NOTE: decrypt synchronously in a guard. A bad-decrypt (e.g. ERR_OSSL_BAD_DECRYPT when the + // export was encrypted with a different key) would otherwise throw here — outside the promise + // chain's .catch — abort the whole marketplace-apps module, and skip writing the + // marketplace_apps uid-mapping, which starves the downstream GF/CT/entry extension remap. + // Instead: warn and skip only this app's configuration; the app stays installed and its + // extension mappings are still recorded. + let decryptedConfiguration: any; + try { + decryptedConfiguration = this.nodeCrypto.decrypt(configuration); + } catch (error: any) { + log.warn( + `Failed to decrypt configuration for '${appName}'; skipping its configuration update. The app is installed and its extension mappings are preserved. (${ + error?.message || error + })`, + this.importConfig.context, + ); + decryptedConfiguration = undefined; + } + + if (decryptedConfiguration !== undefined) { + await this.appSdk + .marketplace(this.importConfig.org_uid) + .installation(installation_uid) + .setConfiguration(decryptedConfiguration) + .then(({ data }: any) => { + if (data?.message) { + log.debug(data, this.importConfig.context); + log.info(formatError(data.message), this.importConfig.context); + } else { + log.success(`${appName} app config updated successfully.!`, this.importConfig.context); + log.debug(`Configuration update successful for: ${appName}`, this.importConfig.context); + } + }) + .catch((error: any) => { + log.debug(error, this.importConfig.context); + log.error(formatError(error), this.importConfig.context); + log.debug(`Configuration update failed for: ${appName}`, this.importConfig.context); + }); + } } if (!isEmpty(server_configuration)) { log.debug(`Updating server configuration for: ${appName}`, this.importConfig.context); - await this.appSdk - .marketplace(this.importConfig.org_uid) - .installation(installation_uid) - .setServerConfig(this.nodeCrypto.decrypt(server_configuration)) - .then(({ data }: any) => { - if (data?.message) { - log.debug(data, this.importConfig.context); - log.error(formatError(data.message), this.importConfig.context); - } else { - log.success(`${appName} app server config updated successfully.!`, this.importConfig.context); - log.debug(`Server configuration update successful for: ${appName}`, this.importConfig.context); - } - }) - .catch((error: any) => { - log.debug(error, this.importConfig.context); - log.error(formatError(error), this.importConfig.context); - log.debug(`Server configuration update failed for: ${appName}`, this.importConfig.context); - }); + // NOTE: guard the decrypt for the same reason as `configuration` above — a bad-decrypt must + // not abort the module or skip the uid-mapping write. + let decryptedServerConfiguration: any; + try { + decryptedServerConfiguration = this.nodeCrypto.decrypt(server_configuration); + } catch (error: any) { + log.warn( + `Failed to decrypt server configuration for '${appName}'; skipping its server configuration update. The app is installed and its extension mappings are preserved. (${ + error?.message || error + })`, + this.importConfig.context, + ); + decryptedServerConfiguration = undefined; + } + + if (decryptedServerConfiguration !== undefined) { + await this.appSdk + .marketplace(this.importConfig.org_uid) + .installation(installation_uid) + .setServerConfig(decryptedServerConfiguration) + .then(({ data }: any) => { + if (data?.message) { + log.debug(data, this.importConfig.context); + log.error(formatError(data.message), this.importConfig.context); + } else { + log.success(`${appName} app server config updated successfully.!`, this.importConfig.context); + log.debug(`Server configuration update successful for: ${appName}`, this.importConfig.context); + } + }) + .catch((error: any) => { + log.debug(error, this.importConfig.context); + log.error(formatError(error), this.importConfig.context); + log.debug(`Server configuration update failed for: ${appName}`, this.importConfig.context); + }); + } } } diff --git a/packages/contentstack-import/src/utils/asset-helper.ts b/packages/contentstack-import/src/utils/asset-helper.ts index e1110656e..99ca238b2 100644 --- a/packages/contentstack-import/src/utils/asset-helper.ts +++ b/packages/contentstack-import/src/utils/asset-helper.ts @@ -93,6 +93,10 @@ export const lookupAssets = function ( let matchedUrls: string[] = []; let find = function (schema: any, entryToFind: any) { + // Guard against a missing schema — e.g. a reference-only global_field stub + // (data_type: 'global_field', reference_to, no expanded `schema`) as produced by + // query-export. Without this, recursing into an undefined schema throws. + if (!Array.isArray(schema)) return; for (let i = 0, _i = schema.length; i < _i; i++) { if ( schema[i].data_type === 'text' && @@ -170,6 +174,32 @@ export const lookupAssets = function ( }); } + // Remap `metadata.extension_uid` anywhere in the entry DATA to the destination app's + // extension_uid via the marketplace_apps mapping (`installedExtensions`). + // + // is_asset custom-field values (e.g. `image_presets`) carry a stack-scoped + // `metadata.extension_uid` in the entry data. Left unmapped, the reference is orphaned on + // the destination and a later audit strips it as a missing reference (silent data loss). + // These fields can be nested inside group / global_field / blocks, and an imported + // content-type schema may represent a global field as a reference-only stub (no expanded + // `schema`) — so this walks the entry DATA directly instead of relying on the schema shape. + // Only known source->destination app UIDs are remapped, so unrelated values are untouched. + function remapEntryMetadataExtensionUids(node: any) { + if (!node || typeof node !== 'object') return; + if (Array.isArray(node)) { + for (const item of node) remapEntryMetadataExtensionUids(item); + return; + } + const currentUid = node.metadata?.extension_uid; + if (currentUid && installedExtensions && installedExtensions[currentUid]) { + log.debug(`Mapping metadata extension UID: ${currentUid} -> ${installedExtensions[currentUid]}`); + node.metadata.extension_uid = installedExtensions[currentUid]; + } + for (const key of Object.keys(node)) { + remapEntryMetadataExtensionUids(node[key]); + } + } + function findAssetIdsFromHtmlRte(entryObj: any, ctSchema: any) { log.debug('Extracting asset UIDs from HTML RTE fields'); const regex = /