Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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;
}

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