From 25162b1a4082748d0c1cdc0a4067153dc9975176 Mon Sep 17 00:00:00 2001 From: chetan-contentstack Date: Mon, 10 Aug 2026 16:52:03 +0530 Subject: [PATCH 1/3] fix(CMG-1108): correct WordPress asset source-uid to match CLI's mapping key extractAssets used the bare wp:post_id as otherCmsAssetUid, but the CLI import stages assets under assets_. The mismatch meant WordPress asset uids never resolved, so migrated assets vanished from the Map Entry Assets screen on delta iterations. --- upload-api/migration-wordpress/libs/extractAssets.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/upload-api/migration-wordpress/libs/extractAssets.ts b/upload-api/migration-wordpress/libs/extractAssets.ts index 35bfd0e46..0136c36de 100644 --- a/upload-api/migration-wordpress/libs/extractAssets.ts +++ b/upload-api/migration-wordpress/libs/extractAssets.ts @@ -78,7 +78,10 @@ const extractAssets = async (filePath: string): Promise => { rows.push({ id, - otherCmsAssetUid: id, + // Must match the `assets_` key wordpress.service.ts uses as the + // asset's customId/uid when staging it for CLI import — that's the key the CLI + // writes back into uid-mapping.json, and the read path looks this up verbatim. + otherCmsAssetUid: `assets_${id}`, filename, title, file_size: '', From 66944bd824540565671c033ca08ae98fd6157783 Mon Sep 17 00:00:00 2001 From: chetan-contentstack Date: Mon, 10 Aug 2026 16:52:04 +0530 Subject: [PATCH 2/3] fix: show all assets on Map Entry Assets screen regardless of iteration Assets were hidden on iteration 2+ unless already migrated, unlike entries which always show new and updated items together. Removes that filter so both screens behave consistently across all CMS connectors. --- api/src/services/contentMapper.service.ts | 42 +++---------------- .../services/contentMapper.service.test.ts | 20 +++++++++ 2 files changed, 25 insertions(+), 37 deletions(-) diff --git a/api/src/services/contentMapper.service.ts b/api/src/services/contentMapper.service.ts index 59572336a..fb51abbe5 100644 --- a/api/src/services/contentMapper.service.ts +++ b/api/src/services/contentMapper.service.ts @@ -2496,20 +2496,6 @@ const getAssetMapping = async (req: Request) => { await uidMapperCurrent.read(); const uidMapperPrev: any = iteration > 1 ? await getNearestPriorUidMapper(projectId, iteration) : null; - // Whether we actually have any uid data to resolve against yet. getUidMapperDb creates - // the file with an empty `assets: {}` default, so a fresh iteration directory (visited - // right after a restart, before this iteration's CLI import has run and written - // writeUidMapping's output) legitimately has none — distinct from "this project simply - // has no previously-migrated assets". Also true if any row already carries a - // pre-resolved uid from creation time (putTestData resolves it then, see ~line 280). - const hasAnyUidData = - Object.keys((uidMapperCurrent?.data as any)?.assets ?? {}).length > 0 || - Object.keys((uidMapperPrev?.data as any)?.assets ?? {}).length > 0 || - (assetMapping ?? []).some((item: any) => { - const uid = item?.contentstackAssetUid; - return uid != null && String(uid).trim() !== ''; - }); - let uidEnriched = (assetMapping ?? []).map((item: any) => { if (!item) return item; const existing = item?.contentstackAssetUid; @@ -2558,29 +2544,11 @@ const getAssetMapping = async (req: Request) => { return { ...item, status: 'ok' }; }); - // Delta migration intent: on iteration 2+ the Assets tab lists ONLY assets that - // were already migrated in a prior iteration — i.e. those with a Contentstack - // uid. The user selects which of those to update with the current file's newer - // version. Brand-new assets in this iteration have no prior uid; they upload - // automatically during the run and don't need a Map Entry row (nothing to - // select or update yet). Iteration 1 is untouched — everything is new then. - // - // Exception: always surface 'failed'/'missing' rows even without a uid. A brand-new - // asset that fails to download NEVER gets a Contentstack uid (it never successfully - // migrates), so the has-uid check alone would hide it from view forever — the user - // would have no way to discover or retry it. - // Only apply the delta filter once we actually have uid data to filter with — - // otherwise a race right after restart (this iteration's uid-mapper.json not written - // yet) would filter out EVERY row and render an empty tab indistinguishable from "no - // previously-migrated assets", which could be mistaken for correct behavior since - // CMG-1097 already gives that empty state a legitimate-looking layout. - const displayMapping = iteration > 1 && hasAnyUidData - ? enrichedMapping.filter((item: any) => { - const uid = item?.contentstackAssetUid; - const hasUid = uid != null && String(uid).trim() !== ''; - return hasUid || item?.status === 'failed' || item?.status === 'missing'; - }) - : enrichedMapping; + // Show every asset regardless of iteration — brand-new-this-iteration assets + // alongside previously-migrated/updatable ones — matching getEntryMapping's + // behavior for entries. Consistent across all CMS connectors since this read + // path is shared. + const displayMapping = enrichedMapping; // Aggregate counts across the FULL (unpaginated, unsearched) visible set — the banner // needs "3 assets won't migrate" regardless of which page or search term is active. diff --git a/api/tests/unit/services/contentMapper.service.test.ts b/api/tests/unit/services/contentMapper.service.test.ts index 52c60d550..d3a61017b 100644 --- a/api/tests/unit/services/contentMapper.service.test.ts +++ b/api/tests/unit/services/contentMapper.service.test.ts @@ -1113,5 +1113,25 @@ describe('contentMapper.service', () => { expect(result.count).toBe(1); expect(result.assetMapping[0].filename).toBe('windmill.jpg'); }); + + it('shows brand-new assets alongside previously-migrated ones on iteration 2+', async () => { + (ProjectModelLowdb.chain.get as ReturnType).mockReturnValue( + createChain({ find: { ...project, iteration: 2 } }) + ); + mockUidMapperDb.data = { entry: {}, assets: { 'src-1': 'cs-1' } }; + (mockAssetMapperDb.chain.get as ReturnType).mockReturnValue( + createChain({ + filter: [ + { projectId: 'proj-1', otherCmsAssetUid: 'src-1', contentstackAssetUid: 'cs-1', filename: 'a.jpg', title: 'A' }, + { projectId: 'proj-1', otherCmsAssetUid: 'src-2', filename: 'b.jpg', title: 'B' }, + ], + }) + ); + + const result = await contentMapperService.getAssetMapping(baseReq()); + + expect(result.count).toBe(2); + expect(result.assetMapping.map((item: any) => item.otherCmsAssetUid).sort()).toEqual(['src-1', 'src-2']); + }); }); }); From d32412f139a66283c895e480fec956672ee46240 Mon Sep 17 00:00:00 2001 From: chetan-contentstack Date: Mon, 10 Aug 2026 17:22:01 +0530 Subject: [PATCH 3/3] fix: address PR review comment on #1143 cs_failed.json write on a failed WordPress attachment download used the bare wp:post_id instead of customId (assets_), which is the only key getAssetMapping looks up via otherCmsAssetUid. Failed WordPress attachments were silently reported as "ok" instead of "failed". --- api/src/services/wordpress.service.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/api/src/services/wordpress.service.ts b/api/src/services/wordpress.service.ts index 33a52615d..e4e5acc4c 100644 --- a/api/src/services/wordpress.service.ts +++ b/api/src/services/wordpress.service.ts @@ -1918,8 +1918,13 @@ async function saveAsset(assets: any, retryCount: number, affix: string, destina return assets["wp:post_id"]; } catch (err: any) { const assetName = assets["title"] || nameWithoutExt; - failedJSON[assets["wp:post_id"]] = { - failedUid: assets["wp:post_id"], + // Must be keyed by customId (assets_), not the bare wp:post_id — that's + // the otherCmsAssetUid extractAssets.ts assigns this row, and the only key + // getAssetMapping looks up in cs_failed.json to resolve a row's status. Keying by the + // bare id here (as this used to) meant a failed attachment was queried as + // `assets_`, never matched, and silently showed as "ok" instead of "failed". + failedJSON[customId] = { + failedUid: customId, name: assetName, url, reason_for_error: err?.message || "error",