Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 2 additions & 2 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ fileignoreconfig:
- filename: packages/contentstack-export/test/unit/export/modules/assets.test.ts
checksum: 73ff01e2d19c8d1384dca2ee7087f8c19e0b1fac6b29c75a02ca523a36b7cb92
- filename: packages/contentstack-export/src/types/default-config.ts
checksum: bf399466aae808342ec013c0179fbc24ac2d969c77fdbef47a842b12497d507e
checksum: a204b00fc47046fd638f952c1326b6d88615dd96e37dc83e4c7e9404dc0435bb
- filename: packages/contentstack-export/src/types/index.ts
checksum: fa36c236abac338b03bf307102a99f25dddac9afe75b6b34fb82e318e7759799
- filename: packages/contentstack-export/src/config/index.ts
checksum: 1eb407ee0bd21597d8a4c673fce99d60fafd151ac843c33dac52ffdcc73e8107
checksum: 3998e30abaf9838f86025c3422cab085441bc38e958ed9e63084f928dbb7995c
- filename: packages/contentstack-export/test/unit/export/modules/stack.test.ts
checksum: 79876b8f635037a2d8ba38dac055e7625bf85db6a3cf5729434e6a97e44857d6
- filename: packages/contentstack-export/src/export/modules/stack.ts
Expand Down
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) {
Comment thread
naman-contentstack marked this conversation as resolved.
Outdated
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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using fs utility, we are already have a way to read to this, no need of this implementation at all "readChunkedJsonItems"


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> };
Comment thread
naman-contentstack marked this conversation as resolved.
Outdated
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