Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
fileignoreconfig:
- filename: packages/contentstack-asset-management/src/export/base.ts
checksum: 01cb2158fea9cbe05449c04efb42d8d416d24868d411bd7300d8fa99c9a4ab01
- filename: packages/contentstack-asset-management/test/unit/export/base.test.ts
checksum: 164fc2e5a4337a2739903499b66eecc66a85bb9b50aa2e71079bdd046a195a94
- filename: packages/contentstack-asset-management/src/import/fields.ts
checksum: 1cd52254ddbfd186d8ade2c73fc799dd1caa0f10bdd3c6b151621c27207ee173
- filename: packages/contentstack-import/src/utils/asset-management-import-options.ts
checksum: 96b61b683109be2cc2dbab5231e1ded19282fbf176cd8492c75cc4861f2efea0
- filename: packages/contentstack-asset-management/src/import/asset-types.ts
checksum: fa2aeea704fd259628f9c86eacd3cf9f6f12543b06b387bd65db7df6b6f5fc49
- filename: packages/contentstack-asset-management/src/types/asset-management-export-flags.ts
checksum: fa2f9e873dd2fdb9170b5526130a855979674c05aca759c5383a432e761b377b
- filename: packages/contentstack-import-setup/src/utils/import-setup-asset-mapper-params.ts
checksum: 947bfd7d7fc071ddbe4310f59056666d373952fd2b40d478e0c6a260c751b132
- filename: packages/contentstack-import-setup/test/unit/import-config-handler.test.ts
checksum: 468fc6a83cbbb8c64debdbeec955ba2fcc4c1ef955f78308ff89e3bddbcf1bbb
- filename: packages/contentstack-import-setup/src/import/modules/assets.ts
checksum: 058d2a93ddaf1922b516781dc5e51e6744b63dd8a7042f64a75428098a3e6215
- filename: packages/contentstack-asset-management/src/utils/detect-asset-management-export.ts
checksum: 6dd65dd6a9d2124ecc9cd124c032fc7b4f51f664a2b64c7e2b79606c04f9dff1
- filename: packages/contentstack-asset-management/test/unit/utils/detect-asset-management-export.test.ts
checksum: 049bfc9dfb1104fb5ef439c0fccd8f36c637e2961f76eec1e1d7093f4c848d89
- filename: packages/contentstack-asset-management/test/unit/import-setup/import-setup-asset-mappers.test.ts
checksum: c8cf31441c637b057bcde9c60e005eb1b23af8125fcff5f28ee733abac4926be
- filename: packages/contentstack-asset-management/src/import-setup/import-setup-asset-mappers.ts
checksum: e7f624db1db13aeef4e7f53d317d5254aabb427616049cebccf6db75dc5ac102
- filename: packages/contentstack-import-setup/test/unit/modules/assets.test.ts
checksum: 02e8a857317c22adfec423b849a6c1454a73195e7c5bf888ba4bb9321cf66c48
- filename: pnpm-lock.yaml
checksum: 848c99284f55d18680d1cf2fe9e0676ffc0ae5a18c7855a8194ce89ac56841ef
version: '1.0'
17 changes: 17 additions & 0 deletions packages/contentstack-asset-management/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ export const FALLBACK_ASSET_TYPES_IMPORT_INVALID_KEYS = [

/** @deprecated Use FALLBACK_AM_CHUNK_FILE_SIZE_MB */
export const CHUNK_FILE_SIZE_MB = FALLBACK_AM_CHUNK_FILE_SIZE_MB;
/**
* Mapper output paths — must stay aligned with contentstack-import `PATH_CONSTANTS`
* (`mapper` / `assets` / uid, url, space-uid file names).
*/
export const IMPORT_ASSETS_MAPPER_DIR_SEGMENTS = ['mapper', 'assets'] as const;
export const IMPORT_ASSETS_MAPPER_FILES = {
UID_MAPPING: 'uid-mapping.json',
URL_MAPPING: 'url-mapping.json',
SPACE_UID_MAPPING: 'space-uid-mapping.json',
DUPLICATE_ASSETS: 'duplicate-assets.json',
} as const;

/**
* Main process name for Asset Management 2.0 export (single progress bar).
Expand All @@ -49,6 +60,8 @@ export const PROCESS_NAMES = {
AM_IMPORT_ASSET_TYPES: 'Import asset types',
AM_IMPORT_FOLDERS: 'Import folders',
AM_IMPORT_ASSETS: 'Import assets',
/** Import-setup (CLI): generate uid/url/space mappers from AM export before full import. */
AM_IMPORT_SETUP_ASSET_MAPPERS: 'Import setup asset mappers',
} as const;

/**
Expand Down Expand Up @@ -95,4 +108,8 @@ export const PROCESS_STATUS = {
IMPORTING: 'Importing assets...',
FAILED: 'Failed to import assets.',
},
[PROCESS_NAMES.AM_IMPORT_SETUP_ASSET_MAPPERS]: {
GENERATING: 'Generating asset mappers...',
FAILED: 'Failed to generate asset mappers.',
},
} as const;
23 changes: 23 additions & 0 deletions packages/contentstack-asset-management/src/import-setup/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { CLIProgressManager } from '@contentstack/cli-utilities';

import type { AssetMapperImportSetupResult, RunAssetMapperImportSetupParams } from '../types/import-setup-asset-mapper';

/**
* Base for CLI import-setup flows that prepare AM exports (mappers, metadata) before full import.
* Mirrors ImportSpaces-style `setParentProgressManager`; callers log via `@contentstack/cli-utilities` `log` + `params.context`.
*/
export abstract class AssetManagementImportSetupAdapter {
private parentProgressManager: CLIProgressManager | null = null;

protected constructor(protected readonly params: RunAssetMapperImportSetupParams) {}

public setParentProgressManager(parent: CLIProgressManager): void {
this.parentProgressManager = parent;
}

protected resolveParentProgress(): CLIProgressManager | null {
return this.parentProgressManager;
}

abstract start(): Promise<AssetMapperImportSetupResult>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { readdirSync, statSync } from 'node:fs';
import { mkdir, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';

import { formatError, log } from '@contentstack/cli-utilities';

import {
IMPORT_ASSETS_MAPPER_DIR_SEGMENTS,
IMPORT_ASSETS_MAPPER_FILES,
PROCESS_NAMES,
PROCESS_STATUS,
} from '../constants/index';
import type { AssetManagementAPIConfig } from '../types/asset-management-api';
import type { AssetMapperImportSetupResult, RunAssetMapperImportSetupParams } from '../types/import-setup-asset-mapper';
import ImportAssets from '../import/assets';
import { AssetManagementAdapter } from '../utils/asset-management-api-adapter';
import { AssetManagementImportSetupAdapter } from './base';

const PROCESS = PROCESS_NAMES.AM_IMPORT_SETUP_ASSET_MAPPERS;

/**
* Builds identity uid/url and space-uid mapper files from an Asset Management export layout
* for spaces that already exist in the target org (reuse path).
*/
export default class ImportSetupAssetMappers extends AssetManagementImportSetupAdapter {
constructor(params: RunAssetMapperImportSetupParams) {
super(params);
}

private async fetchExistingSpaceUidsInOrg(apiConfig: AssetManagementAPIConfig): Promise<Set<string>> {
const adapter = new AssetManagementAdapter(apiConfig);
await adapter.init();
const { spaces } = await adapter.listSpaces();
const uids = new Set<string>();
for (const s of spaces) {
if (s.uid) {
uids.add(s.uid);
}
}
return uids;
}

private listExportedSpaceDirectories(spacesRootPath: string): { spaceDirs: string[]; readFailed: boolean } {
try {
const spaceDirs = readdirSync(spacesRootPath).filter((entry) => {
try {
return statSync(join(spacesRootPath, entry)).isDirectory() && entry.startsWith('am');
} catch {
return false;
}
});
return { spaceDirs, readFailed: false };
} catch {
log.info(`Could not read Asset Management spaces directory: ${spacesRootPath}`, this.params.context);
return { spaceDirs: [], readFailed: true };
}
}

async start(): Promise<AssetMapperImportSetupResult> {
const {
contentDir,
mapperBaseDir,
assetManagementUrl,
org_uid,
source_stack,
apiKey,
host,
context,
fetchConcurrency,
} = this.params;

if (!assetManagementUrl) {
log.info(
'Asset Management export detected but region.assetManagementUrl is not configured. Skipping asset mapper setup.',
context,
);
return { kind: 'skipped', reason: 'missing_asset_management_url' };
}
if (!org_uid) {
log.error('Cannot run Asset Management import-setup: organization UID is missing.', context);
return { kind: 'skipped', reason: 'missing_organization_uid' };
}

const parentProgressManager = this.resolveParentProgress();

const spacesRootPath = resolve(contentDir, 'spaces');
const mapperDirPath = join(mapperBaseDir, ...IMPORT_ASSETS_MAPPER_DIR_SEGMENTS);
const duplicateAssetMapperPath = join(mapperDirPath, IMPORT_ASSETS_MAPPER_FILES.DUPLICATE_ASSETS);

const apiConfig: AssetManagementAPIConfig = {
baseURL: assetManagementUrl,
headers: { organization_uid: org_uid },
context,
};

const importContext = {
spacesRootPath,
sourceApiKey: source_stack,
apiKey,
host,
org_uid,
context,
apiConcurrency: fetchConcurrency,
};

try {
if (parentProgressManager) {
parentProgressManager.addProcess(PROCESS, 1);
parentProgressManager
.startProcess(PROCESS)
.updateStatus(PROCESS_STATUS[PROCESS].GENERATING, PROCESS);
}

const existingSpaceUids = await this.fetchExistingSpaceUidsInOrg(apiConfig);

const { spaceDirs, readFailed } = this.listExportedSpaceDirectories(spacesRootPath);
if (spaceDirs.length === 0 && !readFailed) {
log.info('No Asset Management space directories (am*) found under spaces/.', context);
}

const allUidMap: Record<string, string> = {};
const allUrlMap: Record<string, string> = {};
const spaceUidMap: Record<string, string> = {};

const assetsImporter = new ImportAssets(apiConfig, importContext);

for (const spaceUid of spaceDirs) {
const spaceDir = join(spacesRootPath, spaceUid);
if (existingSpaceUids.has(spaceUid)) {
const { uidMap, urlMap } = await assetsImporter.buildIdentityMappersFromExport(spaceDir);
Object.assign(allUidMap, uidMap);
Object.assign(allUrlMap, urlMap);
spaceUidMap[spaceUid] = spaceUid;
parentProgressManager?.tick(true, `Asset Management space reused: ${spaceUid}`, null, PROCESS);
log.info(
`Asset Management space "${spaceUid}" exists in org; identity asset mappers merged from export.`,
context,
);
} else {
log.info(
`Asset Management space "${spaceUid}" is not in the target org yet. Import assets first, then re-run import-setup to refresh mappers after upload.`,
context,
);
}
}

await mkdir(mapperDirPath, { recursive: true });

await writeFile(
join(mapperDirPath, IMPORT_ASSETS_MAPPER_FILES.UID_MAPPING),
JSON.stringify(allUidMap),
'utf8',
);
await writeFile(
join(mapperDirPath, IMPORT_ASSETS_MAPPER_FILES.URL_MAPPING),
JSON.stringify(allUrlMap),
'utf8',
);
await writeFile(
join(mapperDirPath, IMPORT_ASSETS_MAPPER_FILES.SPACE_UID_MAPPING),
JSON.stringify(spaceUidMap),
'utf8',
);

await writeFile(duplicateAssetMapperPath, JSON.stringify({}), 'utf8');

parentProgressManager?.completeProcess(PROCESS, true);
log.success(
'The required Asset Management setup files for assets have been generated successfully.',
context,
);

return { kind: 'success' };
} catch (error) {
parentProgressManager?.completeProcess(PROCESS, false);
log.error(`Error occurred while generating Asset Management asset mappers: ${formatError(error)}.`, context);
return {
kind: 'error',
errorMessage: (error as Error)?.message || 'Asset Management asset mapper generation failed',
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { AssetManagementImportSetupAdapter } from './base';
export { default as ImportSetupAssetMappers } from './import-setup-asset-mappers';
export type { AssetMapperImportSetupResult, RunAssetMapperImportSetupParams } from '../types/import-setup-asset-mapper';
1 change: 1 addition & 0 deletions packages/contentstack-asset-management/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './types';
export * from './utils';
export * from './export';
export * from './import';
export * from './import-setup';
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Values derived from an on-disk export layout for Asset Management–backed stacks.
* Used by `contentstack-import` and `contentstack-import-setup` config handlers.
*/
export type AssetManagementExportFlags = {
assetManagementEnabled: boolean;
assetManagementUrl?: string;
/** Source stack API key from `branches.json`, when present — used for URL reconstruction. */
source_stack?: string;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export type RunAssetMapperImportSetupParams = {
contentDir: string;
/** Parent of `mapper/assets` (typically import-setup `backupDir`). */
mapperBaseDir: string;
assetManagementUrl?: string;
org_uid?: string;
source_stack?: string;
apiKey: string;
host: string;
context: Record<string, unknown>;
fetchConcurrency?: number;
};

export type AssetMapperImportSetupResult =
| { kind: 'skipped'; reason: 'missing_asset_management_url' | 'missing_organization_uid' }
| { kind: 'success' }
| { kind: 'error'; errorMessage: string };
2 changes: 2 additions & 0 deletions packages/contentstack-asset-management/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './asset-management-export-flags';
export * from './asset-management-api';
export * from './export-types';
export * from './import-setup-asset-mapper';
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as path from 'node:path';
import { existsSync, readFileSync } from 'node:fs';
import { configHandler } from '@contentstack/cli-utilities';

import type { AssetManagementExportFlags } from '../types/asset-management-export-flags';

/** Stack `settings.json` field that marks Asset Management usage (CMA contract). */
const STACK_SETTINGS_ASSET_MANAGEMENT_KEY = 'am_v2' as const;

/**
* Detects Asset Management export layout: `spaces/` + `stack/settings.json` with linked AM settings,
* and optionally reads `source_stack` from `branches.json` (content dir or parent).
*/
export function detectAssetManagementExportFromContentDir(contentDir: string): AssetManagementExportFlags {
const result: AssetManagementExportFlags = { assetManagementEnabled: false };
const spacesDir = path.join(contentDir, 'spaces');
const stackSettingsPath = path.join(contentDir, 'stack', 'settings.json');

if (!existsSync(spacesDir) || !existsSync(stackSettingsPath)) {
return result;
}

try {
const stackSettings = JSON.parse(readFileSync(stackSettingsPath, 'utf8')) as Record<string, unknown>;
if (!stackSettings?.[STACK_SETTINGS_ASSET_MANAGEMENT_KEY]) {
return result;
}

result.assetManagementEnabled = true;
const region = configHandler.get('region') as { assetManagementUrl?: string } | undefined;
result.assetManagementUrl = region?.assetManagementUrl;

const branchesJsonCandidates = [
path.join(contentDir, 'branches.json'),
path.join(contentDir, '..', 'branches.json'),
];
for (const branchesJsonPath of branchesJsonCandidates) {
if (!existsSync(branchesJsonPath)) {
continue;
}
try {
const branches = JSON.parse(readFileSync(branchesJsonPath, 'utf8')) as Array<{
stackHeaders?: { api_key?: string };
}>;
const apiKey = branches?.[0]?.stackHeaders?.api_key;
if (apiKey) {
result.source_stack = apiKey;
}
} catch {
// branches.json unreadable — URL mapping will be skipped
}
break;
}
} catch {
// stack settings unreadable — not an Asset Management export we can process
}

return result;
}
2 changes: 2 additions & 0 deletions packages/contentstack-asset-management/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export {
writeStreamToFile,
} from './export-helpers';
export { chunkArray, runInBatches } from './concurrent-batch';
export { detectAssetManagementExportFromContentDir } from './detect-asset-management-export';
export type { AssetManagementExportFlags } from '../types/asset-management-export-flags';
Loading
Loading