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
29 changes: 29 additions & 0 deletions packages/contentstack-bulk-operations/src/base-am-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Command } from '@contentstack/cli-command';
import { handleAndLogError } from '@contentstack/cli-utilities';

import { fillMissingCsAssetsFlags } from './utils';
import type { CsAssetsFlags } from './interfaces';

/**
* Thin base command for CS Assets operations.
* Handles flag prompting in init() and exposes typed parsedFlags / loggerContext.
* Deliberately does NOT inherit BaseBulkCommand — CS Assets operations use a different API
* surface with no stack setup, queue managers, or rate limiters.
*/
export abstract class BaseCsAssetsCommand extends Command {
protected parsedFlags!: CsAssetsFlags;
protected loggerContext!: { module: string };

protected async init(): Promise<void> {
await super.init();
const { flags } = await this.parse(this.constructor as typeof BaseCsAssetsCommand);
this.loggerContext = { module: this.id ?? 'cm:stacks:bulk-am-assets' };
this.parsedFlags = (await fillMissingCsAssetsFlags(flags)) as CsAssetsFlags;
}

async catch(error: Error): Promise<void> {
handleAndLogError(error);
}

abstract run(): Promise<void>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,14 @@ export abstract class BaseBulkCommand extends Command {

this.parsedFlags = flags;

const commandName = `cm:stacks:bulk-${this.resourceType === ResourceType.ENTRY ? 'entries' : 'assets'}`;
createLogContext(
this.context?.info?.command || commandName,
this.context?.info?.command || this.id,
flags['stack-api-key'] || '',
flags.alias ? 'Management Token' : 'Basic Auth'
);

this.logger = log;
this.loggerContext = { module: commandName };
this.loggerContext = { module: this.id };

// Check for revert/retry EARLY - all config comes from log file
const isRevertOrRetry = flags.revert || flags['retry-failed'];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import chalk from 'chalk';
import { Command } from '@contentstack/cli-command';
import { flags, log, createLogContext, handleAndLogError, cliux, FlagInput } from '@contentstack/cli-utilities';
import { flags, log, createLogContext, cliux, handleAndLogError, FlagInput } from '@contentstack/cli-utilities';

import messages, { $t } from '../../../messages';
import { AmAssetService } from '../../../services';
import { BaseCsAssetsCommand } from '../../../base-am-command';
import { CsAssetsService } from '../../../services';
import {
loadAssetUidsFromFile,
loadBulkDeleteItemsFromFile,
LoadAssetUidsError,
} from '../../../utils/asset-uids-from-file';
import { AmBulkDeleteItem } from '../../../interfaces';
import { generateCsAssetsJobStatusUrl } from '../../../utils/bulk-publish-url-generator';
import { CsAssetsBulkDeleteItem } from '../../../interfaces';

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

type RegionWithOptionalAmUrl = { csAssetsUrl?: string };
type RegionWithOptionalCsAssetsUrl = { csAssetsUrl?: string };

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

static examples = [
'<%= config.bin %> <%= command.id %> --operation delete --space-uid am123 --org-uid bltcOrg --locale en-us --asset-uids-file ./assets.json',
Expand All @@ -29,31 +30,27 @@ export default class BulkAmAssets extends Command {

static flags: FlagInput = {
operation: flags.string({
description: messages.AM_OPERATION_FLAG,
description: messages.CS_ASSETS_OPERATION_FLAG,
options: ['delete', 'move'],
required: true,
}),
'space-uid': flags.string({
description: messages.AM_SPACE_UID_FLAG,
required: true,
description: messages.CS_ASSETS_SPACE_UID_FLAG,
}),
'org-uid': flags.string({
description: messages.AM_ORG_UID_FLAG,
required: true,
description: messages.CS_ASSETS_ORG_UID_FLAG,
}),
workspace: flags.string({
default: 'main',
description: messages.AM_WORKSPACE_FLAG,
description: messages.CS_ASSETS_WORKSPACE_FLAG,
}),
'asset-uids-file': flags.string({
description: messages.AM_ASSET_UIDS_FILE_FLAG,
required: true,
description: messages.CS_ASSETS_ASSET_UIDS_FILE_FLAG,
}),
locale: flags.string({
description: messages.AM_LOCALE_FLAG,
description: messages.CS_ASSETS_LOCALE_FLAG,
}),
'target-folder-uid': flags.string({
description: messages.AM_TARGET_FOLDER_FLAG,
description: messages.CS_ASSETS_TARGET_FOLDER_FLAG,
}),
yes: flags.boolean({
char: 'y',
Expand All @@ -62,66 +59,74 @@ export default class BulkAmAssets extends Command {
}),
};

private readonly loggerContext = { module: COMMAND_ID };
private printCsAssetsSummary(
op: 'delete' | 'move',
opts: { jobId?: string; count?: number; folderUid?: string; notice?: string; error?: string; spaceUid?: string }
): void {
if (opts.error) {
log.error($t(messages.CS_ASSETS_OPERATION_FAILED, { operation: op }), this.loggerContext);
log.error(opts.error, this.loggerContext);
} else if (op === 'delete') {
log.success($t(messages.CS_ASSETS_DELETE_SUCCESS), this.loggerContext);
if (opts.jobId) log.info($t(messages.CS_ASSETS_DELETE_JOB_ID, { jobId: opts.jobId }), this.loggerContext);
log.info($t(messages.CS_ASSETS_DELETE_ASYNC_NOTE), this.loggerContext);
const statusUrl = generateCsAssetsJobStatusUrl(opts.spaceUid);
if (statusUrl) log.info(statusUrl, this.loggerContext);
} else {
log.success($t(messages.CS_ASSETS_MOVE_SUCCESS), this.loggerContext);
if (opts.count !== undefined && opts.folderUid) {
log.info(
$t(messages.CS_ASSETS_MOVE_ASSETS_COUNT, { count: opts.count, folderUid: opts.folderUid }),
this.loggerContext
);
}
const statusUrl = generateCsAssetsJobStatusUrl(opts.spaceUid);
if (statusUrl) log.info(statusUrl, this.loggerContext);
}
if (opts.notice) log.info(opts.notice, this.loggerContext);
}

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 }),
$t(messages.CS_ASSETS_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);
log.error($t(messages.CS_ASSETS_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 f = this.parsedFlags;

const amBaseUrl = (this.region as RegionWithOptionalAmUrl).csAssetsUrl?.trim();
if (!amBaseUrl) {
log.error($t(messages.AM_URL_NOT_CONFIGURED), this.loggerContext);
const csAssetsBaseUrl = (this.region as RegionWithOptionalCsAssetsUrl).csAssetsUrl?.trim();
if (!csAssetsBaseUrl) {
log.error($t(messages.CS_ASSETS_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);
log.error($t(messages.CS_ASSETS_INVALID_OPERATION, { operation: String(op ?? '') }), 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;
}
const spaceUid = f['space-uid'].trim();
const orgUid = f['org-uid'].trim();
const assetUidsPath = f['asset-uids-file'].trim();

let deleteRows: AmBulkDeleteItem[];
let deleteRows: CsAssetsBulkDeleteItem[];

if (op === 'delete') {
const locale = (f.locale ?? '').trim();
if (!locale) {
log.error($t(messages.AM_LOCALE_REQUIRED), this.loggerContext);
log.error($t(messages.CS_ASSETS_LOCALE_REQUIRED), this.loggerContext);
process.exitCode = 1;
return;
}
Expand All @@ -138,18 +143,18 @@ export default class BulkAmAssets extends Command {
}

createLogContext(this.context?.info?.command || COMMAND_ID, spaceUid, 'OAuth/Token');
const amService = new AmAssetService(amBaseUrl, spaceUid, orgUid);
const csAssetsService = new CsAssetsService(csAssetsBaseUrl, 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(' Operation: CS Assets 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`);
console.log(` Total CS Assets delete entries: ${deleteRows.length}\n`);

const confirmed: boolean = await cliux.inquire({
type: 'confirm',
Expand All @@ -163,19 +168,20 @@ export default class BulkAmAssets extends Command {
}
}

log.info($t(messages.AM_DELETING_ASSETS, { count: deleteRows.length, spaceUid }), this.loggerContext);
const result = await amService.bulkDelete(spaceUid, workspace, deleteRows);
log.info($t(messages.CS_ASSETS_DELETING_ASSETS, { count: deleteRows.length, spaceUid }), this.loggerContext);
const result = await csAssetsService.bulkDelete(spaceUid, workspace, deleteRows);
if (!result.success) {
log.error(result.error ?? 'AM bulk delete failed', this.loggerContext);
this.printCsAssetsSummary('delete', { error: result.error ?? 'CS Assets bulk delete failed', spaceUid });
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);
}
this.printCsAssetsSummary('delete', { jobId: result.jobId, notice: result.notice, spaceUid });
return;
}

if (f.locale) {
log.error($t(messages.CS_ASSETS_LOCALE_NOT_ALLOWED_FOR_MOVE), this.loggerContext);
process.exitCode = 1;
return;
}

Expand All @@ -200,12 +206,12 @@ export default class BulkAmAssets extends Command {
}

createLogContext(this.context?.info?.command || COMMAND_ID, spaceUid, 'OAuth/Token');
const amService = new AmAssetService(amBaseUrl, spaceUid, orgUid);
const csAssetsService = new CsAssetsService(csAssetsBaseUrl, 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(' Operation: CS Assets bulk move');
console.log(` Space UID: ${spaceUid}`);
console.log(` Organization UID: ${orgUid}`);
console.log(` Workspace: ${workspace}`);
Expand All @@ -226,19 +232,16 @@ export default class BulkAmAssets extends Command {
}

log.info(
$t(messages.AM_MOVING_ASSETS, { count: uids.length, targetFolderUid: moveFolderUid }),
$t(messages.CS_ASSETS_MOVING_ASSETS, { count: uids.length, targetFolderUid: moveFolderUid }),
this.loggerContext
);
const result = await amService.bulkMove(spaceUid, workspace, uids, moveFolderUid);
const result = await csAssetsService.bulkMove(spaceUid, workspace, uids, moveFolderUid);
if (!result.success) {
log.error(result.error ?? 'AM bulk move failed', this.loggerContext);
this.printCsAssetsSummary('move', { error: result.error ?? 'CS Assets bulk move failed', spaceUid });
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);
this.printCsAssetsSummary('move', { count: uids.length, folderUid: moveFolderUid, notice: result.notice, spaceUid });
} catch (error) {
handleAndLogError(error);
}
Expand Down
23 changes: 18 additions & 5 deletions packages/contentstack-bulk-operations/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export enum ResourceType {
ENTRY = 'entry',
ASSET = 'asset',
TAXONOMY = 'taxonomy',
CS_ASSETS = 'cs-assets',
}

export enum FilterType {
Expand Down Expand Up @@ -197,7 +198,7 @@ export interface CommandFlags {
// Asset-specific flags
'folder-uid'?: string;

/** AM bulk delete/move */
/** CS Assets bulk delete/move */
'space-uid'?: string;
'org-uid'?: string;
workspace?: string;
Expand Down Expand Up @@ -256,20 +257,32 @@ export interface AssetPublishData {
publish_details?: PublishDetails[];
}

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

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

/** Typed flags for the bulk-am-assets command. */
export interface CsAssetsFlags {
operation: string;
'space-uid': string;
'org-uid': string;
workspace: string;
'asset-uids-file': string;
locale?: string;
'target-folder-uid'?: string;
yes: boolean;
}

export interface BulkJobResult {
success: number;
failed: number;
Expand Down
Loading
Loading