Skip to main content
Glama
version-operations.js14.4 kB
/** * Version Operations Module * Handles all AEM version management operations including creating, restoring, comparing, and deleting versions */ import { createAEMError, handleAEMHttpError, safeExecute, createSuccessResponse, AEM_ERROR_CODES, isValidContentPath } from '../error-handler.js'; export class VersionOperations { httpClient; logger; config; constructor(httpClient, logger, config) { this.httpClient = httpClient; this.logger = logger; this.config = config; } /** * Get version history for a content path */ async getVersionHistory(path) { return safeExecute(async () => { if (!isValidContentPath(path)) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, `Invalid content path: ${path}`, { path }); } try { // Get version history using AEM's versioning API const response = await this.httpClient.get(`${path}.versionhistory.json`, { params: { ':depth': '2' } }); const versions = []; let baseVersion; if (response.data && typeof response.data === 'object') { Object.entries(response.data).forEach(([key, value]) => { if (key === 'jcr:versionLabels') { // Handle version labels return; } if (value && typeof value === 'object' && value['jcr:frozenNode']) { const versionInfo = { name: key, label: value['jcr:frozenNode']?.['jcr:versionLabel'], created: value['jcr:created'] || new Date().toISOString(), createdBy: value['jcr:createdBy'] || 'unknown', comment: value['jcr:versionComment'], isBaseVersion: value['jcr:isCheckedOut'] === false }; if (versionInfo.isBaseVersion) { baseVersion = key; } versions.push(versionInfo); } }); } // Sort versions by creation date (newest first) versions.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime()); this.logger.info(`Retrieved version history for path: ${path}`, { versionCount: versions.length, baseVersion }); return createSuccessResponse({ path, versions, totalCount: versions.length, baseVersion }, 'getVersionHistory'); } catch (error) { throw handleAEMHttpError(error, 'getVersionHistory'); } }, 'getVersionHistory'); } /** * Create a new version of content */ async createVersion(path, label, comment) { return safeExecute(async () => { if (!isValidContentPath(path)) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, `Invalid content path: ${path}`, { path }); } try { // Check out the content first await this.checkOutContent(path); // Create version using AEM's versioning API const formData = new URLSearchParams(); formData.append('cmd', 'createVersion'); formData.append('path', path); if (label) { formData.append('label', label); } if (comment) { formData.append('comment', comment); } const response = await this.httpClient.post('/bin/wcm/versioning/createVersion', formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); // Check the content back in await this.checkInContent(path); const versionName = response.data?.versionName || `v${Date.now()}`; this.logger.info(`Created version for path: ${path}`, { versionName, label, comment }); return createSuccessResponse({ path, versionName, label, comment, created: new Date().toISOString(), createdBy: this.config.serviceUser.username }, 'createVersion'); } catch (error) { throw handleAEMHttpError(error, 'createVersion'); } }, 'createVersion'); } /** * Restore content to a specific version */ async restoreVersion(path, versionName) { return safeExecute(async () => { if (!isValidContentPath(path)) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, `Invalid content path: ${path}`, { path }); } if (!versionName || typeof versionName !== 'string') { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, 'Version name is required', { versionName }); } try { // Get current version before restore const versionHistory = await this.getVersionHistory(path); const currentVersion = versionHistory.data.baseVersion; // Restore version using AEM's versioning API const formData = new URLSearchParams(); formData.append('cmd', 'restoreVersion'); formData.append('path', path); formData.append('version', versionName); await this.httpClient.post('/bin/wcm/versioning/restoreVersion', formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); this.logger.info(`Restored version for path: ${path}`, { versionName, previousVersion: currentVersion }); return createSuccessResponse({ path, restoredVersion: versionName, previousVersion: currentVersion, restoredAt: new Date().toISOString(), restoredBy: this.config.serviceUser.username }, 'restoreVersion'); } catch (error) { throw handleAEMHttpError(error, 'restoreVersion'); } }, 'restoreVersion'); } /** * Compare two versions of content */ async compareVersions(path, version1, version2) { return safeExecute(async () => { if (!isValidContentPath(path)) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, `Invalid content path: ${path}`, { path }); } if (!version1 || !version2 || version1 === version2) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, 'Two different version names are required for comparison', { version1, version2 }); } try { // Get both versions const version1Response = await this.httpClient.get(`${path}.version.${version1}.json`, { params: { ':depth': '2' } }); const version2Response = await this.httpClient.get(`${path}.version.${version2}.json`, { params: { ':depth': '2' } }); // Compare the versions const differences = this.compareVersionData(version1Response.data, version2Response.data); const summary = { added: differences.filter(d => d.type === 'added').length, removed: differences.filter(d => d.type === 'removed').length, modified: differences.filter(d => d.type === 'modified').length }; this.logger.info(`Compared versions for path: ${path}`, { version1, version2, differencesCount: differences.length, summary }); return createSuccessResponse({ path, version1, version2, differences, summary }, 'compareVersions'); } catch (error) { throw handleAEMHttpError(error, 'compareVersions'); } }, 'compareVersions'); } /** * Delete a specific version */ async deleteVersion(path, versionName) { return safeExecute(async () => { if (!isValidContentPath(path)) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, `Invalid content path: ${path}`, { path }); } if (!versionName || typeof versionName !== 'string') { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, 'Version name is required', { versionName }); } try { // Delete version using AEM's versioning API const formData = new URLSearchParams(); formData.append('cmd', 'deleteVersion'); formData.append('path', path); formData.append('version', versionName); await this.httpClient.post('/bin/wcm/versioning/deleteVersion', formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); this.logger.info(`Deleted version for path: ${path}`, { versionName }); return createSuccessResponse({ path, deletedVersion: versionName, deletedAt: new Date().toISOString(), deletedBy: this.config.serviceUser.username }, 'deleteVersion'); } catch (error) { throw handleAEMHttpError(error, 'deleteVersion'); } }, 'deleteVersion'); } /** * Update undoChanges to use version operations */ async undoChanges(request) { return safeExecute(async () => { const { jobId, path } = request; // If jobId looks like a version name, try to restore it if (path && (jobId.startsWith('v') || jobId.includes('.'))) { try { const restoreResult = await this.restoreVersion(path, jobId); return createSuccessResponse({ message: `Successfully restored version ${jobId} for path ${path}`, request, versionInfo: { restoredVersion: restoreResult.data.restoredVersion, path: restoreResult.data.path }, timestamp: new Date().toISOString() }, 'undoChanges'); } catch (error) { // If restore fails, fall back to original message } } // Fallback to original implementation return createSuccessResponse({ message: 'undoChanges requires a valid path and version name. Use version operations for proper rollback functionality.', request, timestamp: new Date().toISOString() }, 'undoChanges'); }, 'undoChanges'); } /** * Helper method to check out content */ async checkOutContent(path) { const formData = new URLSearchParams(); formData.append('cmd', 'checkout'); formData.append('path', path); await this.httpClient.post('/bin/wcm/versioning/checkout', formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); } /** * Helper method to check in content */ async checkInContent(path) { const formData = new URLSearchParams(); formData.append('cmd', 'checkin'); formData.append('path', path); await this.httpClient.post('/bin/wcm/versioning/checkin', formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); } /** * Helper method to compare version data */ compareVersionData(data1, data2, prefix = '') { const differences = []; const keys1 = new Set(Object.keys(data1 || {})); const keys2 = new Set(Object.keys(data2 || {})); // Check for added and modified properties for (const key of keys2) { const fullKey = prefix ? `${prefix}.${key}` : key; if (!keys1.has(key)) { differences.push({ property: fullKey, type: 'added', newValue: data2[key] }); } else if (JSON.stringify(data1[key]) !== JSON.stringify(data2[key])) { differences.push({ property: fullKey, type: 'modified', oldValue: data1[key], newValue: data2[key] }); } } // Check for removed properties for (const key of keys1) { if (!keys2.has(key)) { const fullKey = prefix ? `${prefix}.${key}` : key; differences.push({ property: fullKey, type: 'removed', oldValue: data1[key] }); } } return differences; } } //# sourceMappingURL=version-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