Skip to content
110 changes: 74 additions & 36 deletions packages/contentstack-import/src/import/modules/marketplace-apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cant we just return instead of " decryptedServerConfiguration = undefined;"

Same for above block as well , or is there any other reason ?

}

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);
});
}
}
}

Expand Down
35 changes: 35 additions & 0 deletions packages/contentstack-import/src/utils/asset-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' &&
Expand Down Expand Up @@ -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 = /<img asset_uid=\\"([^"]+)\\"/g;
Expand Down Expand Up @@ -266,6 +296,11 @@ export const lookupAssets = function (
}

find(data.content_type.schema, data.entry);
// Remap marketplace-app extension UIDs in entry data (e.g. image_presets' metadata.extension_uid)
// once, unconditionally. find() only reaches the is_asset branch when it can descend the schema,
// so a reference-only global_field stub (no expanded schema) would otherwise be missed. This pass
// is schema-independent and idempotent (already-mapped UIDs are no-ops on re-run).
remapEntryMetadataExtensionUids(data.entry);
// findFileUrls scans the whole entry object, but is only triggered inside find() when a
// text field has markdown/rich_text_type metadata. Content types with no such fields
// (e.g. those storing asset URLs in plain text fields) never call findFileUrls, so URLs
Expand Down
Loading