suitecrm-mcp-server
Version:
Model Context Protocol server for SuiteCRM integration with natural language SQL reporting
254 lines • 9.85 kB
JavaScript
/**
* Module management service for SuiteCRM
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ModuleService = void 0;
const zod_1 = require("zod");
const types_1 = require("../types");
const http_1 = require("../utils/http");
const logger_1 = require("../utils/logger");
// Validation schemas
const ModuleInfoSchema = zod_1.z.object({
name: zod_1.z.string().min(1, 'Module name is required'),
label: zod_1.z.string().min(1, 'Module label is required'),
type: zod_1.z.string().optional(),
fields: zod_1.z.array(zod_1.z.string()).optional(),
relationships: zod_1.z.array(zod_1.z.string()).optional()
});
function safeJsonParse(data) {
if (typeof data === 'string') {
try {
return JSON.parse(data);
}
catch {
return data;
}
}
return data;
}
class ModuleService {
httpClient;
schemaCache = new Map();
cacheTtl = 30 * 60 * 1000; // 30 minutes
constructor(baseUrl, timeout = 30000) {
this.httpClient = new http_1.HttpClient({
baseURL: baseUrl,
timeout
});
}
/**
* Get available modules from SuiteCRM
*/
async getModules(accessToken) {
try {
logger_1.logger.info('Fetching available modules');
this.httpClient.setAuthToken(accessToken);
const response = await this.httpClient.get('/Api/V8/custom/modules');
if (response.status !== 200) {
throw new types_1.SuiteCRMError(`Failed to fetch modules: ${response.status} ${response.statusText}`);
}
// Handle SuiteCRM's actual response structure
const crm = response.data;
const data = safeJsonParse(crm.data);
const modules = [];
for (const moduleData of data) {
try {
const validatedModule = ModuleInfoSchema.parse(moduleData);
// Transform to remove undefined optional properties
const moduleInfo = {
name: validatedModule.name,
label: validatedModule.label,
type: validatedModule.type || 'default',
};
if (validatedModule.fields) {
moduleInfo.fields = validatedModule.fields;
}
if (validatedModule.relationships) {
moduleInfo.relationships = validatedModule.relationships;
}
modules.push(moduleInfo);
}
catch (validationError) {
logger_1.logger.warn('Invalid module data', {
module: moduleData.name || 'unknown',
error: validationError instanceof Error ? validationError.message : 'Unknown validation error'
});
}
}
logger_1.logger.info('Successfully fetched modules', { count: modules.length });
return modules;
}
catch (error) {
logger_1.logger.error('Failed to fetch modules', {}, error instanceof Error ? error : new Error(String(error)));
if (error instanceof types_1.SuiteCRMError || error instanceof types_1.ValidationError) {
throw error;
}
throw new types_1.SuiteCRMError(`Failed to fetch modules: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Get detailed schema for a specific module
*/
async getModuleSchema(accessToken, moduleName) {
try {
// Check cache first
const cacheKey = `${moduleName}:${accessToken.substring(0, 8)}`;
const cached = this.schemaCache.get(cacheKey);
if (cached && Date.now() < cached.expiresAt) {
logger_1.logger.info('Using cached module schema', { module: moduleName });
return cached.schema;
}
logger_1.logger.info('Fetching module schema', { module: moduleName });
this.httpClient.setAuthToken(accessToken);
const response = await this.httpClient.get(`/Api/V8/custom/modules/${moduleName}/schema`);
if (response.status !== 200) {
throw new types_1.SuiteCRMError(`Failed to fetch module schema: ${response.status} ${response.statusText}`);
}
// Handle SuiteCRM's actual response structure
const crm = response.data;
const data = safeJsonParse(crm.data);
const fieldsObj = data?.fields || {};
const relationshipsObj = data?.relationships || {};
// Convert fields object to array
const fields = Object.entries(fieldsObj).map(([name, def]) => ({
name,
label: def.label || name,
type: def.type || 'string',
required: !!def.required,
readonly: false // You may want to infer this if possible
// Add other properties as needed
}));
// Convert relationships object to array
const relationships = Object.entries(relationshipsObj).map(([name, def]) => ({
name,
label: name,
type: def.relationship_type || 'one-to-many',
module: def.rhs_module || '',
key: def.rhs_key || '',
foreign_key: def.lhs_key || ''
// Add other properties as needed
}));
const schema = {
name: moduleName,
label: moduleName, // Or use a better label if available
fields,
relationships,
indexes: []
};
// Cache the schema
this.schemaCache.set(cacheKey, {
schema,
expiresAt: Date.now() + this.cacheTtl
});
logger_1.logger.info('Successfully fetched module schema', {
module: moduleName,
fields: schema.fields.length,
relationships: schema.relationships.length
});
return schema;
}
catch (error) {
logger_1.logger.error('Failed to fetch module schema', { module: moduleName }, error instanceof Error ? error : new Error(String(error)));
if (error instanceof types_1.SuiteCRMError || error instanceof types_1.ValidationError) {
throw error;
}
throw new types_1.SuiteCRMError(`Failed to fetch module schema for ${moduleName}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Get multiple module schemas at once
*/
async getModuleSchemas(accessToken, moduleNames) {
const schemas = {};
const errors = [];
// Fetch schemas in parallel with error handling
const promises = moduleNames.map(async (moduleName) => {
try {
const schema = await this.getModuleSchema(accessToken, moduleName);
schemas[moduleName] = schema;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
errors.push(`${moduleName}: ${errorMessage}`);
logger_1.logger.error('Failed to fetch schema for module', { module: moduleName }, error instanceof Error ? error : new Error(errorMessage));
}
});
await Promise.all(promises);
if (errors.length > 0) {
logger_1.logger.warn('Some module schemas failed to load', { errors });
}
return schemas;
}
/**
* Get field information for a specific module
*/
async getModuleFields(accessToken, moduleName) {
const schema = await this.getModuleSchema(accessToken, moduleName);
return schema.fields;
}
/**
* Get relationship information for a specific module
*/
async getModuleRelationships(accessToken, moduleName) {
const schema = await this.getModuleSchema(accessToken, moduleName);
return schema.relationships;
}
/**
* Check if a module exists
*/
async moduleExists(accessToken, moduleName) {
try {
await this.getModuleSchema(accessToken, moduleName);
return true;
}
catch (error) {
return false;
}
}
/**
* Get cache statistics
*/
getCacheStats() {
let expired = 0;
let valid = 0;
for (const [_, cached] of this.schemaCache.entries()) {
if (Date.now() >= cached.expiresAt) {
expired++;
}
else {
valid++;
}
}
return {
total: this.schemaCache.size,
expired,
valid
};
}
/**
* Clear schema cache
*/
clearCache() {
const count = this.schemaCache.size;
this.schemaCache.clear();
logger_1.logger.info('Module schema cache cleared', { schemasCleared: count });
}
/**
* Clear cache for specific module
*/
clearModuleCache(moduleName) {
const keysToDelete = [];
for (const [key, _] of this.schemaCache.entries()) {
if (key.startsWith(`${moduleName}:`)) {
keysToDelete.push(key);
}
}
keysToDelete.forEach(key => this.schemaCache.delete(key));
if (keysToDelete.length > 0) {
logger_1.logger.info('Module cache cleared', { module: moduleName, keysCleared: keysToDelete.length });
}
}
}
exports.ModuleService = ModuleService;
//# sourceMappingURL=modules.js.map
;