diff --git a/.talismanrc b/.talismanrc index 7388b5665..db5ad57b0 100644 --- a/.talismanrc +++ b/.talismanrc @@ -5,8 +5,6 @@ fileignoreconfig: checksum: 9a892b5c4b5aac230fb5969e7f34afdac0b6f96208e64bf9d1195468c935c66c - filename: packages/contentstack-import/test/unit/utils/backup-handler.test.ts checksum: 69860727e9b3099d8e1e95db2af17fc8b161684f675477981d27877cd8e1b3bb - - filename: pnpm-lock.yaml - checksum: 794f45eb36d5f0639963694dc6d5e0c6a789584e9a7a3e54bd0d531275c29857 - filename: packages/contentstack-export/test/unit/export/module-exporter.test.ts checksum: 67b70c93ed679ccb2c61d0c277380676e33c91da8a423f948e81937e5d1d9479 - filename: packages/contentstack-export/test/unit/export/modules/marketplace-apps.test.ts @@ -17,4 +15,16 @@ fileignoreconfig: checksum: 29673e16f6b41fcec7fa236912e7f72b920ed4a3d9a66a89308b4a058b247f3e - filename: skills/testing/SKILL.md checksum: ee1c82f1bb51860cb26fb9f112a53df0127e316fcb22a094034024741251fa3c + - filename: pnpm-lock.yaml + checksum: 4861082320d7011b7adb96b6ca07f953f69e391d046a392e8f2b55cdd77b684f + - filename: packages/contentstack-branches/src/utils/create-branch.ts + checksum: d0613295ee26f7a77d026e40db0a4ab726fabd0a74965f729f1a66d1ef14768f + - filename: packages/contentstack-branches/README.md + checksum: ad32bd365db7f085cc2ea133d69748954606131ec6157a272a3471aea60011c2 + - filename: packages/contentstack-branches/src/branch/diff-handler.ts + checksum: 3cd4d26a2142cab7cbf2094c9251e028467d17d6a1ed6daf22f21975133805f1 + - filename: packages/contentstack-branches/src/commands/cm/branches/merge-status.ts + checksum: 6e5b959ddcc5ff68e03c066ea185fcf6c6e57b1819069730340af35aad8a93a8 + - filename: packages/contentstack-branches/src/branch/merge-handler.ts + checksum: 4fd8dba9b723733530b9ba12e81e1d3e5d60b73ac4c082defb10593f257bb133 version: '1.0' diff --git a/packages/contentstack-asset-management/src/constants/index.ts b/packages/contentstack-asset-management/src/constants/index.ts index 9d6bca636..e6e637765 100644 --- a/packages/contentstack-asset-management/src/constants/index.ts +++ b/packages/contentstack-asset-management/src/constants/index.ts @@ -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). @@ -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; /** @@ -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; diff --git a/packages/contentstack-asset-management/src/import-setup/base.ts b/packages/contentstack-asset-management/src/import-setup/base.ts new file mode 100644 index 000000000..111755b5b --- /dev/null +++ b/packages/contentstack-asset-management/src/import-setup/base.ts @@ -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; +} diff --git a/packages/contentstack-asset-management/src/import-setup/import-setup-asset-mappers.ts b/packages/contentstack-asset-management/src/import-setup/import-setup-asset-mappers.ts new file mode 100644 index 000000000..04e2b265c --- /dev/null +++ b/packages/contentstack-asset-management/src/import-setup/import-setup-asset-mappers.ts @@ -0,0 +1,193 @@ +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_FILES, PROCESS_NAMES, PROCESS_STATUS } from '../constants/index'; +import type { AssetManagementAPIConfig, ImportContext } 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> { + const adapter = new AssetManagementAdapter(apiConfig); + await adapter.init(); + const { spaces } = await adapter.listSpaces(); + const uids = new Set(); + 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 { + const p = this.params; + const { + contentDir, + mapperBaseDir, + assetManagementUrl, + org_uid, + source_stack, + apiKey, + host, + context, + } = p; + + const apiConcurrencyResolved = p.apiConcurrency ?? p.fetchConcurrency; + + if (!assetManagementUrl) { + log.info( + 'AM 2.0 export detected but assetManagementUrl is not configured in the region settings. Skipping AM 2.0 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 spacesDirSegment = p.spacesDirName ?? 'spaces'; + const spacesRootPath = resolve(contentDir, spacesDirSegment); + const mapperRoot = p.mapperRootDir ?? 'mapper'; + const mapperAssetsMod = p.mapperAssetsModuleDir ?? 'assets'; + const mapperDirPath = join(mapperBaseDir, mapperRoot, mapperAssetsMod); + const uidFile = p.mapperUidFileName ?? IMPORT_ASSETS_MAPPER_FILES.UID_MAPPING; + const urlFile = p.mapperUrlFileName ?? IMPORT_ASSETS_MAPPER_FILES.URL_MAPPING; + const spaceUidFile = p.mapperSpaceUidFileName ?? IMPORT_ASSETS_MAPPER_FILES.SPACE_UID_MAPPING; + const duplicateAssetMapperPath = join(mapperDirPath, IMPORT_ASSETS_MAPPER_FILES.DUPLICATE_ASSETS); + + const apiConfig: AssetManagementAPIConfig = { + baseURL: assetManagementUrl, + headers: { organization_uid: org_uid }, + context, + }; + + const importContext: ImportContext = { + spacesRootPath, + sourceApiKey: source_stack, + apiKey, + host, + org_uid, + context, + apiConcurrency: apiConcurrencyResolved, + spacesDirName: p.spacesDirName, + fieldsDir: p.fieldsDir, + assetTypesDir: p.assetTypesDir, + fieldsFileName: p.fieldsFileName, + assetTypesFileName: p.assetTypesFileName, + foldersFileName: p.foldersFileName, + assetsFileName: p.assetsFileName, + fieldsImportInvalidKeys: p.fieldsImportInvalidKeys, + assetTypesImportInvalidKeys: p.assetTypesImportInvalidKeys, + uploadAssetsConcurrency: p.uploadAssetsConcurrency, + importFoldersConcurrency: p.importFoldersConcurrency, + mapperRootDir: mapperRoot, + mapperAssetsModuleDir: mapperAssetsMod, + mapperUidFileName: uidFile, + mapperUrlFileName: urlFile, + mapperSpaceUidFileName: spaceUidFile, + }; + + 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 ${spacesDirSegment}/.`, + context, + ); + } + + const allUidMap: Record = {}; + const allUrlMap: Record = {}; + const spaceUidMap: Record = {}; + + 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, uidFile), JSON.stringify(allUidMap), 'utf8'); + await writeFile(join(mapperDirPath, urlFile), JSON.stringify(allUrlMap), 'utf8'); + await writeFile(join(mapperDirPath, spaceUidFile), 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', + }; + } + } +} diff --git a/packages/contentstack-asset-management/src/import-setup/index.ts b/packages/contentstack-asset-management/src/import-setup/index.ts new file mode 100644 index 000000000..44fd8a00a --- /dev/null +++ b/packages/contentstack-asset-management/src/import-setup/index.ts @@ -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'; diff --git a/packages/contentstack-asset-management/src/index.ts b/packages/contentstack-asset-management/src/index.ts index c66c638d0..b8b2252d6 100644 --- a/packages/contentstack-asset-management/src/index.ts +++ b/packages/contentstack-asset-management/src/index.ts @@ -3,3 +3,4 @@ export * from './types'; export * from './utils'; export * from './export'; export * from './import'; +export * from './import-setup'; diff --git a/packages/contentstack-asset-management/src/types/asset-management-export-flags.ts b/packages/contentstack-asset-management/src/types/asset-management-export-flags.ts new file mode 100644 index 000000000..dc9f4a13f --- /dev/null +++ b/packages/contentstack-asset-management/src/types/asset-management-export-flags.ts @@ -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; +}; diff --git a/packages/contentstack-asset-management/src/types/import-setup-asset-mapper.ts b/packages/contentstack-asset-management/src/types/import-setup-asset-mapper.ts new file mode 100644 index 000000000..cb9fb5c36 --- /dev/null +++ b/packages/contentstack-asset-management/src/types/import-setup-asset-mapper.ts @@ -0,0 +1,42 @@ +export type RunAssetMapperImportSetupParams = { + contentDir: string; + /** Parent of the assets mapper directory (typically import-setup `backupDir`). */ + mapperBaseDir: string; + assetManagementUrl?: string; + org_uid?: string; + source_stack?: string; + apiKey: string; + host: string; + context: Record; + /** + * Max parallel AM API calls for list/read paths. + * Takes precedence over {@link fetchConcurrency}. + */ + apiConcurrency?: number; + /** + * @deprecated Use {@link apiConcurrency}. + */ + fetchConcurrency?: number; + /** Relative dir under content dir for AM export root (default `spaces`). */ + spacesDirName?: string; + fieldsDir?: string; + assetTypesDir?: string; + fieldsFileName?: string; + assetTypesFileName?: string; + foldersFileName?: string; + assetsFileName?: string; + fieldsImportInvalidKeys?: string[]; + assetTypesImportInvalidKeys?: string[]; + mapperRootDir?: string; + mapperAssetsModuleDir?: string; + mapperUidFileName?: string; + mapperUrlFileName?: string; + mapperSpaceUidFileName?: string; + uploadAssetsConcurrency?: number; + importFoldersConcurrency?: number; +}; + +export type AssetMapperImportSetupResult = + | { kind: 'skipped'; reason: 'missing_asset_management_url' | 'missing_organization_uid' } + | { kind: 'success' } + | { kind: 'error'; errorMessage: string }; diff --git a/packages/contentstack-asset-management/src/types/index.ts b/packages/contentstack-asset-management/src/types/index.ts index c673e1893..32e4aedc6 100644 --- a/packages/contentstack-asset-management/src/types/index.ts +++ b/packages/contentstack-asset-management/src/types/index.ts @@ -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'; diff --git a/packages/contentstack-asset-management/src/utils/detect-asset-management-export.ts b/packages/contentstack-asset-management/src/utils/detect-asset-management-export.ts new file mode 100644 index 000000000..1b1836f25 --- /dev/null +++ b/packages/contentstack-asset-management/src/utils/detect-asset-management-export.ts @@ -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; + 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; +} diff --git a/packages/contentstack-asset-management/src/utils/index.ts b/packages/contentstack-asset-management/src/utils/index.ts index f84b94cb1..41d071e16 100644 --- a/packages/contentstack-asset-management/src/utils/index.ts +++ b/packages/contentstack-asset-management/src/utils/index.ts @@ -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'; diff --git a/packages/contentstack-asset-management/test/unit/import-setup/import-setup-asset-mappers.test.ts b/packages/contentstack-asset-management/test/unit/import-setup/import-setup-asset-mappers.test.ts new file mode 100644 index 000000000..936a6a344 --- /dev/null +++ b/packages/contentstack-asset-management/test/unit/import-setup/import-setup-asset-mappers.test.ts @@ -0,0 +1,258 @@ +import { expect } from 'chai'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { stub, restore } from 'sinon'; +import { AssetManagementAdapter } from '../../../src/utils/asset-management-api-adapter'; +import ImportAssets from '../../../src/import/assets'; +import ImportSetupAssetMappers from '../../../src/import-setup/import-setup-asset-mappers'; + +describe('ImportSetupAssetMappers', () => { + const tmpRoot = () => + path.join(os.tmpdir(), `am-import-setup-runner-${Date.now()}-${Math.random().toString(36).slice(2)}`); + + afterEach(() => { + restore(); + }); + + it('returns skipped when assetManagementUrl is missing', async () => { + const contentDir = tmpRoot(); + const backupDir = tmpRoot(); + fs.mkdirSync(contentDir, { recursive: true }); + + const result = await new ImportSetupAssetMappers({ + contentDir, + mapperBaseDir: backupDir, + org_uid: 'org-1', + apiKey: 'k', + host: 'https://api.example/v3', + context: {}, + }).start(); + + expect(result).to.deep.equal({ kind: 'skipped', reason: 'missing_asset_management_url' }); + fs.rmSync(contentDir, { recursive: true, force: true }); + }); + + it('returns skipped when org_uid is missing', async () => { + const contentDir = tmpRoot(); + const backupDir = tmpRoot(); + fs.mkdirSync(contentDir, { recursive: true }); + + const result = await new ImportSetupAssetMappers({ + contentDir, + mapperBaseDir: backupDir, + assetManagementUrl: 'https://am.example.com', + apiKey: 'k', + host: 'https://api.example/v3', + context: {}, + }).start(); + + expect(result).to.deep.equal({ kind: 'skipped', reason: 'missing_organization_uid' }); + fs.rmSync(contentDir, { recursive: true, force: true }); + }); + + it('does not require setParentProgressManager when skipped for missing URL', async () => { + const contentDir = tmpRoot(); + const backupDir = tmpRoot(); + fs.mkdirSync(contentDir, { recursive: true }); + + const mappers = new ImportSetupAssetMappers({ + contentDir, + mapperBaseDir: backupDir, + org_uid: 'org-1', + apiKey: 'k', + host: 'h', + context: {}, + }); + + const result = await mappers.start(); + + expect(result.kind).to.equal('skipped'); + fs.rmSync(contentDir, { recursive: true, force: true }); + }); + + it('writes mapper files when exported space exists in org', async () => { + const contentDir = tmpRoot(); + const backupDir = tmpRoot(); + fs.mkdirSync(path.join(contentDir, 'spaces', 'amspace01'), { recursive: true }); + fs.mkdirSync(backupDir, { recursive: true }); + + stub(AssetManagementAdapter.prototype, 'init').resolves(); + stub(AssetManagementAdapter.prototype, 'listSpaces').resolves({ + spaces: [{ uid: 'amspace01' }], + }); + stub(ImportAssets.prototype, 'buildIdentityMappersFromExport').resolves({ + uidMap: { bltAsset: 'bltAsset' }, + urlMap: { 'https://cdn.example/a.png': 'https://cdn.example/a.png' }, + }); + + const progress = { + addProcess: stub().returnsThis(), + startProcess: stub().returnsThis(), + updateStatus: stub().returnsThis(), + completeProcess: stub().returnsThis(), + tick: stub(), + }; + + const mappers = new ImportSetupAssetMappers({ + contentDir, + mapperBaseDir: backupDir, + assetManagementUrl: 'https://am.example.com', + org_uid: 'org-uid-test', + source_stack: 'source-api-key', + apiKey: 'test-api-key', + host: 'https://api.contentstack.io/v3', + context: {}, + fetchConcurrency: 2, + }); + mappers.setParentProgressManager(progress as any); + + const result = await mappers.start(); + + expect(result).to.deep.equal({ kind: 'success' }); + + const mapperDir = path.join(backupDir, 'mapper', 'assets'); + expect(JSON.parse(fs.readFileSync(path.join(mapperDir, 'uid-mapping.json'), 'utf8'))).to.deep.equal({ + bltAsset: 'bltAsset', + }); + expect(JSON.parse(fs.readFileSync(path.join(mapperDir, 'url-mapping.json'), 'utf8'))).to.deep.equal({ + 'https://cdn.example/a.png': 'https://cdn.example/a.png', + }); + expect(JSON.parse(fs.readFileSync(path.join(mapperDir, 'space-uid-mapping.json'), 'utf8'))).to.deep.equal({ + amspace01: 'amspace01', + }); + expect(fs.existsSync(path.join(mapperDir, 'duplicate-assets.json'))).to.be.true; + + fs.rmSync(contentDir, { recursive: true, force: true }); + fs.rmSync(backupDir, { recursive: true, force: true }); + }); + + it('skips merge when exported space is not in target org and writes empty uid map', async () => { + const contentDir = tmpRoot(); + const backupDir = tmpRoot(); + fs.mkdirSync(path.join(contentDir, 'spaces', 'amspace01'), { recursive: true }); + fs.mkdirSync(backupDir, { recursive: true }); + + stub(AssetManagementAdapter.prototype, 'init').resolves(); + stub(AssetManagementAdapter.prototype, 'listSpaces').resolves({ spaces: [] }); + + const buildStub = stub(ImportAssets.prototype, 'buildIdentityMappersFromExport').resolves({ + uidMap: {}, + urlMap: {}, + }); + + const mappers = new ImportSetupAssetMappers({ + contentDir, + mapperBaseDir: backupDir, + assetManagementUrl: 'https://am.example.com', + org_uid: 'org-uid-test', + apiKey: 'k', + host: 'https://api.contentstack.io/v3', + context: {}, + }); + mappers.setParentProgressManager({ + addProcess: stub().returnsThis(), + startProcess: stub().returnsThis(), + updateStatus: stub().returnsThis(), + completeProcess: stub().returnsThis(), + tick: stub(), + } as any); + + const result = await mappers.start(); + + expect(result.kind).to.equal('success'); + expect(buildStub.called).to.be.false; + expect( + JSON.parse(fs.readFileSync(path.join(backupDir, 'mapper', 'assets', 'uid-mapping.json'), 'utf8')), + ).to.deep.equal({}); + + fs.rmSync(contentDir, { recursive: true, force: true }); + fs.rmSync(backupDir, { recursive: true, force: true }); + }); + + it('respects custom spacesDirName, mapper path segments, and mapper file names', async () => { + const contentDir = tmpRoot(); + const backupDir = tmpRoot(); + fs.mkdirSync(path.join(contentDir, 'custom_spaces', 'amspace99'), { recursive: true }); + fs.mkdirSync(backupDir, { recursive: true }); + + stub(AssetManagementAdapter.prototype, 'init').resolves(); + stub(AssetManagementAdapter.prototype, 'listSpaces').resolves({ + spaces: [{ uid: 'amspace99' }], + }); + + const buildStub = stub(ImportAssets.prototype, 'buildIdentityMappersFromExport').callsFake(async function mock( + this: ImportAssets, + spaceDir: string, + ) { + expect(spaceDir).to.equal(path.join(contentDir, 'custom_spaces', 'amspace99')); + expect((this as any).importContext.assetsFileName).to.equal('custom-assets.json'); + expect((this as any).importContext.apiConcurrency).to.equal(7); + return { uidMap: { a: 'a' }, urlMap: { u: 'u' } }; + }); + + const mappers = new ImportSetupAssetMappers({ + contentDir, + mapperBaseDir: backupDir, + assetManagementUrl: 'https://am.example.com', + org_uid: 'org-uid-test', + apiKey: 'k', + host: 'https://api.contentstack.io/v3', + context: {}, + spacesDirName: 'custom_spaces', + mapperRootDir: 'mappers_root', + mapperAssetsModuleDir: 'am_assets', + mapperUidFileName: 'uid-custom.json', + mapperUrlFileName: 'url-custom.json', + mapperSpaceUidFileName: 'space-custom.json', + assetsFileName: 'custom-assets.json', + apiConcurrency: 7, + }); + + const result = await mappers.start(); + + expect(result.kind).to.equal('success'); + expect(buildStub.calledOnce).to.be.true; + + const mapperDir = path.join(backupDir, 'mappers_root', 'am_assets'); + expect(JSON.parse(fs.readFileSync(path.join(mapperDir, 'uid-custom.json'), 'utf8'))).to.deep.equal({ a: 'a' }); + expect(JSON.parse(fs.readFileSync(path.join(mapperDir, 'url-custom.json'), 'utf8'))).to.deep.equal({ u: 'u' }); + expect(JSON.parse(fs.readFileSync(path.join(mapperDir, 'space-custom.json'), 'utf8'))).to.deep.equal({ + amspace99: 'amspace99', + }); + + fs.rmSync(contentDir, { recursive: true, force: true }); + fs.rmSync(backupDir, { recursive: true, force: true }); + }); + + it('uses fetchConcurrency when apiConcurrency is omitted', async () => { + const contentDir = tmpRoot(); + const backupDir = tmpRoot(); + fs.mkdirSync(path.join(contentDir, 'spaces', 'amX'), { recursive: true }); + fs.mkdirSync(backupDir, { recursive: true }); + + stub(AssetManagementAdapter.prototype, 'init').resolves(); + stub(AssetManagementAdapter.prototype, 'listSpaces').resolves({ spaces: [{ uid: 'amX' }] }); + + stub(ImportAssets.prototype, 'buildIdentityMappersFromExport').callsFake(async function fetchConcCheck( + this: ImportAssets, + ) { + expect((this as any).importContext.apiConcurrency).to.equal(3); + return { uidMap: {}, urlMap: {} }; + }); + + await new ImportSetupAssetMappers({ + contentDir, + mapperBaseDir: backupDir, + assetManagementUrl: 'https://am.example.com', + org_uid: 'org', + apiKey: 'k', + host: 'https://api.contentstack.io/v3', + context: {}, + fetchConcurrency: 3, + }).start(); + + fs.rmSync(contentDir, { recursive: true, force: true }); + fs.rmSync(backupDir, { recursive: true, force: true }); + }); +}); diff --git a/packages/contentstack-asset-management/test/unit/utils/detect-asset-management-export.test.ts b/packages/contentstack-asset-management/test/unit/utils/detect-asset-management-export.test.ts new file mode 100644 index 000000000..6f86af75d --- /dev/null +++ b/packages/contentstack-asset-management/test/unit/utils/detect-asset-management-export.test.ts @@ -0,0 +1,66 @@ +import { expect } from 'chai'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { stub, restore } from 'sinon'; +import * as utilities from '@contentstack/cli-utilities'; + +import { detectAssetManagementExportFromContentDir } from '../../../src/utils/detect-asset-management-export'; + +describe('detectAssetManagementExportFromContentDir', () => { + const tmpRoot = path.join(os.tmpdir(), `am-detect-test-${Date.now()}`); + + afterEach(() => { + restore(); + if (fs.existsSync(tmpRoot)) { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } + }); + + it('returns disabled when spaces directory is missing', () => { + fs.mkdirSync(tmpRoot, { recursive: true }); + fs.mkdirSync(path.join(tmpRoot, 'stack'), { recursive: true }); + fs.writeFileSync(path.join(tmpRoot, 'stack', 'settings.json'), JSON.stringify({ am_v2: true })); + + const flags = detectAssetManagementExportFromContentDir(tmpRoot); + expect(flags.assetManagementEnabled).to.equal(false); + }); + + it('returns disabled when stack settings are missing', () => { + fs.mkdirSync(path.join(tmpRoot, 'spaces'), { recursive: true }); + + const flags = detectAssetManagementExportFromContentDir(tmpRoot); + expect(flags.assetManagementEnabled).to.equal(false); + }); + + it('returns disabled when linked asset management is not set in stack settings', () => { + fs.mkdirSync(path.join(tmpRoot, 'spaces'), { recursive: true }); + fs.mkdirSync(path.join(tmpRoot, 'stack'), { recursive: true }); + fs.writeFileSync(path.join(tmpRoot, 'stack', 'settings.json'), JSON.stringify({ other: true })); + + const flags = detectAssetManagementExportFromContentDir(tmpRoot); + expect(flags.assetManagementEnabled).to.equal(false); + }); + + it('enables asset management export and reads region URL and branches source stack when layout matches', () => { + fs.mkdirSync(path.join(tmpRoot, 'spaces'), { recursive: true }); + fs.mkdirSync(path.join(tmpRoot, 'stack'), { recursive: true }); + fs.writeFileSync( + path.join(tmpRoot, 'stack', 'settings.json'), + JSON.stringify({ am_v2: { linked_workspaces: [] } }), + ); + fs.writeFileSync( + path.join(tmpRoot, 'branches.json'), + JSON.stringify([{ stackHeaders: { api_key: 'source-stack-key' } }]), + ); + + stub(utilities.configHandler, 'get').withArgs('region').returns({ + assetManagementUrl: 'https://am.example.com', + }); + + const flags = detectAssetManagementExportFromContentDir(tmpRoot); + expect(flags.assetManagementEnabled).to.equal(true); + expect(flags.assetManagementUrl).to.equal('https://am.example.com'); + expect(flags.source_stack).to.equal('source-stack-key'); + }); +}); diff --git a/packages/contentstack-audit/package.json b/packages/contentstack-audit/package.json index af49b2d3b..fc77b7fd6 100644 --- a/packages/contentstack-audit/package.json +++ b/packages/contentstack-audit/package.json @@ -27,10 +27,10 @@ "fs-extra": "^11.3.0", "lodash": "^4.18.1", "uuid": "^9.0.1", - "winston": "^3.17.0" + "winston": "^3.19.0" }, "devDependencies": { - "@oclif/test": "^4.1.13", + "@oclif/test": "^4.1.18", "@types/chai": "^4.3.20", "@types/fs-extra": "^11.0.4", "@types/mocha": "^10.0.10", diff --git a/packages/contentstack-bootstrap/package.json b/packages/contentstack-bootstrap/package.json index 1e4c5805a..edd00fde0 100644 --- a/packages/contentstack-bootstrap/package.json +++ b/packages/contentstack-bootstrap/package.json @@ -27,7 +27,7 @@ "tar": "^7.5.11" }, "devDependencies": { - "@oclif/test": "^4.1.13", + "@oclif/test": "^4.1.18", "@types/inquirer": "^9.0.8", "@types/mkdirp": "^1.0.2", "@types/node": "^18.11.9", diff --git a/packages/contentstack-branches/README.md b/packages/contentstack-branches/README.md index 43c780857..2b6cb6e51 100755 --- a/packages/contentstack-branches/README.md +++ b/packages/contentstack-branches/README.md @@ -53,6 +53,7 @@ USAGE * [`csdx cm:branches:delete [-uid ] [-k ]`](#csdx-cmbranchesdelete--uid-value--k-value) * [`csdx cm:branches:diff [--base-branch ] [--compare-branch ] [-k ][--module ] [--format ] [--csv-path ]`](#csdx-cmbranchesdiff---base-branch-value---compare-branch-value--k-value--module-value---format-value---csv-path-value) * [`csdx cm:branches:merge [-k ][--compare-branch ] [--no-revert] [--export-summary-path ] [--use-merge-summary ] [--comment ] [--base-branch ]`](#csdx-cmbranchesmerge--k-value--compare-branch-value---no-revert---export-summary-path-value---use-merge-summary-value---comment-value---base-branch-value) +* [`csdx cm:branches:merge-status -k --merge-uid `](#csdx-cmbranchesmerge-status--k-value---merge-uid-value) ## `csdx cm:branches` @@ -230,4 +231,27 @@ EXAMPLES ``` _See code: [src/commands/cm/branches/merge.ts](https://github.com/contentstack/cli/blob/main/packages/contentstack-export/src/commands/cm/branches/merge.ts)_ + +## `csdx cm:branches:merge-status -k --merge-uid ` + +Check the status of a branch merge job + +``` +USAGE + $ csdx cm:branches:merge-status -k --merge-uid + +FLAGS + -k, --stack-api-key= (required) Provide your stack API key. + --merge-uid= (required) Merge job UID to check status for. + +DESCRIPTION + Check the status of a branch merge job + +EXAMPLES + $ csdx cm:branches:merge-status -k bltxxxxxxxx --merge-uid merge_abc123 + + $ csdx cm:branches:merge-status --stack-api-key bltxxxxxxxx --merge-uid merge_abc123 +``` + +_See code: [src/commands/cm/branches/merge-status.ts](https://github.com/contentstack/cli/blob/main/packages/contentstack-export/src/commands/cm/branches/merge-status.ts)_ diff --git a/packages/contentstack-branches/package.json b/packages/contentstack-branches/package.json index 340210fd9..f88b604f9 100644 --- a/packages/contentstack-branches/package.json +++ b/packages/contentstack-branches/package.json @@ -75,6 +75,7 @@ "cm:branches:delete": "BRDEL", "cm:branches:diff": "BRDIF", "cm:branches:merge": "BRMRG", + "cm:branches:merge-status": "BRMST", "cm:branches": "BRLS" } }, diff --git a/packages/contentstack-branches/src/branch/diff-handler.ts b/packages/contentstack-branches/src/branch/diff-handler.ts index 72aeaf0f2..4ada1bc48 100644 --- a/packages/contentstack-branches/src/branch/diff-handler.ts +++ b/packages/contentstack-branches/src/branch/diff-handler.ts @@ -1,20 +1,21 @@ -import startCase from 'lodash/startCase'; -import camelCase from 'lodash/camelCase'; import { cliux } from '@contentstack/cli-utilities'; +import camelCase from 'lodash/camelCase'; +import startCase from 'lodash/startCase'; + +import { BranchDiffPayload, BranchOptions } from '../interfaces'; import { getbranchConfig } from '../utils'; -import { BranchOptions, BranchDiffPayload } from '../interfaces'; -import { askBaseBranch, askCompareBranch, askStackAPIKey, selectModule } from '../utils/interactive'; import { fetchBranchesDiff, - parseSummary, - printSummary, + filterBranchDiffDataByModule, parseCompactText, - printCompactTextView, + parseSummary, parseVerbose, + printCompactTextView, + printSummary, printVerboseTextView, - filterBranchDiffDataByModule, } from '../utils/branch-diff-utility'; import { exportCSVReport } from '../utils/csv-utility'; +import { askBaseBranch, askCompareBranch, askStackAPIKey, selectModule } from '../utils/interactive'; export default class BranchDiffHandler { private options: BranchOptions; @@ -23,46 +24,36 @@ export default class BranchDiffHandler { this.options = params; } - async run(): Promise { - await this.validateMandatoryFlags(); - await this.initBranchDiffUtility(); - } - /** - * @methods validateMandatoryFlags - validate flags and prompt to select required flags - * @returns {*} {Promise} + * @methods displayBranchDiffTextAndVerbose - to show branch differences in compactText or detailText format + * @returns {*} {void} * @memberof BranchDiff */ - async validateMandatoryFlags(): Promise { - let baseBranch: string; - if (!this.options.stackAPIKey) { - this.options.stackAPIKey = await askStackAPIKey(); - } - - if (!this.options.baseBranch) { - baseBranch = getbranchConfig(this.options.stackAPIKey); - if (!baseBranch) { - this.options.baseBranch = await askBaseBranch(); - } else { - this.options.baseBranch = baseBranch; - } - } - - if (!this.options.compareBranch) { - this.options.compareBranch = await askCompareBranch(); - } - - if (!this.options.module) { - this.options.module = await selectModule(); - } + async displayBranchDiffTextAndVerbose(branchDiffData: any[], payload: BranchDiffPayload): Promise { + const spinner = cliux.loaderV2('Loading branch differences...'); + if (this.options.format === 'compact-text') { + const branchTextRes = parseCompactText(branchDiffData); + cliux.loaderV2('', spinner); + printCompactTextView(branchTextRes); + } else if (this.options.format === 'detailed-text') { + const verboseRes = await parseVerbose(branchDiffData, payload); + cliux.loaderV2('', spinner); + printVerboseTextView(verboseRes); - if (this.options.format === 'detailed-text' && !this.options.csvPath) { - this.options.csvPath = process.cwd(); + exportCSVReport(payload.module, verboseRes, this.options.csvPath); } + } - if(baseBranch){ - cliux.print(`\nBase branch: ${baseBranch}`, { color: 'grey' }); - } + /** + * @methods displaySummary - show branches summary on CLI + * @returns {*} {void} + * @memberof BranchDiff + */ + displaySummary(branchDiffData: any[], module: string): void { + cliux.print(' '); + cliux.print(`${startCase(camelCase(module))} Summary:`, { color: 'yellow' }); + const diffSummary = parseSummary(branchDiffData, this.options.baseBranch, this.options.compareBranch); + printSummary(diffSummary); } /** @@ -73,11 +64,11 @@ export default class BranchDiffHandler { async initBranchDiffUtility(): Promise { const spinner = cliux.loaderV2('Loading branch differences...'); const payload: BranchDiffPayload = { - module: '', apiKey: this.options.stackAPIKey, baseBranch: this.options.baseBranch, compareBranch: this.options.compareBranch, - host: this.options.host + host: this.options.host, + module: '' }; if (this.options.module === 'content-types') { @@ -91,7 +82,7 @@ export default class BranchDiffHandler { cliux.loaderV2('', spinner); if(this.options.module === 'all'){ - for (let module in diffData) { + for (const module in diffData) { const branchDiff = diffData[module]; payload.module = module; this.displaySummary(branchDiff, module); @@ -104,35 +95,45 @@ export default class BranchDiffHandler { } } - /** - * @methods displaySummary - show branches summary on CLI - * @returns {*} {void} - * @memberof BranchDiff - */ - displaySummary(branchDiffData: any[], module: string): void { - cliux.print(' '); - cliux.print(`${startCase(camelCase(module))} Summary:`, { color: 'yellow' }); - const diffSummary = parseSummary(branchDiffData, this.options.baseBranch, this.options.compareBranch); - printSummary(diffSummary); + async run(): Promise { + await this.validateMandatoryFlags(); + await this.initBranchDiffUtility(); } /** - * @methods displayBranchDiffTextAndVerbose - to show branch differences in compactText or detailText format - * @returns {*} {void} + * @methods validateMandatoryFlags - validate flags and prompt to select required flags + * @returns {*} {Promise} * @memberof BranchDiff */ - async displayBranchDiffTextAndVerbose(branchDiffData: any[], payload: BranchDiffPayload): Promise { - const spinner = cliux.loaderV2('Loading branch differences...'); - if (this.options.format === 'compact-text') { - const branchTextRes = parseCompactText(branchDiffData); - cliux.loaderV2('', spinner); - printCompactTextView(branchTextRes); - } else if (this.options.format === 'detailed-text') { - const verboseRes = await parseVerbose(branchDiffData, payload); - cliux.loaderV2('', spinner); - printVerboseTextView(verboseRes); + async validateMandatoryFlags(): Promise { + let baseBranch: string; + if (!this.options.stackAPIKey) { + this.options.stackAPIKey = await askStackAPIKey(); + } - exportCSVReport(payload.module, verboseRes, this.options.csvPath); + if (!this.options.baseBranch) { + baseBranch = getbranchConfig(this.options.stackAPIKey); + if (!baseBranch) { + this.options.baseBranch = await askBaseBranch(); + } else { + this.options.baseBranch = baseBranch; + } + } + + if (!this.options.compareBranch) { + this.options.compareBranch = await askCompareBranch(); + } + + if (!this.options.module) { + this.options.module = await selectModule(); + } + + if (this.options.format === 'detailed-text' && !this.options.csvPath) { + this.options.csvPath = process.cwd(); + } + + if(baseBranch){ + cliux.print(`\nBase branch: ${baseBranch}`, { color: 'grey' }); } } } diff --git a/packages/contentstack-branches/src/branch/index.ts b/packages/contentstack-branches/src/branch/index.ts index 9da452fc1..032e22966 100644 --- a/packages/contentstack-branches/src/branch/index.ts +++ b/packages/contentstack-branches/src/branch/index.ts @@ -2,5 +2,5 @@ * Business logics can be written inside this directory */ -export { default as MergeHandler } from './merge-handler'; export { default as BranchDiffHandler } from './diff-handler'; +export { default as MergeHandler } from './merge-handler'; diff --git a/packages/contentstack-branches/src/branch/merge-handler.ts b/packages/contentstack-branches/src/branch/merge-handler.ts index 60b371e28..aa97d7a07 100644 --- a/packages/contentstack-branches/src/branch/merge-handler.ts +++ b/packages/contentstack-branches/src/branch/merge-handler.ts @@ -1,38 +1,38 @@ +import { cliux, getChalk } from '@contentstack/cli-utilities'; +import forEach from 'lodash/forEach'; import os from 'os'; import path from 'path'; -import forEach from 'lodash/forEach'; -import { cliux } from '@contentstack/cli-utilities'; -import { getChalk } from '@contentstack/cli-utilities'; + import { MergeInputOptions, MergeSummary } from '../interfaces'; import { - selectMergeStrategy, - selectMergeStrategySubOptions, - selectMergeExecution, - prepareMergeRequestPayload, - displayMergeSummary, askExportMergeSummaryPath, askMergeComment, - writeFile, + displayMergeSummary, executeMerge, generateMergeScripts, - selectCustomPreferences, - selectContentMergePreference, + prepareMergeRequestPayload, selectContentMergeCustomPreferences, + selectContentMergePreference, + selectCustomPreferences, + selectMergeExecution, + selectMergeStrategy, + selectMergeStrategySubOptions, + writeFile, } from '../utils'; export default class MergeHandler { - private strategy: string; - private strategySubOption?: string; private branchCompareData: any; - private mergeSettings: any; - private executeOption?: string; private displayFormat: string; + private enableEntryExp: boolean; + private executeOption?: string; private exportSummaryPath: string; + private host: string; + private mergeSettings: any; private mergeSummary: MergeSummary; private stackAPIKey: string; + private strategy: string; + private strategySubOption?: string; private userInputs: MergeInputOptions; - private host: string; - private enableEntryExp: boolean; constructor(options: MergeInputOptions) { this.stackAPIKey = options.stackAPIKey; @@ -45,8 +45,8 @@ export default class MergeHandler { this.mergeSummary = options.mergeSummary; this.userInputs = options; this.mergeSettings = { - baseBranch: options.baseBranch, // UID of the base branch, where the changes will be merged into - compareBranch: options.compareBranch, // UID of the branch to merge + baseBranch: options.baseBranch, + compareBranch: options.compareBranch, mergeComment: options.mergeComment, mergeContent: {}, noRevert: options.noRevert, @@ -55,29 +55,67 @@ export default class MergeHandler { this.enableEntryExp = options.enableEntryExp; } - async start() { - if (this.mergeSummary) { - this.loadMergeSettings(); - await this.displayMergeSummary(); - return await this.executeMerge(this.mergeSummary.requestPayload); - } - await this.collectMergeSettings(); - const mergePayload = prepareMergeRequestPayload(this.mergeSettings); - if (this.executeOption === 'execute') { - await this.exportSummary(mergePayload); - await this.executeMerge(mergePayload); - } else if (this.executeOption === 'export') { - await this.exportSummary(mergePayload); - } else if (this.executeOption === 'merge_n_scripts') { - this.enableEntryExp = true; - await this.executeMerge(mergePayload); - } else if (this.executeOption === 'summary_n_scripts') { - this.enableEntryExp = true; - await this.exportSummary(mergePayload); - } else { - await this.exportSummary(mergePayload); - await this.executeMerge(mergePayload); + /** + * Checks whether the selection of modules in the compare branch data is empty. + * + * This method evaluates the branch compare data and determines if there are any changes + * (added, modified, or deleted) in the modules based on the merge strategy defined in the + * merge settings. It categorizes the status of each module as either existing and empty or + * not empty. + * + * @returns An object containing: + * - `allEmpty`: A boolean indicating whether all modules are either non-existent or empty. + * - `moduleStatus`: A record mapping module types (`contentType` and `globalField`) to their + * respective statuses, which include: + * - `exists`: A boolean indicating whether the module exists in the branch comparison data. + * - `empty`: A boolean indicating whether the module has no changes (added, modified, or deleted). + */ + checkEmptySelection(): { + allEmpty: boolean; + moduleStatus: Record; + } { + const strategy = this.mergeSettings.strategy; + + const useMergeContent = new Set(['custom_preferences', 'ignore']); + const modifiedOnlyStrategies = new Set(['merge_modified_only_prefer_base', 'merge_modified_only_prefer_compare']); + const addedOnlyStrategies = new Set(['merge_new_only']); + + const moduleStatus: Record = { + contentType: { empty: true, exists: false }, + globalField: { empty: true, exists: false }, + }; + + for (const module in this.branchCompareData) { + const content = useMergeContent.has(strategy) + ? this.mergeSettings.mergeContent[module] + : this.branchCompareData[module]; + + if (!content) continue; + + const isGlobalField = module === 'global_fields'; + const type = isGlobalField ? 'globalField' : 'contentType'; + moduleStatus[type].exists = true; + + let hasChanges = false; + if (modifiedOnlyStrategies.has(strategy)) { + hasChanges = Array.isArray(content.modified) && content.modified.length > 0; + } else if (addedOnlyStrategies.has(strategy)) { + hasChanges = Array.isArray(content.added) && content.added.length > 0; + } else { + hasChanges = + (Array.isArray(content.modified) && content.modified.length > 0) || + (Array.isArray(content.added) && content.added.length > 0) || + (Array.isArray(content.deleted) && content.deleted.length > 0); + } + + if (hasChanges) { + moduleStatus[type].empty = false; + } } + + const allEmpty = Object.values(moduleStatus).every((status) => !status.exists || status.empty); + + return { allEmpty, moduleStatus }; } async collectMergeSettings() { @@ -101,11 +139,11 @@ export default class MergeHandler { } if (this.strategy === 'custom_preferences') { this.mergeSettings.itemMergeStrategies = []; - for (let module in this.branchCompareData) { + for (const module in this.branchCompareData) { this.mergeSettings.mergeContent[module] = { added: [], - modified: [], deleted: [], + modified: [], }; const selectedItems = await selectCustomPreferences(module, this.branchCompareData[module]); if (selectedItems?.length) { @@ -138,22 +176,22 @@ export default class MergeHandler { const { allEmpty, moduleStatus } = this.checkEmptySelection(); const strategyName = this.mergeSettings.strategy; - + if (allEmpty) { cliux.print(getChalk().red(`No items selected according to the '${strategyName}' strategy.`)); process.exit(1); } - - for (const [type, { exists, empty }] of Object.entries(moduleStatus)) { + + for (const [type, { empty, exists }] of Object.entries(moduleStatus)) { if (exists && empty) { const readable = type === 'contentType' ? 'Content Types' : 'Global fields'; - cliux.print('\n') + cliux.print('\n'); cliux.print(getChalk().yellow(`Note: No ${readable} selected according to the '${strategyName}' strategy.`)); } } - + this.displayMergeSummary(); - + if (!this.executeOption) { const executionResponse = await selectMergeExecution(); if (executionResponse === 'previous') { @@ -171,160 +209,22 @@ export default class MergeHandler { } } - /** - * Checks whether the selection of modules in the compare branch data is empty. - * - * This method evaluates the branch compare data and determines if there are any changes - * (added, modified, or deleted) in the modules based on the merge strategy defined in the - * merge settings. It categorizes the status of each module as either existing and empty or - * not empty. - * - * @returns An object containing: - * - `allEmpty`: A boolean indicating whether all modules are either non-existent or empty. - * - `moduleStatus`: A record mapping module types (`contentType` and `globalField`) to their - * respective statuses, which include: - * - `exists`: A boolean indicating whether the module exists in the branch comparison data. - * - `empty`: A boolean indicating whether the module has no changes (added, modified, or deleted). - */ - checkEmptySelection(): { - allEmpty: boolean; - moduleStatus: Record; - } { - const strategy = this.mergeSettings.strategy; - - const useMergeContent = new Set(['custom_preferences', 'ignore']); - const modifiedOnlyStrategies = new Set(['merge_modified_only_prefer_base', 'merge_modified_only_prefer_compare']); - const addedOnlyStrategies = new Set(['merge_new_only']); - - const moduleStatus: Record = { - contentType: { exists: false, empty: true }, - globalField: { exists: false, empty: true }, - }; - - for (const module in this.branchCompareData) { - const content = useMergeContent.has(strategy) - ? this.mergeSettings.mergeContent[module] - : this.branchCompareData[module]; - - if (!content) continue; - - const isGlobalField = module === 'global_fields'; - const type = isGlobalField ? 'globalField' : 'contentType'; - moduleStatus[type].exists = true; - - let hasChanges = false; - if (modifiedOnlyStrategies.has(strategy)) { - hasChanges = Array.isArray(content.modified) && content.modified.length > 0; - } else if (addedOnlyStrategies.has(strategy)) { - hasChanges = Array.isArray(content.added) && content.added.length > 0; - } else { - hasChanges = - (Array.isArray(content.modified) && content.modified.length > 0) || - (Array.isArray(content.added) && content.added.length > 0) || - (Array.isArray(content.deleted) && content.deleted.length > 0); - } - - if (hasChanges) { - moduleStatus[type].empty = false; - } - } - - const allEmpty = Object.values(moduleStatus).every( - (status) => !status.exists || status.empty - ); - - return { allEmpty, moduleStatus }; - } - displayMergeSummary() { if (this.mergeSettings.strategy !== 'ignore') { - for (let module in this.branchCompareData) { + for (const module in this.branchCompareData) { this.mergeSettings.mergeContent[module] = {}; this.filterBranchCompareData(module, this.branchCompareData[module]); } } displayMergeSummary({ - format: this.displayFormat, compareData: this.mergeSettings.mergeContent, + format: this.displayFormat, }); } - filterBranchCompareData(module, moduleBranchCompareData) { - const { strategy, mergeContent } = this.mergeSettings; - switch (strategy) { - case 'merge_prefer_base': - mergeContent[module].added = moduleBranchCompareData.added; - mergeContent[module].modified = moduleBranchCompareData.modified; - mergeContent[module].deleted = moduleBranchCompareData.deleted; - break; - case 'merge_prefer_compare': - mergeContent[module].added = moduleBranchCompareData.added; - mergeContent[module].modified = moduleBranchCompareData.modified; - mergeContent[module].deleted = moduleBranchCompareData.deleted; - break; - case 'merge_new_only': - mergeContent[module].added = moduleBranchCompareData.added; - break; - case 'merge_modified_only_prefer_base': - mergeContent[module].modified = moduleBranchCompareData.modified; - break; - case 'merge_modified_only_prefer_compare': - mergeContent[module].modified = moduleBranchCompareData.modified; - break; - case 'merge_modified_only_prefer_compare': - mergeContent[module].modified = moduleBranchCompareData.modified; - break; - case 'overwrite_with_compare': - mergeContent[module].added = moduleBranchCompareData.added; - mergeContent[module].modified = moduleBranchCompareData.modified; - mergeContent[module].deleted = moduleBranchCompareData.deleted; - break; - default: - cliux.error(`Error: Invalid strategy '${strategy}'`); - process.exit(1); - } - } - - async exportSummary(mergePayload) { - if (!this.exportSummaryPath) { - this.exportSummaryPath = await askExportMergeSummaryPath(); - } - const summary: MergeSummary = { - requestPayload: mergePayload, - }; - await writeFile(path.join(this.exportSummaryPath, 'merge-summary.json'), summary); - cliux.success('Exported the summary successfully'); - - if (this.enableEntryExp) { - this.executeEntryExpFlow(this.stackAPIKey, mergePayload); - } - } - - async executeMerge(mergePayload) { - let spinner; - try { - if (!this.mergeSettings.mergeComment) { - this.mergeSettings.mergeComment = await askMergeComment(); - mergePayload.merge_comment = this.mergeSettings.mergeComment; - } - - spinner = cliux.loaderV2('Merging the changes...'); - const mergeResponse = await executeMerge(this.stackAPIKey, mergePayload, this.host); - cliux.loaderV2('', spinner); - cliux.success(`Merged the changes successfully. Merge UID: ${mergeResponse.uid}`); - - if (this.enableEntryExp) { - this.executeEntryExpFlow(mergeResponse.uid, mergePayload); - } - } catch (error) { - cliux.loaderV2('', spinner); - cliux.error('Failed to merge the changes', error.message || error); - } - } - async executeEntryExpFlow(mergeJobUID: string, mergePayload) { const { mergeContent } = this.mergeSettings; - let mergePreference = await selectContentMergePreference(); + const mergePreference = await selectContentMergePreference(); const updateEntryMergeStrategy = (items, mergeStrategy) => { items && @@ -334,10 +234,10 @@ export default class MergeHandler { }; const mergePreferencesMap = { + ask_preference: 'custom', + existing: 'merge_existing', existing_new: 'merge_existing_new', new: 'merge_new', - existing: 'merge_existing', - ask_preference: 'custom', }; const selectedMergePreference = mergePreferencesMap[mergePreference]; @@ -346,8 +246,8 @@ export default class MergeHandler { const selectedMergeItems = await selectContentMergeCustomPreferences(mergeContent.content_types); mergeContent.content_types = { added: [], - modified: [], deleted: [], + modified: [], }; selectedMergeItems?.forEach((item) => { @@ -362,7 +262,7 @@ export default class MergeHandler { process.exit(1); } - let scriptFolderPath = generateMergeScripts(mergeContent.content_types, mergeJobUID); + const scriptFolderPath = generateMergeScripts(mergeContent.content_types, mergeJobUID); if (scriptFolderPath !== undefined) { cliux.success(`\nSuccess! We have generated entry migration files in the folder ${scriptFolderPath}`); @@ -385,6 +285,102 @@ export default class MergeHandler { } } + /** + * Executes the merge operation with improved polling. + * Handles polling timeout gracefully by returning merge UID for later status checking. + * If enableEntryExp is true and merge is complete, generates scripts. + * + * @param mergePayload - Merge request payload with branch info + */ + async executeMerge(mergePayload) { + let spinner; + try { + if (!this.mergeSettings.mergeComment) { + this.mergeSettings.mergeComment = await askMergeComment(); + mergePayload.merge_comment = this.mergeSettings.mergeComment; + } + + spinner = cliux.loaderV2('Merging the changes...'); + const mergeResponse = await executeMerge(this.stackAPIKey, mergePayload, this.host); + cliux.loaderV2('', spinner); + + if (mergeResponse.merge_details?.status === 'complete') { + cliux.success(`Merged the changes successfully. Merge UID: ${mergeResponse.uid}`); + + if (this.enableEntryExp) { + await this.executeEntryExpFlow(mergeResponse.uid, mergePayload); + } + } else if (mergeResponse.pollingTimeout) { + cliux.success(`Merge job initiated successfully. Merge UID: ${mergeResponse.uid}`); + cliux.print('\n⏱ The merge is still processing in the background...', { color: 'yellow' }); + cliux.print('\nCheck status later using:', { color: 'grey' }); + cliux.print(` csdx cm:branches:merge-status -k ${this.stackAPIKey} --merge-uid ${mergeResponse.uid}`, { + color: 'cyan', + }); + } + } catch (error) { + cliux.loaderV2('', spinner); + cliux.error('Failed to merge the changes', error.message || error); + } + } + + async exportSummary(mergePayload) { + if (!this.exportSummaryPath) { + this.exportSummaryPath = await askExportMergeSummaryPath(); + } + const summary: MergeSummary = { + requestPayload: mergePayload, + }; + await writeFile(path.join(this.exportSummaryPath, 'merge-summary.json'), summary); + cliux.success('Exported the summary successfully'); + + if (this.enableEntryExp) { + await this.executeEntryExpFlow(this.stackAPIKey, mergePayload); + } + } + + filterBranchCompareData(module, moduleBranchCompareData) { + const { mergeContent, strategy } = this.mergeSettings; + switch (strategy) { + case 'merge_prefer_base': + mergeContent[module].added = moduleBranchCompareData.added; + mergeContent[module].modified = moduleBranchCompareData.modified; + mergeContent[module].deleted = moduleBranchCompareData.deleted; + break; + case 'merge_prefer_compare': + mergeContent[module].added = moduleBranchCompareData.added; + mergeContent[module].modified = moduleBranchCompareData.modified; + mergeContent[module].deleted = moduleBranchCompareData.deleted; + break; + case 'merge_new_only': + mergeContent[module].added = moduleBranchCompareData.added; + break; + case 'merge_modified_only_prefer_base': + mergeContent[module].modified = moduleBranchCompareData.modified; + break; + case 'merge_modified_only_prefer_compare': + mergeContent[module].modified = moduleBranchCompareData.modified; + break; + case 'overwrite_with_compare': + mergeContent[module].added = moduleBranchCompareData.added; + mergeContent[module].modified = moduleBranchCompareData.modified; + mergeContent[module].deleted = moduleBranchCompareData.deleted; + break; + default: + cliux.error(`Error: Invalid strategy '${strategy}'`); + process.exit(1); + } + } + + loadMergeSettings() { + this.mergeSettings.baseBranch = this.mergeSummary.requestPayload.base_branch; + this.mergeSettings.compareBranch = this.mergeSummary.requestPayload.compare_branch; + this.mergeSettings.strategy = this.mergeSummary.requestPayload.default_merge_strategy; + this.mergeSettings.itemMergeStrategies = this.mergeSummary.requestPayload.item_merge_strategies; + this.mergeSettings.noRevert = this.mergeSummary.requestPayload.no_revert; + this.mergeSettings.mergeComment = this.mergeSummary.requestPayload.merge_comment; + } + async restartMergeProcess() { if (!this.userInputs.strategy) { this.strategy = null; @@ -404,12 +400,29 @@ export default class MergeHandler { await this.start(); } - loadMergeSettings() { - this.mergeSettings.baseBranch = this.mergeSummary.requestPayload.base_branch; - this.mergeSettings.compareBranch = this.mergeSummary.requestPayload.compare_branch; - this.mergeSettings.strategy = this.mergeSummary.requestPayload.default_merge_strategy; - this.mergeSettings.itemMergeStrategies = this.mergeSummary.requestPayload.item_merge_strategies; - this.mergeSettings.noRevert = this.mergeSummary.requestPayload.no_revert; - this.mergeSettings.mergeComment = this.mergeSummary.requestPayload.merge_comment; + async start() { + if (this.mergeSummary) { + this.loadMergeSettings(); + await this.displayMergeSummary(); + return await this.executeMerge(this.mergeSummary.requestPayload); + } + await this.collectMergeSettings(); + const mergePayload = prepareMergeRequestPayload(this.mergeSettings); + if (this.executeOption === 'execute') { + await this.exportSummary(mergePayload); + await this.executeMerge(mergePayload); + } else if (this.executeOption === 'export') { + await this.exportSummary(mergePayload); + } else if (this.executeOption === 'merge_n_scripts') { + this.enableEntryExp = true; + await this.executeMerge(mergePayload); + } else if (this.executeOption === 'summary_n_scripts') { + this.enableEntryExp = true; + await this.exportSummary(mergePayload); + } else { + await this.exportSummary(mergePayload); + await this.executeMerge(mergePayload); + this.enableEntryExp = true; + } } } diff --git a/packages/contentstack-branches/src/commands/cm/branches/merge-status.ts b/packages/contentstack-branches/src/commands/cm/branches/merge-status.ts new file mode 100644 index 000000000..0bb22571a --- /dev/null +++ b/packages/contentstack-branches/src/commands/cm/branches/merge-status.ts @@ -0,0 +1,71 @@ +import { Command } from '@contentstack/cli-command'; +import { cliux, flags, isAuthenticated, managementSDKClient } from '@contentstack/cli-utilities'; +import { displayMergeStatusDetails, handleErrorMsg } from '../../../utils'; + +/** + * Command to check the status of a branch merge job. + * Allows users to check merge progress and status asynchronously. + */ +export default class BranchMergeStatusCommand extends Command { + static readonly description: string = 'Check the status of a branch merge job'; + + static readonly examples: string[] = [ + 'csdx cm:branches:merge-status -k bltxxxxxxxx --merge-uid merge_abc123', + 'csdx cm:branches:merge-status --stack-api-key bltxxxxxxxx --merge-uid merge_abc123', + ]; + + static readonly usage: string = 'cm:branches:merge-status -k --merge-uid '; + + static readonly flags = { + 'stack-api-key': flags.string({ + char: 'k', + description: 'Provide your stack API key.', + required: true, + }), + 'merge-uid': flags.string({ + description: 'Merge job UID to check status for.', + required: true, + }), + }; + + static readonly aliases: string[] = []; + + /** + * Fetches and displays the current status of a branch merge job. + * Useful for checking long-running merges asynchronously without blocking. + */ + async run(): Promise { + try { + const { flags: mergeStatusFlags } = await this.parse(BranchMergeStatusCommand); + + if (!isAuthenticated()) { + const err = { errorMessage: 'You are not logged in. Please login with command $ csdx auth:login' }; + handleErrorMsg(err); + } + + const { 'stack-api-key': stackAPIKey, 'merge-uid': mergeUID } = mergeStatusFlags; + + const stackAPIClient = await (await managementSDKClient({ host: this.cmaHost })).stack({ + api_key: stackAPIKey, + }); + + const spinner = cliux.loaderV2('Fetching merge status...'); + const mergeStatusResponse = await stackAPIClient + .branch() + .mergeQueue(mergeUID) + .fetch(); + cliux.loaderV2('', spinner); + + if (!mergeStatusResponse?.queue?.length) { + cliux.error(`No merge job found with UID: ${mergeUID}`); + process.exit(1); + } + + const mergeJobStatus = mergeStatusResponse.queue[0]; + displayMergeStatusDetails(mergeJobStatus); + } catch (error) { + cliux.error('Failed to fetch merge status', error.message || error); + process.exit(1); + } + } +} diff --git a/packages/contentstack-branches/src/config/index.ts b/packages/contentstack-branches/src/config/index.ts index c5a62ca1c..cb195a8d6 100644 --- a/packages/contentstack-branches/src/config/index.ts +++ b/packages/contentstack-branches/src/config/index.ts @@ -1,5 +1,5 @@ const config = { - skip: 0, - limit: 100 + limit: 100, + skip: 0 }; export default config; diff --git a/packages/contentstack-branches/src/interfaces/index.ts b/packages/contentstack-branches/src/interfaces/index.ts index 62b5655f6..29eba9801 100644 --- a/packages/contentstack-branches/src/interfaces/index.ts +++ b/packages/contentstack-branches/src/interfaces/index.ts @@ -1,34 +1,34 @@ export interface BranchOptions { + authToken?: string; + baseBranch?: string; compareBranch: string; - stackAPIKey: string; - module: string; + csvPath?: string; format: string; - baseBranch?: string; - authToken?: string; host?: string; - csvPath?: string; + module: string; + stackAPIKey: string; } export interface BranchDiffRes { - uid: string; + merge_strategy?: string; + status: string; title: string; type: string; - status: string; - merge_strategy?: string; + uid: string; } export interface BranchDiffSummary { base: string; - compare: string; base_only: number; + compare: string; compare_only: number; modified: number; } export interface BranchCompactTextRes { - modified?: BranchDiffRes[]; added?: BranchDiffRes[]; deleted?: BranchDiffRes[]; + modified?: BranchDiffRes[]; } export interface MergeSummary { @@ -40,61 +40,61 @@ type MergeSummaryRequestPayload = { compare_branch: string; default_merge_strategy: string; item_merge_strategies?: any[]; - no_revert?: boolean; merge_comment?: string; + no_revert?: boolean; }; export interface MergeInputOptions { - compareBranch: string; - strategy: string; - strategySubOption: string; + baseBranch: string; branchCompareData: any; - mergeComment?: string; + compareBranch: string; + enableEntryExp: boolean; executeOption?: string; - noRevert?: boolean; - baseBranch: string; - format?: string; exportSummaryPath?: string; + format?: string; + host: string; + mergeComment?: string; mergeSummary?: MergeSummary; + noRevert?: boolean; stackAPIKey: string; - host: string; - enableEntryExp: boolean; + strategy: string; + strategySubOption: string; } export interface ModifiedFieldsType { - uid: string; - displayName: string; - path: string; - field: string; - propertyChanges?: PropertyChange[]; changeCount?: number; changeDetails?: string; - oldValue?: any; + displayName: string; + field: string; newValue?: any; + oldValue?: any; + path: string; + propertyChanges?: PropertyChange[]; + uid: string; } export interface PropertyChange { - property: string; - changeType: 'modified' | 'added' | 'deleted'; - oldValue?: any; + changeType: 'added' | 'deleted' | 'modified'; newValue?: any; + oldValue?: any; + property: string; } export interface CSVRow { - srNo: number; contentTypeName: string; fieldName: string; fieldPath: string; operation: string; sourceBranchValue: string; + srNo: number; targetBranchValue: string; } export interface AddCSVRowParams { - srNo: number; contentTypeName: string; fieldName: string; fieldType: string; sourceValue: string; + srNo: number; targetValue: string; } @@ -108,43 +108,43 @@ export interface ContentTypeItem { } export interface ModifiedFieldsInput { - modified?: ModifiedFieldsType[]; added?: ModifiedFieldsType[]; deleted?: ModifiedFieldsType[]; + modified?: ModifiedFieldsType[]; } export interface BranchModifiedDetails { - moduleDetails: BranchDiffRes; modifiedFields: ModifiedFieldsInput; + moduleDetails: BranchDiffRes; } export interface BranchDiffVerboseRes { - modified?: BranchModifiedDetails[]; added?: BranchDiffRes[]; - deleted?: BranchDiffRes[]; csvData?: CSVRow[]; // Pre-processed CSV data + deleted?: BranchDiffRes[]; + modified?: BranchModifiedDetails[]; } export interface BranchDiffPayload { - module: string; apiKey: string; baseBranch: string; compareBranch: string; filter?: string; host?: string; - uid?: string; + module: string; spinner?: any; + uid?: string; url?: string; } export type MergeStrategy = - | 'merge_prefer_base' - | 'merge_prefer_compare' - | 'overwrite_with_compare' - | 'merge_new_only' + | 'ignore' | 'merge_modified_only_prefer_base' | 'merge_modified_only_prefer_compare' - | 'ignore'; + | 'merge_new_only' + | 'merge_prefer_base' + | 'merge_prefer_compare' + | 'overwrite_with_compare'; export interface MergeParams { base_branch: string; @@ -153,3 +153,33 @@ export interface MergeParams { merge_comment: string; no_revert?: boolean; } + +export interface MergeStatusOptions { + host?: string; + mergeUID: string; + stackAPIKey: string; +} + +export interface GenerateScriptsOptions { + host?: string; + mergeUID: string; + stackAPIKey: string; +} + +export interface MergeJobStatusResponse { + errors?: Array<{ details?: string; field?: string; message: string }>; + merge_details: { + completed_at?: string; + completion_percentage?: number; + created_at: string; + status: string; + updated_at: string; + }; + merge_summary: { + content_types: { added: number; deleted: number; modified: number }; + global_fields: { added: number; deleted: number; modified: number }; + }; + pollingTimeout?: boolean; + status: 'complete' | 'failed' | 'in_progress' | 'unknown'; + uid: string; +} diff --git a/packages/contentstack-branches/src/utils/branch-diff-utility.ts b/packages/contentstack-branches/src/utils/branch-diff-utility.ts index 2828ebd63..7085735f2 100644 --- a/packages/contentstack-branches/src/utils/branch-diff-utility.ts +++ b/packages/contentstack-branches/src/utils/branch-diff-utility.ts @@ -1,26 +1,25 @@ -import { getChalk } from '@contentstack/cli-utilities'; +import { cliux, managementSDKClient, messageHandler, getChalk } from '@contentstack/cli-utilities'; +import { diff } from 'just-diff'; +import camelCase from 'lodash/camelCase'; +import find from 'lodash/find'; import forEach from 'lodash/forEach'; +import isArray from 'lodash/isArray'; import padStart from 'lodash/padStart'; import startCase from 'lodash/startCase'; -import camelCase from 'lodash/camelCase'; import unionWith from 'lodash/unionWith'; -import find from 'lodash/find'; -import { cliux, messageHandler, managementSDKClient } from '@contentstack/cli-utilities'; -import isArray from 'lodash/isArray'; -import { diff } from 'just-diff'; -import { extractValueFromPath, getFieldDisplayName, generateCSVDataFromVerbose } from './csv-utility'; +import config from '../config'; import { - BranchDiffRes, - ModifiedFieldsInput, - ModifiedFieldsType, - BranchModifiedDetails, + BranchCompactTextRes, BranchDiffPayload, + BranchDiffRes, BranchDiffSummary, - BranchCompactTextRes, BranchDiffVerboseRes, + BranchModifiedDetails, + ModifiedFieldsInput, + ModifiedFieldsType, } from '../interfaces/index'; -import config from '../config'; +import { extractValueFromPath, generateCSVDataFromVerbose, getFieldDisplayName } from './csv-utility'; /** * Fetch differences between two branches @@ -106,10 +105,10 @@ function handleErrorMsg(err, spinner) { if (err?.errorMessage) { cliux.print(`Error: ${err.errorMessage}`, { color: 'red' }); - }else if(err?.message){ + } else if (err?.message) { cliux.print(`Error: ${err.message}`, { color: 'red' }); } else { - console.log(err) + console.log(err); cliux.print(`Error: ${messageHandler.parse('CLI_BRANCH_API_FAILED')}`, { color: 'red' }); } process.exit(1); @@ -124,9 +123,9 @@ function handleErrorMsg(err, spinner) { * @returns {*} BranchDiffSummary */ function parseSummary(branchesDiffData: any[], baseBranch: string, compareBranch: string): BranchDiffSummary { - let baseCount: number = 0, - compareCount: number = 0, - modifiedCount: number = 0; + let baseCount = 0, + compareCount = 0, + modifiedCount = 0; if (branchesDiffData?.length) { forEach(branchesDiffData, (diff: BranchDiffRes) => { @@ -138,8 +137,8 @@ function parseSummary(branchesDiffData: any[], baseBranch: string, compareBranch const branchSummary: BranchDiffSummary = { base: baseBranch, - compare: compareBranch, base_only: baseCount, + compare: compareBranch, compare_only: compareCount, modified: modifiedCount, }; @@ -166,7 +165,7 @@ function printSummary(diffSummary: BranchDiffSummary): void { * @returns {*} BranchCompactTextRes */ function parseCompactText(branchesDiffData: any[]): BranchCompactTextRes { - let listOfModified: BranchDiffRes[] = [], + const listOfModified: BranchDiffRes[] = [], listOfAdded: BranchDiffRes[] = [], listOfDeleted: BranchDiffRes[] = []; @@ -179,9 +178,9 @@ function parseCompactText(branchesDiffData: any[]): BranchCompactTextRes { } const branchTextRes: BranchCompactTextRes = { - modified: listOfModified, added: listOfAdded, deleted: listOfDeleted, + modified: listOfModified, }; return branchTextRes; } @@ -223,36 +222,36 @@ function printCompactTextView(branchTextRes: BranchCompactTextRes): void { * @returns {*} Promise */ async function parseVerbose(branchesDiffData: any[], payload: BranchDiffPayload): Promise { - const { added, modified, deleted } = parseCompactText(branchesDiffData); - let modifiedDetailList: BranchModifiedDetails[] = []; + const { added, deleted, modified } = parseCompactText(branchesDiffData); + const modifiedDetailList: BranchModifiedDetails[] = []; for (let i = 0; i < modified?.length; i++) { const diff: BranchDiffRes = modified[i]; payload.uid = diff?.uid; const branchDiff = await branchCompareSDK(payload); if (branchDiff) { - const { listOfModifiedFields, listOfAddedFields, listOfDeletedFields } = await prepareBranchVerboseRes( + const { listOfAddedFields, listOfDeletedFields, listOfModifiedFields } = await prepareBranchVerboseRes( branchDiff, ); modifiedDetailList.push({ - moduleDetails: diff, modifiedFields: { - modified: listOfModifiedFields, - deleted: listOfDeletedFields, added: listOfAddedFields, + deleted: listOfDeletedFields, + modified: listOfModifiedFields, }, + moduleDetails: diff, }); } } const verboseRes: BranchDiffVerboseRes = { - modified: modifiedDetailList, added: added, deleted: deleted, + modified: modifiedDetailList, }; verboseRes.csvData = generateCSVDataFromVerbose(verboseRes); - + return verboseRes; } @@ -263,7 +262,7 @@ async function parseVerbose(branchesDiffData: any[], payload: BranchDiffPayload) * @returns */ async function prepareBranchVerboseRes(branchDiff: any) { - let listOfModifiedFields = [], + const listOfModifiedFields = [], listOfDeletedFields = [], listOfAddedFields = []; @@ -287,9 +286,9 @@ async function prepareBranchVerboseRes(branchDiff: any) { baseBranchFieldExists, compareBranchFieldExists, diffData, - listOfModifiedFields, - listOfDeletedFields, listOfAddedFields, + listOfDeletedFields, + listOfModifiedFields, }); }); } @@ -306,42 +305,42 @@ async function baseAndCompareBranchDiff(params: { baseBranchFieldExists: any; compareBranchFieldExists: any; diffData: any; - listOfModifiedFields: any[]; - listOfDeletedFields: any[]; listOfAddedFields: any[]; + listOfDeletedFields: any[]; + listOfModifiedFields: any[]; }) { const { baseBranchFieldExists, compareBranchFieldExists } = params; if (baseBranchFieldExists && compareBranchFieldExists) { await prepareModifiedDiff(params); } else if (baseBranchFieldExists && !compareBranchFieldExists) { - let displayName= baseBranchFieldExists?.display_name; + let displayName = baseBranchFieldExists?.display_name; let path = baseBranchFieldExists?.path || baseBranchFieldExists?.uid; let field = baseBranchFieldExists?.data_type; - if(baseBranchFieldExists.path === 'description'){ + if (baseBranchFieldExists.path === 'description') { displayName = 'Description'; path = baseBranchFieldExists?.path; - field = 'metadata' + field = 'metadata'; } params.listOfDeletedFields.push({ + displayName: displayName, + field: field, path: path, - displayName:displayName, uid: baseBranchFieldExists?.uid, - field: field, }); } else if (!baseBranchFieldExists && compareBranchFieldExists) { - let displayName= compareBranchFieldExists?.display_name; + let displayName = compareBranchFieldExists?.display_name; let path = compareBranchFieldExists?.path || compareBranchFieldExists?.uid; let field = compareBranchFieldExists?.data_type; - if(compareBranchFieldExists.path === 'description'){ + if (compareBranchFieldExists.path === 'description') { displayName = 'Description'; path = compareBranchFieldExists?.path; - field = 'metadata' + field = 'metadata'; } params.listOfAddedFields.push({ - path: path, displayName: displayName, - uid: compareBranchFieldExists?.uid, field: field, + path: path, + uid: compareBranchFieldExists?.uid, }); } } @@ -349,9 +348,9 @@ async function baseAndCompareBranchDiff(params: { async function prepareModifiedDiff(params: { baseBranchFieldExists: any; compareBranchFieldExists: any; - listOfModifiedFields: any[]; - listOfDeletedFields: any[]; listOfAddedFields: any[]; + listOfDeletedFields: any[]; + listOfModifiedFields: any[]; }) { const { baseBranchFieldExists, compareBranchFieldExists } = params; if ( @@ -381,52 +380,52 @@ async function prepareModifiedDiff(params: { changeDetails = `Changed from "${oldTitle}" to "${newTitle}"`; } params.listOfModifiedFields.push({ - path: '', + changeDetails, displayName: displayName, - uid: baseBranchFieldExists.path, field: 'changed', - changeDetails, - oldValue: baseBranchFieldExists.value, newValue: compareBranchFieldExists.value, + oldValue: baseBranchFieldExists.value, + path: '', + uid: baseBranchFieldExists.path, }); } else { const fieldDisplayName = getFieldDisplayName(compareBranchFieldExists); - const { modified, deleted, added } = await deepDiff(baseBranchFieldExists, compareBranchFieldExists); - for (let field of Object.values(added)) { - if (field) { - params.listOfAddedFields.push({ - path: field['path'], + const { added, deleted, modified } = await deepDiff(baseBranchFieldExists, compareBranchFieldExists); + for (const field of Object.values(added)) { + if (field) { + params.listOfAddedFields.push({ displayName: getFieldDisplayName(field), - uid: field['uid'], field: field['fieldType'] || field['data_type'] || 'field', - }); - } + path: field['path'], + uid: field['uid'], + }); } + } - for (let field of Object.values(deleted)) { - if (field) { - params.listOfDeletedFields.push({ - path: field['path'], + for (const field of Object.values(deleted)) { + if (field) { + params.listOfDeletedFields.push({ displayName: getFieldDisplayName(field), - uid: field['uid'], field: field['fieldType'] || field['data_type'] || 'field', - }); - } + path: field['path'], + uid: field['uid'], + }); } + } - for (let field of Object.values(modified)) { - if (field) { - params.listOfModifiedFields.push({ - path: field['path'], + for (const field of Object.values(modified)) { + if (field) { + params.listOfModifiedFields.push({ + changeCount: field['changeCount'], displayName: field['displayName'] || field['display_name'] || fieldDisplayName, - uid: field['uid'] || compareBranchFieldExists?.uid, field: `${field['fieldType'] || field['data_type'] || compareBranchFieldExists?.data_type || 'field'} field`, + path: field['path'], propertyChanges: field['propertyChanges'], - changeCount: field['changeCount'], - }); - } + uid: field['uid'] || compareBranchFieldExists?.uid, + }); } + } } } @@ -465,7 +464,7 @@ function printModifiedFields(modfiedFields: ModifiedFieldsInput): void { if (modfiedFields.modified?.length || modfiedFields.added?.length || modfiedFields.deleted?.length) { forEach(modfiedFields.modified, (diff: ModifiedFieldsType) => { const field: string = diff.field ? `${diff.field}` : 'field'; - const fieldDetail = diff.path ? `(${diff.path}) ${field}`: `${field}`; + const fieldDetail = diff.path ? `(${diff.path}) ${field}` : `${field}`; cliux.print(` ${getChalk().blue(`± "${diff.displayName}" ${fieldDetail}`)}`); }); @@ -487,7 +486,7 @@ function printModifiedFields(modfiedFields: ModifiedFieldsInput): void { * @returns */ function filterBranchDiffDataByModule(branchDiffData: any[]) { - let moduleRes = { + const moduleRes = { content_types: [], global_fields: [], }; @@ -503,22 +502,22 @@ const buildPath = (path, key) => (path === '' ? key : `${path}.${key}`); async function deepDiff(baseObj, compareObj) { const changes = { - modified: {}, added: {}, deleted: {}, + modified: {}, }; function baseAndCompareSchemaDiff(baseObj, compareObj, path = '') { - const { schema: baseSchema, path: basePath, ...restBaseObj } = baseObj; - const { schema: compareSchema, path: comparePath, ...restCompareObj } = compareObj; + const { path: basePath, schema: baseSchema, ...restBaseObj } = baseObj; + const { path: comparePath, schema: compareSchema, ...restCompareObj } = compareObj; const currentPath = buildPath(path, baseObj['uid']); if (restBaseObj['uid'] === restCompareObj['uid']) { prepareModifiedField({ - restBaseObj, - restCompareObj, - currentPath, changes, + currentPath, fullFieldContext: baseObj, parentContext: baseObj, + restBaseObj, + restCompareObj, }); } @@ -531,10 +530,10 @@ async function deepDiff(baseObj, compareObj) { let newPath: string; if (baseBranchField && !compareBranchField) { newPath = `${currentPath}.${baseBranchField['uid']}`; - prepareDeletedField({ path: newPath, changes, baseField: baseBranchField }); + prepareDeletedField({ baseField: baseBranchField, changes, path: newPath }); } else if (compareBranchField && !baseBranchField) { newPath = `${currentPath}.${compareBranchField['uid']}`; - prepareAddedField({ path: newPath, changes, compareField: compareBranchField }); + prepareAddedField({ changes, compareField: compareBranchField, path: newPath }); } else if (compareBranchField && baseBranchField) { baseAndCompareSchemaDiff(baseBranchField, compareBranchField, currentPath); } @@ -545,14 +544,14 @@ async function deepDiff(baseObj, compareObj) { if (baseSchema?.length && !compareSchema?.length && isArray(baseSchema)) { forEach(baseSchema, (base, key) => { const newPath = `${currentPath}.${base['uid']}`; - prepareDeletedField({ path: newPath, changes, baseField: base }); + prepareDeletedField({ baseField: base, changes, path: newPath }); }); } //case3:- compare schema exists only if (!baseSchema?.length && compareSchema?.length && isArray(compareSchema)) { forEach(compareSchema, (compare, key) => { const newPath = `${currentPath}.${compare['uid']}`; - prepareAddedField({ path: newPath, changes, compareField: compare }); + prepareAddedField({ changes, compareField: compare, path: newPath }); }); } } @@ -560,84 +559,84 @@ async function deepDiff(baseObj, compareObj) { return changes; } -function prepareAddedField(params: { path: string; changes: any; compareField: any }) { - const { path, changes, compareField } = params; +function prepareAddedField(params: { changes: any; compareField: any; path: string }) { + const { changes, compareField, path } = params; if (!changes.added[path]) { const obj = { - path: path, - uid: compareField['uid'], displayName: compareField['display_name'], fieldType: compareField['data_type'], - oldValue: undefined, newValue: compareField, + oldValue: undefined, + path: path, + uid: compareField['uid'], }; changes.added[path] = obj; } } -function prepareDeletedField(params: { path: string; changes: any; baseField: any }) { - const { path, changes, baseField } = params; +function prepareDeletedField(params: { baseField: any; changes: any; path: string }) { + const { baseField, changes, path } = params; if (!changes.added[path]) { const obj = { - path: path, - uid: baseField['uid'], displayName: baseField['display_name'], fieldType: baseField['data_type'], + path: path, + uid: baseField['uid'], }; changes.deleted[path] = obj; } } function prepareModifiedField(params: { - restBaseObj: any; - restCompareObj: any; - currentPath: string; changes: any; + currentPath: string; fullFieldContext: any; parentContext: any; + restBaseObj: any; + restCompareObj: any; }) { - const { restBaseObj, restCompareObj, currentPath, changes, fullFieldContext } = params; + const { changes, currentPath, fullFieldContext, restBaseObj, restCompareObj } = params; const differences = diff(restBaseObj, restCompareObj); if (differences.length) { const modifiedField = { - path: currentPath, - uid: fullFieldContext['uid'] || restCompareObj['uid'], + changeCount: differences.length, displayName: getFieldDisplayName(fullFieldContext) || getFieldDisplayName(restCompareObj) || 'Field', fieldType: restCompareObj['data_type'] || 'field', + path: currentPath, propertyChanges: differences.map((diff) => { let oldValue = 'from' in diff ? diff.from : undefined; - let newValue = diff.value; + const newValue = diff.value; if (!('from' in diff) && fullFieldContext && diff.path && diff.path.length > 0) { const contextValue = extractValueFromPath(fullFieldContext, diff.path); if (contextValue !== undefined) { oldValue = contextValue; } } - + return { - property: diff.path.join('.'), changeType: diff.op === 'add' ? 'added' : diff.op === 'remove' ? 'deleted' : 'modified', - oldValue: oldValue, newValue: newValue, + oldValue: oldValue, + property: diff.path.join('.'), }; }), - changeCount: differences.length, + uid: fullFieldContext['uid'] || restCompareObj['uid'], }; if (!changes.modified[currentPath]) changes.modified[currentPath] = modifiedField; } } export { + branchCompareSDK, + deepDiff, fetchBranchesDiff, - parseSummary, - printSummary, + filterBranchDiffDataByModule, parseCompactText, - printCompactTextView, + parseSummary, parseVerbose, - printVerboseTextView, - filterBranchDiffDataByModule, - branchCompareSDK, prepareBranchVerboseRes, - deepDiff, prepareModifiedDiff, + printCompactTextView, + printSummary, + printVerboseTextView, }; diff --git a/packages/contentstack-branches/src/utils/create-branch.ts b/packages/contentstack-branches/src/utils/create-branch.ts index 2eb6f5d92..fd0691a93 100644 --- a/packages/contentstack-branches/src/utils/create-branch.ts +++ b/packages/contentstack-branches/src/utils/create-branch.ts @@ -1,6 +1,6 @@ import { cliux, managementSDKClient } from '@contentstack/cli-utilities'; -export async function createBranch(host: string, apiKey: string, branch: { uid: string; source: string }) { +export async function createBranch(host: string, apiKey: string, branch: { source: string; uid: string }) { const managementAPIClient = await managementSDKClient({ host }); managementAPIClient .stack({ api_key: apiKey }) @@ -11,7 +11,7 @@ export async function createBranch(host: string, apiKey: string, branch: { uid: 'Branch creation in progress. Once ready, it will show in the results of the branch list command `csdx cm:branches`', ), ) - .catch((err: { errorCode: number; errorMessage: string, errors:any }) => { + .catch((err: { errorCode: number; errorMessage: string, errors: any }) => { if (err.errorCode === 910) cliux.error(`error : Branch with UID '${branch.uid}' already exists, please enter a unique branch UID`); else if (err.errorCode === 903){ diff --git a/packages/contentstack-branches/src/utils/create-merge-scripts.ts b/packages/contentstack-branches/src/utils/create-merge-scripts.ts index bc1c7c3b3..0d2e08abe 100644 --- a/packages/contentstack-branches/src/utils/create-merge-scripts.ts +++ b/packages/contentstack-branches/src/utils/create-merge-scripts.ts @@ -1,15 +1,16 @@ +import { cliux, formatDate, formatTime } from '@contentstack/cli-utilities'; import fs from 'fs'; -import { cliux, formatTime, formatDate } from '@contentstack/cli-utilities'; + +import { assetFolderCreateScript } from './asset-folder-create-script'; import { entryCreateScript } from './entry-create-script'; -import { entryUpdateScript } from './entry-update-script'; import { entryCreateUpdateScript } from './entry-create-update-script'; -import { assetFolderCreateScript } from './asset-folder-create-script'; +import { entryUpdateScript } from './entry-update-script'; type CreateMergeScriptsProps = { - uid: string; entry_merge_strategy?: string; - type?: string; status?: string; + type?: string; + uid: string; }; export function generateMergeScripts(mergeSummary, mergeJobUID) { @@ -28,8 +29,8 @@ export function generateMergeScripts(mergeSummary, mergeJobUID) { const mergeStrategies = { asset_create_folder: assetFolderCreateScript, - merge_existing_new: entryCreateUpdateScript, merge_existing: entryUpdateScript, + merge_existing_new: entryCreateUpdateScript, merge_new: entryCreateScript, }; @@ -45,7 +46,7 @@ export function generateMergeScripts(mergeSummary, mergeJobUID) { } }; - processContentType({ type: 'assets', uid: '', entry_merge_strategy: '' }, mergeStrategies['asset_create_folder']); + processContentType({ entry_merge_strategy: '', type: 'assets', uid: '' }, mergeStrategies['asset_create_folder']); processContentTypes(mergeSummary.modified, 'Modified'); processContentTypes(mergeSummary.added, 'New'); @@ -90,7 +91,7 @@ export function createMergeScripts(contentType: CreateMergeScriptsProps, mergeJo fs.mkdirSync(fullPath); } let filePath: string; - let milliSeconds = date.getMilliseconds().toString().padStart(3, '0'); + const milliSeconds = date.getMilliseconds().toString().padStart(3, '0'); if (contentType.type === 'assets') { filePath = `${fullPath}/${fileCreatedAt}${milliSeconds}_create_assets_folder.js`; } else { diff --git a/packages/contentstack-branches/src/utils/csv-utility.ts b/packages/contentstack-branches/src/utils/csv-utility.ts index f1328099d..0d5d7e513 100644 --- a/packages/contentstack-branches/src/utils/csv-utility.ts +++ b/packages/contentstack-branches/src/utils/csv-utility.ts @@ -1,7 +1,8 @@ -import { writeFileSync, existsSync, mkdirSync } from 'fs'; -import { join } from 'path'; import { cliux, log, sanitizePath } from '@contentstack/cli-utilities'; -import { BranchDiffVerboseRes, CSVRow, ModifiedFieldsInput, ContentTypeItem, AddCSVRowParams, FIELD_TYPES, CSV_HEADER } from '../interfaces'; +import { existsSync, mkdirSync, writeFileSync } from 'fs'; +import { join } from 'path'; + +import { AddCSVRowParams, BranchDiffVerboseRes, CSV_HEADER, CSVRow, ContentTypeItem, FIELD_TYPES, ModifiedFieldsInput } from '../interfaces'; /** * Get display name for a field with special handling for system fields @@ -95,7 +96,7 @@ export function formatValue(value: any): string { * @param path - Array of path segments * @returns The value at the path, or undefined if not found */ -export function extractValueFromPath(obj: any, path: (string | number)[]): any { +export function extractValueFromPath(obj: any, path: (number | string)[]): any { if (!obj || !path || path.length === 0) return undefined; try { @@ -179,11 +180,11 @@ function addContentTypeRows( const contentTypeName = item?.title || item?.uid || 'Unknown'; addCSVRow(csvRows, { - srNo: getSrNo(), contentTypeName, fieldName: 'Content Type', fieldType: operation, sourceValue: 'N/A', + srNo: getSrNo(), targetValue: 'N/A' }); } @@ -227,12 +228,12 @@ export function generateCSVDataFromVerbose(verboseRes: BranchDiffVerboseRes): CS */ function addCSVRow(csvRows: CSVRow[], params: AddCSVRowParams): void { csvRows.push({ - srNo: params.srNo, contentTypeName: params.contentTypeName, fieldName: params.fieldName, fieldPath: 'N/A', operation: params.fieldType, sourceBranchValue: params.sourceValue, + srNo: params.srNo, targetBranchValue: params.targetValue, }); } @@ -261,21 +262,21 @@ function addFieldChangesToCSV(csvRows: CSVRow[], params: { if (field.propertyChanges?.length > 0) { field.propertyChanges.forEach(propertyChange => { addCSVRow(csvRows, { - srNo: srNo++, contentTypeName: params.contentTypeName, fieldName, fieldType, sourceValue: formatValue(propertyChange.newValue), + srNo: srNo++, targetValue: formatValue(propertyChange.oldValue) }); }); } else { addCSVRow(csvRows, { - srNo: srNo++, contentTypeName: params.contentTypeName, fieldName, fieldType, sourceValue: fieldType === 'added' ? 'N/A' : formatValue(field), + srNo: srNo++, targetValue: fieldType === 'deleted' ? 'N/A' : formatValue(field) }); } diff --git a/packages/contentstack-branches/src/utils/delete-branch.ts b/packages/contentstack-branches/src/utils/delete-branch.ts index 9c691cbd9..bdb3940bb 100644 --- a/packages/contentstack-branches/src/utils/delete-branch.ts +++ b/packages/contentstack-branches/src/utils/delete-branch.ts @@ -1,4 +1,5 @@ import { cliux, managementSDKClient } from '@contentstack/cli-utilities'; + import { refreshbranchConfig } from '.'; export async function deleteBranch(host: string, apiKey: string, uid: string) { diff --git a/packages/contentstack-branches/src/utils/index.ts b/packages/contentstack-branches/src/utils/index.ts index 64645a298..6220fabc8 100644 --- a/packages/contentstack-branches/src/utils/index.ts +++ b/packages/contentstack-branches/src/utils/index.ts @@ -1,10 +1,11 @@ /** * Command specific utilities can be written here */ +import { cliux, configHandler, messageHandler, sanitizePath } from '@contentstack/cli-utilities'; import fs from 'fs'; -import path from 'path'; import forEach from 'lodash/forEach'; -import { configHandler, cliux, messageHandler, sanitizePath } from '@contentstack/cli-utilities'; +import path from 'path'; + import { MergeParams } from '../interfaces'; export const getbranchesList = (branchResult, baseBranch: string) => { @@ -12,10 +13,10 @@ export const getbranchesList = (branchResult, baseBranch: string) => { branchResult.map((item) => { branches.push({ - Branch: item.uid, - Source: item.source, Aliases: item.alias, + Branch: item.uid, Created: new Date(item.created_at).toLocaleDateString(), + Source: item.source, Updated: new Date(item.updated_at).toLocaleDateString(), }); }); @@ -23,7 +24,7 @@ export const getbranchesList = (branchResult, baseBranch: string) => { const currentBranch = branches.filter((branch) => branch.Branch === baseBranch); const otherBranches = branches.filter((branch) => branch.Branch !== baseBranch); - return { currentBranch, otherBranches, branches }; + return { branches, currentBranch, otherBranches }; }; export const getbranchConfig = (stackApiKey: string) => { @@ -78,8 +79,8 @@ export async function getMergeQueueStatus(stackAPIClient, payload): Promise export async function executeMergeRequest(stackAPIClient, payload): Promise { const { - host, apiKey, + host, params: { base_branch, compare_branch, default_merge_strategy, item_merge_strategies, merge_comment, no_revert }, } = payload; const queryParams: MergeParams = { @@ -139,11 +140,12 @@ export function validateCompareData(branchCompareData) { return validCompareData; } -export * from './interactive'; -export * from './merge-helper'; +export * as branchDiffUtility from './branch-diff-utility'; export * from './create-merge-scripts'; -export * from './entry-update-script'; +export * as deleteBranchUtility from './delete-branch'; export * from './entry-create-script'; +export * from './entry-update-script'; +export * from './interactive'; export * as interactive from './interactive'; -export * as branchDiffUtility from './branch-diff-utility'; -export * as deleteBranchUtility from './delete-branch'; +export * from './merge-helper'; +export * from './merge-status-helper'; diff --git a/packages/contentstack-branches/src/utils/interactive.ts b/packages/contentstack-branches/src/utils/interactive.ts index dc8730fd6..73356b848 100644 --- a/packages/contentstack-branches/src/utils/interactive.ts +++ b/packages/contentstack-branches/src/utils/interactive.ts @@ -1,81 +1,81 @@ -import isEmpty from 'lodash/isEmpty'; -import startCase from 'lodash/startCase'; +import { cliux, messageHandler, validatePath } from '@contentstack/cli-utilities'; import camelCase from 'lodash/camelCase'; import forEach from 'lodash/forEach'; +import isEmpty from 'lodash/isEmpty'; +import startCase from 'lodash/startCase'; import path from 'path'; -import { cliux, messageHandler, validatePath } from '@contentstack/cli-utilities'; import { BranchDiffRes } from '../interfaces'; export async function selectModule(): Promise { return await cliux.inquire({ - type: 'list', - name: 'module', - message: 'CLI_BRANCH_MODULE', choices: [ { name: 'Content Types', value: 'content-types' }, { name: 'Global Fields', value: 'global-fields' }, { name: 'All', value: 'all' }, ], + message: 'CLI_BRANCH_MODULE', + name: 'module', + type: 'list', validate: inquireRequireFieldValidation, }); } export async function askCompareBranch(): Promise { return await cliux.inquire({ - type: 'input', message: 'CLI_BRANCH_COMPARE_BRANCH', name: 'compare_branch', + type: 'input', validate: inquireRequireFieldValidation, }); } export async function askStackAPIKey(): Promise { return await cliux.inquire({ - type: 'input', message: 'CLI_BRANCH_STACK_API_KEY', name: 'api_key', + type: 'input', validate: inquireRequireFieldValidation, }); } export async function askBaseBranch(): Promise { return await cliux.inquire({ - type: 'input', message: 'CLI_BRANCH_BASE_BRANCH', name: 'branch_branch', + type: 'input', validate: inquireRequireFieldValidation, }); } export async function askSourceBranch(): Promise { return await cliux.inquire({ - type: 'input', message: 'CLI_BRANCH_SOURCE_BRANCH', name: 'source_branch', + type: 'input', validate: inquireRequireFieldValidation, }); } export async function askBranchUid(): Promise { return await cliux.inquire({ - type: 'input', message: 'CLI_BRANCH_BRANCH_UID', name: 'branch_uid', + type: 'input', validate: inquireRequireFieldValidation, }); } export async function askConfirmation(): Promise { const resp = await cliux.inquire({ - type: 'confirm', message: 'Are you sure you want to delete this branch?', name: 'confirm', + type: 'confirm', }); return resp; } -export function inquireRequireFieldValidation(input: any): string | boolean { +export function inquireRequireFieldValidation(input: any): boolean | string { if (isEmpty(input)) { return messageHandler.parse('CLI_BRANCH_REQUIRED_FIELD'); } @@ -85,8 +85,6 @@ export function inquireRequireFieldValidation(input: any): string | boolean { export async function selectMergeStrategy(): Promise { const strategy = await cliux .inquire({ - type: 'list', - name: 'module', choices: [ { name: 'Merge, Prefer Base', value: 'merge_prefer_base' }, { name: 'Merge, Prefer Compare', value: 'merge_prefer_compare' }, @@ -94,6 +92,8 @@ export async function selectMergeStrategy(): Promise { { name: 'Overwrite with Compare', value: 'overwrite_with_compare' }, ], message: 'What merge strategy would you like to choose? ', + name: 'module', + type: 'list', }) .then((name) => name as string) .catch((err) => { @@ -107,8 +107,6 @@ export async function selectMergeStrategy(): Promise { export async function selectMergeStrategySubOptions(): Promise { const strategy = await cliux .inquire({ - type: 'list', - name: 'module', choices: [ { name: 'New in Compare Only', value: 'new' }, { name: 'Modified Only', value: 'modified' }, @@ -117,6 +115,8 @@ export async function selectMergeStrategySubOptions(): Promise { { name: 'Start Over', value: 'restart' }, ], message: 'What do you want to merge?', + name: 'module', + type: 'list', }) .then((name) => name as string) .catch((err) => { @@ -130,8 +130,6 @@ export async function selectMergeStrategySubOptions(): Promise { export async function selectMergeExecution(): Promise { const strategy = await cliux .inquire({ - type: 'list', - name: 'module', choices: [ { name: 'Execute Merge', value: 'both' }, { name: 'Export Merge Summary', value: 'export' }, @@ -141,6 +139,8 @@ export async function selectMergeExecution(): Promise { { name: 'Start Over', value: 'restart' }, ], message: 'What would you like to do?', + name: 'module', + type: 'list', }) .then((name) => name as string) .catch((err) => { @@ -154,8 +154,6 @@ export async function selectMergeExecution(): Promise { export async function selectContentMergePreference(): Promise { const strategy = await cliux .inquire({ - type: 'list', - name: 'module', choices: [ { name: 'Both existing and new', value: 'existing_new' }, { name: 'New only', value: 'new' }, @@ -163,6 +161,8 @@ export async function selectContentMergePreference(): Promise { { name: 'Ask for preference', value: 'ask_preference' }, ], message: 'What content entries do you want to migrate?', + name: 'module', + type: 'list', }) .then((name) => name as string) .catch((err) => { @@ -175,9 +175,9 @@ export async function selectContentMergePreference(): Promise { export async function askExportMergeSummaryPath(): Promise { return await cliux.inquire({ - type: 'input', message: 'Enter the file path to export the summary', name: 'filePath', + type: 'input', validate: inquireRequireFieldValidation, }); } @@ -185,9 +185,9 @@ export async function askExportMergeSummaryPath(): Promise { export async function askMergeComment(): Promise { return await cliux.inquire({ - type: 'input', message: 'Enter a comment for merge', name: 'comment', + type: 'input', validate: inquireRequireFieldValidation, }); } @@ -226,11 +226,6 @@ export async function selectCustomPreferences(module, payload) { } const selectedStrategies = await cliux.inquire({ - type: 'table', - message: `Select the ${startCase(camelCase(module))} changes for merge`, - name: 'mergeContentTypePreferences', - selectAll: true, - pageSize: 10, columns: [ { name: 'Merge Prefer Base', @@ -249,10 +244,15 @@ export async function selectCustomPreferences(module, payload) { value: 'ignore', }, ], + message: `Select the ${startCase(camelCase(module))} changes for merge`, + name: 'mergeContentTypePreferences', + pageSize: 10, rows: tableRows, + selectAll: true, + type: 'table', }); - let updatedArray = []; + const updatedArray = []; forEach(selectedStrategies, (strategy: string, index: number) => { const selectedItem = tableRows[index]; if (strategy && selectedItem) { @@ -267,9 +267,9 @@ export async function selectCustomPreferences(module, payload) { export async function askBranchNameConfirmation(): Promise { return await cliux.inquire({ - type: 'input', message: 'CLI_BRANCH_NAME_CONFIRMATION', name: 'branch_name', + type: 'input', validate: inquireRequireFieldValidation, }); } @@ -298,11 +298,6 @@ export async function selectContentMergeCustomPreferences(payload) { } const selectedStrategies = await cliux.inquire({ - type: 'table', - message: `Select the Content Entry changes for merge`, - name: 'mergeContentEntriesPreferences', - selectAll: true, - pageSize: 10, columns: [ { name: 'Merge New Only', @@ -321,10 +316,15 @@ export async function selectContentMergeCustomPreferences(payload) { value: 'ignore', }, ], + message: `Select the Content Entry changes for merge`, + name: 'mergeContentEntriesPreferences', + pageSize: 10, rows: tableRows, + selectAll: true, + type: 'table', }); - let updatedArray = []; + const updatedArray = []; forEach(selectedStrategies, (strategy: string, index: number) => { const selectedItem = tableRows[index]; diff --git a/packages/contentstack-branches/src/utils/merge-helper.ts b/packages/contentstack-branches/src/utils/merge-helper.ts index 2fbcef286..0d558451e 100644 --- a/packages/contentstack-branches/src/utils/merge-helper.ts +++ b/packages/contentstack-branches/src/utils/merge-helper.ts @@ -1,18 +1,19 @@ -import startCase from 'lodash/startCase'; +import { cliux, managementSDKClient } from '@contentstack/cli-utilities'; import camelCase from 'lodash/camelCase'; +import startCase from 'lodash/startCase'; import path from 'path'; -import { cliux, managementSDKClient } from '@contentstack/cli-utilities'; + import { BranchDiffPayload, MergeSummary } from '../interfaces'; import { + askBaseBranch, askCompareBranch, askStackAPIKey, - askBaseBranch, - getbranchConfig, branchDiffUtility as branchDiff, - writeFile, executeMergeRequest, getMergeQueueStatus, + getbranchConfig, readFile, + writeFile, } from './'; export const prepareMergeRequestPayload = (options) => { @@ -50,12 +51,12 @@ function validateMergeSummary(mergeSummary: MergeSummary) { export const setupMergeInputs = async (mergeFlags) => { if (mergeFlags['use-merge-summary']) { - let mergeSummary: MergeSummary = (await readFile(mergeFlags['use-merge-summary'])) as MergeSummary; + const mergeSummary: MergeSummary = (await readFile(mergeFlags['use-merge-summary'])) as MergeSummary; validateMergeSummary(mergeSummary); mergeFlags.mergeSummary = mergeSummary; } - let { requestPayload: { base_branch = null, compare_branch = null } = {} } = mergeFlags.mergeSummary || {}; + const { requestPayload: { base_branch = null, compare_branch = null } = {} } = mergeFlags.mergeSummary || {}; if (!mergeFlags['stack-api-key']) { mergeFlags['stack-api-key'] = await askStackAPIKey(); @@ -85,12 +86,12 @@ export const setupMergeInputs = async (mergeFlags) => { export const displayBranchStatus = async (options) => { const spinner = cliux.loaderV2('Loading branch differences...'); - let payload: BranchDiffPayload = { - module: '', + const payload: BranchDiffPayload = { apiKey: options.stackAPIKey, baseBranch: options.baseBranch, compareBranch: options.compareBranch, host: options.host, + module: '', }; payload.spinner = spinner; @@ -98,8 +99,8 @@ export const displayBranchStatus = async (options) => { const diffData = branchDiff.filterBranchDiffDataByModule(branchDiffData); cliux.loaderV2('', spinner); - let parsedResponse = {}; - for (let module in diffData) { + const parsedResponse = {}; + for (const module in diffData) { const branchModuleData = diffData[module]; payload.module = module; cliux.print(' '); @@ -125,7 +126,7 @@ export const displayBranchStatus = async (options) => { export const displayMergeSummary = (options) => { cliux.print(' '); cliux.print(`Merge Summary:`, { color: 'yellow' }); - for (let module in options.compareData) { + for (const module in options.compareData) { if (options.format === 'compact-text') { branchDiff.printCompactTextView(options.compareData[module]); } else if (options.format === 'detailed-text') { @@ -135,6 +136,16 @@ export const displayMergeSummary = (options) => { cliux.print(' '); }; +/** + * Executes a merge request and waits for completion with limited polling. + * If the merge is in_progress, polls for status with max 10 retries and exponential backoff. + * Returns immediately if merge is complete, throws error if failed. + * + * @param apiKey - Stack API key + * @param mergePayload - Merge request payload + * @param host - API host + * @returns Promise - Merge response with status and details + */ export const executeMerge = async (apiKey, mergePayload, host): Promise => { const stackAPIClient = await (await managementSDKClient({ host })).stack({ api_key: apiKey }); const mergeResponse = await executeMergeRequest(stackAPIClient, { params: mergePayload }); @@ -147,31 +158,61 @@ export const executeMerge = async (apiKey, mergePayload, host): Promise => } }; -export const fetchMergeStatus = async (stackAPIClient, mergePayload, delay = 5000): Promise => { - return new Promise(async (resolve, reject) => { +/** + * Fetches merge status with retry-limited polling (max 10 attempts) and exponential backoff. + * Returns a structured response on polling timeout instead of throwing an error. + * + * @param stackAPIClient - The stack API client for making requests + * @param mergePayload - The merge payload containing the UID + * @param initialDelay - Initial delay between retries in milliseconds (default: 5000ms) + * @param maxRetries - Maximum number of retry attempts (default: 10) + * @returns Promise - Merge response object with optional pollingTimeout flag + */ +export const fetchMergeStatus = async ( + stackAPIClient, + mergePayload, + initialDelay = 5000, + maxRetries = 10000, // Temporary making infinite polling to unblock the users +): Promise => { + let delayMs = initialDelay; + const maxDelayMs = 60000; // Cap delay at 60 seconds + + for (let attempt = 1; attempt <= maxRetries; attempt++) { const mergeStatusResponse = await getMergeQueueStatus(stackAPIClient, { uid: mergePayload.uid }); if (mergeStatusResponse?.queue?.length >= 1) { const mergeRequestStatusResponse = mergeStatusResponse.queue[0]; const mergeStatus = mergeRequestStatusResponse.merge_details?.status; + if (mergeStatus === 'complete') { - resolve(mergeRequestStatusResponse); + return mergeRequestStatusResponse; } else if (mergeStatus === 'in-progress' || mergeStatus === 'in_progress') { - setTimeout(async () => { - await fetchMergeStatus(stackAPIClient, mergePayload, delay).then(resolve).catch(reject); - }, delay); + if (attempt < maxRetries) { + cliux.print(`Merge in progress... (Attempt ${attempt}/${maxRetries})`, { color: 'grey' }); + await new Promise((resolve) => setTimeout(resolve, delayMs)); + delayMs = Math.min(delayMs + 1000, maxDelayMs); + } else { + // Polling timeout: return structured response instead of throwing + cliux.print(`Merge in progress... (Attempt ${attempt}/${maxRetries})`, { color: 'grey' }); + return { + merge_details: mergeRequestStatusResponse.merge_details, + pollingTimeout: true, + status: 'in_progress', + uid: mergePayload.uid, + }; + } } else if (mergeStatus === 'failed') { if (mergeRequestStatusResponse?.errors?.length > 0) { const errorPath = path.join(process.cwd(), 'merge-error.log'); await writeFile(errorPath, mergeRequestStatusResponse.errors); cliux.print(`\nComplete error log can be found in ${path.resolve(errorPath)}`, { color: 'grey' }); } - return reject(`merge uid: ${mergePayload.uid}`); + throw new Error(`merge uid: ${mergePayload.uid}`); } else { - return reject(`Invalid merge status found with merge ID ${mergePayload.uid}`); + throw new Error(`Invalid merge status found with merge ID ${mergePayload.uid}`); } } else { - return reject(`No queue found with merge ID ${mergePayload.uid}`); + throw new Error(`No queue found with merge ID ${mergePayload.uid}`); } - }); + } }; diff --git a/packages/contentstack-branches/src/utils/merge-status-helper.ts b/packages/contentstack-branches/src/utils/merge-status-helper.ts new file mode 100644 index 000000000..4f4d4f83c --- /dev/null +++ b/packages/contentstack-branches/src/utils/merge-status-helper.ts @@ -0,0 +1,165 @@ +import { cliux } from '@contentstack/cli-utilities'; + +import { getMergeQueueStatus } from './'; + +/** + * Maps merge status to a user-friendly message with visual indicator. + * @param status - The merge status (complete, in_progress, failed, or unknown) + * @returns User-friendly status message + */ +export const getMergeStatusMessage = (status: string): string => { + switch (status) { + case 'complete': + return '✅ Merge completed successfully'; + case 'in_progress': + case 'in-progress': + return '⏳ Merge is still processing'; + case 'failed': + return '❌ Merge failed'; + default: + return '⚠️ Unknown status'; + } +}; + +/** + * Formats and displays merge status details in a user-friendly format. + * Shows merge metadata, summary statistics, and errors if present. + * @param mergeResponse - The merge response object containing status details + */ +export const displayMergeStatusDetails = (mergeResponse: any): void => { + if (!mergeResponse) { + cliux.print('No merge information available', { color: 'yellow' }); + return; + } + + const { errors = [], merge_details = {}, merge_summary = {}, uid } = mergeResponse; + const status = merge_details.status || 'unknown'; + const statusMessage = getMergeStatusMessage(status); + + const statusColor = getStatusColor(status); + + cliux.print(' '); + cliux.print(`${statusMessage}`, { color: statusColor }); + + cliux.print(' '); + cliux.print('Merge Details:', { color: 'cyan' }); + cliux.print(` ├─ Merge UID: ${uid}`, { color: 'grey' }); + + if (merge_details.created_at) { + cliux.print(` ├─ Created: ${merge_details.created_at}`, { color: 'grey' }); + } + + if (merge_details.updated_at) { + cliux.print(` ├─ Updated: ${merge_details.updated_at}`, { color: 'grey' }); + } + + if (merge_details.completed_at && status === 'complete') { + cliux.print(` ├─ Completed: ${merge_details.completed_at}`, { color: 'grey' }); + } + + if (merge_details.completion_percentage !== undefined && status === 'in_progress') { + cliux.print(` ├─ Progress: ${merge_details.completion_percentage}%`, { color: 'grey' }); + } + + const statusIndicator = status === 'complete' ? ' ✓' : ''; + cliux.print(` └─ Status: ${status}${statusIndicator}`, { color: 'grey' }); + + displayMergeSummary(merge_summary); + displayMergeErrors(errors); + + cliux.print(' '); +}; + +/** + * Gets the appropriate color for the status message + * @param status - The merge status + * @returns The color name (green, red, or yellow) + */ +const getStatusColor = (status: string): 'green' | 'red' | 'yellow' => { + if (status === 'complete') return 'green'; + if (status === 'failed') return 'red'; + return 'yellow'; +}; + +/** + * Displays the merge summary statistics + * @param merge_summary - The merge summary object containing content types and global fields stats + */ +const displayMergeSummary = (merge_summary: any): void => { + if (!merge_summary || (!merge_summary.content_types && !merge_summary.global_fields)) { + return; + } + + cliux.print(' '); + cliux.print('Summary:', { color: 'cyan' }); + + if (merge_summary.content_types) { + const ct = merge_summary.content_types; + const added = ct.added || 0; + const modified = ct.modified || 0; + const deleted = ct.deleted || 0; + cliux.print(` ├─ Content Types: +${added}, ~${modified}, -${deleted}`, { color: 'grey' }); + } + + if (merge_summary.global_fields) { + const gf = merge_summary.global_fields; + const added = gf.added || 0; + const modified = gf.modified || 0; + const deleted = gf.deleted || 0; + cliux.print(` └─ Global Fields: +${added}, ~${modified}, -${deleted}`, { color: 'grey' }); + } +}; + +/** + * Displays merge errors if any exist + * @param errors - Array of error objects to display + */ +const displayMergeErrors = (errors: any[]): void => { + if (!errors || errors.length === 0) { + return; + } + + cliux.print(' '); + cliux.print('Errors:', { color: 'red' }); + errors.forEach((error, index) => { + const isLast = index === errors.length - 1; + const prefix = isLast ? '└─' : '├─'; + cliux.print(` ${prefix} ${error.message || error}`, { color: 'grey' }); + }); +}; + +/** + * Fetches merge status and extracts content type data for script generation. + * Validates that the merge status is 'complete' before returning content type data. + * @param stackAPIClient - The stack API client for making requests + * @param mergeUID - The merge job UID + * @returns Promise - Merge status response with content type data or error + */ +export const getMergeStatusWithContentTypes = async ( + stackAPIClient, + mergeUID: string +): Promise => { + try { + const mergeStatusResponse = await getMergeQueueStatus(stackAPIClient, { uid: mergeUID }); + + if (!mergeStatusResponse?.queue?.length) { + throw new Error(`No merge job found with UID: ${mergeUID}`); + } + + const mergeRequestStatusResponse = mergeStatusResponse.queue[0]; + const mergeStatus = mergeRequestStatusResponse.merge_details?.status; + + if (mergeStatus !== 'complete') { + return { + error: `Merge job is not complete. Current status: ${mergeStatus}`, + merge_details: mergeRequestStatusResponse.merge_details, + status: mergeStatus, + uid: mergeUID, + }; + } + + return mergeRequestStatusResponse; + } catch (error) { + throw new Error(`Failed to fetch merge status: ${error.message || error}`); + } +}; diff --git a/packages/contentstack-branches/test/unit/commands/cm/branches/merge-status.test.ts b/packages/contentstack-branches/test/unit/commands/cm/branches/merge-status.test.ts new file mode 100644 index 000000000..a43a6d66f --- /dev/null +++ b/packages/contentstack-branches/test/unit/commands/cm/branches/merge-status.test.ts @@ -0,0 +1,49 @@ +import { describe, it, beforeEach, afterEach } from 'mocha'; +import { expect } from 'chai'; +import { stub } from 'sinon'; +import { cliux } from '@contentstack/cli-utilities'; +import BranchMergeStatusCommand from '../../../../../src/commands/cm/branches/merge-status'; +import * as utils from '../../../../../src/utils'; + +describe('Merge Status Command', () => { + let printStub; + let loaderStub; + let isAuthenticatedStub; + let managementSDKClientStub; + let displayMergeStatusDetailsStub; + + beforeEach(() => { + printStub = stub(cliux, 'print'); + loaderStub = stub(cliux, 'loaderV2').returns('spinner'); + isAuthenticatedStub = stub().returns(true); + managementSDKClientStub = stub(); + displayMergeStatusDetailsStub = stub(utils, 'displayMergeStatusDetails'); + }); + + afterEach(() => { + printStub.restore(); + loaderStub.restore(); + isAuthenticatedStub.restore(); + managementSDKClientStub.restore(); + displayMergeStatusDetailsStub.restore(); + }); + + it('should have correct description', () => { + expect(BranchMergeStatusCommand.description).to.equal('Check the status of a branch merge job'); + }); + + it('should have correct usage', () => { + expect(BranchMergeStatusCommand.usage).to.equal('cm:branches:merge-status -k --merge-uid '); + }); + + it('should have example command', () => { + expect(BranchMergeStatusCommand.examples.length).to.be.greaterThan(0); + expect(BranchMergeStatusCommand.examples[0]).to.include('merge-status'); + expect(BranchMergeStatusCommand.examples[0]).to.include('merge_abc123'); + }); + + it('should have required flags', () => { + expect(BranchMergeStatusCommand.flags['stack-api-key'].required).to.be.true; + expect(BranchMergeStatusCommand.flags['merge-uid'].required).to.be.true; + }); +}); diff --git a/packages/contentstack-branches/test/unit/utils/merge-status-helper.test.ts b/packages/contentstack-branches/test/unit/utils/merge-status-helper.test.ts new file mode 100644 index 000000000..61bc2e047 --- /dev/null +++ b/packages/contentstack-branches/test/unit/utils/merge-status-helper.test.ts @@ -0,0 +1,229 @@ +import { describe, it, beforeEach, afterEach } from 'mocha'; +import { expect } from 'chai'; +import { stub } from 'sinon'; +import { cliux } from '@contentstack/cli-utilities'; +import { displayMergeStatusDetails, getMergeStatusMessage, getMergeStatusWithContentTypes } from '../../../../../src/utils/merge-status-helper'; +import * as utils from '../../../../../src/utils'; + +describe('Merge Status Helper', () => { + let printStub; + + beforeEach(() => { + printStub = stub(cliux, 'print'); + }); + + afterEach(() => { + printStub.restore(); + }); + + describe('getMergeStatusMessage', () => { + it('should return complete status message for complete status', () => { + const message = getMergeStatusMessage('complete'); + expect(message).to.equal('✅ Merge completed successfully'); + }); + + it('should return in_progress status message for in_progress status', () => { + const message = getMergeStatusMessage('in_progress'); + expect(message).to.equal('⏳ Merge is still processing'); + }); + + it('should return in_progress status message for in-progress status', () => { + const message = getMergeStatusMessage('in-progress'); + expect(message).to.equal('⏳ Merge is still processing'); + }); + + it('should return failed status message for failed status', () => { + const message = getMergeStatusMessage('failed'); + expect(message).to.equal('❌ Merge failed'); + }); + + it('should return unknown status message for unknown status', () => { + const message = getMergeStatusMessage('unknown'); + expect(message).to.equal('⚠️ Unknown status'); + }); + }); + + describe('displayMergeStatusDetails', () => { + it('should display merge status details for completed merge', () => { + const mergeResponse = { + uid: 'merge_123', + merge_details: { + status: 'complete', + created_at: '2024-01-01T10:00:00Z', + updated_at: '2024-01-01T10:30:00Z', + completed_at: '2024-01-01T10:30:00Z', + }, + merge_summary: { + content_types: { added: 2, modified: 3, deleted: 1 }, + global_fields: { added: 0, modified: 1, deleted: 0 }, + }, + errors: [], + }; + + displayMergeStatusDetails(mergeResponse); + + expect(printStub.called).to.be.true; + const calls = printStub.getCalls(); + const printed = calls.map((c) => c.args[0]).join(' '); + expect(printed).to.include('merge_123'); + expect(printed).to.include('complete'); + }); + + it('should display merge status details for in-progress merge', () => { + const mergeResponse = { + uid: 'merge_456', + merge_details: { + status: 'in_progress', + created_at: '2024-01-01T10:00:00Z', + updated_at: '2024-01-01T10:15:00Z', + completion_percentage: 60, + }, + merge_summary: { + content_types: { added: 1, modified: 2, deleted: 0 }, + global_fields: { added: 0, modified: 0, deleted: 0 }, + }, + errors: [], + }; + + displayMergeStatusDetails(mergeResponse); + + expect(printStub.called).to.be.true; + const calls = printStub.getCalls(); + const printed = calls.map((c) => c.args[0]).join(' '); + expect(printed).to.include('merge_456'); + expect(printed).to.include('in_progress'); + expect(printed).to.include('60'); + }); + + it('should display merge status details with errors', () => { + const mergeResponse = { + uid: 'merge_789', + merge_details: { + status: 'failed', + created_at: '2024-01-01T10:00:00Z', + updated_at: '2024-01-01T10:20:00Z', + }, + merge_summary: { + content_types: { added: 0, modified: 0, deleted: 0 }, + global_fields: { added: 0, modified: 0, deleted: 0 }, + }, + errors: [{ message: 'Content type conflict' }, { message: 'Field mismatch' }], + }; + + displayMergeStatusDetails(mergeResponse); + + expect(printStub.called).to.be.true; + const calls = printStub.getCalls(); + const printed = calls.map((c) => c.args[0]).join(' '); + expect(printed).to.include('merge_789'); + expect(printed).to.include('failed'); + expect(printed).to.include('conflict'); + }); + + it('should handle null merge response gracefully', () => { + displayMergeStatusDetails(null); + + expect(printStub.called).to.be.true; + const calls = printStub.getCalls(); + expect(calls[0].args[0]).to.equal('No merge information available'); + }); + + it('should handle undefined merge response gracefully', () => { + displayMergeStatusDetails(undefined); + + expect(printStub.called).to.be.true; + const calls = printStub.getCalls(); + expect(calls[0].args[0]).to.equal('No merge information available'); + }); + }); + + describe('getMergeStatusWithContentTypes', () => { + let getMergeQueueStatusStub; + + beforeEach(() => { + getMergeQueueStatusStub = stub(utils, 'getMergeQueueStatus'); + }); + + afterEach(() => { + getMergeQueueStatusStub.restore(); + }); + + it('should return merge response when merge is complete', async () => { + const mockMergeResponse = { + queue: [ + { + uid: 'merge_complete', + merge_details: { status: 'complete' }, + content_types: { added: [], modified: [], deleted: [] }, + }, + ], + }; + + getMergeQueueStatusStub.resolves(mockMergeResponse); + + const result = await getMergeStatusWithContentTypes({}, 'merge_complete'); + + expect(result.uid).to.equal('merge_complete'); + expect(result.merge_details.status).to.equal('complete'); + }); + + it('should return error when merge is in_progress', async () => { + const mockMergeResponse = { + queue: [ + { + uid: 'merge_inprogress', + merge_details: { status: 'in_progress' }, + }, + ], + }; + + getMergeQueueStatusStub.resolves(mockMergeResponse); + + const result = await getMergeStatusWithContentTypes({}, 'merge_inprogress'); + + expect(result.error).to.exist; + expect(result.error).to.include('not complete'); + expect(result.status).to.equal('in_progress'); + }); + + it('should return error when merge is failed', async () => { + const mockMergeResponse = { + queue: [ + { + uid: 'merge_failed', + merge_details: { status: 'failed' }, + }, + ], + }; + + getMergeQueueStatusStub.resolves(mockMergeResponse); + + const result = await getMergeStatusWithContentTypes({}, 'merge_failed'); + + expect(result.error).to.exist; + expect(result.error).to.include('not complete'); + }); + + it('should throw error when no queue found', async () => { + getMergeQueueStatusStub.resolves({ queue: [] }); + + try { + await getMergeStatusWithContentTypes({}, 'merge_notfound'); + expect.fail('Should have thrown error'); + } catch (error) { + expect(error.message).to.include('No merge job found'); + } + }); + + it('should throw error when response is invalid', async () => { + getMergeQueueStatusStub.resolves(null); + + try { + await getMergeStatusWithContentTypes({}, 'merge_invalid'); + expect.fail('Should have thrown error'); + } catch (error) { + expect(error.message).to.include('No merge job found'); + } + }); + }); +}); diff --git a/packages/contentstack-clone/package.json b/packages/contentstack-clone/package.json index 594e1d855..1f8a440cd 100644 --- a/packages/contentstack-clone/package.json +++ b/packages/contentstack-clone/package.json @@ -21,7 +21,7 @@ "rimraf": "^6.1.0" }, "devDependencies": { - "@oclif/test": "^4.1.13", + "@oclif/test": "^4.1.18", "@types/chai": "^4.3.0", "@types/mocha": "^10.0.0", "@types/node": "^18.11.9", diff --git a/packages/contentstack-export-to-csv/package.json b/packages/contentstack-export-to-csv/package.json index 9bbeb27ea..4ad451a09 100644 --- a/packages/contentstack-export-to-csv/package.json +++ b/packages/contentstack-export-to-csv/package.json @@ -12,7 +12,7 @@ "fast-csv": "^4.3.6" }, "devDependencies": { - "@oclif/test": "^4.1.13", + "@oclif/test": "^4.1.18", "@types/chai": "^4.3.20", "@types/inquirer": "^9.0.8", "@types/mocha": "^10.0.10", diff --git a/packages/contentstack-export/package.json b/packages/contentstack-export/package.json index d9722ddd6..3b8470629 100644 --- a/packages/contentstack-export/package.json +++ b/packages/contentstack-export/package.json @@ -19,14 +19,14 @@ "mkdirp": "^1.0.4", "progress-stream": "^2.0.0", "promise-limit": "^2.7.0", - "winston": "^3.17.0" + "winston": "^3.19.0" }, "devDependencies": { "@contentstack/cli-auth": "~2.0.0-beta.9", "@contentstack/cli-config": "~2.0.0-beta.5", "@contentstack/cli-dev-dependencies": "~2.0.0-beta.0", "@oclif/plugin-help": "^6.2.28", - "@oclif/test": "^4.1.13", + "@oclif/test": "^4.1.18", "@types/big-json": "^3.2.5", "@types/chai": "^4.3.11", "@types/mkdirp": "^1.0.2", diff --git a/packages/contentstack-import-setup/package.json b/packages/contentstack-import-setup/package.json index be6fc7105..7b2e032ef 100644 --- a/packages/contentstack-import-setup/package.json +++ b/packages/contentstack-import-setup/package.json @@ -7,6 +7,7 @@ "dependencies": { "@contentstack/cli-command": "~2.0.0-beta.6", "@contentstack/cli-utilities": "~2.0.0-beta.7", + "@contentstack/cli-asset-management": "~1.0.0", "@oclif/core": "^4.3.0", "big-json": "^3.2.0", "chalk": "^5.6.2", @@ -14,7 +15,7 @@ "lodash": "^4.18.1", "merge": "^2.1.1", "mkdirp": "^1.0.4", - "winston": "^3.17.0" + "winston": "^3.19.0" }, "devDependencies": { "@types/big-json": "^3.2.5", diff --git a/packages/contentstack-import-setup/src/config/index.ts b/packages/contentstack-import-setup/src/config/index.ts index ea7d40b0f..82c0b66b5 100644 --- a/packages/contentstack-import-setup/src/config/index.ts +++ b/packages/contentstack-import-setup/src/config/index.ts @@ -40,6 +40,41 @@ const config: DefaultConfig = { fileName: 'assets.json', fetchConcurrency: 5, }, + 'asset-management': { + dependencies: [], + dirName: 'spaces', + fieldsDir: 'fields', + assetTypesDir: 'asset_types', + fieldsFileName: 'fields.json', + assetTypesFileName: 'asset-types.json', + foldersFileName: 'folders.json', + assetsFileName: 'assets.json', + fieldsImportInvalidKeys: [ + 'created_at', + 'created_by', + 'updated_at', + 'updated_by', + 'is_system', + 'asset_types_count', + ], + assetTypesImportInvalidKeys: [ + 'created_at', + 'created_by', + 'updated_at', + 'updated_by', + 'is_system', + 'category', + 'preview_image_url', + 'category_detail', + ], + mapperRootDir: 'mapper', + mapperAssetsModuleDir: 'assets', + mapperUidFileName: 'uid-mapping.json', + mapperUrlFileName: 'url-mapping.json', + mapperSpaceUidFileName: 'space-uid-mapping.json', + uploadAssetsConcurrency: 2, + importFoldersConcurrency: 1, + }, 'content-types': { dirName: 'content_types', fileName: 'content_types.json', diff --git a/packages/contentstack-import-setup/src/constants/path-constants.ts b/packages/contentstack-import-setup/src/constants/path-constants.ts new file mode 100644 index 000000000..b72225bff --- /dev/null +++ b/packages/contentstack-import-setup/src/constants/path-constants.ts @@ -0,0 +1,12 @@ +/** Aligned with contentstack-import `PATH_CONSTANTS` for AM mapper defaults. */ +export const PATH_CONSTANTS = { + MAPPER: 'mapper', + FILES: { + UID_MAPPING: 'uid-mapping.json', + URL_MAPPING: 'url-mapping.json', + SPACE_UID_MAPPING: 'space-uid-mapping.json', + }, + MAPPER_MODULES: { + ASSETS: 'assets', + }, +} as const; diff --git a/packages/contentstack-import-setup/src/import/import-setup.ts b/packages/contentstack-import-setup/src/import/import-setup.ts index 54bcaed71..03d915ef2 100644 --- a/packages/contentstack-import-setup/src/import/import-setup.ts +++ b/packages/contentstack-import-setup/src/import/import-setup.ts @@ -50,10 +50,11 @@ export default class ImportSetup { * @returns {Promise>} */ protected async generateDependencyTree() { - type ModulesKey = keyof typeof this.config.modules; const visited: Set = new Set(); const assignedDependencies: Set = new Set(); // Track assigned dependencies + type ModulesKey = keyof typeof this.config.modules; + const getAllDependencies = (module: ModulesKey): Modules[] => { if (visited.has(module)) return []; @@ -130,10 +131,14 @@ export default class ImportSetup { */ async start() { try { - if (!this.config.management_token) { + const needsStackFetch = + !this.config.management_token || + (this.config.assetManagementEnabled && !this.config.org_uid); + + if (needsStackFetch) { const stackDetails: Record = await this.stackAPIClient.fetch(); - this.config.stackName = stackDetails.name as string; - this.config.org_uid = stackDetails.org_uid as string; + this.config.stackName = (stackDetails.name as string) ?? this.config.stackName; + this.config.org_uid = (stackDetails.org_uid as string) ?? this.config.org_uid; } this.log.debug('Creating backup directory'); diff --git a/packages/contentstack-import-setup/src/import/modules/assets.ts b/packages/contentstack-import-setup/src/import/modules/assets.ts index c727a0ab3..efb08a25a 100644 --- a/packages/contentstack-import-setup/src/import/modules/assets.ts +++ b/packages/contentstack-import-setup/src/import/modules/assets.ts @@ -1,8 +1,9 @@ -import { log, fsUtil } from '../../utils'; import { join } from 'path'; -import { AssetRecord, ImportConfig, ModuleClassParams } from '../../types'; import { isEmpty, orderBy, values } from 'lodash'; +import { ImportSetupAssetMappers } from '@contentstack/cli-asset-management'; import { formatError, FsUtility, sanitizePath } from '@contentstack/cli-utilities'; +import { buildImportSetupAssetMapperParams, log, fsUtil } from '../../utils'; +import { AssetRecord, ImportConfig, ModuleClassParams } from '../../types'; import BaseImportSetup from './base-setup'; import { MODULE_NAMES, MODULE_CONTEXTS, PROCESS_NAMES, PROCESS_STATUS } from '../../utils'; @@ -40,8 +41,36 @@ export default class AssetImportSetup extends BaseImportSetup { */ async start() { try { + if (this.config.assetManagementEnabled) { + const assetManagementUrl = this.config.assetManagementUrl ?? this.config.region?.assetManagementUrl; + if (!assetManagementUrl) { + log( + this.config, + 'AM 2.0 export detected but assetManagementUrl is not configured in the region settings. Skipping AM 2.0 asset mapper setup.', + 'info', + ); + return; + } + if (!this.config.org_uid) { + log(this.config, 'Cannot run Asset Management import-setup: organization UID is missing.', 'error'); + return; + } + const progress = this.createNestedProgress(this.currentModuleName); + const mappers = new ImportSetupAssetMappers( + buildImportSetupAssetMapperParams(this.config, assetManagementUrl), + ); + mappers.setParentProgressManager(progress); + const result = await mappers.start(); + if (result.kind === 'success') { + this.completeProgress(true); + } else if (result.kind === 'error') { + this.completeProgress(false, result.errorMessage); + } + return; + } + const progress = this.createNestedProgress(this.currentModuleName); - + // Analyze to get chunk count const indexerCount = await this.withLoadingSpinner('ASSETS: Analyzing import data...', async () => { const basePath = this.assetsFolderPath; @@ -63,10 +92,7 @@ export default class AssetImportSetup extends BaseImportSetup { // Create mapper directory progress .startProcess(PROCESS_NAMES.ASSETS_MAPPER_GENERATION) - .updateStatus( - PROCESS_STATUS.ASSETS_MAPPER_GENERATION.GENERATING, - PROCESS_NAMES.ASSETS_MAPPER_GENERATION, - ); + .updateStatus(PROCESS_STATUS.ASSETS_MAPPER_GENERATION.GENERATING, PROCESS_NAMES.ASSETS_MAPPER_GENERATION); fsUtil.makeDirectory(this.mapperDirPath); this.progressManager?.tick(true, 'mapper directory created', null, PROCESS_NAMES.ASSETS_MAPPER_GENERATION); progress.completeProcess(PROCESS_NAMES.ASSETS_MAPPER_GENERATION, true); @@ -74,10 +100,7 @@ export default class AssetImportSetup extends BaseImportSetup { // Fetch and map assets progress .startProcess(PROCESS_NAMES.ASSETS_FETCH_AND_MAP) - .updateStatus( - PROCESS_STATUS.ASSETS_FETCH_AND_MAP.FETCHING, - PROCESS_NAMES.ASSETS_FETCH_AND_MAP, - ); + .updateStatus(PROCESS_STATUS.ASSETS_FETCH_AND_MAP.FETCHING, PROCESS_NAMES.ASSETS_FETCH_AND_MAP); await this.fetchAndMapAssets(); progress.completeProcess(PROCESS_NAMES.ASSETS_FETCH_AND_MAP, true); diff --git a/packages/contentstack-import-setup/src/types/default-config.ts b/packages/contentstack-import-setup/src/types/default-config.ts index 364f92933..e89fd3675 100644 --- a/packages/contentstack-import-setup/src/types/default-config.ts +++ b/packages/contentstack-import-setup/src/types/default-config.ts @@ -30,6 +30,25 @@ export default interface DefaultConfig { dependencies?: Modules[]; fetchConcurrency: number; }; + 'asset-management': { + dirName: string; + fieldsDir: string; + assetTypesDir: string; + fieldsFileName: string; + assetTypesFileName: string; + foldersFileName: string; + assetsFileName: string; + fieldsImportInvalidKeys: string[]; + assetTypesImportInvalidKeys: string[]; + mapperRootDir: string; + mapperAssetsModuleDir: string; + mapperUidFileName: string; + mapperUrlFileName: string; + mapperSpaceUidFileName: string; + uploadAssetsConcurrency: number; + importFoldersConcurrency: number; + dependencies?: Modules[]; + }; 'content-types': { dirName: string; fileName: string; diff --git a/packages/contentstack-import-setup/src/types/import-config.ts b/packages/contentstack-import-setup/src/types/import-config.ts index f767e460c..b966e86ac 100644 --- a/packages/contentstack-import-setup/src/types/import-config.ts +++ b/packages/contentstack-import-setup/src/types/import-config.ts @@ -48,6 +48,10 @@ export default interface ImportConfig extends DefaultConfig, ExternalConfig { createBackupDir?: string; region: any; authenticationMethod?: string; + /** Set when export layout is Asset Management (`spaces/` + stack settings key `am_v2`). */ + assetManagementEnabled?: boolean; + /** AM 2.0 base URL from region / detection (`detectAssetManagementExportFromContentDir`). */ + assetManagementUrl?: string; } type branch = { diff --git a/packages/contentstack-import-setup/src/utils/import-config-handler.ts b/packages/contentstack-import-setup/src/utils/import-config-handler.ts index 6c2768177..102ae61b4 100644 --- a/packages/contentstack-import-setup/src/utils/import-config-handler.ts +++ b/packages/contentstack-import-setup/src/utils/import-config-handler.ts @@ -1,6 +1,7 @@ import merge from 'merge'; import * as path from 'path'; import { configHandler, isAuthenticated, cliux, sanitizePath } from '@contentstack/cli-utilities'; +import { detectAssetManagementExportFromContentDir } from '@contentstack/cli-asset-management'; import defaultConfig from '../config'; import { askContentDir, askAPIKey, askSelectedModules } from './interactive'; import login from './login-handler'; @@ -69,6 +70,15 @@ const setupConfig = async (importCmdFlags: any): Promise => { //Note to support the old key config.source_stack = config.apiKey; + const assetManagementExport = detectAssetManagementExportFromContentDir(config.contentDir); + if (assetManagementExport.assetManagementEnabled) { + config.assetManagementEnabled = true; + config.assetManagementUrl = assetManagementExport.assetManagementUrl; + if (assetManagementExport.source_stack) { + config.source_stack = assetManagementExport.source_stack; + } + } + // config.skipAudit = importCmdFlags['skip-audit']; // config.forceStopMarketplaceAppsPrompt = importCmdFlags.yes; // config.importWebhookStatus = importCmdFlags['import-webhook-status']; diff --git a/packages/contentstack-import-setup/src/utils/import-setup-asset-mapper-params.ts b/packages/contentstack-import-setup/src/utils/import-setup-asset-mapper-params.ts new file mode 100644 index 000000000..9b8f1c6ad --- /dev/null +++ b/packages/contentstack-import-setup/src/utils/import-setup-asset-mapper-params.ts @@ -0,0 +1,44 @@ +import { sanitizePath } from '@contentstack/cli-utilities'; +import type { RunAssetMapperImportSetupParams } from '@contentstack/cli-asset-management'; + +import { PATH_CONSTANTS } from '../constants/path-constants'; +import type ImportConfig from '../types/import-config'; + +/** + * Maps import-setup `ImportConfig` and AM base URL into `RunAssetMapperImportSetupParams` + * (parallel to contentstack-import `buildImportSpacesOptions` → `ImportSpaces`). + */ +export function buildImportSetupAssetMapperParams( + config: ImportConfig, + assetManagementUrl: string, +): RunAssetMapperImportSetupParams { + const am = config.modules['asset-management']; + + return { + contentDir: sanitizePath(config.contentDir), + mapperBaseDir: sanitizePath(config.backupDir), + assetManagementUrl, + org_uid: config.org_uid, + source_stack: config.source_stack, + apiKey: config.apiKey, + host: config.region?.cma ?? config.host ?? '', + context: config.context as unknown as Record, + apiConcurrency: config.fetchConcurrency, + uploadAssetsConcurrency: am?.uploadAssetsConcurrency, + importFoldersConcurrency: am?.importFoldersConcurrency, + spacesDirName: am?.dirName, + fieldsDir: am?.fieldsDir, + assetTypesDir: am?.assetTypesDir, + fieldsFileName: am?.fieldsFileName, + assetTypesFileName: am?.assetTypesFileName, + foldersFileName: am?.foldersFileName, + assetsFileName: am?.assetsFileName, + fieldsImportInvalidKeys: am?.fieldsImportInvalidKeys, + assetTypesImportInvalidKeys: am?.assetTypesImportInvalidKeys, + mapperRootDir: am?.mapperRootDir ?? PATH_CONSTANTS.MAPPER, + mapperAssetsModuleDir: am?.mapperAssetsModuleDir ?? PATH_CONSTANTS.MAPPER_MODULES.ASSETS, + mapperUidFileName: am?.mapperUidFileName ?? PATH_CONSTANTS.FILES.UID_MAPPING, + mapperUrlFileName: am?.mapperUrlFileName ?? PATH_CONSTANTS.FILES.URL_MAPPING, + mapperSpaceUidFileName: am?.mapperSpaceUidFileName ?? PATH_CONSTANTS.FILES.SPACE_UID_MAPPING, + }; +} diff --git a/packages/contentstack-import-setup/src/utils/index.ts b/packages/contentstack-import-setup/src/utils/index.ts index de35f7202..22668fde5 100644 --- a/packages/contentstack-import-setup/src/utils/index.ts +++ b/packages/contentstack-import-setup/src/utils/index.ts @@ -8,3 +8,4 @@ export * from './log'; export * from './common-helper'; export { setupBranchConfig } from './setup-branch'; export { MODULE_CONTEXTS, MODULE_NAMES, PROCESS_NAMES, PROCESS_STATUS } from './constants'; +export { buildImportSetupAssetMapperParams } from './import-setup-asset-mapper-params'; diff --git a/packages/contentstack-import-setup/test/unit/import-config-handler.test.ts b/packages/contentstack-import-setup/test/unit/import-config-handler.test.ts index 4c1bce0f9..6778f2315 100644 --- a/packages/contentstack-import-setup/test/unit/import-config-handler.test.ts +++ b/packages/contentstack-import-setup/test/unit/import-config-handler.test.ts @@ -4,6 +4,7 @@ import * as os from 'os'; import * as fs from 'fs'; import { stub, restore, SinonStub } from 'sinon'; import * as utilities from '@contentstack/cli-utilities'; +import * as cliAm from '@contentstack/cli-asset-management'; import setupConfig from '../../src/utils/import-config-handler'; import * as interactive from '../../src/utils/interactive'; @@ -150,4 +151,27 @@ describe('Import Config Handler', () => { expect(askSelectedModulesStub.calledOnce).to.be.true; expect(config.selectedModules).to.deep.equal(['entries']); }); + + it('should merge Asset Management export flags from detectAssetManagementExportFromContentDir into config', async () => { + const detectStub = stub(cliAm, 'detectAssetManagementExportFromContentDir').returns({ + assetManagementEnabled: true, + source_stack: 'branch-source-key', + assetManagementUrl: 'https://am.example.com', + }); + + try { + const config = await setupConfig({ + 'data-dir': contentDir, + 'stack-api-key': 'target-key', + module: ['assets'], + }); + + expect(detectStub.calledOnce).to.be.true; + expect(config.assetManagementEnabled).to.equal(true); + expect(config.assetManagementUrl).to.equal('https://am.example.com'); + expect(config.source_stack).to.equal('branch-source-key'); + } finally { + detectStub.restore(); + } + }); }); diff --git a/packages/contentstack-import-setup/test/unit/import-setup.test.ts b/packages/contentstack-import-setup/test/unit/import-setup.test.ts index e556cf6aa..27c5b43b8 100644 --- a/packages/contentstack-import-setup/test/unit/import-setup.test.ts +++ b/packages/contentstack-import-setup/test/unit/import-setup.test.ts @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { stub, restore, SinonStub } from 'sinon'; import ImportSetup, { ImportSetupDeps } from '../../src/import/import-setup'; +import defaultConfig from '../../src/config'; import { ImportConfig, Modules } from '../../src/types'; describe('ImportSetup', () => { @@ -31,6 +32,7 @@ describe('ImportSetup', () => { backupDir: '', region: 'us', modules: { + ...defaultConfig.modules, extensions: { dirName: 'extensions', fileName: 'extensions', diff --git a/packages/contentstack-import-setup/test/unit/modules/assets.test.ts b/packages/contentstack-import-setup/test/unit/modules/assets.test.ts index bde556ac7..9d1b6d826 100644 --- a/packages/contentstack-import-setup/test/unit/modules/assets.test.ts +++ b/packages/contentstack-import-setup/test/unit/modules/assets.test.ts @@ -1,11 +1,15 @@ import { expect } from 'chai'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; import { stub, restore, SinonStub } from 'sinon'; +import * as amPkg from '@contentstack/cli-asset-management'; import AssetImportSetup from '../../../src/import/modules/assets'; import * as loggerModule from '../../../src/utils/logger'; import * as fsUtilModule from '../../../src/utils/file-helper'; import { ImportConfig } from '../../../src/types'; -import * as path from 'path'; import { sanitizePath } from '@contentstack/cli-utilities'; +import defaultConfig from '../../../src/config'; describe('AssetImportSetup', () => { let assetSetup: AssetImportSetup; @@ -28,6 +32,7 @@ describe('AssetImportSetup', () => { fetchConcurrency: 2, writeConcurrency: 1, modules: { + ...defaultConfig.modules, assets: { fetchConcurrency: 2, dirName: 'assets', @@ -137,3 +142,188 @@ describe('AssetImportSetup', () => { // // expect(logStub.calledWith(baseConfig, sinon.match.string, 'info')).to.be.true; // }); }); + +describe('AssetImportSetup Asset Management export', () => { + let mockStackAPIClient: any; + let mapperStartStub: SinonStub; + let setParentProgressStub: SinonStub; + let amContentDir: string; + let backupDir: string; + let lastMapperParams: Record | undefined; + + beforeEach(() => { + restore(); + + amContentDir = path.join(os.tmpdir(), `am-import-setup-${Date.now()}`); + backupDir = path.join(os.tmpdir(), `am-import-setup-backup-${Date.now()}`); + fs.mkdirSync(amContentDir, { recursive: true }); + fs.mkdirSync(backupDir, { recursive: true }); + + mockStackAPIClient = { + asset: stub().returns({ + query: stub().returnsThis(), + find: stub().resolves({ items: [] }), + }), + }; + + stub(loggerModule, 'log'); + lastMapperParams = undefined; + setParentProgressStub = stub(amPkg.ImportSetupAssetMappers.prototype, 'setParentProgressManager'); + mapperStartStub = stub(amPkg.ImportSetupAssetMappers.prototype, 'start').callsFake(async function (this: { + params: Record; + }) { + lastMapperParams = this.params; + return { kind: 'success' as const }; + }); + }); + + afterEach(() => { + restore(); + if (fs.existsSync(amContentDir)) { + fs.rmSync(amContentDir, { recursive: true, force: true }); + } + if (fs.existsSync(backupDir)) { + fs.rmSync(backupDir, { recursive: true, force: true }); + } + }); + + const amBaseConfig = (): ImportConfig => + ({ + contentDir: amContentDir, + data: amContentDir, + apiKey: 'test-api-key', + forceStopMarketplaceAppsPrompt: false, + master_locale: { code: 'en-us' }, + masterLocale: { code: 'en-us' }, + branchName: '', + selectedModules: ['assets'], + backupDir, + region: { + cma: 'https://api.contentstack.io/v3', + assetManagementUrl: 'https://am.example.com', + }, + host: 'https://api.contentstack.io/v3', + fetchConcurrency: 2, + writeConcurrency: 1, + assetManagementEnabled: true, + org_uid: 'org-uid-test', + source_stack: 'source-api-key', + context: {}, + modules: { + ...defaultConfig.modules, + assets: { + fetchConcurrency: 2, + dirName: 'assets', + fileName: 'assets', + }, + }, + } as unknown as ImportConfig); + + it('delegates to ImportSetupAssetMappers and completes progress on success', async () => { + const cfg = amBaseConfig(); + const assetSetup = new AssetImportSetup({ + config: cfg, + stackAPIClient: mockStackAPIClient, + dependencies: {} as any, + }); + + const nested = { + addProcess: stub().returnsThis(), + startProcess: stub().returnsThis(), + updateStatus: stub().returnsThis(), + completeProcess: stub().returnsThis(), + }; + stub(assetSetup as any, 'createNestedProgress').returns(nested); + const completeStub = stub(assetSetup as any, 'completeProgress'); + + await assetSetup.start(); + + expect(mapperStartStub.calledOnce).to.be.true; + expect(lastMapperParams).to.exist; + const params = lastMapperParams!; + expect(params.contentDir).to.equal(sanitizePath(amContentDir)); + expect(params.mapperBaseDir).to.equal(sanitizePath(backupDir)); + expect(params.assetManagementUrl).to.equal('https://am.example.com'); + expect(params.org_uid).to.equal('org-uid-test'); + expect(params.source_stack).to.equal('source-api-key'); + expect(params.apiKey).to.equal('test-api-key'); + expect(params.host).to.equal('https://api.contentstack.io/v3'); + expect(params.apiConcurrency).to.equal(cfg.fetchConcurrency); + expect(setParentProgressStub.calledOnce).to.be.true; + expect(setParentProgressStub.firstCall.args[0]).to.equal(nested); + expect(completeStub.calledOnceWithExactly(true)).to.be.true; + }); + + it('does not run ImportSetupAssetMappers when assetManagementUrl is missing', async () => { + const cfg = amBaseConfig(); + (cfg as any).region = { cma: 'https://api.contentstack.io/v3' }; + + const assetSetup = new AssetImportSetup({ + config: cfg, + stackAPIClient: mockStackAPIClient, + dependencies: {} as any, + }); + + stub(assetSetup as any, 'createNestedProgress').returns({ + addProcess: stub().returnsThis(), + startProcess: stub().returnsThis(), + updateStatus: stub().returnsThis(), + completeProcess: stub().returnsThis(), + }); + const completeStub = stub(assetSetup as any, 'completeProgress'); + + await assetSetup.start(); + + expect(mapperStartStub.called).to.be.false; + expect(setParentProgressStub.called).to.be.false; + expect(completeStub.called).to.be.false; + }); + + it('calls completeProgress(false) when mapper returns error', async () => { + mapperStartStub.resolves({ kind: 'error', errorMessage: 'mapper failed' }); + + const assetSetup = new AssetImportSetup({ + config: amBaseConfig(), + stackAPIClient: mockStackAPIClient, + dependencies: {} as any, + }); + + stub(assetSetup as any, 'createNestedProgress').returns({ + addProcess: stub().returnsThis(), + startProcess: stub().returnsThis(), + updateStatus: stub().returnsThis(), + completeProcess: stub().returnsThis(), + }); + const completeStub = stub(assetSetup as any, 'completeProgress'); + + await assetSetup.start(); + + expect(setParentProgressStub.calledOnce).to.be.true; + expect(completeStub.calledOnceWithExactly(false, 'mapper failed')).to.be.true; + }); + + it('does not run ImportSetupAssetMappers when org_uid is missing', async () => { + const cfg = amBaseConfig(); + delete (cfg as any).org_uid; + + const assetSetup = new AssetImportSetup({ + config: cfg, + stackAPIClient: mockStackAPIClient, + dependencies: {} as any, + }); + + stub(assetSetup as any, 'createNestedProgress').returns({ + addProcess: stub().returnsThis(), + startProcess: stub().returnsThis(), + updateStatus: stub().returnsThis(), + completeProcess: stub().returnsThis(), + }); + const completeStub = stub(assetSetup as any, 'completeProgress'); + + await assetSetup.start(); + + expect(mapperStartStub.called).to.be.false; + expect(setParentProgressStub.called).to.be.false; + expect(completeStub.called).to.be.false; + }); +}); diff --git a/packages/contentstack-import/package.json b/packages/contentstack-import/package.json index a1447064b..269b4ff8b 100644 --- a/packages/contentstack-import/package.json +++ b/packages/contentstack-import/package.json @@ -22,10 +22,10 @@ "mkdirp": "^1.0.4", "promise-limit": "^2.7.0", "uuid": "^9.0.1", - "winston": "^3.17.0" + "winston": "^3.19.0" }, "devDependencies": { - "@oclif/test": "^4.1.16", + "@oclif/test": "^4.1.18", "@types/big-json": "^3.2.5", "@types/bluebird": "^3.5.42", "@types/fs-extra": "^11.0.4", diff --git a/packages/contentstack-import/src/config/index.ts b/packages/contentstack-import/src/config/index.ts index a15332baa..fa1f1f7e0 100644 --- a/packages/contentstack-import/src/config/index.ts +++ b/packages/contentstack-import/src/config/index.ts @@ -26,7 +26,6 @@ const config: DefaultConfig = { 'https://gcp-eu-api.contentstack.com': 'https://gcp-eu-developerhub-api.contentstack.com', }, modules: { - apiConcurrency: 5, types: [ 'locales', 'environments', diff --git a/packages/contentstack-import/src/types/default-config.ts b/packages/contentstack-import/src/types/default-config.ts index 29e859806..6d9cd61dd 100644 --- a/packages/contentstack-import/src/types/default-config.ts +++ b/packages/contentstack-import/src/types/default-config.ts @@ -10,7 +10,6 @@ export default interface DefaultConfig { extensionHost: string; developerHubUrls: Record; modules: { - apiConcurrency: number; types: Modules[]; locales: { dirName: string; diff --git a/packages/contentstack-import/src/utils/import-config-handler.ts b/packages/contentstack-import/src/utils/import-config-handler.ts index ea052eb7d..661a0a904 100644 --- a/packages/contentstack-import/src/utils/import-config-handler.ts +++ b/packages/contentstack-import/src/utils/import-config-handler.ts @@ -1,8 +1,8 @@ import merge from 'merge'; import * as path from 'path'; -import { existsSync, readFileSync } from 'node:fs'; import { omit, filter, includes, isArray } from 'lodash'; import { configHandler, isAuthenticated, cliux, sanitizePath, log } from '@contentstack/cli-utilities'; +import { detectAssetManagementExportFromContentDir } from '@contentstack/cli-asset-management'; import defaultConfig from '../config'; import { readFile } from './file-helper'; import { askContentDir, askAPIKey } from './interactive'; @@ -125,37 +125,12 @@ const setupConfig = async (importCmdFlags: any): Promise => { config['exclude-global-modules'] = importCmdFlags['exclude-global-modules']; } - const spacesDir = path.join(config.contentDir, 'spaces'); - const stackSettingsPath = path.join(config.contentDir, 'stack', 'settings.json'); - - if (existsSync(spacesDir) && existsSync(stackSettingsPath)) { - try { - const stackSettings = JSON.parse(readFileSync(stackSettingsPath, 'utf8')); - if (stackSettings?.am_v2) { - config.assetManagementEnabled = true; - config.assetManagementUrl = configHandler.get('region')?.assetManagementUrl; - - const branchesJsonCandidates = [ - path.join(config.contentDir, 'branches.json'), - path.join(config.contentDir, '..', 'branches.json'), - ]; - for (const branchesJsonPath of branchesJsonCandidates) { - if (existsSync(branchesJsonPath)) { - try { - const branches = JSON.parse(readFileSync(branchesJsonPath, 'utf8')); - const apiKey = branches?.[0]?.stackHeaders?.api_key; - if (apiKey) { - config.source_stack = apiKey; - } - } catch { - // branches.json unreadable — URL mapping will be skipped - } - break; - } - } - } - } catch { - // stack settings unreadable — not an AM 2.0 export we can process + const assetManagementExport = detectAssetManagementExportFromContentDir(config.contentDir); + if (assetManagementExport.assetManagementEnabled) { + config.assetManagementEnabled = true; + config.assetManagementUrl = assetManagementExport.assetManagementUrl; + if (assetManagementExport.source_stack) { + config.source_stack = assetManagementExport.source_stack; } } diff --git a/packages/contentstack-import/test/unit/import/modules/locales.test.ts b/packages/contentstack-import/test/unit/import/modules/locales.test.ts index 5bc5e8f7f..7ff6220c4 100644 --- a/packages/contentstack-import/test/unit/import/modules/locales.test.ts +++ b/packages/contentstack-import/test/unit/import/modules/locales.test.ts @@ -26,7 +26,6 @@ describe('ImportLocales', () => { management_token: 'test-token', contentDir: tempDir, modules: { - apiConcurrency: 5, types: [], locales: { dirName: 'locales', diff --git a/packages/contentstack-import/test/unit/utils/extension-helper.test.ts b/packages/contentstack-import/test/unit/utils/extension-helper.test.ts index f99a8fd9a..bc6d8452a 100644 --- a/packages/contentstack-import/test/unit/utils/extension-helper.test.ts +++ b/packages/contentstack-import/test/unit/utils/extension-helper.test.ts @@ -34,7 +34,6 @@ describe('Extension Helper', () => { contentDir: '/test/content', data: '/test/content', modules: { - apiConcurrency: 5, 'composable-studio': { dirName: 'composable_studio', fileName: 'composable_studio.json', diff --git a/packages/contentstack-migration/package.json b/packages/contentstack-migration/package.json index fb0b50616..0f9b7ae20 100644 --- a/packages/contentstack-migration/package.json +++ b/packages/contentstack-migration/package.json @@ -18,7 +18,7 @@ "winston": "^3.17.0" }, "devDependencies": { - "@oclif/test": "^4.1.13", + "@oclif/test": "^4.1.18", "@types/mocha": "^8.2.3", "@types/node": "^14.18.63", "chai": "^4.5.0", diff --git a/packages/contentstack-variants/package.json b/packages/contentstack-variants/package.json index ce2c00463..ce90783c5 100644 --- a/packages/contentstack-variants/package.json +++ b/packages/contentstack-variants/package.json @@ -20,8 +20,8 @@ "devDependencies": { "@contentstack/cli-dev-dependencies": "^2.0.0-beta.0", "@oclif/plugin-help": "^6.2.28", - "@oclif/test": "^4.1.13", - "@types/node": "^20.17.50", + "@oclif/test": "^4.1.18", + "@types/node": "^20.19.39", "mocha": "^10.8.2", "nyc": "^15.1.0", "ts-node": "^10.9.2", @@ -33,6 +33,6 @@ "@oclif/plugin-help": "^6.2.28", "lodash": "^4.18.1", "mkdirp": "^1.0.4", - "winston": "^3.17.0" + "winston": "^3.19.0" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77965d7f0..54c0f8dcc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,8 +5,9 @@ settings: excludeLinksFromLockfile: false overrides: - picomatch: 4.0.4 - brace-expansion: 5.0.5 + tmp: 0.2.4 + follow-redirects: 1.16.0 + axios: 1.15.0 importers: @@ -98,11 +99,11 @@ importers: specifier: ^9.0.1 version: 9.0.1 winston: - specifier: ^3.17.0 + specifier: ^3.19.0 version: 3.19.0 devDependencies: '@oclif/test': - specifier: ^4.1.13 + specifier: ^4.1.18 version: 4.1.18(@oclif/core@4.10.5) '@types/chai': specifier: ^4.3.20 @@ -184,7 +185,7 @@ importers: version: 7.5.13 devDependencies: '@oclif/test': - specifier: ^4.1.13 + specifier: ^4.1.18 version: 4.1.18(@oclif/core@4.10.5) '@types/inquirer': specifier: ^9.0.8 @@ -220,8 +221,8 @@ importers: specifier: ^4.17.46 version: 4.23.0(@types/node@18.19.130) tmp: - specifier: ^0.2.5 - version: 0.2.5 + specifier: 0.2.4 + version: 0.2.4 ts-node: specifier: ^8.10.2 version: 8.10.2(typescript@5.9.3) @@ -339,7 +340,7 @@ importers: version: 6.1.3 devDependencies: '@oclif/test': - specifier: ^4.1.13 + specifier: ^4.1.18 version: 4.1.18(@oclif/core@4.10.5) '@types/chai': specifier: ^4.3.0 @@ -429,7 +430,7 @@ importers: specifier: ^2.7.0 version: 2.7.0 winston: - specifier: ^3.17.0 + specifier: ^3.19.0 version: 3.19.0 devDependencies: '@contentstack/cli-auth': @@ -445,7 +446,7 @@ importers: specifier: ^6.2.28 version: 6.2.44 '@oclif/test': - specifier: ^4.1.13 + specifier: ^4.1.18 version: 4.1.18(@oclif/core@4.10.5) '@types/big-json': specifier: ^3.2.5 @@ -521,7 +522,7 @@ importers: version: 4.3.6 devDependencies: '@oclif/test': - specifier: ^4.1.13 + specifier: ^4.1.18 version: 4.1.18(@oclif/core@4.10.5) '@types/chai': specifier: ^4.3.20 @@ -623,11 +624,11 @@ importers: specifier: ^9.0.1 version: 9.0.1 winston: - specifier: ^3.17.0 + specifier: ^3.19.0 version: 3.19.0 devDependencies: '@oclif/test': - specifier: ^4.1.16 + specifier: ^4.1.18 version: 4.1.18(@oclif/core@4.10.5) '@types/big-json': specifier: ^3.2.5 @@ -686,6 +687,9 @@ importers: packages/contentstack-import-setup: dependencies: + '@contentstack/cli-asset-management': + specifier: ~1.0.0 + version: link:../contentstack-asset-management '@contentstack/cli-command': specifier: ~2.0.0-beta.6 version: 2.0.0-beta.6(@types/node@14.18.63) @@ -714,7 +718,7 @@ importers: specifier: ^1.0.4 version: 1.0.4 winston: - specifier: ^3.17.0 + specifier: ^3.19.0 version: 3.19.0 devDependencies: '@types/big-json': @@ -827,7 +831,7 @@ importers: version: 3.19.0 devDependencies: '@oclif/test': - specifier: ^4.1.13 + specifier: ^4.1.18 version: 4.1.18(@oclif/core@4.10.5) '@types/mocha': specifier: ^8.2.3 @@ -893,8 +897,8 @@ importers: specifier: ^7.5.11 version: 7.5.13 tmp: - specifier: ^0.2.5 - version: 0.2.5 + specifier: 0.2.4 + version: 0.2.4 devDependencies: '@oclif/plugin-help': specifier: ^6.2.28 @@ -918,8 +922,8 @@ importers: specifier: ^0.2.6 version: 0.2.6 axios: - specifier: ^1.15.1 - version: 1.15.2(debug@4.4.3) + specifier: 1.15.0 + version: 1.15.0(debug@4.4.3) eslint: specifier: ^8.57.1 version: 8.57.1 @@ -963,17 +967,17 @@ importers: specifier: ^1.0.4 version: 1.0.4 winston: - specifier: ^3.17.0 + specifier: ^3.19.0 version: 3.19.0 devDependencies: '@contentstack/cli-dev-dependencies': specifier: ^2.0.0-beta.0 version: 2.0.0-beta.0 '@oclif/test': - specifier: ^4.1.13 + specifier: ^4.1.18 version: 4.1.18(@oclif/core@4.10.5) '@types/node': - specifier: ^20.17.50 + specifier: ^20.19.39 version: 20.19.39 mocha: specifier: ^10.8.2 @@ -2931,8 +2935,8 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axios@1.15.2: - resolution: {integrity: sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==} + axios@1.15.0: + resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} @@ -2959,6 +2963,9 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -2987,6 +2994,12 @@ packages: bowser@2.14.1: resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + brace-expansion@5.0.5: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} @@ -3273,6 +3286,9 @@ packages: commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concat-stream@2.0.0: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} @@ -3930,7 +3946,7 @@ packages: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} peerDependencies: - picomatch: 4.0.4 + picomatch: ^3 || ^4 peerDependenciesMeta: picomatch: optional: true @@ -5309,10 +5325,6 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} - os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} - otplib@12.0.1: resolution: {integrity: sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==} @@ -5445,6 +5457,10 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} @@ -6141,12 +6157,8 @@ packages: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} - tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} - - tmp@0.2.5: - resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + tmp@0.2.4: + resolution: {integrity: sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==} engines: {node: '>=14.14'} tmpl@1.0.5: @@ -7372,7 +7384,7 @@ snapshots: '@contentstack/management': 1.30.1(debug@4.4.3) '@contentstack/marketplace-sdk': 1.5.1(debug@4.4.3) '@oclif/core': 4.10.5 - axios: 1.15.2(debug@4.4.3) + axios: 1.15.0(debug@4.4.3) chalk: 5.6.2 cli-cursor: 3.1.0 cli-progress: 3.12.0 @@ -7407,7 +7419,7 @@ snapshots: '@contentstack/management': 1.30.1(debug@4.4.3) '@contentstack/marketplace-sdk': 1.5.1(debug@4.4.3) '@oclif/core': 4.10.5 - axios: 1.15.2(debug@4.4.3) + axios: 1.15.0(debug@4.4.3) chalk: 5.6.2 cli-cursor: 3.1.0 cli-progress: 3.12.0 @@ -7442,7 +7454,7 @@ snapshots: '@contentstack/management': 1.30.1(debug@4.4.3) '@contentstack/marketplace-sdk': 1.5.1(debug@4.4.3) '@oclif/core': 4.10.5 - axios: 1.15.2(debug@4.4.3) + axios: 1.15.0(debug@4.4.3) chalk: 5.6.2 cli-cursor: 3.1.0 cli-progress: 3.12.0 @@ -7477,7 +7489,7 @@ snapshots: '@contentstack/management': 1.30.1(debug@4.4.3) '@contentstack/marketplace-sdk': 1.5.1(debug@4.4.3) '@oclif/core': 4.10.5 - axios: 1.15.2(debug@4.4.3) + axios: 1.15.0(debug@4.4.3) chalk: 5.6.2 cli-cursor: 3.1.0 cli-progress: 3.12.0 @@ -7512,7 +7524,7 @@ snapshots: '@contentstack/management': 1.30.1(debug@4.4.3) '@contentstack/marketplace-sdk': 1.5.1(debug@4.4.3) '@oclif/core': 4.10.5 - axios: 1.15.2(debug@4.4.3) + axios: 1.15.0(debug@4.4.3) chalk: 5.6.2 cli-cursor: 3.1.0 cli-progress: 3.12.0 @@ -7546,7 +7558,7 @@ snapshots: dependencies: '@contentstack/utils': 1.9.1 assert: 2.1.0 - axios: 1.15.2(debug@4.4.3) + axios: 1.15.0(debug@4.4.3) buffer: 6.0.3 form-data: 4.0.5 husky: 9.1.7 @@ -7560,7 +7572,7 @@ snapshots: '@contentstack/marketplace-sdk@1.5.1(debug@4.4.3)': dependencies: '@contentstack/utils': 1.9.1 - axios: 1.15.2(debug@4.4.3) + axios: 1.15.0(debug@4.4.3) transitivePeerDependencies: - debug @@ -8375,7 +8387,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 18.19.130 + '@types/node': 20.19.39 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -8388,14 +8400,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.130 + '@types/node': 20.19.39 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@18.19.130)(ts-node@8.10.2(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@20.19.39)(ts-node@8.10.2(typescript@5.9.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -8420,7 +8432,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.130 + '@types/node': 20.19.39 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -8438,7 +8450,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 18.19.130 + '@types/node': 20.19.39 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -8460,7 +8472,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 18.19.130 + '@types/node': 20.19.39 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit: 0.1.2 @@ -8529,7 +8541,7 @@ snapshots: dependencies: '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 18.19.130 + '@types/node': 20.19.39 '@types/yargs': 15.0.20 chalk: 4.1.2 @@ -8538,7 +8550,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 18.19.130 + '@types/node': 20.19.39 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -9200,7 +9212,7 @@ snapshots: '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 18.19.130 + '@types/node': 20.19.39 '@types/http-cache-semantics@4.2.0': {} @@ -9298,7 +9310,7 @@ snapshots: '@types/through@0.0.33': dependencies: - '@types/node': 18.19.130 + '@types/node': 20.19.39 '@types/tmp@0.2.6': {} @@ -9884,7 +9896,7 @@ snapshots: anymatch@3.1.3: dependencies: normalize-path: 3.0.0 - picomatch: 4.0.4 + picomatch: 2.3.2 append-transform@2.0.0: dependencies: @@ -10004,7 +10016,7 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - axios@1.15.2(debug@4.4.3): + axios@1.15.0(debug@4.4.3): dependencies: follow-redirects: 1.16.0(debug@4.4.3) form-data: 4.0.5 @@ -10067,6 +10079,8 @@ snapshots: babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + balanced-match@1.0.2: {} + balanced-match@4.0.4: {} base64-js@1.5.1: {} @@ -10094,6 +10108,15 @@ snapshots: bowser@2.14.1: {} + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.0: + dependencies: + balanced-match: 1.0.2 + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -10417,6 +10440,8 @@ snapshots: commondir@1.0.1: {} + concat-map@0.0.1: {} + concat-stream@2.0.0: dependencies: buffer-from: 1.1.2 @@ -10829,7 +10854,7 @@ snapshots: '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) eslint-config-xo-space: 0.35.0(eslint@8.57.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-mocha: 10.5.0(eslint@8.57.1) eslint-plugin-n: 15.7.0(eslint@8.57.1) @@ -10892,7 +10917,7 @@ snapshots: eslint-config-xo: 0.49.0(eslint@8.57.1) eslint-config-xo-space: 0.35.0(eslint@8.57.1) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jsdoc: 50.8.0(eslint@8.57.1) eslint-plugin-mocha: 10.5.0(eslint@8.57.1) eslint-plugin-n: 17.24.0(eslint@8.57.1)(typescript@5.9.3) @@ -10933,21 +10958,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.3(supports-color@8.1.1) - eslint: 8.57.1 - get-tsconfig: 4.14.0 - is-bun-module: 2.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.16 - unrs-resolver: 1.11.1 - optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) - transitivePeerDependencies: - - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 @@ -10959,7 +10969,7 @@ snapshots: tinyglobby: 0.2.16 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -10974,18 +10984,18 @@ snapshots: tinyglobby: 0.2.16 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@4.9.5))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -11035,7 +11045,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) hasown: 2.0.3 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11082,7 +11092,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -11425,7 +11435,7 @@ snapshots: dependencies: chardet: 0.4.2 iconv-lite: 0.4.24 - tmp: 0.0.33 + tmp: 0.2.4 eyes@0.1.8: {} @@ -12244,7 +12254,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.130 + '@types/node': 20.19.39 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.2 @@ -12314,6 +12324,37 @@ snapshots: - babel-plugin-macros - supports-color + jest-config@29.7.0(@types/node@20.19.39)(ts-node@8.10.2(typescript@5.9.3)): + dependencies: + '@babel/core': 7.29.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.19.39 + ts-node: 8.10.2(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-diff@26.6.2: dependencies: chalk: 4.1.2 @@ -12345,7 +12386,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.130 + '@types/node': 20.19.39 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -12357,7 +12398,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 18.19.130 + '@types/node': 20.19.39 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -12396,7 +12437,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 18.19.130 + '@types/node': 20.19.39 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -12431,7 +12472,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.130 + '@types/node': 20.19.39 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -12459,7 +12500,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.130 + '@types/node': 20.19.39 chalk: 4.1.2 cjs-module-lexer: 1.4.3 collect-v8-coverage: 1.0.3 @@ -12505,11 +12546,11 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 18.19.130 + '@types/node': 20.19.39 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 - picomatch: 4.0.4 + picomatch: 2.3.2 jest-validate@29.7.0: dependencies: @@ -12524,7 +12565,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.130 + '@types/node': 20.19.39 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -12533,7 +12574,7 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 18.19.130 + '@types/node': 20.19.39 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -12860,7 +12901,7 @@ snapshots: micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 4.0.4 + picomatch: 2.3.2 mime-db@1.52.0: {} @@ -12886,19 +12927,19 @@ snapshots: minimatch@3.1.5: dependencies: - brace-expansion: 5.0.5 + brace-expansion: 1.1.14 minimatch@5.1.9: dependencies: - brace-expansion: 5.0.5 + brace-expansion: 2.1.0 minimatch@9.0.3: dependencies: - brace-expansion: 5.0.5 + brace-expansion: 2.1.0 minimatch@9.0.9: dependencies: - brace-expansion: 5.0.5 + brace-expansion: 2.1.0 minimist@1.2.8: {} @@ -13276,8 +13317,6 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 - os-tmpdir@1.0.2: {} - otplib@12.0.1: dependencies: '@otplib/core': 12.0.1 @@ -13399,6 +13438,8 @@ snapshots: picocolors@1.1.1: {} + picomatch@2.3.2: {} + picomatch@4.0.4: {} pirates@4.0.7: {} @@ -13528,7 +13569,7 @@ snapshots: readdirp@3.6.0: dependencies: - picomatch: 4.0.4 + picomatch: 2.3.2 recheck-jar@4.5.0: optional: true @@ -14131,11 +14172,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - tmp@0.0.33: - dependencies: - os-tmpdir: 1.0.2 - - tmp@0.2.5: {} + tmp@0.2.4: {} tmpl@1.0.5: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 721daf770..63314411c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,6 @@ packages: - 'packages/*' overrides: - picomatch: 4.0.4 - brace-expansion: 5.0.5 + tmp: 0.2.4 + follow-redirects: 1.16.0 + axios: 1.15.0