Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 6 additions & 2 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
fileignoreconfig:
- filename: pnpm-lock.yaml
checksum: 71b97a29a577e59d746967b4717acd013d624fd1cbf7d796647c9ef6d7652165
- filename: packages/contentstack-bulk-operations/src/services/am-asset-service.ts
checksum: 5f6c0ecba74e27399a7079ca15e65e77ef692697093c9fb1d57213728c4fe985
- filename: packages/contentstack-bulk-operations/src/utils/asset-uids-from-file.ts
checksum: 580932f192dd3fdd8bb2c55b7a7a78f1694f646ef5c5041f86c75668778f7ecb
- filename: packages/contentstack-bulk-operations/test/unit/utils/asset-uids-from-file.test.ts
checksum: 8123f7a675a0275795b59b15d0f2d5f8f1e57ccbecf3f97249a0dc5a037b9203
version: '1.0'
1 change: 1 addition & 0 deletions packages/contentstack-bulk-operations/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"/oclif.manifest.json"
],
"dependencies": {
"@contentstack/cli-asset-management": "1.0.0-beta.2",
"@contentstack/cli-command": "~2.0.0-beta.7",
"@contentstack/cli-utilities": "~2.0.0-beta.8",
"@contentstack/delivery-sdk": "^5.2.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import chalk from 'chalk';
import { Command } from '@contentstack/cli-command';
import { flags, log, createLogContext, handleAndLogError, cliux, FlagInput } from '@contentstack/cli-utilities';

import messages, { $t } from '../../../messages';
import { AmAssetService } from '../../../services';
import {
loadAssetUidsFromFile,
loadBulkDeleteItemsFromFile,
LoadAssetUidsError,
} from '../../../utils/asset-uids-from-file';
import { AmBulkDeleteItem } from '../../../interfaces';

const COMMAND_ID = 'cm:stacks:bulk-am-assets';

type RegionWithOptionalAmUrl = { csAssetsUrl?: string };

/**
* AM bulk delete (job) / bulk move — CS Assets API only; asset UIDs come from a JSON file `{ "uids": [...] }`.
*/
export default class BulkAmAssets extends Command {
static description = messages.BULK_AM_ASSETS_DESCRIPTION;

static examples = [
'<%= config.bin %> <%= command.id %> --operation delete --space-uid am123 --org-uid bltcOrg --locale en-us --asset-uids-file ./assets.json',
'<%= config.bin %> <%= command.id %> --operation move --space-uid am123 --org-uid bltcOrg --target-folder-uid amFolder --asset-uids-file ./assets.json',
'<%= config.bin %> <%= command.id %> --operation delete --space-uid am123 --org-uid bltcOrg --workspace main --locale en-us --asset-uids-file ./uids.json -y',
];

static flags:FlagInput = {
operation: flags.string({
description: messages.AM_OPERATION_FLAG,
options: ['delete', 'move'],
required: true,
}),
'space-uid': flags.string({
description: messages.AM_SPACE_UID_FLAG,
required: true,
}),
'org-uid': flags.string({
description: messages.AM_ORG_UID_FLAG,
required: true,
}),
workspace: flags.string({
default: 'main',
description: messages.AM_WORKSPACE_FLAG,
}),
'asset-uids-file': flags.string({
description: messages.AM_ASSET_UIDS_FILE_FLAG,
required: true,
}),
locale: flags.string({
description: messages.AM_LOCALE_FLAG,
}),
'target-folder-uid': flags.string({
description: messages.AM_TARGET_FOLDER_FLAG,
}),
yes: flags.boolean({
char: 'y',
description: messages.YES,
default: false,
}),
};

private readonly loggerContext = { module: COMMAND_ID };

private handleAssetUidsFileError(e: LoadAssetUidsError): void {
const pathShown = e.filePath;
if (e.kind === 'READ') {
log.error(
$t(messages.AM_ASSET_UIDS_FILE_READ_FAILED, { path: pathShown, detail: e.message }),
this.loggerContext
);
} else {
log.error($t(messages.AM_ASSET_UIDS_FILE_INVALID, { path: pathShown, detail: e.message }), this.loggerContext);
}
process.exitCode = 1;
}

async run(): Promise<void> {
try {
const { flags: f } = await this.parse(BulkAmAssets);

const amBaseUrl = (this.region as RegionWithOptionalAmUrl).csAssetsUrl?.trim();
if (!amBaseUrl) {
log.error($t(messages.AM_URL_NOT_CONFIGURED), this.loggerContext);
process.exitCode = 1;
return;
}

const op = f.operation;
if (op !== 'delete' && op !== 'move') {
log.error($t(messages.AM_INVALID_OPERATION, { operation: String(op ?? '') }), this.loggerContext);
process.exitCode = 1;
return;
}

const spaceUid = (f['space-uid'] ?? '').trim();
if (!spaceUid) {
log.error($t(messages.SPACE_UID_REQUIRED), this.loggerContext);
process.exitCode = 1;
return;
}

const orgUid = (f['org-uid'] ?? '').trim();
if (!orgUid) {
log.error($t(messages.ORG_UID_REQUIRED), this.loggerContext);
process.exitCode = 1;
return;
}

const assetUidsPath = (f['asset-uids-file'] ?? '').trim();
if (!assetUidsPath) {
log.error($t(messages.AM_ASSET_UIDS_FILE_REQUIRED), this.loggerContext);
process.exitCode = 1;
return;
}

let deleteRows: AmBulkDeleteItem[];

if (op === 'delete') {
const locale = (f.locale ?? '').trim();
if (!locale) {
log.error($t(messages.AM_LOCALE_REQUIRED), this.loggerContext);
process.exitCode = 1;
return;
}
try {
deleteRows = loadBulkDeleteItemsFromFile(assetUidsPath, locale);
} catch (e: unknown) {
if (e instanceof LoadAssetUidsError) {
this.handleAssetUidsFileError(e);
} else {
handleAndLogError(e);
process.exitCode = 1;
}
return;
}

createLogContext(this.context?.info?.command || COMMAND_ID, spaceUid, 'OAuth/Token');
const amService = new AmAssetService(amBaseUrl, spaceUid, orgUid);
const workspace = f.workspace ?? 'main';

if (!f.yes) {
console.log(chalk.yellow(`\n${$t(messages.OPERATION_CONFIG_HEADER)}\n`));
console.log(' Operation: AM bulk delete');
console.log(` Space UID: ${spaceUid}`);
console.log(` Organization UID: ${orgUid}`);
console.log(` Workspace: ${workspace}`);
console.log(` Locale: ${locale}`);
console.log(` Asset UIDs file: ${assetUidsPath}`);
console.log(` Total AM delete entries: ${deleteRows.length}\n`);

const confirmed: boolean = await cliux.inquire({
type: 'confirm',
name: 'proceed',
message: chalk.grey($t(messages.CONTINUE_WITH_CONFIG)),
default: false,
});
if (!confirmed) {
log.warn($t(messages.OPERATION_CANCELLED), this.loggerContext);
return;
}
}

log.info($t(messages.AM_DELETING_ASSETS, { count: deleteRows.length, spaceUid }), this.loggerContext);
const result = await amService.bulkDelete(spaceUid, workspace, deleteRows);
if (!result.success) {
log.error(result.error ?? 'AM bulk delete failed', this.loggerContext);
process.exitCode = 1;
return;
}
if (result.notice) {
log.info($t(messages.AM_OPERATION_NOTICE, { notice: result.notice }), this.loggerContext);
}
if (result.jobId) {
log.info($t(messages.AM_DELETE_SUBMITTED, { jobId: result.jobId }), this.loggerContext);
}
return;
}

const moveFolderUid = (f['target-folder-uid'] ?? '').trim();
if (!moveFolderUid) {
log.error($t(messages.TARGET_FOLDER_REQUIRED), this.loggerContext);
process.exitCode = 1;
return;
}

let uids: string[];
try {
uids = loadAssetUidsFromFile(assetUidsPath);
} catch (e: unknown) {
if (e instanceof LoadAssetUidsError) {
this.handleAssetUidsFileError(e);
} else {
handleAndLogError(e);
process.exitCode = 1;
}
return;
}

createLogContext(this.context?.info?.command || COMMAND_ID, spaceUid, 'OAuth/Token');
const amService = new AmAssetService(amBaseUrl, spaceUid, orgUid);
const workspace = f.workspace ?? 'main';

if (!f.yes) {
console.log(chalk.yellow(`\n${$t(messages.OPERATION_CONFIG_HEADER)}\n`));
console.log(' Operation: AM bulk move');
console.log(` Space UID: ${spaceUid}`);
console.log(` Organization UID: ${orgUid}`);
console.log(` Workspace: ${workspace}`);
console.log(` Target folder UID: ${moveFolderUid}`);
console.log(` Asset UIDs file: ${assetUidsPath}`);
console.log(` Assets: ${uids.length}\n`);

const confirmed: boolean = await cliux.inquire({
type: 'confirm',
name: 'proceed',
message: chalk.grey($t(messages.CONTINUE_WITH_CONFIG)),
default: false,
});
if (!confirmed) {
log.warn($t(messages.OPERATION_CANCELLED), this.loggerContext);
return;
}
}

log.info(
$t(messages.AM_MOVING_ASSETS, { count: uids.length, targetFolderUid: moveFolderUid }),
this.loggerContext
);
const result = await amService.bulkMove(spaceUid, workspace, uids, moveFolderUid);
if (!result.success) {
log.error(result.error ?? 'AM bulk move failed', this.loggerContext);
process.exitCode = 1;
return;
}
if (result.notice) {
log.info($t(messages.AM_OPERATION_NOTICE, { notice: result.notice }), this.loggerContext);
}
log.info($t(messages.AM_MOVE_SUBMITTED), this.loggerContext);
} catch (error) {
handleAndLogError(error);
}
}
}
26 changes: 25 additions & 1 deletion packages/contentstack-bulk-operations/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ type ManagementStack = ReturnType<ContentstackClient['stack']>;
export enum OperationType {
PUBLISH = 'publish',
UNPUBLISH = 'unpublish',
DELETE = 'delete',
MOVE = 'move',
}

export enum PublishMode {
Expand Down Expand Up @@ -195,6 +197,14 @@ export interface CommandFlags {
// Asset-specific flags
'folder-uid'?: string;

/** AM bulk delete/move */
'space-uid'?: string;
'org-uid'?: string;
workspace?: string;
locale?: string;
'asset-uids-file'?: string;
'target-folder-uid'?: string;

// Target environments and locales
environments?: string[];
locales?: string[];
Expand Down Expand Up @@ -246,6 +256,20 @@ export interface AssetPublishData {
publish_details?: PublishDetails[];
}

/** One row for AM bulk-delete payload `{ uid, locale }[]`. */
export interface AmBulkDeleteItem {
uid: string;
locale: string;
}

/** Normalized outcome from AM bulk delete/move calls (CLI layer). */
export interface AmBulkOperationResult {
success: boolean;
notice?: string;
jobId?: string;
error?: string;
}

export interface BulkJobResult {
success: number;
failed: number;
Expand Down Expand Up @@ -420,4 +444,4 @@ export interface CrossPublishConfig {
contentTypes?: string[];
resourceType: ResourceType;
deliveryStack: DeliveryStack | null; // Pass delivery stack from command (optional)
}
}
37 changes: 36 additions & 1 deletion packages/contentstack-bulk-operations/src/messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,38 @@ const bulkAssetsMsg = {
ASSETS_READY_FOR_CROSS_PUBLISH: '{count} assets ready for cross-publish',
};

/**
* AM bulk delete/move (CS Assets API) messages
*/
const amBulkAssetsMsg = {
BULK_AM_ASSETS_DESCRIPTION:
'Bulk delete or move assets via Asset Management API (AM-enabled regions). Loads asset UIDs from a JSON file `{ "uids": [...] }`; pass organization via `--org-uid`.',
AM_URL_NOT_CONFIGURED:
'AM operations require assetManagementUrl in your region settings. Ensure your region is configured correctly.',
SPACE_UID_REQUIRED: '--space-uid is required for AM operations',
ORG_UID_REQUIRED: '--org-uid is required for AM operations (organization_uid header)',
TARGET_FOLDER_REQUIRED: '--target-folder-uid is required for bulk move',
AM_LOCALE_REQUIRED: '--locale is required for bulk delete (AM deletes per asset and locale)',
AM_ASSET_UIDS_FILE_REQUIRED: '--asset-uids-file is required (path to JSON `{ "uids": string[] }`)',
AM_ASSET_UIDS_FILE_READ_FAILED: 'Failed to read asset UIDs file "{path}": {detail}',
AM_ASSET_UIDS_FILE_INVALID: 'Invalid asset UIDs file "{path}": {detail}',
AM_DELETING_ASSETS: 'Deleting {count} asset/locale pair(s) from space {spaceUid}...',
AM_MOVING_ASSETS: 'Moving {count} asset(s) to folder {targetFolderUid}...',
AM_DELETE_SUBMITTED: 'Bulk delete job submitted. Job ID: {jobId}',
AM_MOVE_SUBMITTED: 'Bulk move initiated successfully.',
AM_OPERATION_NOTICE: '{notice}',
AM_OPERATION_FLAG: 'Operation: delete (AM bulk delete) or move (AM bulk move)',
AM_SPACE_UID_FLAG: 'Asset Management space UID',
AM_ORG_UID_FLAG: 'Organization UID for AM API (organization_uid header)',
AM_WORKSPACE_FLAG: 'AM workspace query parameter (default: main)',
AM_ASSET_UIDS_FILE_FLAG:
'Path to UTF-8 JSON file: exactly `{ "uids": ["uid1", "uid2"] }` (non-empty string array, no trimming; large lists: see docs for NODE_OPTIONS)',
AM_LOCALE_FLAG: 'Locale code for bulk delete (single locale per run)',
AM_TARGET_FOLDER_FLAG: 'Destination AM folder UID (required for move)',
AM_INVALID_OPERATION: 'Invalid operation: {operation}. Must be delete or move',
AM_CONFIRM_SUMMARY: 'Proceed with AM {operation} on {count} item(s)?',
};

/**
* Bulk operation service messages
*/
Expand Down Expand Up @@ -371,6 +403,7 @@ const commandInfo = {
BULK_ASSETS_DESCRIPTION: 'Bulk operations for assets (publish/unpublish/cross-publish)',
BULK_TAXONOMIES_DESCRIPTION:
'Publish taxonomies to environments and locales (CMA POST /v3/taxonomies/publish; initiates a publish job)',
BULK_AM_ASSETS_DESCRIPTION: amBulkAssetsMsg.BULK_AM_ASSETS_DESCRIPTION,
};

/**
Expand All @@ -386,7 +419,8 @@ const messages: typeof errors &
typeof summaryMsg &
typeof interactiveMsg &
typeof flagDescriptions &
typeof commandInfo = {
typeof commandInfo &
typeof amBulkAssetsMsg = {
...errors,
...commonMsg,
...entryServiceMsg,
Expand All @@ -398,6 +432,7 @@ const messages: typeof errors &
...interactiveMsg,
...flagDescriptions,
...commandInfo,
...amBulkAssetsMsg,
};

/**
Expand Down
Loading
Loading