UNPKG

n8n

Version:

n8n Workflow Automation Tool

319 lines • 15 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ExecutionPersistence = void 0; const backend_common_1 = require("@n8n/backend-common"); const config_1 = require("@n8n/config"); const constants_1 = require("@n8n/constants"); const db_1 = require("@n8n/db"); const di_1 = require("@n8n/di"); const flatted_1 = require("flatted"); const n8n_core_1 = require("n8n-core"); const n8n_workflow_1 = require("n8n-workflow"); const db_store_1 = require("./execution-data/db-store"); const fs_store_1 = require("./execution-data/fs-store"); const missing_execution_data_error_1 = require("./execution-data/missing-execution-data.error"); const duplicate_execution_error_1 = require("../errors/duplicate-execution.error"); let ExecutionPersistence = class ExecutionPersistence { constructor(executionRepository, binaryDataService, fsStore, dbStore, storageConfig, executionsConfig, databaseConfig, errorReporter) { this.executionRepository = executionRepository; this.binaryDataService = binaryDataService; this.fsStore = fsStore; this.dbStore = dbStore; this.storageConfig = storageConfig; this.executionsConfig = executionsConfig; this.databaseConfig = databaseConfig; this.errorReporter = errorReporter; } async create(payload) { const { data: rawData, workflowData, ...rest } = payload; const { connections, nodes, name, settings, id } = workflowData; const workflowSnapshot = { connections, nodes, name, settings, id }; const storedAt = this.storageConfig.modeTag; const executionEntity = { ...rest, createdAt: new Date(), storedAt }; const data = (0, flatted_1.stringify)(rawData); const workflowVersionId = workflowData.versionId ?? null; try { return await this.executionRepository.manager.transaction(async (tx) => { const { identifiers } = await tx.insert(db_1.ExecutionEntity, executionEntity); const executionId = String(identifiers[0].id); const ref = { workflowId: id, executionId }; const bundle = { data, workflowData: workflowSnapshot, workflowVersionId }; await this.getStoreFor(storedAt).write(ref, bundle, tx); return executionId; }); } catch (error) { if (executionEntity.deduplicationKey && this.isDuplicateExecutionError(error)) { throw new duplicate_execution_error_1.DuplicateExecutionError(executionEntity.deduplicationKey, error); } throw error; } } async updateExistingExecution(executionId, execution, conditions) { const hasDataField = execution.data !== undefined || execution.workflowData !== undefined; if (!hasDataField) { return await this.updateEntityOnly(executionId, execution, conditions); } const entity = await this.executionRepository.findOne({ where: this.buildEntityWhereCondition(executionId, conditions), select: ['id', 'workflowId', 'storedAt'], }); if (!entity) return false; const ref = { workflowId: entity.workflowId, executionId }; const store = this.getStoreFor(entity.storedAt); return await this.applyDataUpdate(ref, store, execution, conditions); } async findSingleExecution(id, options) { if (!options?.includeData) { return await this.executionRepository.findSingleExecution(id, options); } const entity = await this.executionRepository.findOne({ where: { id, ...options.where }, relations: { metadata: true, ...(options.includeAnnotation ? { annotation: { tags: true } } : {}), }, }); if (!entity) return undefined; const store = this.getStoreFor(entity.storedAt); const bundle = await store.read({ workflowId: entity.workflowId, executionId: entity.id }); if (!bundle) { if (entity.storedAt === 'db') { this.executionRepository.reportInvalidExecutions([entity]); return undefined; } throw new missing_execution_data_error_1.MissingExecutionDataError({ workflowId: entity.workflowId, executionId: entity.id, }); } return (await this.assembleExecution(entity, bundle, options)); } async findMultipleExecutions(queryParams, options) { if (!options?.includeData) { return await this.executionRepository.findMultipleExecutions(queryParams, options); } queryParams.relations ??= []; if (Array.isArray(queryParams.relations)) { if (!queryParams.relations.includes('metadata')) queryParams.relations.push('metadata'); } else { queryParams.relations.metadata = true; } if (queryParams.select) { if (Array.isArray(queryParams.select)) { for (const field of ['id', 'workflowId', 'storedAt']) { if (!queryParams.select.includes(field)) queryParams.select.push(field); } } else { queryParams.select.id = true; queryParams.select.workflowId = true; queryParams.select.storedAt = true; } } const entities = await this.executionRepository.find(queryParams); if (entities.length === 0) return []; const entitiesByLocation = new Map(); for (const entity of entities) { const group = entitiesByLocation.get(entity.storedAt) ?? []; group.push(entity); entitiesByLocation.set(entity.storedAt, group); } const bundlesById = new Map(); await Promise.all([...entitiesByLocation].map(async ([location, group]) => { const refs = group.map((e) => ({ workflowId: e.workflowId, executionId: e.id })); const bundles = await this.getStoreFor(location).readMany(refs); for (const [id, bundle] of bundles) bundlesById.set(id, bundle); })); const invalidEntities = entities.filter((e) => !bundlesById.has(e.id)); if (invalidEntities.length > 0) { this.executionRepository.reportInvalidExecutions(invalidEntities); } const assembled = await Promise.all(entities.map(async (entity) => { const bundle = bundlesById.get(entity.id); if (!bundle) return null; return await this.assembleExecution(entity, bundle, options); })); return assembled.filter((e) => e !== null); } async deleteInFlightExecution(target) { if (this.executionsConfig.pruneData) { const bufferMs = this.executionsConfig.pruneDataHardDeleteBuffer * constants_1.Time.hours.toMilliseconds; const deletedAt = new Date(Date.now() - bufferMs); await this.executionRepository.update(target.executionId, { deletedAt }); } else { await this.hardDelete(target); } } async hardDelete(target) { const targets = Array.isArray(target) ? target : [target]; if (targets.length === 0) return; const fsTargets = targets.filter((t) => t.storedAt === 'fs'); await Promise.all([ this.executionRepository.deleteByIds(targets.map((t) => t.executionId)), this.binaryDataService.deleteMany(targets.map((t) => ({ type: 'execution', ...t }))), fsTargets.length > 0 ? this.fsStore.delete(fsTargets) : Promise.resolve(), ]); } async hardDeleteBy(criteria) { const refs = await this.executionRepository.deleteExecutionsByFilter(criteria); const fsRefs = refs.filter((r) => r.storedAt === 'fs'); if (fsRefs.length > 0) await this.fsStore.delete(fsRefs); } async updateEntityOnly(executionId, execution, conditions) { const updatableColumns = this.pickUpdatableEntityColumns(execution); if (Object.keys(updatableColumns).length === 0) return true; const whereCondition = this.buildEntityWhereCondition(executionId, conditions); const result = await this.executionRepository.update(whereCondition, updatableColumns); return (result.affected ?? 0) > 0; } async applyDataUpdate(ref, store, execution, conditions) { const { data, workflowData } = execution; const updatableColumns = this.pickUpdatableEntityColumns(execution); return await this.executionRepository.manager.transaction(async (tx) => { const whereCondition = this.buildEntityWhereCondition(ref.executionId, conditions); if (Object.keys(updatableColumns).length > 0) { const result = await tx.update(db_1.ExecutionEntity, whereCondition, updatableColumns); if ((result.affected ?? 0) === 0) return false; } else if (conditions) { const matchingRows = await tx.count(db_1.ExecutionEntity, { where: whereCondition }); if (matchingRows === 0) return false; } const existing = await store.read(ref, tx); if (!existing) throw new missing_execution_data_error_1.MissingExecutionDataError(ref); await store.write(ref, { data: data !== undefined ? (0, flatted_1.stringify)(data) : existing.data, workflowData: workflowData ? this.toWorkflowSnapshot(workflowData) : existing.workflowData, workflowVersionId: existing.workflowVersionId, }, tx); return true; }); } pickUpdatableEntityColumns(execution) { const { id: _id, data: _data, workflowId: _workflowId, workflowData: _workflowData, workflowVersionId: _workflowVersionId, createdAt: _createdAt, startedAt: _startedAt, customData: _customData, ...updatableColumns } = execution; return updatableColumns; } buildEntityWhereCondition(executionId, conditions) { if (conditions?.requireStatus && conditions?.requireNotCanceled) { throw new n8n_workflow_1.UnexpectedError('`requireStatus` and `requireNotCanceled` cannot be combined'); } const where = { id: executionId }; if (conditions?.requireStatus) where.status = conditions.requireStatus; if (conditions?.requireNotFinished) where.finished = false; if (conditions?.requireNotCanceled) where.status = (0, db_1.Not)('canceled'); return where; } getStoreFor(location) { switch (location) { case 'db': return this.dbStore; case 'fs': return this.fsStore; } const _exhaustive = location; throw new Error(`Unknown storage location: ${String(_exhaustive)}`); } toWorkflowSnapshot(workflowData) { const { id, name, nodes, connections, settings } = workflowData; return { id, name, nodes, connections, settings }; } async assembleExecution(entity, bundle, options) { const { metadata, annotation, ...rest } = entity; const data = await this.parseExecutionData(bundle.data, options); const serializedAnnotation = this.serializeAnnotation(annotation); if (entity.status === 'success' && bundle.data === '[]') { this.errorReporter.error('Found successful execution where data is empty stringified array', { extra: { executionId: entity.id, workflowId: bundle.workflowData.id }, }); } return { ...rest, data, workflowData: bundle.workflowData, workflowVersionId: bundle.workflowVersionId ?? null, customData: Object.fromEntries(metadata.map((m) => [m.key, m.value])), ...(options.includeAnnotation && serializedAnnotation ? { annotation: serializedAnnotation } : {}), }; } async parseExecutionData(data, options) { if (!options.unflattenData) return data; const deserialized = await (0, backend_common_1.parseFlatted)(data); if (!deserialized) return undefined; return (0, n8n_workflow_1.migrateRunExecutionData)(deserialized); } serializeAnnotation(annotation) { if (!annotation) return null; const { id, vote, tags } = annotation; return { id, vote, tags: tags?.map(({ id, name }) => ({ id, name })) ?? [], }; } isDuplicateExecutionError(error) { if (!(error instanceof Error) || !('driverError' in error)) return false; const { driverError } = error; if (typeof driverError !== 'object' || driverError === null || !('code' in driverError)) { return false; } const { code } = driverError; if (typeof code !== 'string') return false; if (!error.message.includes('deduplicationKey')) return false; if (this.databaseConfig.type === 'postgresdb') { return code === '23505'; } return (code === 'SQLITE_CONSTRAINT_UNIQUE' || (code === 'SQLITE_CONSTRAINT' && error.message.includes('UNIQUE constraint failed'))); } }; exports.ExecutionPersistence = ExecutionPersistence; exports.ExecutionPersistence = ExecutionPersistence = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [db_1.ExecutionRepository, n8n_core_1.BinaryDataService, fs_store_1.FsStore, db_store_1.DbStore, n8n_core_1.StorageConfig, config_1.ExecutionsConfig, config_1.DatabaseConfig, n8n_core_1.ErrorReporter]) ], ExecutionPersistence); //# sourceMappingURL=execution-persistence.js.map