n8n
Version:
n8n Workflow Automation Tool
319 lines • 15 kB
JavaScript
"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