Skip to content

Commit a01a6fc

Browse files
author
naman-contentstack
committed
chore: update implementation for AM import
1 parent 64c6e36 commit a01a6fc

File tree

11 files changed

+161
-59
lines changed

11 files changed

+161
-59
lines changed

.talismanrc

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,6 @@
11
fileignoreconfig:
2-
- filename: packages/contentstack-export/src/export/modules/assets.ts
3-
checksum: 1eacc8e86cb50fe283febe6688965854f420e02cf1b49555a15661fa0c3e3c7a
4-
- filename: packages/contentstack-import/src/utils/import-config-handler.ts
5-
checksum: f831cef1b7c3bd97bdbc170cff452350cee0f448d97df02e25aa41d6c4d64ad3
62
- filename: packages/contentstack-asset-management/src/import/asset-types.ts
7-
checksum: a39caa373b2a736d1e57063326cfb2073ae78376efa931b27d2c7110997708a5
8-
- filename: packages/contentstack-asset-management/src/export/spaces.ts
9-
checksum: 3ec11c8f710b60ae495c69344025587df2e6195c872a0c82feaf04ac044ecefa
10-
- filename: packages/contentstack-import/src/import/modules/assets.ts
11-
checksum: d9f4a29a29e8b8a2a36e498f2380d39e1c5c0ec13ff894ef450abd817f2a646e
3+
checksum: 78f9334477ccb4b59a2b14b09214377f2eaf271e2dc68d1976321910d3b4e91c
124
- filename: packages/contentstack-asset-management/src/import/fields.ts
13-
checksum: bbae69c28ec69bf67c2c7b4df3620380ef3fca488b3288e137b65a60ee738b9e
14-
- filename: packages/contentstack-asset-management/src/import/spaces.ts
15-
checksum: 79cf2f1b55523d28c218d970155f887255a00dc095a941556b709d1f19c6a8a0
16-
- filename: packages/contentstack-asset-management/src/types/asset-management-api.ts
17-
checksum: 716df03dcba70b2cc0f77b1f6338524553ba740080d7087a8699147c3ce8f0ba
18-
- filename: packages/contentstack-asset-management/src/utils/asset-management-api-adapter.ts
19-
checksum: 92bcad2feabc1954ead89b370d284b7af5f38ec1dca60a41752371977ef106ff
20-
- filename: packages/contentstack-asset-management/src/import/base.ts
21-
checksum: 513b55cf7e92bdbe8b815141822ba10579b6a728bec4424fc664794246ad33bb
22-
- filename: packages/contentstack-asset-management/src/import/assets.ts
23-
checksum: 2d34fa57f5ab269f6c535dff3242cc1135dbe1decd84fa0bc8997d0410d520b2
5+
checksum: 2a079cb7b58c11cf69068a9373f4b9a871487bc7ad03d2bc1efb45a3c1e333be
246
version: '1.0'

packages/contentstack-asset-management/src/constants/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
export const BATCH_SIZE = 50;
22
export const CHUNK_FILE_SIZE_MB = 1;
33

4+
/** Default parallel AM API calls when import caller does not set apiConcurrency. */
5+
export const DEFAULT_AM_API_CONCURRENCY = 5;
6+
7+
/**
8+
* Mapper output paths — must stay aligned with contentstack-import `PATH_CONSTANTS`
9+
* (`mapper` / `assets` / uid, url, space-uid file names).
10+
*/
11+
export const IMPORT_ASSETS_MAPPER_DIR_SEGMENTS = ['mapper', 'assets'] as const;
12+
export const IMPORT_ASSETS_MAPPER_FILES = {
13+
UID_MAPPING: 'uid-mapping.json',
14+
URL_MAPPING: 'url-mapping.json',
15+
SPACE_UID_MAPPING: 'space-uid-mapping.json',
16+
} as const;
17+
418
/**
519
* Main process name for Asset Management 2.0 export (single progress bar).
620
* Use this when adding/starting the process and for all ticks.

packages/contentstack-asset-management/src/import/asset-types.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { log } from '@contentstack/cli-utilities';
55
import type { AssetManagementAPIConfig, ImportContext } from '../types/asset-management-api';
66
import { AssetManagementImportAdapter } from './base';
77
import { PROCESS_NAMES, PROCESS_STATUS } from '../constants/index';
8+
import { runInBatches } from '../utils/concurrent-batch';
89

910
const STRIP_KEYS = ['created_at', 'created_by', 'updated_at', 'updated_by', 'is_system', 'category', 'preview_image_url', 'category_detail'];
1011

@@ -50,6 +51,9 @@ export default class ImportAssetTypes extends AssetManagementImportAdapter {
5051

5152
this.updateStatus(PROCESS_STATUS[PROCESS_NAMES.AM_IMPORT_ASSET_TYPES].IMPORTING, PROCESS_NAMES.AM_IMPORT_ASSET_TYPES);
5253

54+
type ToCreate = { uid: string; payload: Record<string, unknown> };
55+
const toCreate: ToCreate[] = [];
56+
5357
for (const assetType of items) {
5458
const uid = assetType.uid as string;
5559

@@ -74,15 +78,23 @@ export default class ImportAssetTypes extends AssetManagementImportAdapter {
7478
continue;
7579
}
7680

77-
const payload = omit(assetType, STRIP_KEYS);
81+
toCreate.push({ uid, payload: omit(assetType, STRIP_KEYS) as Record<string, unknown> });
82+
}
83+
84+
await runInBatches(toCreate, this.apiConcurrency, async ({ uid, payload }) => {
7885
try {
7986
await this.createAssetType(payload as any);
8087
this.tick(true, `asset-type: ${uid}`, null, PROCESS_NAMES.AM_IMPORT_ASSET_TYPES);
8188
log.debug(`Imported asset type: ${uid}`, this.importContext.context);
8289
} catch (e) {
83-
this.tick(false, `asset-type: ${uid}`, (e as Error)?.message ?? PROCESS_STATUS[PROCESS_NAMES.AM_IMPORT_ASSET_TYPES].FAILED, PROCESS_NAMES.AM_IMPORT_ASSET_TYPES);
90+
this.tick(
91+
false,
92+
`asset-type: ${uid}`,
93+
(e as Error)?.message ?? PROCESS_STATUS[PROCESS_NAMES.AM_IMPORT_ASSET_TYPES].FAILED,
94+
PROCESS_NAMES.AM_IMPORT_ASSET_TYPES,
95+
);
8496
log.debug(`Failed to import asset type ${uid}: ${e}`, this.importContext.context);
8597
}
86-
}
98+
});
8799
}
88100
}

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

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { log } from '@contentstack/cli-utilities';
55
import type { AssetManagementAPIConfig, ImportContext } from '../types/asset-management-api';
66
import { AssetManagementImportAdapter } from './base';
77
import { getArrayFromResponse } from '../utils/export-helpers';
8+
import { runInBatches } from '../utils/concurrent-batch';
89
import { PROCESS_NAMES, PROCESS_STATUS } from '../constants/index';
910

1011
type FolderRecord = {
@@ -123,6 +124,14 @@ export default class ImportAssets extends AssetManagementImportAdapter {
123124
this.updateStatus(PROCESS_STATUS[PROCESS_NAMES.AM_IMPORT_ASSETS].IMPORTING, PROCESS_NAMES.AM_IMPORT_ASSETS);
124125
log.debug(`Uploading ${assetItems.length} asset(s) for space ${newSpaceUid}`, this.importContext.context);
125126

127+
type UploadJob = {
128+
asset: AssetRecord;
129+
filePath: string;
130+
mappedParentUid: string | undefined;
131+
oldUid: string;
132+
};
133+
const uploadJobs: UploadJob[] = [];
134+
126135
for (const asset of assetItems) {
127136
const oldUid = asset.uid;
128137
const filename = asset.filename ?? asset.file_name ?? 'asset';
@@ -137,6 +146,11 @@ export default class ImportAssets extends AssetManagementImportAdapter {
137146
const assetParent = asset.parent_uid && asset.parent_uid !== 'root' ? asset.parent_uid : undefined;
138147
const mappedParentUid = assetParent ? folderUidMap[assetParent] ?? undefined : undefined;
139148

149+
uploadJobs.push({ asset, filePath, mappedParentUid, oldUid });
150+
}
151+
152+
await runInBatches(uploadJobs, this.apiConcurrency, async ({ asset, filePath, mappedParentUid, oldUid }) => {
153+
const filename = asset.filename ?? asset.file_name ?? 'asset';
140154
try {
141155
const { asset: created } = await this.uploadAsset(newSpaceUid, filePath, {
142156
title: asset.title ?? filename,
@@ -146,7 +160,6 @@ export default class ImportAssets extends AssetManagementImportAdapter {
146160

147161
uidMap[oldUid] = created.uid;
148162

149-
// Map old AM direct URL → new AM direct URL.
150163
if (asset.url && created.url) {
151164
urlMap[asset.url] = created.url;
152165
}
@@ -162,7 +175,7 @@ export default class ImportAssets extends AssetManagementImportAdapter {
162175
);
163176
log.debug(`Failed to upload asset ${oldUid}: ${e}`, this.importContext.context);
164177
}
165-
}
178+
});
166179

167180
return { uidMap, urlMap };
168181
}
@@ -181,24 +194,29 @@ export default class ImportAssets extends AssetManagementImportAdapter {
181194

182195
while (remaining.length > 0 && remaining.length !== prevLength) {
183196
prevLength = remaining.length;
197+
const ready: FolderRecord[] = [];
184198
const nextPass: FolderRecord[] = [];
185199

186200
for (const folder of remaining) {
187201
const { parent_uid: parentUid } = folder;
188-
// "root" is the AM API sentinel for a top-level folder
189202
const isRootParent = !parentUid || parentUid === 'root';
190203
const parentMapped = isRootParent || folderUidMap[parentUid] !== undefined;
191204

192205
if (!parentMapped) {
193206
nextPass.push(folder);
194-
continue;
207+
} else {
208+
ready.push(folder);
195209
}
210+
}
196211

212+
await runInBatches(ready, this.apiConcurrency, async (folder) => {
213+
const { parent_uid: parentUid } = folder;
214+
const isRootParent = !parentUid || parentUid === 'root';
197215
try {
198216
const { folder: created } = await this.createFolder(newSpaceUid, {
199217
title: folder.title,
200218
description: folder.description,
201-
parent_uid: isRootParent ? undefined : folderUidMap[parentUid],
219+
parent_uid: isRootParent ? undefined : folderUidMap[parentUid!],
202220
});
203221
folderUidMap[folder.uid] = created.uid;
204222
this.tick(true, `folder: ${folder.uid}`, null, PROCESS_NAMES.AM_IMPORT_FOLDERS);
@@ -212,7 +230,7 @@ export default class ImportAssets extends AssetManagementImportAdapter {
212230
);
213231
log.debug(`Failed to create folder ${folder.uid}: ${e}`, this.importContext.context);
214232
}
215-
}
233+
});
216234

217235
remaining = nextPass;
218236
}

packages/contentstack-asset-management/src/import/base.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { FsUtility, log, CLIProgressManager, configHandler } from '@contentstack
33

44
import type { AssetManagementAPIConfig, ImportContext } from '../types/asset-management-api';
55
import { AssetManagementAdapter } from '../utils/asset-management-api-adapter';
6-
import { AM_MAIN_PROCESS_NAME } from '../constants/index';
6+
import { AM_MAIN_PROCESS_NAME, DEFAULT_AM_API_CONCURRENCY } from '../constants/index';
77

88
export type { ImportContext };
99

@@ -61,6 +61,11 @@ export class AssetManagementImportAdapter extends AssetManagementAdapter {
6161
return this.importContext.spacesRootPath;
6262
}
6363

64+
/** Parallel AM API limit for import batches. */
65+
protected get apiConcurrency(): number {
66+
return this.importContext.apiConcurrency ?? DEFAULT_AM_API_CONCURRENCY;
67+
}
68+
6469
protected getAssetTypesDir(): string {
6570
return pResolve(this.importContext.spacesRootPath, 'asset_types');
6671
}

packages/contentstack-asset-management/src/import/fields.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { log } from '@contentstack/cli-utilities';
55
import type { AssetManagementAPIConfig, ImportContext } from '../types/asset-management-api';
66
import { AssetManagementImportAdapter } from './base';
77
import { PROCESS_NAMES, PROCESS_STATUS } from '../constants/index';
8+
import { runInBatches } from '../utils/concurrent-batch';
89

910
const STRIP_KEYS = ['created_at', 'created_by', 'updated_at', 'updated_by', 'is_system', 'asset_types_count'];
1011

@@ -50,6 +51,9 @@ export default class ImportFields extends AssetManagementImportAdapter {
5051

5152
this.updateStatus(PROCESS_STATUS[PROCESS_NAMES.AM_IMPORT_FIELDS].IMPORTING, PROCESS_NAMES.AM_IMPORT_FIELDS);
5253

54+
type ToCreate = { uid: string; payload: Record<string, unknown> };
55+
const toCreate: ToCreate[] = [];
56+
5357
for (const field of items) {
5458
const uid = field.uid as string;
5559

@@ -74,15 +78,23 @@ export default class ImportFields extends AssetManagementImportAdapter {
7478
continue;
7579
}
7680

77-
const payload = omit(field, STRIP_KEYS);
81+
toCreate.push({ uid, payload: omit(field, STRIP_KEYS) as Record<string, unknown> });
82+
}
83+
84+
await runInBatches(toCreate, this.apiConcurrency, async ({ uid, payload }) => {
7885
try {
7986
await this.createField(payload as any);
8087
this.tick(true, `field: ${uid}`, null, PROCESS_NAMES.AM_IMPORT_FIELDS);
8188
log.debug(`Imported field: ${uid}`, this.importContext.context);
8289
} catch (e) {
83-
this.tick(false, `field: ${uid}`, (e as Error)?.message ?? PROCESS_STATUS[PROCESS_NAMES.AM_IMPORT_FIELDS].FAILED, PROCESS_NAMES.AM_IMPORT_FIELDS);
90+
this.tick(
91+
false,
92+
`field: ${uid}`,
93+
(e as Error)?.message ?? PROCESS_STATUS[PROCESS_NAMES.AM_IMPORT_FIELDS].FAILED,
94+
PROCESS_NAMES.AM_IMPORT_FIELDS,
95+
);
8496
log.debug(`Failed to import field ${uid}: ${e}`, this.importContext.context);
8597
}
86-
}
98+
});
8799
}
88100
}

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

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { resolve as pResolve, join } from 'node:path';
2-
import { readdirSync, statSync } from 'node:fs';
2+
import { mkdirSync, readdirSync, statSync } from 'node:fs';
3+
import { writeFile } from 'node:fs/promises';
34
import { log, CLIProgressManager, configHandler } from '@contentstack/cli-utilities';
45

56
import type {
@@ -9,7 +10,11 @@ import type {
910
ImportResult,
1011
SpaceMapping,
1112
} from '../types/asset-management-api';
12-
import { AM_MAIN_PROCESS_NAME } from '../constants/index';
13+
import {
14+
AM_MAIN_PROCESS_NAME,
15+
IMPORT_ASSETS_MAPPER_DIR_SEGMENTS,
16+
IMPORT_ASSETS_MAPPER_FILES,
17+
} from '../constants/index';
1318
import { AssetManagementAdapter } from '../utils/asset-management-api-adapter';
1419
import ImportAssetTypes from './asset-types';
1520
import ImportFields from './fields';
@@ -34,7 +39,16 @@ export class ImportSpaces {
3439
}
3540

3641
async start(): Promise<ImportResult> {
37-
const { contentDir, assetManagementUrl, org_uid, apiKey, host, sourceApiKey, context } = this.options;
42+
const {
43+
contentDir,
44+
assetManagementUrl,
45+
org_uid,
46+
apiKey,
47+
host,
48+
sourceApiKey,
49+
context,
50+
apiConcurrency,
51+
} = this.options;
3852

3953
const spacesRootPath = pResolve(contentDir, 'spaces');
4054

@@ -45,6 +59,7 @@ export class ImportSpaces {
4559
host,
4660
org_uid,
4761
context,
62+
apiConcurrency,
4863
};
4964

5065
const apiConfig: AssetManagementAPIConfig = {
@@ -142,6 +157,19 @@ export class ImportSpaces {
142157
}
143158
}
144159

160+
if (this.options.backupDir) {
161+
const mapperDir = join(this.options.backupDir, ...IMPORT_ASSETS_MAPPER_DIR_SEGMENTS);
162+
mkdirSync(mapperDir, { recursive: true });
163+
await writeFile(join(mapperDir, IMPORT_ASSETS_MAPPER_FILES.UID_MAPPING), JSON.stringify(allUidMap), 'utf8');
164+
await writeFile(join(mapperDir, IMPORT_ASSETS_MAPPER_FILES.URL_MAPPING), JSON.stringify(allUrlMap), 'utf8');
165+
await writeFile(
166+
join(mapperDir, IMPORT_ASSETS_MAPPER_FILES.SPACE_UID_MAPPING),
167+
JSON.stringify(allSpaceUidMap),
168+
'utf8',
169+
);
170+
log.debug('Wrote AM 2.0 mapper files (uid, url, space-uid)', context);
171+
}
172+
145173
progress.completeProcess(AM_MAIN_PROCESS_NAME, !hasFailures);
146174
log.debug('Asset Management 2.0 import completed', context);
147175
} catch (err) {

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,11 @@ export type ImportContext = {
171171
org_uid: string;
172172
/** Optional logging context (same shape as ExportConfig.context). */
173173
context?: Record<string, unknown>;
174+
/**
175+
* Max parallel AM API calls for import (fields, asset types, folders batch, uploads).
176+
* Set from `AssetManagementImportOptions.apiConcurrency`.
177+
*/
178+
apiConcurrency?: number;
174179
};
175180

176181
/**
@@ -191,6 +196,12 @@ export type AssetManagementImportOptions = {
191196
sourceApiKey?: string;
192197
/** Optional logging context. */
193198
context?: Record<string, unknown>;
199+
/**
200+
* When set, mapper files are written under `{backupDir}/mapper/assets/` after import.
201+
*/
202+
backupDir?: string;
203+
/** Parallel AM API limit; defaults to package constant when omitted. */
204+
apiConcurrency?: number;
194205
};
195206

196207
/**
@@ -206,8 +217,8 @@ export type SpaceMapping = {
206217

207218
/**
208219
* The value returned by `ImportSpaces.start()`.
209-
* Written to `mapper/assets/uid-mapping.json` and `mapper/assets/url-mapping.json`
210-
* by the bridge module so `entries.ts` can resolve asset references.
220+
* When `backupDir` is set on options, the AM package also writes these maps under
221+
* `mapper/assets/` for `entries.ts` to resolve asset references.
211222
*/
212223
export type ImportResult = {
213224
uidMap: Record<string, string>;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Split an array into chunks of at most `size` elements.
3+
*/
4+
export function chunkArray<T>(items: T[], size: number): T[][] {
5+
if (size <= 0) {
6+
return items.length ? [items] : [];
7+
}
8+
const out: T[][] = [];
9+
for (let i = 0; i < items.length; i += size) {
10+
out.push(items.slice(i, i + size));
11+
}
12+
return out;
13+
}
14+
15+
/**
16+
* Run async work in batches of at most `concurrency` parallel tasks at a time.
17+
* Uses Promise.allSettled per batch so one failure does not abort the batch.
18+
*/
19+
export async function runInBatches<T>(
20+
items: T[],
21+
concurrency: number,
22+
fn: (item: T, index: number) => Promise<void>,
23+
): Promise<void> {
24+
if (items.length === 0) {
25+
return;
26+
}
27+
const limit = Math.max(1, concurrency);
28+
const batches = chunkArray(items, limit);
29+
let offset = 0;
30+
for (const batch of batches) {
31+
await Promise.allSettled(batch.map((item, j) => fn(item, offset + j)));
32+
offset += batch.length;
33+
}
34+
}

packages/contentstack-asset-management/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export {
66
getReadableStreamFromDownloadResponse,
77
writeStreamToFile,
88
} from './export-helpers';
9+
export { chunkArray, runInBatches } from './concurrent-batch';

0 commit comments

Comments
 (0)