Skip to main content
Glama
component-operations.js15.2 kB
/** * Component Operations Module * Handles all AEM component-related operations including CRUD, validation, and bulk updates */ import { createAEMError, handleAEMHttpError, safeExecute, createSuccessResponse, AEM_ERROR_CODES, isValidContentPath, isValidComponentType, validateComponentOperation } from '../error-handler.js'; export class ComponentOperations { httpClient; logger; config; constructor(httpClient, logger, config) { this.httpClient = httpClient; this.logger = logger; this.config = config; } /** * Create a new component on a page */ async createComponent(request) { return safeExecute(async () => { const { pagePath, componentType, resourceType, properties = {}, name } = request; if (!isValidContentPath(pagePath)) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, `Invalid page path: ${String(pagePath)}`, { pagePath }); } if (!isValidComponentType(componentType)) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, `Invalid component type: ${componentType}`, { componentType }); } const componentName = name || `${componentType}_${Date.now()}`; const componentPath = `${pagePath}/jcr:content/${componentName}`; await this.httpClient.post(componentPath, { 'jcr:primaryType': 'nt:unstructured', 'sling:resourceType': resourceType, ...properties, ':operation': 'import', ':contentType': 'json', ':replace': 'true', }); return createSuccessResponse({ success: true, componentPath, componentType, resourceType, properties, timestamp: new Date().toISOString(), }, 'createComponent'); }, 'createComponent'); } /** * Update component properties in AEM */ async updateComponent(request) { return safeExecute(async () => { const { componentPath, properties } = request; if (!componentPath || typeof componentPath !== 'string') { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, 'Component path is required and must be a string'); } if (!properties || typeof properties !== 'object') { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, 'Properties are required and must be an object'); } if (!isValidContentPath(componentPath)) { throw createAEMError(AEM_ERROR_CODES.INVALID_PATH, `Component path '${componentPath}' is not within allowed content roots`, { path: componentPath, allowedRoots: Object.values(this.config.contentPaths) }); } // Verify component exists try { await this.httpClient.get(`${componentPath}.json`); } catch (error) { if (error.response?.status === 404) { throw createAEMError(AEM_ERROR_CODES.COMPONENT_NOT_FOUND, `Component not found at path: ${componentPath}`, { componentPath }); } throw handleAEMHttpError(error, 'updateComponent'); } // Prepare form data for AEM const formData = new URLSearchParams(); Object.entries(properties).forEach(([key, value]) => { if (value === null || value === undefined) { formData.append(`${key}@Delete`, ''); } else if (Array.isArray(value)) { value.forEach((item) => { formData.append(`${key}`, item.toString()); }); } else if (typeof value === 'object') { formData.append(key, JSON.stringify(value)); } else { formData.append(key, value.toString()); } }); // Update the component const response = await this.httpClient.post(componentPath, formData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json', }, timeout: this.config.queries.timeoutMs, }); // Verify the update const verificationResponse = await this.httpClient.get(`${componentPath}.json`); return createSuccessResponse({ message: 'Component updated successfully', path: componentPath, properties, updatedProperties: verificationResponse.data, response: response.data, verification: { success: true, propertiesChanged: Object.keys(properties).length, timestamp: new Date().toISOString(), }, }, 'updateComponent'); }, 'updateComponent'); } /** * Delete a component from AEM */ async deleteComponent(request) { return safeExecute(async () => { const { componentPath, force = false } = request; if (!isValidContentPath(componentPath)) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, `Invalid component path: ${String(componentPath)}`, { componentPath }); } let deleted = false; try { await this.httpClient.delete(componentPath); deleted = true; } catch (err) { if (err.response && err.response.status === 405) { try { await this.httpClient.post(componentPath, { ':operation': 'delete' }); deleted = true; } catch (slingErr) { this.logger.error('Sling POST delete failed', { error: slingErr.response?.status, data: slingErr.response?.data }); throw slingErr; } } else { this.logger.error('DELETE failed', { status: err.response?.status, data: err.response?.data }); throw err; } } return createSuccessResponse({ success: deleted, deletedPath: componentPath, timestamp: new Date().toISOString(), }, 'deleteComponent'); }, 'deleteComponent'); } /** * Validate component changes before applying them */ async validateComponent(request) { return safeExecute(async () => { const { locale, pagePath, component, props } = request; validateComponentOperation(locale, pagePath, component, props); // Validate locale const normalizedLocale = locale.toLowerCase(); const isValidLocale = this.config.validation.allowedLocales.some(l => l.toLowerCase() === normalizedLocale || (normalizedLocale === 'en' && l.toLowerCase().startsWith('en'))); if (!isValidLocale) { throw createAEMError(AEM_ERROR_CODES.INVALID_LOCALE, `Locale '${locale}' is not supported`, { locale, allowedLocales: this.config.validation.allowedLocales }); } // Validate content path if (!isValidContentPath(pagePath)) { throw createAEMError(AEM_ERROR_CODES.INVALID_PATH, `Path '${pagePath}' is not within allowed content roots`, { path: pagePath, allowedRoots: Object.values(this.config.contentPaths) }); } // Validate component type if (!isValidComponentType(component)) { throw createAEMError(AEM_ERROR_CODES.INVALID_COMPONENT_TYPE, `Component type '${component}' is not allowed`, { component, allowedTypes: this.config.components.allowedTypes }); } // Get page data for validation const response = await this.httpClient.get(`${pagePath}.json`, { params: { ':depth': '2' }, timeout: this.config.queries.timeoutMs, }); const validation = this.validateComponentProps(response.data, component, props); return createSuccessResponse({ message: 'Component validation completed successfully', pageData: response.data, component, locale, validation, configUsed: { allowedLocales: this.config.validation.allowedLocales, allowedComponents: this.config.components.allowedTypes, }, }, 'validateComponent'); }, 'validateComponent'); } /** * Scan a page to discover all components and their properties */ async scanPageComponents(pagePath) { return safeExecute(async () => { const response = await this.httpClient.get(`${pagePath}.infinity.json`); const components = []; const processNode = (node, nodePath) => { if (!node || typeof node !== 'object') return; if (node['sling:resourceType']) { components.push({ path: nodePath, resourceType: node['sling:resourceType'], properties: { ...node }, }); } Object.entries(node).forEach(([key, value]) => { if (typeof value === 'object' && value !== null && !key.startsWith('rep:') && !key.startsWith('oak:')) { const childPath = nodePath ? `${nodePath}/${key}` : key; processNode(value, childPath); } }); }; if (response.data['jcr:content']) { processNode(response.data['jcr:content'], 'jcr:content'); } else { processNode(response.data, pagePath); } return createSuccessResponse({ pagePath, components, totalComponents: components.length, }, 'scanPageComponents'); }, 'scanPageComponents'); } /** * Update multiple components in a single operation with validation and rollback support */ async bulkUpdateComponents(request) { return safeExecute(async () => { const { updates, validateFirst = true, continueOnError = false } = request; if (!Array.isArray(updates) || updates.length === 0) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, 'Updates array is required and cannot be empty'); } const results = []; // Validation phase if requested if (validateFirst) { for (const update of updates) { try { await this.httpClient.get(`${update.componentPath}.json`); } catch (error) { if (error.response?.status === 404) { results.push({ componentPath: update.componentPath, success: false, error: `Component not found: ${update.componentPath}`, phase: 'validation' }); if (!continueOnError) { return createSuccessResponse({ success: false, message: 'Bulk update failed during validation phase', results, totalUpdates: updates.length, successfulUpdates: 0 }, 'bulkUpdateComponents'); } } } } } // Update phase let successCount = 0; for (const update of updates) { try { const result = await this.updateComponent({ componentPath: update.componentPath, properties: update.properties }); results.push({ componentPath: update.componentPath, success: true, result: result, phase: 'update' }); successCount++; } catch (error) { results.push({ componentPath: update.componentPath, success: false, error: error.message, phase: 'update' }); if (!continueOnError) { break; } } } return createSuccessResponse({ success: successCount === updates.length, message: `Bulk update completed: ${successCount}/${updates.length} successful`, results, totalUpdates: updates.length, successfulUpdates: successCount, failedUpdates: updates.length - successCount }, 'bulkUpdateComponents'); }, 'bulkUpdateComponents'); } /** * Update the image path for an image component and verify the update */ async updateImagePath(componentPath, newImagePath) { return this.updateComponent({ componentPath, properties: { fileReference: newImagePath } }); } /** * Validate component properties based on component type */ validateComponentProps(pageData, componentType, props) { const warnings = []; const errors = []; if (componentType === 'text' && !props.text && !props.richText) { warnings.push('Text component should have text or richText property'); } if (componentType === 'image' && !props.fileReference && !props.src) { errors.push('Image component requires fileReference or src property'); } return { valid: errors.length === 0, errors, warnings, componentType, propsValidated: Object.keys(props).length, }; } } //# sourceMappingURL=component-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