Skip to main content
Glama
replication-operations.js21.1 kB
/** * Replication Operations Module * Handles all AEM replication and publishing operations including activation, deactivation, and content distribution */ import { createAEMError, handleAEMHttpError, safeExecute, createSuccessResponse, AEM_ERROR_CODES, isValidContentPath } from '../error-handler.js'; export class ReplicationOperations { httpClient; logger; config; constructor(httpClient, logger, config) { this.httpClient = httpClient; this.logger = logger; this.config = config; } /** * Activate (publish) a single page */ async activatePage(request) { return safeExecute(async () => { const { pagePath, activateTree = false } = request; if (!isValidContentPath(pagePath)) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, `Invalid page path: ${String(pagePath)}`, { pagePath }); } try { // Use the correct AEM replication servlet endpoint const formData = new URLSearchParams(); formData.append('cmd', 'Activate'); formData.append('path', pagePath); formData.append('ignoredeactivated', 'false'); formData.append('onlymodified', 'false'); if (activateTree) { formData.append('deep', 'true'); } const response = await this.httpClient.post('/bin/replicate.json', formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); return createSuccessResponse({ success: true, activatedPath: pagePath, activateTree, response: response.data, timestamp: new Date().toISOString(), }, 'activatePage'); } catch (error) { // Fallback to alternative replication methods try { const wcmResponse = await this.httpClient.post('/bin/wcmcommand', { cmd: 'activate', path: pagePath, ignoredeactivated: false, onlymodified: false, }); return createSuccessResponse({ success: true, activatedPath: pagePath, activateTree, response: wcmResponse.data, fallbackUsed: 'WCM Command', timestamp: new Date().toISOString(), }, 'activatePage'); } catch (fallbackError) { throw handleAEMHttpError(error, 'activatePage'); } } }, 'activatePage'); } /** * Deactivate (unpublish) a single page */ async deactivatePage(request) { return safeExecute(async () => { const { pagePath, deactivateTree = false } = request; if (!isValidContentPath(pagePath)) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, `Invalid page path: ${String(pagePath)}`, { pagePath }); } try { // Use the correct AEM replication servlet endpoint const formData = new URLSearchParams(); formData.append('cmd', 'Deactivate'); formData.append('path', pagePath); formData.append('ignoredeactivated', 'false'); formData.append('onlymodified', 'false'); if (deactivateTree) { formData.append('deep', 'true'); } const response = await this.httpClient.post('/bin/replicate.json', formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); return createSuccessResponse({ success: true, deactivatedPath: pagePath, deactivateTree, response: response.data, timestamp: new Date().toISOString(), }, 'deactivatePage'); } catch (error) { // Fallback to alternative replication methods try { const wcmResponse = await this.httpClient.post('/bin/wcmcommand', { cmd: 'deactivate', path: pagePath, ignoredeactivated: false, onlymodified: false, }); return createSuccessResponse({ success: true, deactivatedPath: pagePath, deactivateTree, response: wcmResponse.data, fallbackUsed: 'WCM Command', timestamp: new Date().toISOString(), }, 'deactivatePage'); } catch (fallbackError) { throw handleAEMHttpError(error, 'deactivatePage'); } } }, 'deactivatePage'); } /** * Unpublish content from the publish environment */ async unpublishContent(request) { return safeExecute(async () => { const { contentPaths, unpublishTree = false } = request; if (!contentPaths || (Array.isArray(contentPaths) && contentPaths.length === 0)) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, 'Content paths array is required and cannot be empty', { contentPaths }); } const results = []; // Process each path individually using the correct AEM replication API for (const path of Array.isArray(contentPaths) ? contentPaths : [contentPaths]) { try { // Use the correct AEM replication servlet endpoint const formData = new URLSearchParams(); formData.append('cmd', 'Deactivate'); formData.append('path', path); formData.append('ignoredeactivated', 'false'); formData.append('onlymodified', 'false'); if (unpublishTree) { formData.append('deep', 'true'); } const response = await this.httpClient.post('/bin/replicate.json', formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); results.push({ path, success: true, response: response.data }); } catch (error) { results.push({ path, success: false, error: error.response?.data || error.message }); } } return createSuccessResponse({ success: results.every(r => r.success), results, unpublishedPaths: contentPaths, unpublishTree, timestamp: new Date().toISOString(), }, 'unpublishContent'); }, 'unpublishContent'); } /** * Replicate and publish content to selected locales with real AEM integration */ async replicateAndPublish(selectedLocales, componentData, localizedOverrides) { return safeExecute(async () => { if (!selectedLocales || selectedLocales.length === 0) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, 'selectedLocales array is required and cannot be empty', { selectedLocales }); } const results = []; const successfulReplications = []; let hasFailures = false; for (const locale of selectedLocales) { try { this.logger.info(`Starting replication to locale: ${locale}`, { componentData: typeof componentData, hasOverrides: !!localizedOverrides }); // Step 1: Validate locale path exists const localePath = this.validateAndBuildLocalePath(locale); // Step 2: Check if content is part of MSM structure const isMSMContent = await this.checkMSMStructure(localePath); let replicationResult; if (isMSMContent) { // Step 3a: Use MSM APIs for live copy updates replicationResult = await this.handleMSMReplication(localePath, componentData, localizedOverrides); } else { // Step 3b: Standard replication process replicationResult = await this.handleStandardReplication(localePath, componentData, localizedOverrides); } // Step 4: Activate content in the target locale await this.activateContentInLocale(localePath); results.push({ locale, success: true, message: `Content replicated and activated successfully to ${locale}`, path: localePath, replicationId: replicationResult.replicationId }); successfulReplications.push(locale); this.logger.info(`Content replicated successfully to locale: ${locale}`, { path: localePath, replicationId: replicationResult.replicationId }); } catch (error) { hasFailures = true; results.push({ locale, success: false, message: `Failed to replicate to ${locale}: ${error.message}`, error: error.message }); this.logger.error(`Failed to replicate to locale: ${locale}`, { error: error.message, stack: error.stack }); // If we have a strict policy, rollback successful replications if (this.config.strictReplication) { await this.rollbackSuccessfulReplications(successfulReplications); } } } return createSuccessResponse({ message: hasFailures ? `Replication completed with errors. ${successfulReplications.length}/${selectedLocales.length} locales successful.` : 'All replications completed successfully', selectedLocales, successfulLocales: successfulReplications, failedLocales: results.filter(r => !r.success).map(r => r.locale), componentData, localizedOverrides, results, success: !hasFailures, summary: { total: selectedLocales.length, successful: successfulReplications.length, failed: selectedLocales.length - successfulReplications.length } }, 'replicateAndPublish'); }, 'replicateAndPublish'); } /** * Validate and build locale-specific path */ validateAndBuildLocalePath(locale) { if (!locale || typeof locale !== 'string') { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, 'Locale must be a non-empty string', { locale }); } // Validate locale format (e.g., en-us, en_US, en-US) const localePattern = /^[a-z]{2}(-[A-Z]{2})?$/i; if (!localePattern.test(locale)) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, 'Invalid locale format. Expected format: en, en-US, en_US', { locale }); } // Normalize locale to AEM format (e.g., en-us -> en_us) const normalizedLocale = locale.toLowerCase().replace('-', '_'); // Build locale-specific path return `/content/${this.config.siteName || 'we-retail'}/${normalizedLocale}`; } /** * Check if content is part of MSM (Multi-Site Manager) structure */ async checkMSMStructure(localePath) { try { const response = await this.httpClient.get(`${localePath}.json`, { params: { ':depth': '1' } }); // Check for MSM properties return !!(response.data?.['jcr:content']?.['cq:liveSyncConfig'] || response.data?.['jcr:content']?.['cq:blueprint']); } catch (error) { this.logger.warn(`Could not check MSM structure for ${localePath}`, { error: error.message }); return false; } } /** * Handle MSM replication using live copy APIs */ async handleMSMReplication(localePath, componentData, localizedOverrides) { const formData = new URLSearchParams(); formData.append('cmd', 'Rollout'); formData.append('path', localePath); formData.append('deep', 'true'); // Add component data if provided if (componentData) { formData.append('componentData', JSON.stringify(componentData)); } // Add localized overrides if provided if (localizedOverrides) { formData.append('localizedOverrides', JSON.stringify(localizedOverrides)); } const response = await this.httpClient.post('/bin/wcm/msm/rollout', formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); return { replicationId: response.data?.replicationId || `msm_${Date.now()}` }; } /** * Handle standard replication process */ async handleStandardReplication(localePath, componentData, localizedOverrides) { // Step 1: Create or update content in target locale if (componentData) { const formData = new URLSearchParams(); formData.append('cmd', 'update'); formData.append('_charset_', 'utf-8'); // Add component data Object.entries(componentData).forEach(([key, value]) => { formData.append(key, String(value)); }); // Add localized overrides if (localizedOverrides) { Object.entries(localizedOverrides).forEach(([key, value]) => { formData.append(`localized_${key}`, String(value)); }); } await this.httpClient.post(localePath, formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); } return { replicationId: `std_${Date.now()}` }; } /** * Activate content in the target locale */ async activateContentInLocale(localePath) { const formData = new URLSearchParams(); formData.append('cmd', 'Activate'); formData.append('path', localePath); formData.append('ignoredeactivated', 'false'); formData.append('onlymodified', 'false'); await this.httpClient.post('/bin/replicate.json', formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); } /** * Rollback successful replications on failure */ async rollbackSuccessfulReplications(successfulLocales) { this.logger.info(`Rolling back ${successfulLocales.length} successful replications`); for (const locale of successfulLocales) { try { const localePath = this.validateAndBuildLocalePath(locale); // Deactivate content const formData = new URLSearchParams(); formData.append('cmd', 'Deactivate'); formData.append('path', localePath); formData.append('ignoredeactivated', 'false'); formData.append('onlymodified', 'false'); await this.httpClient.post('/bin/replicate.json', formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); this.logger.info(`Rolled back replication for locale: ${locale}`); } catch (error) { this.logger.error(`Failed to rollback locale: ${locale}`, { error: error.message }); } } } /** * Get replication status for content */ async getReplicationStatus(contentPath) { return safeExecute(async () => { try { // Query replication status using AEM's replication status API const response = await this.httpClient.get('/bin/querybuilder.json', { params: { type: 'cq:ReplicationStatus', path: contentPath, 'p.limit': 1 } }); if (response.data.hits && response.data.hits.length > 0) { const status = response.data.hits[0]; return { contentPath, status: status['jcr:content/status'] || 'unknown', lastReplicated: status['jcr:content/cq:lastReplicated'], replicationAgent: status['jcr:content/cq:replicationAgent'] }; } return { contentPath, status: 'inactive' }; } catch (error) { return { contentPath, status: 'error', error: error.message }; } }, 'getReplicationStatus'); } /** * Bulk activate multiple pages */ async bulkActivatePages(pagePaths, activateTree = false) { return safeExecute(async () => { const results = []; let successfulActivations = 0; for (const pagePath of pagePaths) { try { const result = await this.activatePage({ pagePath, activateTree }); results.push({ pagePath, success: result.data.success }); if (result.data.success) { successfulActivations++; } } catch (error) { results.push({ pagePath, success: false, error: error.message }); } } return { success: successfulActivations === pagePaths.length, results, totalPages: pagePaths.length, successfulActivations }; }, 'bulkActivatePages'); } /** * Bulk deactivate multiple pages */ async bulkDeactivatePages(pagePaths, deactivateTree = false) { return safeExecute(async () => { const results = []; let successfulDeactivations = 0; for (const pagePath of pagePaths) { try { const result = await this.deactivatePage({ pagePath, deactivateTree }); results.push({ pagePath, success: result.data.success }); if (result.data.success) { successfulDeactivations++; } } catch (error) { results.push({ pagePath, success: false, error: error.message }); } } return { success: successfulDeactivations === pagePaths.length, results, totalPages: pagePaths.length, successfulDeactivations }; }, 'bulkDeactivatePages'); } } //# sourceMappingURL=replication-operations.js.map

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/indrasishbanerjee/aem-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server