Skip to content

Commit 09ace55

Browse files
fix(import): remap nested entry-data extension UIDs for is_asset custom fields
Marketplace-app / extension references embedded in entry DATA (e.g. an `image_presets` value's `metadata.extension_uid`) are stack-scoped and must be remapped to the destination app's extension_uid during import. The remap in `lookupAssets` (`findAssetIdsFromJsonCustomFields`) only walked top-level schema fields, so any `is_asset` JSON custom field nested inside a group / global_field / blocks was skipped. Consequences of the miss: - the entry kept the source app's extension_uid, leaving an orphaned reference on the destination stack; and - on a subsequent import the audit flagged that UID as a missing reference and stripped the whole field (silent data loss of image-preset configs). Replace the flat `ctSchema.map` with a recursive walk (`remapJsonCustomFieldExtensionUids`) that follows the entry-data shape through group / global_field / blocks and handles multiple-valued fields, remapping `metadata.extension_uid` (and the schema field's extension_uid) via the marketplace_apps mapping. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4fb99ed commit 09ace55

1 file changed

Lines changed: 51 additions & 14 deletions

File tree

packages/contentstack-import/src/utils/asset-helper.ts

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -149,25 +149,62 @@ export const lookupAssets = function (
149149

150150
function findAssetIdsFromJsonCustomFields(entryObj: any, ctSchema: any) {
151151
log.debug('Processing JSON custom fields for asset references');
152-
ctSchema.map((row: any) => {
153-
if (row.data_type === 'json') {
154-
if (entryObj[row.uid] && row.field_metadata.extension && row.field_metadata.is_asset) {
155-
if (installedExtensions && installedExtensions[row.extension_uid]) {
156-
log.debug(`Mapping extension UID in custom field: ${row.extension_uid}`);
157-
row.extension_uid = installedExtensions[row.extension_uid];
158-
}
152+
// NOTE: image-preset (and other is_asset custom-field) values carry a stack-scoped
153+
// `metadata.extension_uid` inside the entry DATA. This must be remapped to the destination
154+
// app's extension_uid, otherwise the reference is orphaned in the destination (and a later
155+
// audit strips it as a missing reference -> silent data loss). These fields can be nested
156+
// inside group / global_field / blocks, so walk the schema and entry data recursively rather
157+
// than only the top level.
158+
remapJsonCustomFieldExtensionUids(ctSchema, entryObj);
159+
}
159160

160-
if (entryObj[row.uid].metadata && entryObj[row.uid].metadata.extension_uid) {
161-
if (installedExtensions && installedExtensions[entryObj[row.uid].metadata.extension_uid]) {
162-
log.debug(`Mapping metadata extension UID: ${entryObj[row.uid].metadata.extension_uid}`);
163-
entryObj[row.uid].metadata.extension_uid = installedExtensions[entryObj[row.uid].metadata.extension_uid];
161+
// Recursively remap `metadata.extension_uid` (and the schema field's extension_uid) for
162+
// is_asset JSON custom fields, following the entry-data shape through group / global_field /
163+
// blocks and multiple-valued fields.
164+
function remapJsonCustomFieldExtensionUids(schema: any[], dataNode: any) {
165+
if (!Array.isArray(schema) || !dataNode || typeof dataNode !== 'object') return;
166+
// A `multiple: true` group/field is stored as an array of nodes.
167+
const dataNodes = Array.isArray(dataNode) ? dataNode : [dataNode];
168+
169+
for (const node of dataNodes) {
170+
if (!node || typeof node !== 'object') continue;
171+
172+
for (const field of schema) {
173+
const { uid, data_type } = field || {};
174+
if (!uid) continue;
175+
const value = node[uid];
176+
if (value === undefined || value === null) continue;
177+
178+
if (data_type === 'json' && field.field_metadata?.extension && field.field_metadata?.is_asset) {
179+
// Remap the schema field's extension_uid (parity with previous behavior).
180+
if (installedExtensions && installedExtensions[field.extension_uid]) {
181+
log.debug(`Mapping extension UID in custom field: ${field.extension_uid}`);
182+
field.extension_uid = installedExtensions[field.extension_uid];
183+
}
184+
// Remap the entry-data metadata.extension_uid for each value (single or multiple).
185+
const values = Array.isArray(value) ? value : [value];
186+
for (const val of values) {
187+
const currentUid = val?.metadata?.extension_uid;
188+
if (currentUid && installedExtensions && installedExtensions[currentUid]) {
189+
log.debug(`Mapping metadata extension UID: ${currentUid} -> ${installedExtensions[currentUid]}`);
190+
val.metadata.extension_uid = installedExtensions[currentUid];
191+
}
192+
}
193+
} else if (data_type === 'group' || data_type === 'global_field') {
194+
remapJsonCustomFieldExtensionUids(field.schema, value);
195+
} else if (data_type === 'blocks' && Array.isArray(field.blocks)) {
196+
const blockInstances = Array.isArray(value) ? value : [value];
197+
for (const blockInstance of blockInstances) {
198+
if (!blockInstance || typeof blockInstance !== 'object') continue;
199+
for (const blockDef of field.blocks) {
200+
if (blockDef?.uid && blockInstance[blockDef.uid] && blockDef.schema) {
201+
remapJsonCustomFieldExtensionUids(blockDef.schema, blockInstance[blockDef.uid]);
202+
}
164203
}
165204
}
166205
}
167206
}
168-
169-
return row;
170-
});
207+
}
171208
}
172209

173210
function findAssetIdsFromHtmlRte(entryObj: any, ctSchema: any) {

0 commit comments

Comments
 (0)