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
12 changes: 10 additions & 2 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
fileignoreconfig:
- filename: pnpm-lock.yaml
checksum: 9ad01ca900e007d8ea4f5ffcef9f166665aba852b3b9a0a5f4506c04fd362138
- filename: packages/contentstack-asset-management/src/import/fields.ts
checksum: e46c3eb94bba78ae06af0139a1b0fd4113c3dcfe879ed40cfe77659781526d5c
- filename: packages/contentstack-asset-management/src/import/asset-types.ts
checksum: 3525703fd2ac0f7ab3e963966c0abfa53d87c09f73dabab0889c7f41a5f1b003
- filename: packages/contentstack-import/src/types/default-config.ts
checksum: 1c09acba953cfd7058a3e0d63f0a9bfbb8f28e903538eaa015fdc611402bbd4f
- filename: packages/contentstack-asset-management/src/types/asset-management-api.ts
checksum: 9394e67f4d0dea9ad27a8592adf99441f40731b788335cd7699c78205bdaad58
- filename: packages/contentstack-import/src/import/modules/assets.ts
checksum: 98cea49283eb5d975168dc51e27b0e2fb541a9ed9b4368206f99b434e104f096
version: '1.0'
58 changes: 55 additions & 3 deletions packages/contentstack-asset-management/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
export const BATCH_SIZE = 50;
export const CHUNK_FILE_SIZE_MB = 1;
/** Fallback when export/import do not pass `chunkWriteBatchSize`. */
export const FALLBACK_AM_CHUNK_WRITE_BATCH_SIZE = 50;
/** Fallback when export/import do not pass `chunkFileSizeMb`. */
export const FALLBACK_AM_CHUNK_FILE_SIZE_MB = 1;
/** Fallback when import does not pass `apiConcurrency`. */
export const FALLBACK_AM_API_CONCURRENCY = 5;
/** @deprecated Use FALLBACK_AM_API_CONCURRENCY */
export const DEFAULT_AM_API_CONCURRENCY = FALLBACK_AM_API_CONCURRENCY;

/** Fallback strip lists when import options omit `fieldsImportInvalidKeys` / `assetTypesImportInvalidKeys`. */
export const FALLBACK_FIELDS_IMPORT_INVALID_KEYS = [
'created_at',
'created_by',
'updated_at',
'updated_by',
'is_system',
'asset_types_count',
] as const;
export const FALLBACK_ASSET_TYPES_IMPORT_INVALID_KEYS = [
'created_at',
'created_by',
'updated_at',
'updated_by',
'is_system',
'category',
'preview_image_url',
'category_detail',
] as const;

/** @deprecated Use FALLBACK_AM_CHUNK_WRITE_BATCH_SIZE */
export const BATCH_SIZE = FALLBACK_AM_CHUNK_WRITE_BATCH_SIZE;
/** @deprecated Use FALLBACK_AM_CHUNK_FILE_SIZE_MB */
export const CHUNK_FILE_SIZE_MB = FALLBACK_AM_CHUNK_FILE_SIZE_MB;

/**
* Main process name for Asset Management 2.0 export (single progress bar).
Expand All @@ -17,10 +48,15 @@ export const PROCESS_NAMES = {
AM_FIELDS: 'Fields',
AM_ASSET_TYPES: 'Asset types',
AM_DOWNLOADS: 'Asset downloads',
// Import process names
AM_IMPORT_FIELDS: 'Import fields',
AM_IMPORT_ASSET_TYPES: 'Import asset types',
AM_IMPORT_FOLDERS: 'Import folders',
AM_IMPORT_ASSETS: 'Import assets',
} as const;

/**
* Status messages for each process (exporting, fetching, failed).
* Status messages for each process (exporting, fetching, importing, failed).
*/
export const PROCESS_STATUS = {
[PROCESS_NAMES.AM_SPACE_METADATA]: {
Expand All @@ -47,4 +83,20 @@ export const PROCESS_STATUS = {
DOWNLOADING: 'Downloading asset files...',
FAILED: 'Failed to download assets.',
},
[PROCESS_NAMES.AM_IMPORT_FIELDS]: {
IMPORTING: 'Importing shared fields...',
FAILED: 'Failed to import fields.',
},
[PROCESS_NAMES.AM_IMPORT_ASSET_TYPES]: {
IMPORTING: 'Importing shared asset types...',
FAILED: 'Failed to import asset types.',
},
[PROCESS_NAMES.AM_IMPORT_FOLDERS]: {
IMPORTING: 'Importing folders...',
FAILED: 'Failed to import folders.',
},
[PROCESS_NAMES.AM_IMPORT_ASSETS]: {
IMPORTING: 'Importing assets...',
FAILED: 'Failed to import assets.',
},
} as const;
4 changes: 2 additions & 2 deletions packages/contentstack-asset-management/src/export/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export default class ExportAssets extends AssetManagementExportAdapter {
log.debug(`Fetching folders and assets for space ${workspace.space_uid}`, this.exportContext.context);

const [folders, assetsData] = await Promise.all([
this.getWorkspaceFolders(workspace.space_uid),
this.getWorkspaceAssets(workspace.space_uid),
this.getWorkspaceFolders(workspace.space_uid, workspace.uid),
this.getWorkspaceAssets(workspace.space_uid, workspace.uid),
]);

await writeFile(pResolve(assetsDir, 'folders.json'), JSON.stringify(folders, null, 2));
Expand Down
15 changes: 10 additions & 5 deletions packages/contentstack-asset-management/src/export/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import { FsUtility, log, CLIProgressManager, configHandler } from '@contentstack
import type { AssetManagementAPIConfig } from '../types/asset-management-api';
import type { ExportContext } from '../types/export-types';
import { AssetManagementAdapter } from '../utils/asset-management-api-adapter';
import { AM_MAIN_PROCESS_NAME } from '../constants/index';
import { BATCH_SIZE, CHUNK_FILE_SIZE_MB } from '../constants/index';
import {
AM_MAIN_PROCESS_NAME,
FALLBACK_AM_CHUNK_FILE_SIZE_MB,
FALLBACK_AM_CHUNK_WRITE_BATCH_SIZE,
} from '../constants/index';

export type { ExportContext };

Expand Down Expand Up @@ -83,17 +86,19 @@ export class AssetManagementExportAdapter extends AssetManagementAdapter {
await writeFile(pResolve(dir, indexFileName), '{}');
return;
}
const chunkMb = this.exportContext.chunkFileSizeMb ?? FALLBACK_AM_CHUNK_FILE_SIZE_MB;
const batchSize = this.exportContext.chunkWriteBatchSize ?? FALLBACK_AM_CHUNK_WRITE_BATCH_SIZE;
const fs = new FsUtility({
basePath: dir,
indexFileName,
chunkFileSize: CHUNK_FILE_SIZE_MB,
chunkFileSize: chunkMb,
moduleName,
fileExt: 'json',
metaPickKeys,
keepMetadata: true,
});
for (let i = 0; i < items.length; i += BATCH_SIZE) {
const batch = items.slice(i, i + BATCH_SIZE);
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
fs.writeIntoFile(batch as Record<string, string>[], { mapKeyVal: true });
}
fs.completeFile(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ export class ExportSpaces {
branchName,
assetManagementUrl,
org_uid,
apiKey,
context,
securedAssets,
chunkWriteBatchSize,
chunkFileSizeMb,
} = this.options;

if (!linkedWorkspaces.length) {
Expand All @@ -54,7 +57,9 @@ export class ExportSpaces {
const totalSteps = 2 + linkedWorkspaces.length * 4;
const progress = this.createProgress();
progress.addProcess(AM_MAIN_PROCESS_NAME, totalSteps);
progress.startProcess(AM_MAIN_PROCESS_NAME).updateStatus(PROCESS_STATUS[PROCESS_NAMES.AM_FIELDS].FETCHING, AM_MAIN_PROCESS_NAME);
progress
.startProcess(AM_MAIN_PROCESS_NAME)
.updateStatus(PROCESS_STATUS[PROCESS_NAMES.AM_FIELDS].FETCHING, AM_MAIN_PROCESS_NAME);

const apiConfig: AssetManagementAPIConfig = {
baseURL: assetManagementUrl,
Expand All @@ -65,6 +70,8 @@ export class ExportSpaces {
spacesRootPath,
context,
securedAssets,
chunkWriteBatchSize,
chunkFileSizeMb,
};

const sharedFieldsDir = pResolve(spacesRootPath, 'fields');
Expand Down
101 changes: 101 additions & 0 deletions packages/contentstack-asset-management/src/import/asset-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import omit from 'lodash/omit';
import isEqual from 'lodash/isEqual';
import { log } from '@contentstack/cli-utilities';

import type { AssetManagementAPIConfig, ImportContext } from '../types/asset-management-api';
import { AssetManagementImportAdapter } from './base';
import { FALLBACK_ASSET_TYPES_IMPORT_INVALID_KEYS, PROCESS_NAMES, PROCESS_STATUS } from '../constants/index';
import { runInBatches } from '../utils/concurrent-batch';
import { readChunkedJsonItems } from '../utils/chunked-json-read';

/**
* Reads shared asset types from `spaces/asset_types/asset-types.json` and POSTs
* each to the target org-level AM endpoint (`POST /api/asset_types`).
*
* Strategy: Fetch → Diff → Create only missing, warn on conflict
* 1. Fetch asset types that already exist in the target org.
* 2. Skip entries where is_system=true (platform-owned, cannot be created via API).
* 3. If uid already exists and definition differs → warn and skip.
* 4. If uid already exists and definition matches → silently skip.
* 5. Strip read-only/computed keys from the POST body before creating new asset types.
*/
export default class ImportAssetTypes extends AssetManagementImportAdapter {
constructor(apiConfig: AssetManagementAPIConfig, importContext: ImportContext) {
super(apiConfig, importContext);
}

async start(): Promise<void> {
await this.init();

const stripKeys = this.importContext.assetTypesImportInvalidKeys ?? [...FALLBACK_ASSET_TYPES_IMPORT_INVALID_KEYS];
const dir = this.getAssetTypesDir();
const indexName = this.importContext.assetTypesFileName ?? 'asset-types.json';
const items = await readChunkedJsonItems<Record<string, unknown>>(dir, indexName, this.importContext.context);

if (items.length === 0) {
log.debug('No shared asset types to import', this.importContext.context);
return;
}

// Fetch existing asset types from the target org keyed by uid for diff comparison.
// Asset types are org-level; the spaceUid param in getWorkspaceAssetTypes is unused in the path.
const existingByUid = new Map<string, Record<string, unknown>>();
try {
const existing = await this.getWorkspaceAssetTypes('');
for (const at of existing.asset_types ?? []) {
existingByUid.set(at.uid, at as Record<string, unknown>);
}
log.debug(`Target org has ${existingByUid.size} existing asset type(s)`, this.importContext.context);
} catch (e) {
log.debug(`Could not fetch existing asset types, will attempt to create all: ${e}`, this.importContext.context);
}

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

type ToCreate = { uid: string; payload: Record<string, unknown> };
const toCreate: ToCreate[] = [];

for (const assetType of items) {
const uid = assetType.uid as string;

if (assetType.is_system) {
log.debug(`Skipping system asset type: ${uid}`, this.importContext.context);
continue;
}

const existing = existingByUid.get(uid);
if (existing) {
const exportedClean = omit(assetType, stripKeys);
const existingClean = omit(existing, stripKeys);
if (!isEqual(exportedClean, existingClean)) {
log.warn(
`Asset type "${uid}" already exists in the target org with a different definition. Skipping — to apply the exported definition, delete the asset type from the target org first.`,
this.importContext.context,
);
} else {
log.debug(`Asset type "${uid}" already exists with matching definition, skipping`, this.importContext.context);
}
this.tick(true, `asset-type: ${uid} (skipped, already exists)`, null, PROCESS_NAMES.AM_IMPORT_ASSET_TYPES);
continue;
}

toCreate.push({ uid, payload: omit(assetType, stripKeys) as Record<string, unknown> });
}

await runInBatches(toCreate, this.apiConcurrency, async ({ uid, payload }) => {
try {
await this.createAssetType(payload as any);
this.tick(true, `asset-type: ${uid}`, null, PROCESS_NAMES.AM_IMPORT_ASSET_TYPES);
log.debug(`Imported asset type: ${uid}`, this.importContext.context);
} catch (e) {
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,
);
log.debug(`Failed to import asset type ${uid}: ${e}`, this.importContext.context);
}
});
}
}
Loading
Loading