Skip to content

Commit a2cd18f

Browse files
Merge pull request #295 from contentstack/feat/DX-9336
feat: integrate asset count API for exact pagination totals (DX-9336)
2 parents 8c675f9 + eebe2f4 commit a2cd18f

9 files changed

Lines changed: 482 additions & 128 deletions

File tree

.talismanrc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
fileignoreconfig:
22
- filename: pnpm-lock.yaml
3-
checksum: f149ffa2864c1e5aa5b3f3363ea0d690e288168167d103775cb1f61be81151d5
3+
checksum: 8eb82c59ad8b81af64587af55c6788d09a34d1b651d030d1ef01dce472716f2a
44
- filename: packages/contentstack-branches/test/unit/helpers/stub-auth.ts
55
checksum: 8cafd5994d3ec13ba9af74c80b330bfd14721ea4e0359b456598964a6c2913ce
66
- filename: packages/contentstack-export/src/config/index.ts
@@ -18,7 +18,7 @@ fileignoreconfig:
1818
- filename: packages/contentstack-cli-cm-regex-validate/test/utils/connect-stack.test.ts
1919
checksum: 018980aa2b919967b9ef9ab1bdf635d4867fe21593fba5890afa443f440228ff
2020
- filename: packages/contentstack-bulk-operations/src/messages/index.ts
21-
checksum: 07c2bf3f3130ad5e8b6a2971d76139e9b643c70a9ff5f7450adfb5c9bf3d7164
21+
checksum: ba38daefd561d9acd018b6ebcb65847eceab6d375b28988d7b5624dcea9ccfe0
2222
- filename: packages/contentstack-bulk-operations/src/utils/operation-flag-matrix.ts
2323
checksum: 99a3c3eb422a17f73f4c8f15088004fa4c95df3545bdf510310c1f3a20e4c2c2
2424
- filename: packages/contentstack-bulk-operations/src/base-bulk-command.ts
@@ -39,4 +39,6 @@ fileignoreconfig:
3939
checksum: 88c7e1dee308fa4ae25e7815ead0842b1aac7ed669b02859cc8b25cba879595d
4040
- filename: packages/contentstack-bulk-operations/test/unit/services/taxonomy-service.test.ts
4141
checksum: abc5ac707341760cf59d5b8b1c4e13cf2c79955e2735c33e2db3ec6bc48eddb6
42+
- filename: packages/contentstack-import/src/import/modules/assets.ts
43+
checksum: cda61a9c90bb39f27c09951b8a2623851296aefd0d3220d066032287a712d899
4244
version: '1.0'

packages/contentstack-asset-management/src/export/assets.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ const ASSET_META_KEYS = ['uid', 'url', 'filename', 'file_name', 'parent_uid'];
1717

1818
type AssetRecord = { uid?: string; _uid?: string; url?: string; filename?: string; file_name?: string };
1919

20-
/** Per-space export counts surfaced to the summary (assets = downloaded binaries; folders = entities). */
21-
export type SpaceExportCounts = { assets: number; folders: number };
20+
/**
21+
* Per-space export counts surfaced to the summary (assets = downloaded binaries; folders = entities;
22+
* failedAssets = metadata records missing after permanent page failures + binary download failures).
23+
*/
24+
export type SpaceExportCounts = { assets: number; folders: number; failedAssets: number };
2225

2326
export default class ExportAssets extends CSAssetsExportAdapter {
2427
constructor(apiConfig: CSAssetsAPIConfig, exportContext: ExportContext) {
@@ -53,11 +56,22 @@ export default class ExportAssets extends CSAssetsExportAdapter {
5356
};
5457

5558
log.debug(`Fetching folders and streaming assets for space ${workspace.space_uid}`, this.exportContext.context);
56-
const [folders] = await Promise.all([
59+
const [folders, streamResult] = await Promise.all([
5760
this.getWorkspaceFolders(workspace.space_uid, workspace.uid, this.apiPageSize, this.apiFetchConcurrency),
5861
this.streamWorkspaceAssets(workspace.space_uid, workspace.uid, onPage, this.apiPageSize, this.apiFetchConcurrency),
5962
]);
6063

64+
// Permanently failed metadata pages (or mid-export drift) — these records never reached
65+
// assets.json, so the download loop can't see them. Count them as failures in the summary;
66+
// they are recoverable via a re-export or a targeted query-export, so don't abort the run.
67+
if (streamResult.missing > 0) {
68+
log.error(
69+
`${streamResult.missing} asset metadata record(s) could not be fetched for space ${workspace.space_uid} — ` +
70+
'they are missing from this export. Re-export (or use query-export) to recover them.',
71+
this.exportContext.context,
72+
);
73+
}
74+
6175
if (fsWriter) fsWriter.completeFile(true);
6276
else await this.writeEmptyChunkedJson(assetsDir, 'assets.json');
6377
log.debug(`Wrote chunked assets metadata (${totalStreamed} item(s)) under ${assetsDir}`, this.exportContext.context);
@@ -78,17 +92,21 @@ export default class ExportAssets extends CSAssetsExportAdapter {
7892
this.tick(true, `metadata: ${workspace.space_uid} (${totalStreamed})`, null);
7993

8094
log.debug(`Starting binary downloads for space ${workspace.space_uid}`, this.exportContext.context);
81-
const assetsDownloaded = await this.downloadWorkspaceAssets(assetsDir, workspace.space_uid, downloadableCount);
95+
const downloads = await this.downloadWorkspaceAssets(assetsDir, workspace.space_uid, downloadableCount);
8296

8397
const folderCount = getArrayFromResponse(folders, 'folders').length;
84-
return { assets: assetsDownloaded, folders: folderCount };
98+
return { assets: downloads.ok, folders: folderCount, failedAssets: streamResult.missing + downloads.fail };
8599
}
86100

87101
/**
88102
* Download asset binaries by reading the just-written chunked `assets.json` back from disk
89103
* (one chunk at a time), so we never re-materialize the whole asset list in memory.
90104
*/
91-
private async downloadWorkspaceAssets(assetsDir: string, spaceUid: string, expectedDownloads: number): Promise<number> {
105+
private async downloadWorkspaceAssets(
106+
assetsDir: string,
107+
spaceUid: string,
108+
expectedDownloads: number,
109+
): Promise<{ ok: number; fail: number }> {
92110
const filesDir = pResolve(assetsDir, 'files');
93111
await mkdir(filesDir, { recursive: true });
94112

@@ -202,7 +220,7 @@ export default class ExportAssets extends CSAssetsExportAdapter {
202220
this.exportContext.context,
203221
);
204222

205-
return downloadOk;
223+
return { ok: downloadOk, fail: downloadFail };
206224
}
207225

208226
}

packages/contentstack-asset-management/src/export/spaces.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export type AssetExportCounts = {
1818
folders: number;
1919
assetTypes: number;
2020
fields: number;
21+
/** Assets missing from the export: permanently failed metadata pages + failed binary downloads. */
22+
failedAssets: number;
2123
};
2224

2325
/**
@@ -53,7 +55,7 @@ export class ExportSpaces {
5355

5456
if (!linkedWorkspaces.length) {
5557
log.debug('No linked workspaces to export', context);
56-
return { assets: 0, folders: 0, assetTypes: 0, fields: 0 };
58+
return { assets: 0, folders: 0, assetTypes: 0, fields: 0, failedAssets: 0 };
5759
}
5860

5961
log.debug('Starting Contentstack Assets export process...', context);
@@ -105,6 +107,7 @@ export class ExportSpaces {
105107
// Real entity counts accumulated for the summary (Bug 3).
106108
let assetsTotal = 0;
107109
let foldersTotal = 0;
110+
let failedAssetsTotal = 0;
108111
let assetTypesCount = 0;
109112
let fieldsCount = 0;
110113
try {
@@ -140,6 +143,7 @@ export class ExportSpaces {
140143
const spaceCounts = await exportWorkspace.start(ws, spaceDir, branchName || 'main', spaceProcess);
141144
assetsTotal += spaceCounts.assets;
142145
foldersTotal += spaceCounts.folders;
146+
failedAssetsTotal += spaceCounts.failedAssets;
143147
progress.completeProcess(spaceProcess, true);
144148
log.debug(`Exported workspace structure for space ${ws.space_uid}`, context);
145149
} catch (err) {
@@ -157,14 +161,20 @@ export class ExportSpaces {
157161
}
158162

159163
log.info(
160-
anySpaceFailed
164+
anySpaceFailed || failedAssetsTotal > 0
161165
? 'Contentstack Assets export completed with errors in one or more spaces'
162166
: 'Contentstack Assets export completed successfully',
163167
context,
164168
);
165169
log.debug('Contentstack Assets export completed', context);
166170

167-
return { assets: assetsTotal, folders: foldersTotal, assetTypes: assetTypesCount, fields: fieldsCount };
171+
return {
172+
assets: assetsTotal,
173+
folders: foldersTotal,
174+
assetTypes: assetTypesCount,
175+
fields: fieldsCount,
176+
failedAssets: failedAssetsTotal,
177+
};
168178
} catch (err) {
169179
if (!bootstrapFailed) {
170180
// Mark any spaces that hadn't been processed as failed so the multibar

packages/contentstack-asset-management/src/types/cs-assets-api.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,21 @@ export type SearchAssetsParams = {
159159
limit?: number;
160160
};
161161

162+
/** Response shape of GET /api/bff/spaces/{space_uid}/assets/count. */
163+
export type AssetCountResponse = {
164+
count: number;
165+
};
166+
167+
/**
168+
* Result of streaming a workspace's assets. `missing` is the gap between the authoritative
169+
* count-API total and the records actually streamed (permanently failed pages and/or items
170+
* changed mid-export) — surfaced as failures in the export summary rather than aborting the run.
171+
*/
172+
export type StreamWorkspaceAssetsResult = {
173+
streamed: number;
174+
missing: number;
175+
};
176+
162177
/** Response shape from POST /api/search for assets. */
163178
export type SearchAssetsResponse = {
164179
count?: number;
@@ -174,14 +189,15 @@ export interface ICSAssetsAdapter {
174189
listSpaces(pageSize?: number, fetchConcurrency?: number): Promise<SpacesListResponse>;
175190
getSpace(spaceUid: string): Promise<SpaceResponse>;
176191
getWorkspaceFields(spaceUid: string, pageSize?: number, fetchConcurrency?: number): Promise<FieldsResponse>;
177-
getWorkspaceAssets(spaceUid: string, workspaceUid?: string, pageSize?: number, fetchConcurrency?: number): Promise<unknown>;
192+
getAssetsCount(spaceUid: string, workspaceUid?: string): Promise<number>;
193+
getFoldersCount(spaceUid: string, workspaceUid?: string): Promise<number>;
178194
streamWorkspaceAssets(
179195
spaceUid: string,
180196
workspaceUid: string | undefined,
181197
onPage: (items: unknown[]) => void | Promise<void>,
182198
pageSize?: number,
183199
fetchConcurrency?: number,
184-
): Promise<number>;
200+
): Promise<StreamWorkspaceAssetsResult>;
185201
getWorkspaceFolders(spaceUid: string, workspaceUid?: string, pageSize?: number, fetchConcurrency?: number): Promise<unknown>;
186202
getWorkspaceAssetTypes(spaceUid: string, pageSize?: number, fetchConcurrency?: number): Promise<AssetTypesResponse>;
187203
searchAssets(params: SearchAssetsParams): Promise<SearchAssetsResponse>;

0 commit comments

Comments
 (0)