Skip to content

Commit 178bbf5

Browse files
author
naman-contentstack
committed
feat: Add AM2.0 support in import setup command
1 parent 2ef8e3a commit 178bbf5

24 files changed

Lines changed: 3677 additions & 4234 deletions

.talismanrc

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
fileignoreconfig:
2-
- filename: packages/contentstack-asset-management/src/export/base.ts
3-
checksum: 01cb2158fea9cbe05449c04efb42d8d416d24868d411bd7300d8fa99c9a4ab01
4-
- filename: packages/contentstack-asset-management/test/unit/export/base.test.ts
5-
checksum: 164fc2e5a4337a2739903499b66eecc66a85bb9b50aa2e71079bdd046a195a94
6-
- filename: packages/contentstack-asset-management/src/import/fields.ts
7-
checksum: 1cd52254ddbfd186d8ade2c73fc799dd1caa0f10bdd3c6b151621c27207ee173
8-
- filename: packages/contentstack-import/src/utils/asset-management-import-options.ts
9-
checksum: 96b61b683109be2cc2dbab5231e1ded19282fbf176cd8492c75cc4861f2efea0
10-
- filename: packages/contentstack-asset-management/src/import/asset-types.ts
11-
checksum: fa2aeea704fd259628f9c86eacd3cf9f6f12543b06b387bd65db7df6b6f5fc49
2+
- filename: packages/contentstack-asset-management/src/types/asset-management-export-flags.ts
3+
checksum: fa2f9e873dd2fdb9170b5526130a855979674c05aca759c5383a432e761b377b
4+
- filename: packages/contentstack-import-setup/src/utils/import-setup-asset-mapper-params.ts
5+
checksum: 947bfd7d7fc071ddbe4310f59056666d373952fd2b40d478e0c6a260c751b132
6+
- filename: packages/contentstack-import-setup/test/unit/import-config-handler.test.ts
7+
checksum: 468fc6a83cbbb8c64debdbeec955ba2fcc4c1ef955f78308ff89e3bddbcf1bbb
8+
- filename: packages/contentstack-import-setup/src/import/modules/assets.ts
9+
checksum: 058d2a93ddaf1922b516781dc5e51e6744b63dd8a7042f64a75428098a3e6215
10+
- filename: packages/contentstack-asset-management/src/utils/detect-asset-management-export.ts
11+
checksum: 6dd65dd6a9d2124ecc9cd124c032fc7b4f51f664a2b64c7e2b79606c04f9dff1
12+
- filename: packages/contentstack-asset-management/test/unit/utils/detect-asset-management-export.test.ts
13+
checksum: 049bfc9dfb1104fb5ef439c0fccd8f36c637e2961f76eec1e1d7093f4c848d89
14+
- filename: packages/contentstack-asset-management/test/unit/import-setup/import-setup-asset-mappers.test.ts
15+
checksum: c8cf31441c637b057bcde9c60e005eb1b23af8125fcff5f28ee733abac4926be
16+
- filename: packages/contentstack-asset-management/src/import-setup/import-setup-asset-mappers.ts
17+
checksum: e7f624db1db13aeef4e7f53d317d5254aabb427616049cebccf6db75dc5ac102
18+
- filename: packages/contentstack-import-setup/test/unit/modules/assets.test.ts
19+
checksum: 02e8a857317c22adfec423b849a6c1454a73195e7c5bf888ba4bb9321cf66c48
20+
- filename: pnpm-lock.yaml
21+
checksum: 848c99284f55d18680d1cf2fe9e0676ffc0ae5a18c7855a8194ce89ac56841ef
1222
version: '1.0'

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 Asset Management 2.0 export (single progress bar).
@@ -49,6 +60,8 @@ export const PROCESS_NAMES = {
4960
AM_IMPORT_ASSET_TYPES: 'Import asset types',
5061
AM_IMPORT_FOLDERS: 'Import folders',
5162
AM_IMPORT_ASSETS: 'Import assets',
63+
/** Import-setup (CLI): generate uid/url/space mappers from AM export before full import. */
64+
AM_IMPORT_SETUP_ASSET_MAPPERS: 'Import setup asset mappers',
5265
} as const;
5366

5467
/**
@@ -95,4 +108,8 @@ export const PROCESS_STATUS = {
95108
IMPORTING: 'Importing assets...',
96109
FAILED: 'Failed to import assets.',
97110
},
111+
[PROCESS_NAMES.AM_IMPORT_SETUP_ASSET_MAPPERS]: {
112+
GENERATING: 'Generating asset mappers...',
113+
FAILED: 'Failed to generate asset mappers.',
114+
},
98115
} 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: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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 {
8+
IMPORT_ASSETS_MAPPER_DIR_SEGMENTS,
9+
IMPORT_ASSETS_MAPPER_FILES,
10+
PROCESS_NAMES,
11+
PROCESS_STATUS,
12+
} from '../constants/index';
13+
import type { AssetManagementAPIConfig } from '../types/asset-management-api';
14+
import type { AssetMapperImportSetupResult, RunAssetMapperImportSetupParams } from '../types/import-setup-asset-mapper';
15+
import ImportAssets from '../import/assets';
16+
import { AssetManagementAdapter } from '../utils/asset-management-api-adapter';
17+
import { AssetManagementImportSetupAdapter } from './base';
18+
19+
const PROCESS = PROCESS_NAMES.AM_IMPORT_SETUP_ASSET_MAPPERS;
20+
21+
/**
22+
* Builds identity uid/url and space-uid mapper files from an Asset Management export layout
23+
* for spaces that already exist in the target org (reuse path).
24+
*/
25+
export default class ImportSetupAssetMappers extends AssetManagementImportSetupAdapter {
26+
constructor(params: RunAssetMapperImportSetupParams) {
27+
super(params);
28+
}
29+
30+
private async fetchExistingSpaceUidsInOrg(apiConfig: AssetManagementAPIConfig): Promise<Set<string>> {
31+
const adapter = new AssetManagementAdapter(apiConfig);
32+
await adapter.init();
33+
const { spaces } = await adapter.listSpaces();
34+
const uids = new Set<string>();
35+
for (const s of spaces) {
36+
if (s.uid) {
37+
uids.add(s.uid);
38+
}
39+
}
40+
return uids;
41+
}
42+
43+
private listExportedSpaceDirectories(spacesRootPath: string): { spaceDirs: string[]; readFailed: boolean } {
44+
try {
45+
const spaceDirs = readdirSync(spacesRootPath).filter((entry) => {
46+
try {
47+
return statSync(join(spacesRootPath, entry)).isDirectory() && entry.startsWith('am');
48+
} catch {
49+
return false;
50+
}
51+
});
52+
return { spaceDirs, readFailed: false };
53+
} catch {
54+
log.info(`Could not read Asset Management spaces directory: ${spacesRootPath}`, this.params.context);
55+
return { spaceDirs: [], readFailed: true };
56+
}
57+
}
58+
59+
async start(): Promise<AssetMapperImportSetupResult> {
60+
const {
61+
contentDir,
62+
mapperBaseDir,
63+
assetManagementUrl,
64+
org_uid,
65+
source_stack,
66+
apiKey,
67+
host,
68+
context,
69+
fetchConcurrency,
70+
} = this.params;
71+
72+
if (!assetManagementUrl) {
73+
log.info(
74+
'Asset Management export detected but region.assetManagementUrl is not configured. Skipping asset mapper setup.',
75+
context,
76+
);
77+
return { kind: 'skipped', reason: 'missing_asset_management_url' };
78+
}
79+
if (!org_uid) {
80+
log.error('Cannot run Asset Management import-setup: organization UID is missing.', context);
81+
return { kind: 'skipped', reason: 'missing_organization_uid' };
82+
}
83+
84+
const parentProgressManager = this.resolveParentProgress();
85+
86+
const spacesRootPath = resolve(contentDir, 'spaces');
87+
const mapperDirPath = join(mapperBaseDir, ...IMPORT_ASSETS_MAPPER_DIR_SEGMENTS);
88+
const duplicateAssetMapperPath = join(mapperDirPath, IMPORT_ASSETS_MAPPER_FILES.DUPLICATE_ASSETS);
89+
90+
const apiConfig: AssetManagementAPIConfig = {
91+
baseURL: assetManagementUrl,
92+
headers: { organization_uid: org_uid },
93+
context,
94+
};
95+
96+
const importContext = {
97+
spacesRootPath,
98+
sourceApiKey: source_stack,
99+
apiKey,
100+
host,
101+
org_uid,
102+
context,
103+
apiConcurrency: fetchConcurrency,
104+
};
105+
106+
try {
107+
if (parentProgressManager) {
108+
parentProgressManager.addProcess(PROCESS, 1);
109+
parentProgressManager
110+
.startProcess(PROCESS)
111+
.updateStatus(PROCESS_STATUS[PROCESS].GENERATING, PROCESS);
112+
}
113+
114+
const existingSpaceUids = await this.fetchExistingSpaceUidsInOrg(apiConfig);
115+
116+
const { spaceDirs, readFailed } = this.listExportedSpaceDirectories(spacesRootPath);
117+
if (spaceDirs.length === 0 && !readFailed) {
118+
log.info('No Asset Management space directories (am*) found under spaces/.', context);
119+
}
120+
121+
const allUidMap: Record<string, string> = {};
122+
const allUrlMap: Record<string, string> = {};
123+
const spaceUidMap: Record<string, string> = {};
124+
125+
const assetsImporter = new ImportAssets(apiConfig, importContext);
126+
127+
for (const spaceUid of spaceDirs) {
128+
const spaceDir = join(spacesRootPath, spaceUid);
129+
if (existingSpaceUids.has(spaceUid)) {
130+
const { uidMap, urlMap } = await assetsImporter.buildIdentityMappersFromExport(spaceDir);
131+
Object.assign(allUidMap, uidMap);
132+
Object.assign(allUrlMap, urlMap);
133+
spaceUidMap[spaceUid] = spaceUid;
134+
parentProgressManager?.tick(true, `Asset Management space reused: ${spaceUid}`, null, PROCESS);
135+
log.info(
136+
`Asset Management space "${spaceUid}" exists in org; identity asset mappers merged from export.`,
137+
context,
138+
);
139+
} else {
140+
log.info(
141+
`Asset Management space "${spaceUid}" is not in the target org yet. Import assets first, then re-run import-setup to refresh mappers after upload.`,
142+
context,
143+
);
144+
}
145+
}
146+
147+
await mkdir(mapperDirPath, { recursive: true });
148+
149+
await writeFile(
150+
join(mapperDirPath, IMPORT_ASSETS_MAPPER_FILES.UID_MAPPING),
151+
JSON.stringify(allUidMap),
152+
'utf8',
153+
);
154+
await writeFile(
155+
join(mapperDirPath, IMPORT_ASSETS_MAPPER_FILES.URL_MAPPING),
156+
JSON.stringify(allUrlMap),
157+
'utf8',
158+
);
159+
await writeFile(
160+
join(mapperDirPath, IMPORT_ASSETS_MAPPER_FILES.SPACE_UID_MAPPING),
161+
JSON.stringify(spaceUidMap),
162+
'utf8',
163+
);
164+
165+
await writeFile(duplicateAssetMapperPath, JSON.stringify({}), 'utf8');
166+
167+
parentProgressManager?.completeProcess(PROCESS, true);
168+
log.success(
169+
'The required Asset Management setup files for assets have been generated successfully.',
170+
context,
171+
);
172+
173+
return { kind: 'success' };
174+
} catch (error) {
175+
parentProgressManager?.completeProcess(PROCESS, false);
176+
log.error(`Error occurred while generating Asset Management asset mappers: ${formatError(error)}.`, context);
177+
return {
178+
kind: 'error',
179+
errorMessage: (error as Error)?.message || 'Asset Management asset mapper generation failed',
180+
};
181+
}
182+
}
183+
}
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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export type RunAssetMapperImportSetupParams = {
2+
contentDir: string;
3+
/** Parent of `mapper/assets` (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+
fetchConcurrency?: number;
12+
};
13+
14+
export type AssetMapperImportSetupResult =
15+
| { kind: 'skipped'; reason: 'missing_asset_management_url' | 'missing_organization_uid' }
16+
| { kind: 'success' }
17+
| { kind: 'error'; errorMessage: string };
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1+
export * from './asset-management-export-flags';
12
export * from './asset-management-api';
23
export * from './export-types';
4+
export * from './import-setup-asset-mapper';
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as path from 'node:path';
2+
import { existsSync, readFileSync } from 'node:fs';
3+
import { configHandler } from '@contentstack/cli-utilities';
4+
5+
import type { AssetManagementExportFlags } from '../types/asset-management-export-flags';
6+
7+
/** Stack `settings.json` field that marks Asset Management usage (CMA contract). */
8+
const STACK_SETTINGS_ASSET_MANAGEMENT_KEY = 'am_v2' as const;
9+
10+
/**
11+
* Detects Asset Management export layout: `spaces/` + `stack/settings.json` with linked AM settings,
12+
* and optionally reads `source_stack` from `branches.json` (content dir or parent).
13+
*/
14+
export function detectAssetManagementExportFromContentDir(contentDir: string): AssetManagementExportFlags {
15+
const result: AssetManagementExportFlags = { assetManagementEnabled: false };
16+
const spacesDir = path.join(contentDir, 'spaces');
17+
const stackSettingsPath = path.join(contentDir, 'stack', 'settings.json');
18+
19+
if (!existsSync(spacesDir) || !existsSync(stackSettingsPath)) {
20+
return result;
21+
}
22+
23+
try {
24+
const stackSettings = JSON.parse(readFileSync(stackSettingsPath, 'utf8')) as Record<string, unknown>;
25+
if (!stackSettings?.[STACK_SETTINGS_ASSET_MANAGEMENT_KEY]) {
26+
return result;
27+
}
28+
29+
result.assetManagementEnabled = true;
30+
const region = configHandler.get('region') as { assetManagementUrl?: string } | undefined;
31+
result.assetManagementUrl = region?.assetManagementUrl;
32+
33+
const branchesJsonCandidates = [
34+
path.join(contentDir, 'branches.json'),
35+
path.join(contentDir, '..', 'branches.json'),
36+
];
37+
for (const branchesJsonPath of branchesJsonCandidates) {
38+
if (!existsSync(branchesJsonPath)) {
39+
continue;
40+
}
41+
try {
42+
const branches = JSON.parse(readFileSync(branchesJsonPath, 'utf8')) as Array<{
43+
stackHeaders?: { api_key?: string };
44+
}>;
45+
const apiKey = branches?.[0]?.stackHeaders?.api_key;
46+
if (apiKey) {
47+
result.source_stack = apiKey;
48+
}
49+
} catch {
50+
// branches.json unreadable — URL mapping will be skipped
51+
}
52+
break;
53+
}
54+
} catch {
55+
// stack settings unreadable — not an Asset Management export we can process
56+
}
57+
58+
return result;
59+
}

0 commit comments

Comments
 (0)