UNPKG

@traien/n8n-nodes-espocrm

Version:
485 lines 25.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EspoCRMTool = void 0; const n8n_workflow_1 = require("n8n-workflow"); const GenericFunctions_1 = require("../EspoCRM/GenericFunctions"); const OPERATION_VALUES = ['get', 'getAll', 'create', 'update', 'delete', 'getSchema']; const OPERATION_ALIAS_MAP = { get: 'get', retrieve: 'get', getone: 'get', findone: 'get', read: 'get', getall: 'getAll', list: 'getAll', find: 'getAll', search: 'getAll', count: 'getAll', query: 'getAll', create: 'create', insert: 'create', add: 'create', make: 'create', update: 'update', modify: 'update', patch: 'update', change: 'update', delete: 'delete', remove: 'delete', destroy: 'delete', schema: 'getSchema', metadata: 'getSchema', describe: 'getSchema', structure: 'getSchema', }; class EspoCRMTool { constructor() { this.description = { displayName: 'EspoCRM Tool', name: 'espoCrmTool', icon: 'file:espocrm.svg', group: ['transform'], version: 1, usableAsTool: true, subtitle: 'Interact with EspoCRM records', description: 'Create, read, update, delete, and COUNT records in EspoCRM. Supports natural language commands like "Find all accounts" or "Create a new case". Note: You can only interact with ONE entity type per tool call. To interact with multiple entities, make multiple tool calls.', defaults: { name: 'EspoCRM Tool', }, inputs: [{ type: 'main', required: true }], outputs: [{ type: 'main', required: true }], documentationUrl: 'https://docs.espocrm.com/development/api/', credentials: [ { name: 'espoCRMApi', required: true, }, ], properties: [ { displayName: 'Entity Type', name: 'entityType', type: 'string', default: '', required: false, description: 'The EspoCRM entity type (e.g., Account, Contact, Lead). Required for all operations except "Get Schema".', }, { displayName: 'Operations', name: 'operations', type: 'multiOptions', options: [ { name: 'Get Record', value: 'get' }, { name: 'List/Search Records', value: 'getAll' }, { name: 'Create Record', value: 'create' }, { name: 'Update Record', value: 'update' }, { name: 'Delete Record', value: 'delete' }, { name: 'Get Schema/Metadata', value: 'getSchema' }, ], default: ['get'], description: 'The action to perform. Use "Get Schema" to discover available entity types and their fields. "List/Search Records" returns the TOTAL count of records matching the filter, so use it for counting tasks.', }, { displayName: 'Record ID', name: 'recordId', type: 'string', default: '', displayOptions: { show: { operations: ['get', 'update', 'delete'], }, }, description: 'The ID of the record to get, update, or delete', }, { displayName: 'Data', name: 'data', type: 'json', default: '{}', displayOptions: { show: { operations: ['create', 'update'], }, }, description: 'The data object with fields to create or update. \n\n**CRITICAL**: When creating a Case, Account, Contact, or Task, you **MUST** include a "name" field. \n\nExample: { "name": "Review Tasks", "status": "New" }', }, { displayName: 'Filters', name: 'filters', type: 'json', default: '{}', displayOptions: { show: { operations: ['getAll'], }, }, description: 'Filters to apply to the search (EspoCRM where clause). E.g. { "status": "Completed" } or { "name": "Acme%" }.', }, { displayName: 'Return All', name: 'returnAll', type: 'boolean', default: false, displayOptions: { show: { operations: ['getAll'], }, }, description: 'Whether to return all results or use pagination', }, { displayName: 'Limit', name: 'limit', type: 'number', default: 50, typeOptions: { minValue: 1, maxValue: 500, }, displayOptions: { show: { operations: ['getAll'], returnAll: [false], }, }, description: 'Maximum number of records to return', }, ], }; } async execute() { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p; const items = this.getInputData(); const returnData = []; for (let i = 0; i < items.length; i++) { try { const inputJson = items[i].json; const entityTypeParam = this.getNodeParameter('entityType', i).trim(); const entityType = (inputJson.entityType || entityTypeParam).trim(); const configuredOperationsRaw = this.getNodeParameter('operations', i); const configuredOperationsArray = (Array.isArray(configuredOperationsRaw) ? configuredOperationsRaw : [configuredOperationsRaw]); const allowedOperations = new Set(); for (const rawOp of configuredOperationsArray) { try { const mapped = mapOperationName(rawOp, this, i); allowedOperations.add(mapped); } catch (error) { } } if (allowedOperations.size === 0) allowedOperations.add('get'); let rawOperationsInput = (_b = (_a = inputJson.operations) !== null && _a !== void 0 ? _a : inputJson.Operations) !== null && _b !== void 0 ? _b : configuredOperationsArray; if (!Array.isArray(rawOperationsInput)) { rawOperationsInput = [rawOperationsInput]; } const operations = []; for (const entry of rawOperationsInput) { if (typeof entry === 'string') { try { const op = mapOperationName(entry, this, i); if (allowedOperations.has(op)) { operations.push(op); } } catch (e) { } } else if (typeof entry === 'object' && entry !== null) { const obj = entry; const opName = (obj.operation || obj.op || obj.type || obj.name); if (opName) { const op = mapOperationName(opName, this, i); if (allowedOperations.has(op)) { operations.push(op); } } if (obj.where && inputJson.filters === undefined) inputJson.filters = { where: obj.where }; if (obj.filters && inputJson.filters === undefined) inputJson.filters = obj.filters; if (obj.data && inputJson.data === undefined) inputJson.data = obj.data; if (obj.returnAll !== undefined && inputJson.returnAll === undefined) inputJson.returnAll = obj.returnAll; if (obj.limit !== undefined && inputJson.limit === undefined) inputJson.limit = obj.limit; } } if (operations.length === 0) { operations.push('get'); } const results = []; for (const operation of operations) { let result; try { if (operation === 'getSchema') { const response = await GenericFunctions_1.espoApiRequest.call(this, 'GET', 'Metadata'); if (entityType) { const scopes = response.scopes || {}; const entityDef = ((_c = response.entityDefs) === null || _c === void 0 ? void 0 : _c[entityType]) || {}; result = { entity: entityType, scope: scopes[entityType], fields: entityDef.fields, }; } else { const scopes = response.scopes || {}; const entities = Object.keys(scopes).filter(key => scopes[key].object && !scopes[key].disabled); result = { availableEntityTypes: entities, message: 'Specify an entityType to get detailed field definitions.', }; } results.push({ operation, result }); continue; } if (!entityType) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Entity Type is required for this operation.', { itemIndex: i }); } if (!/^[a-zA-Z0-9_-]+$/.test(entityType)) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid Entity Type: "${entityType}". Must contain only letters, numbers, underscores, or hyphens.`, { itemIndex: i }); } const encodedEntityType = encodeURIComponent(entityType); const endpoint = `/${encodedEntityType}`; switch (operation) { case 'get': { const fallbackRecordId = this.getNodeParameter('recordId', i, ''); const recordIdRaw = (_e = (_d = inputJson.recordId) !== null && _d !== void 0 ? _d : inputJson.id) !== null && _e !== void 0 ? _e : fallbackRecordId; const recordId = recordIdRaw === undefined || recordIdRaw === null ? '' : `${recordIdRaw}`.trim(); if (!recordId) { if (operations.length === 1) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'recordId is required for get operations', { itemIndex: i }); } continue; } if (!/^[a-zA-Z0-9_-]+$/.test(recordId)) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid recordId: "${recordId}"`, { itemIndex: i }); } const encodeId = (value) => encodeURIComponent(value); result = await GenericFunctions_1.espoApiRequest.call(this, 'GET', `${endpoint}/${encodeId(recordId)}`); break; } case 'delete': { const fallbackRecordId = this.getNodeParameter('recordId', i, ''); const recordIdRaw = (_g = (_f = inputJson.recordId) !== null && _f !== void 0 ? _f : inputJson.id) !== null && _g !== void 0 ? _g : fallbackRecordId; const recordId = recordIdRaw === undefined || recordIdRaw === null ? '' : `${recordIdRaw}`.trim(); if (!recordId) { if (operations.length === 1) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'recordId is required for delete operations', { itemIndex: i }); } continue; } if (!/^[a-zA-Z0-9_-]+$/.test(recordId)) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid recordId: "${recordId}"`, { itemIndex: i }); } const encodeId = (value) => encodeURIComponent(value); await GenericFunctions_1.espoApiRequest.call(this, 'DELETE', `${endpoint}/${encodeId(recordId)}`); result = { success: true, entityType, id: recordId }; break; } case 'update': { const fallbackRecordId = this.getNodeParameter('recordId', i, ''); const recordIdRaw = (_j = (_h = inputJson.recordId) !== null && _h !== void 0 ? _h : inputJson.id) !== null && _j !== void 0 ? _j : fallbackRecordId; const recordId = recordIdRaw === undefined || recordIdRaw === null ? '' : `${recordIdRaw}`.trim(); if (!recordId) { if (operations.length === 1) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'recordId is required for update operations', { itemIndex: i }); } continue; } if (!/^[a-zA-Z0-9_-]+$/.test(recordId)) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid recordId: "${recordId}"`, { itemIndex: i }); } const fallbackData = this.getNodeParameter('data', i, undefined); const parsedData = buildDataPayload(inputJson, this, i, fallbackData); if (!parsedData || Object.keys(parsedData).length === 0) { if (operations.length === 1) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Provide a data object with fields to update', { itemIndex: i }); } continue; } const encodeId = (value) => encodeURIComponent(value); result = await GenericFunctions_1.espoApiRequest.call(this, 'PATCH', `${endpoint}/${encodeId(recordId)}`, parsedData); break; } case 'create': { const fallbackData = this.getNodeParameter('data', i, undefined); const parsedData = buildDataPayload(inputJson, this, i, fallbackData); if (!parsedData || Object.keys(parsedData).length === 0) { if (operations.length === 1) { throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Provide a data object with fields to create', { itemIndex: i }); } continue; } const entitiesRequiringName = ['Case', 'Account', 'Contact', 'Task', 'Opportunity', 'Lead']; if (entitiesRequiringName.includes(entityType) && !parsedData.name) { const fallbackName = parsedData.subject || parsedData.title || parsedData.description || `New ${entityType} (Auto-created)`; parsedData.name = fallbackName; } result = await GenericFunctions_1.espoApiRequest.call(this, 'POST', endpoint, parsedData); break; } case 'getAll': { const fallbackFilters = this.getNodeParameter('filters', i, undefined); const filtersSource = (_l = (_k = inputJson.filters) !== null && _k !== void 0 ? _k : (inputJson.where ? { where: inputJson.where } : undefined)) !== null && _l !== void 0 ? _l : fallbackFilters; const parsedFiltersInput = parseJsonInput(filtersSource, 'filters', this, i); const parsedFilters = normalizeFilters(parsedFiltersInput, this, i); const { qs, headers } = buildQueryParts(parsedFilters, this, i); const returnAllInput = (_m = inputJson.returnAll) !== null && _m !== void 0 ? _m : inputJson.all; const returnAll = typeof returnAllInput === 'boolean' ? returnAllInput : this.getNodeParameter('returnAll', i, false); const limitInput = (_p = (_o = inputJson.limit) !== null && _o !== void 0 ? _o : inputJson.maxSize) !== null && _p !== void 0 ? _p : this.getNodeParameter('limit', i, 50); let limit = Number(limitInput); if (!Number.isFinite(limit) || limit < 1) limit = 1; if (returnAll) { const items = await GenericFunctions_1.espoApiRequestAllItems.call(this, 'GET', endpoint, {}, qs); result = { list: items, total: items.length }; } else { const cappedLimit = Math.max(1, Math.min(limit, 500)); qs.maxSize = cappedLimit; const response = await GenericFunctions_1.espoApiRequest.call(this, 'GET', endpoint, {}, qs, undefined, headers); result = response; } break; } default: throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Operation "${operation}" is not supported`, { itemIndex: i }); } results.push({ operation, result: typeof result === 'string' ? { result } : result, }); } catch (operationError) { if (operations.length === 1) { throw operationError; } results.push({ operation, result: { error: operationError.message }, }); } } const outputJson = results.length === 1 ? results[0].result : { operations: results }; returnData.push({ json: outputJson, pairedItem: { item: i }, }); } catch (error) { if (!this.continueOnFail()) throw error; returnData.push({ json: { error: error.message }, pairedItem: { item: i }, }); } } return [returnData]; } } exports.EspoCRMTool = EspoCRMTool; function parseJsonInput(value, fieldName, ctx, itemIndex) { if (value === undefined || value === null || value === '') { return undefined; } if (typeof value !== 'string') { return value; } const trimmed = value.trim(); if (!trimmed) return undefined; try { return JSON.parse(trimmed); } catch (error) { throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `Invalid JSON provided for ${fieldName}: ${error.message}`, { itemIndex }); } } function normalizeFilters(filtersInput, ctx, itemIndex) { if (filtersInput === undefined || filtersInput === null || filtersInput === '') { return undefined; } if (typeof filtersInput === 'string') { try { return JSON.parse(filtersInput); } catch (error) { throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Failed to parse filters JSON string', { itemIndex }); } } if (typeof filtersInput === 'object' && !Array.isArray(filtersInput)) { return filtersInput; } throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Filters must be a JSON object or JSON string', { itemIndex }); } function buildQueryParts(filters, ctx, itemIndex) { const qs = {}; const headers = {}; if (!filters) { return { qs, headers }; } for (const [key, value] of Object.entries(filters)) { if (value === undefined || value === null || value === '') { continue; } if (key === 'where') { if (typeof value === 'string') { try { qs.where = JSON.parse(value); } catch (error) { throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), "Invalid JSON supplied for filters.where", { itemIndex }); } } else { qs.where = value; } continue; } if (key === 'skipTotalCount' && value === true) { headers['X-No-Total'] = 'true'; continue; } qs[key] = value; } return { qs, headers }; } function buildDataPayload(inputJson, ctx, itemIndex, fallbackData) { const valueFromInput = parseJsonInput(inputJson.data, 'data', ctx, itemIndex); if (valueFromInput && Object.keys(valueFromInput).length > 0) { return valueFromInput; } const valueFromFallback = parseJsonInput(fallbackData, 'data', ctx, itemIndex); if (valueFromFallback && Object.keys(valueFromFallback).length > 0) { return valueFromFallback; } const { entityType: _et, recordId: _rid, id: _id, operations: _ops, filters: _filters, returnAll: _returnAll, limit: _limit, data: _data, ...rest } = inputJson; const cleaned = rest; return Object.keys(cleaned).length > 0 ? cleaned : undefined; } function mapOperationName(raw, ctx, itemIndex) { const key = raw.trim().toLowerCase(); if (!key) { throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'Operation name cannot be empty', { itemIndex }); } if (key.startsWith('get all ') || key.startsWith('find ') || key.startsWith('search ') || key.startsWith('list ') || key.startsWith('query ') || key.startsWith('count ')) return 'getAll'; if (key.startsWith('get ') || key.startsWith('read ') || key.startsWith('retrieve ')) return 'get'; if (key.startsWith('create ') || key.startsWith('add ') || key.startsWith('insert ') || key.startsWith('make ')) return 'create'; if (key.startsWith('update ') || key.startsWith('modify ') || key.startsWith('change ') || key.startsWith('patch ')) return 'update'; if (key.startsWith('delete ') || key.startsWith('remove ') || key.startsWith('destroy ')) return 'delete'; const mapped = OPERATION_ALIAS_MAP[key] || OPERATION_VALUES.find((op) => op.toLowerCase() === key); if (!mapped) { throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `Operation "${raw}" is not supported`, { itemIndex }); } return mapped; } //# sourceMappingURL=EspoCRMTool.node.js.map