Skip to content

Commit d7751c6

Browse files
Merge pull request #64 from contentstack/feat/DX-5418
feat: Add AM2.0 support in import setup command
2 parents 8c00c36 + 2fd259f commit d7751c6

35 files changed

Lines changed: 1104 additions & 76 deletions

.talismanrc

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
11
fileignoreconfig:
22
- filename: pnpm-lock.yaml
3-
checksum: e5b5f8ab64f4f27a1483e426cc01d7388482263e1d6fe3baf1caafcf2878ebb2
3+
checksum: 264c0294416c2aa2028c8283831aa7ed17d63e8c69553a7b2b9774336bc0811f
4+
- filename: packages/contentstack-bootstrap/test/bootstrap.test.js
5+
checksum: 37b502482fc32831c39091dd1755e8b0a9f6f8bac89e5eb9d397c6af1f213d83
6+
- filename: packages/contentstack-bootstrap/test/utils.test.js
7+
checksum: e0ca2eb58ab1c3ac3b4d9c14a17bb8f4788678074dd65f6acc128a8a8f51997f
8+
- filename: packages/contentstack-export-to-csv/test/unit/base-command.test.ts
9+
checksum: 4c2befce053135453c1db31f21351bf3f797ff2f15723ca52e183ed6ed9e48e4
10+
- filename: packages/contentstack-export-to-csv/test/unit/utils/teams-export.functional.test.ts
11+
checksum: 17cdae91c22a935309bf4514b6616f1aea91fd0166fede182e673ade43667a36
12+
- filename: packages/contentstack-export-to-csv/test/unit/utils/interactive.test.ts
13+
checksum: 72c1e719e5c51a42debc817a5fe5bb1adee63b2d823df2e9ee36d0b970db7886
14+
- filename: packages/contentstack-export-to-csv/test/unit/utils/api-client.functional.test.ts
15+
checksum: 6d6638919ef7260f642d32cf730e2654d159d2d3582714fb2d7a9c209b9c6eeb
16+
- filename: packages/contentstack-export-to-csv/test/unit/commands/export-to-csv.test.ts
17+
checksum: 9621a9d013796e99a25e894404c4d7c6799bd33bde77852f09d5e32b4d1c1c49
418
version: '1.0'

packages/contentstack-asset-management/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@contentstack/cli-asset-management",
3-
"version": "1.0.0-beta.1",
3+
"version": "1.0.0-beta.2",
44
"description": "Contentstack Assets API adapter for export and import",
55
"main": "lib/index.js",
66
"types": "lib/index.d.ts",

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ export const FALLBACK_ASSET_TYPES_IMPORT_INVALID_KEYS = [
2727

2828
/** @deprecated Use FALLBACK_AM_CHUNK_FILE_SIZE_MB */
2929
export const CHUNK_FILE_SIZE_MB = FALLBACK_AM_CHUNK_FILE_SIZE_MB;
30+
/**
31+
* Mapper output paths — must stay aligned with contentstack-import `PATH_CONSTANTS`
32+
* (`mapper` / `assets` / uid, url, space-uid file names).
33+
*/
34+
export const IMPORT_ASSETS_MAPPER_DIR_SEGMENTS = ['mapper', 'assets'] as const;
35+
export const IMPORT_ASSETS_MAPPER_FILES = {
36+
UID_MAPPING: 'uid-mapping.json',
37+
URL_MAPPING: 'url-mapping.json',
38+
SPACE_UID_MAPPING: 'space-uid-mapping.json',
39+
DUPLICATE_ASSETS: 'duplicate-assets.json',
40+
} as const;
3041

3142
/**
3243
* Main process name for Contentstack Assets export (single progress bar).
@@ -61,6 +72,8 @@ export const PROCESS_NAMES = {
6172
AM_IMPORT_ASSET_TYPES: 'Import asset types',
6273
AM_IMPORT_FOLDERS: 'Import folders',
6374
AM_IMPORT_ASSETS: 'Import assets',
75+
/** Import-setup (CLI): generate uid/url/space mappers from AM export before full import. */
76+
AM_IMPORT_SETUP_ASSET_MAPPERS: 'Import setup asset mappers',
6477
} as const;
6578

6679
/**
@@ -139,4 +152,8 @@ export const PROCESS_STATUS = {
139152
IMPORTING: 'Importing assets...',
140153
FAILED: 'Failed to import assets.',
141154
},
155+
[PROCESS_NAMES.AM_IMPORT_SETUP_ASSET_MAPPERS]: {
156+
GENERATING: 'Generating asset mappers...',
157+
FAILED: 'Failed to generate asset mappers.',
158+
},
142159
} as const;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { CLIProgressManager } from '@contentstack/cli-utilities';
2+
3+
import type { AssetMapperImportSetupResult, RunAssetMapperImportSetupParams } from '../types/import-setup-asset-mapper';
4+
5+
/**
6+
* Base for CLI import-setup flows that prepare AM exports (mappers, metadata) before full import.
7+
* Mirrors ImportSpaces-style `setParentProgressManager`; callers log via `@contentstack/cli-utilities` `log` + `params.context`.
8+
*/
9+
export abstract class AssetManagementImportSetupAdapter {
10+
private parentProgressManager: CLIProgressManager | null = null;
11+
12+
protected constructor(protected readonly params: RunAssetMapperImportSetupParams) {}
13+
14+
public setParentProgressManager(parent: CLIProgressManager): void {
15+
this.parentProgressManager = parent;
16+
}
17+
18+
protected resolveParentProgress(): CLIProgressManager | null {
19+
return this.parentProgressManager;
20+
}
21+
22+
abstract start(): Promise<AssetMapperImportSetupResult>;
23+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { readdirSync, statSync } from 'node:fs';
2+
import { mkdir, writeFile } from 'node:fs/promises';
3+
import { join, resolve } from 'node:path';
4+
5+
import { formatError, log } from '@contentstack/cli-utilities';
6+
7+
import { IMPORT_ASSETS_MAPPER_FILES, PROCESS_NAMES, PROCESS_STATUS } from '../constants/index';
8+
import type { CSAssetsAPIConfig, ImportContext } from '../types/cs-assets-api';
9+
import type { AssetMapperImportSetupResult, RunAssetMapperImportSetupParams } from '../types/import-setup-asset-mapper';
10+
import ImportAssets from '../import/assets';
11+
import { CSAssetsAdapter } from '../utils/cs-assets-api-adapter';
12+
import { AssetManagementImportSetupAdapter } from './base';
13+
14+
const PROCESS = PROCESS_NAMES.AM_IMPORT_SETUP_ASSET_MAPPERS;
15+
16+
/**
17+
* Builds identity uid/url and space-uid mapper files from an Asset Management export layout
18+
* for spaces that already exist in the target org (reuse path).
19+
*/
20+
export default class ImportSetupAssetMappers extends AssetManagementImportSetupAdapter {
21+
constructor(params: RunAssetMapperImportSetupParams) {
22+
super(params);
23+
}
24+
25+
private async fetchExistingSpaceUidsInOrg(apiConfig: CSAssetsAPIConfig): Promise<Set<string>> {
26+
const adapter = new CSAssetsAdapter(apiConfig);
27+
await adapter.init();
28+
const { spaces } = await adapter.listSpaces();
29+
const uids = new Set<string>();
30+
for (const s of spaces) {
31+
if (s.uid) {
32+
uids.add(s.uid);
33+
}
34+
}
35+
return uids;
36+
}
37+
38+
private listExportedSpaceDirectories(spacesRootPath: string): { spaceDirs: string[]; readFailed: boolean } {
39+
try {
40+
const spaceDirs = readdirSync(spacesRootPath).filter((entry) => {
41+
try {
42+
return statSync(join(spacesRootPath, entry)).isDirectory() && entry.startsWith('am');
43+
} catch {
44+
return false;
45+
}
46+
});
47+
return { spaceDirs, readFailed: false };
48+
} catch {
49+
log.info(`Could not read Asset Management spaces directory: ${spacesRootPath}`, this.params.context);
50+
return { spaceDirs: [], readFailed: true };
51+
}
52+
}
53+
54+
async start(): Promise<AssetMapperImportSetupResult> {
55+
const p = this.params;
56+
const { contentDir, mapperBaseDir, assetManagementUrl, org_uid, source_stack, apiKey, host, context } = p;
57+
58+
const apiConcurrencyResolved = p.apiConcurrency ?? p.fetchConcurrency;
59+
60+
if (!assetManagementUrl) {
61+
log.info(
62+
'AM 2.0 export detected but assetManagementUrl is not configured in the region settings. Skipping AM 2.0 asset mapper setup.',
63+
context,
64+
);
65+
return { kind: 'skipped', reason: 'missing_asset_management_url' };
66+
}
67+
if (!org_uid) {
68+
log.error('Cannot run Asset Management import-setup: organization UID is missing.', context);
69+
return { kind: 'skipped', reason: 'missing_organization_uid' };
70+
}
71+
72+
const parentProgressManager = this.resolveParentProgress();
73+
74+
const spacesDirSegment = p.spacesDirName ?? 'spaces';
75+
const spacesRootPath = resolve(contentDir, spacesDirSegment);
76+
const mapperRoot = p.mapperRootDir ?? 'mapper';
77+
const mapperAssetsMod = p.mapperAssetsModuleDir ?? 'assets';
78+
const mapperDirPath = join(mapperBaseDir, mapperRoot, mapperAssetsMod);
79+
const uidFile = p.mapperUidFileName ?? IMPORT_ASSETS_MAPPER_FILES.UID_MAPPING;
80+
const urlFile = p.mapperUrlFileName ?? IMPORT_ASSETS_MAPPER_FILES.URL_MAPPING;
81+
const spaceUidFile = p.mapperSpaceUidFileName ?? IMPORT_ASSETS_MAPPER_FILES.SPACE_UID_MAPPING;
82+
const duplicateAssetMapperPath = join(mapperDirPath, IMPORT_ASSETS_MAPPER_FILES.DUPLICATE_ASSETS);
83+
84+
const apiConfig: CSAssetsAPIConfig = {
85+
baseURL: assetManagementUrl,
86+
headers: { organization_uid: org_uid },
87+
context,
88+
};
89+
90+
const importContext: ImportContext = {
91+
spacesRootPath,
92+
sourceApiKey: source_stack,
93+
apiKey,
94+
host,
95+
org_uid,
96+
context,
97+
apiConcurrency: apiConcurrencyResolved,
98+
spacesDirName: p.spacesDirName,
99+
fieldsDir: p.fieldsDir,
100+
assetTypesDir: p.assetTypesDir,
101+
fieldsFileName: p.fieldsFileName,
102+
assetTypesFileName: p.assetTypesFileName,
103+
foldersFileName: p.foldersFileName,
104+
assetsFileName: p.assetsFileName,
105+
fieldsImportInvalidKeys: p.fieldsImportInvalidKeys,
106+
assetTypesImportInvalidKeys: p.assetTypesImportInvalidKeys,
107+
uploadAssetsConcurrency: p.uploadAssetsConcurrency,
108+
importFoldersConcurrency: p.importFoldersConcurrency,
109+
mapperRootDir: mapperRoot,
110+
mapperAssetsModuleDir: mapperAssetsMod,
111+
mapperUidFileName: uidFile,
112+
mapperUrlFileName: urlFile,
113+
mapperSpaceUidFileName: spaceUidFile,
114+
};
115+
116+
try {
117+
if (parentProgressManager) {
118+
parentProgressManager.addProcess(PROCESS, 1);
119+
parentProgressManager.startProcess(PROCESS).updateStatus(PROCESS_STATUS[PROCESS].GENERATING, PROCESS);
120+
}
121+
122+
const existingSpaceUids = await this.fetchExistingSpaceUidsInOrg(apiConfig);
123+
124+
const { spaceDirs, readFailed } = this.listExportedSpaceDirectories(spacesRootPath);
125+
if (spaceDirs.length === 0 && !readFailed) {
126+
log.info(`No Asset Management space directories (am*) found under ${spacesDirSegment}/.`, context);
127+
}
128+
129+
const allUidMap: Record<string, string> = {};
130+
const allUrlMap: Record<string, string> = {};
131+
const spaceUidMap: Record<string, string> = {};
132+
133+
const assetsImporter = new ImportAssets(apiConfig, importContext);
134+
135+
for (const spaceUid of spaceDirs) {
136+
const spaceDir = join(spacesRootPath, spaceUid);
137+
if (existingSpaceUids.has(spaceUid)) {
138+
const { uidMap, urlMap } = await assetsImporter.buildIdentityMappersFromExport(spaceDir);
139+
Object.assign(allUidMap, uidMap);
140+
Object.assign(allUrlMap, urlMap);
141+
spaceUidMap[spaceUid] = spaceUid;
142+
parentProgressManager?.tick(true, `Asset Management space reused: ${spaceUid}`, null, PROCESS);
143+
log.info(
144+
`Asset Management space "${spaceUid}" exists in org; identity asset mappers merged from export.`,
145+
context,
146+
);
147+
} else {
148+
log.info(
149+
`Asset Management space "${spaceUid}" is not in the target org yet. Import assets first, then re-run import-setup to refresh mappers after upload.`,
150+
context,
151+
);
152+
}
153+
}
154+
155+
await mkdir(mapperDirPath, { recursive: true });
156+
157+
await writeFile(join(mapperDirPath, uidFile), JSON.stringify(allUidMap), 'utf8');
158+
await writeFile(join(mapperDirPath, urlFile), JSON.stringify(allUrlMap), 'utf8');
159+
await writeFile(join(mapperDirPath, spaceUidFile), JSON.stringify(spaceUidMap), 'utf8');
160+
161+
await writeFile(duplicateAssetMapperPath, JSON.stringify({}), 'utf8');
162+
163+
parentProgressManager?.completeProcess(PROCESS, true);
164+
log.success('The required Asset Management setup files for assets have been generated successfully.', context);
165+
166+
return { kind: 'success' };
167+
} catch (error) {
168+
parentProgressManager?.completeProcess(PROCESS, false);
169+
log.error(`Error occurred while generating Asset Management asset mappers: ${formatError(error)}.`, context);
170+
return {
171+
kind: 'error',
172+
errorMessage: (error as Error)?.message || 'Asset Management asset mapper generation failed',
173+
};
174+
}
175+
}
176+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { AssetManagementImportSetupAdapter } from './base';
2+
export { default as ImportSetupAssetMappers } from './import-setup-asset-mappers';
3+
export type { AssetMapperImportSetupResult, RunAssetMapperImportSetupParams } from '../types/import-setup-asset-mapper';

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from './types';
33
export * from './utils';
44
export * from './export';
55
export * from './import';
6+
export * from './import-setup';
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Values derived from an on-disk export layout for Asset Management–backed stacks.
3+
* Used by `contentstack-import` and `contentstack-import-setup` config handlers.
4+
*/
5+
export type AssetManagementExportFlags = {
6+
assetManagementEnabled: boolean;
7+
assetManagementUrl?: string;
8+
/** Source stack API key from `branches.json`, when present — used for URL reconstruction. */
9+
source_stack?: string;
10+
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
export type RunAssetMapperImportSetupParams = {
2+
contentDir: string;
3+
/** Parent of the assets mapper directory (typically import-setup `backupDir`). */
4+
mapperBaseDir: string;
5+
assetManagementUrl?: string;
6+
org_uid?: string;
7+
source_stack?: string;
8+
apiKey: string;
9+
host: string;
10+
context: Record<string, unknown>;
11+
/**
12+
* Max parallel AM API calls for list/read paths.
13+
* Takes precedence over {@link fetchConcurrency}.
14+
*/
15+
apiConcurrency?: number;
16+
/**
17+
* @deprecated Use {@link apiConcurrency}.
18+
*/
19+
fetchConcurrency?: number;
20+
/** Relative dir under content dir for AM export root (default `spaces`). */
21+
spacesDirName?: string;
22+
fieldsDir?: string;
23+
assetTypesDir?: string;
24+
fieldsFileName?: string;
25+
assetTypesFileName?: string;
26+
foldersFileName?: string;
27+
assetsFileName?: string;
28+
fieldsImportInvalidKeys?: string[];
29+
assetTypesImportInvalidKeys?: string[];
30+
mapperRootDir?: string;
31+
mapperAssetsModuleDir?: string;
32+
mapperUidFileName?: string;
33+
mapperUrlFileName?: string;
34+
mapperSpaceUidFileName?: string;
35+
uploadAssetsConcurrency?: number;
36+
importFoldersConcurrency?: number;
37+
};
38+
39+
export type AssetMapperImportSetupResult =
40+
| { kind: 'skipped'; reason: 'missing_asset_management_url' | 'missing_organization_uid' }
41+
| { kind: 'success' }
42+
| { kind: 'error'; errorMessage: string };
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './cs-assets-api';
22
export * from './export-types';
3+
export * from './import-setup-asset-mapper';

0 commit comments

Comments
 (0)