"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.ExecutionService = exports.allowedExecutionsQueryFilterFields = exports.schemaGetExecutionsQueryFilter = void 0;
const api_types_1 = require("@n8n/api-types");
const backend_common_1 = require("@n8n/backend-common");
const config_1 = require("@n8n/config");
const db_1 = require("@n8n/db");
const di_1 = require("@n8n/di");
const flatted_1 = require("flatted");
const jsonschema_1 = require("jsonschema");
const n8n_workflow_1 = require("n8n-workflow");
const active_executions_1 = require("../active-executions");
const concurrency_control_service_1 = require("../concurrency/concurrency-control.service");
const aborted_execution_retry_error_1 = require("../errors/aborted-execution-retry.error");
const missing_execution_stop_error_1 = require("../errors/missing-execution-stop.error");
const queued_execution_retry_error_1 = require("../errors/queued-execution-retry.error");
const conflict_error_1 = require("../errors/response-errors/conflict.error");
const internal_server_error_1 = require("../errors/response-errors/internal-server.error");
const not_found_error_1 = require("../errors/response-errors/not-found.error");
const event_service_1 = require("../events/event.service");
const license_1 = require("../license");
const node_types_1 = require("../node-types");
const wait_tracker_1 = require("../wait-tracker");
const workflow_runner_1 = require("../workflow-runner");
const workflow_sharing_service_1 = require("../workflows/workflow-sharing.service");
const execution_persistence_1 = require("./execution-persistence");
const execution_redaction_proxy_service_1 = require("./execution-redaction-proxy.service");
exports.schemaGetExecutionsQueryFilter = {
    $id: '/IGetExecutionsQueryFilter',
    type: 'object',
    properties: {
        id: { type: 'string' },
        finished: { type: 'boolean' },
        mode: { type: 'string' },
        retryOf: { type: 'string' },
        retrySuccessId: { type: 'string' },
        status: {
            type: 'array',
            items: { type: 'string' },
        },
        waitTill: { type: 'boolean' },
        workflowId: { anyOf: [{ type: 'integer' }, { type: 'string' }] },
        metadata: { type: 'array', items: { $ref: '#/$defs/metadata' } },
        startedAfter: { type: 'date-time' },
        startedBefore: { type: 'date-time' },
        annotationTags: { type: 'array', items: { type: 'string' } },
        vote: { type: 'string' },
        projectId: { type: 'string' },
        workflowVersionId: { type: 'string' },
    },
    $defs: {
        metadata: {
            type: 'object',
            required: ['key', 'value'],
            properties: {
                key: {
                    type: 'string',
                },
                value: { type: 'string' },
                exactMatch: {
                    type: 'boolean',
                    default: true,
                },
            },
        },
    },
};
exports.allowedExecutionsQueryFilterFields = Object.keys(exports.schemaGetExecutionsQueryFilter.properties);
let ExecutionService = class ExecutionService {
    constructor(globalConfig, logger, activeExecutions, executionAnnotationRepository, annotationTagMappingRepository, executionRepository, executionPersistence, workflowHistoryRepository, workflowRepository, nodeTypes, waitTracker, workflowRunner, concurrencyControl, license, workflowSharingService, eventService, executionRedactionServiceProxy) {
        this.globalConfig = globalConfig;
        this.logger = logger;
        this.activeExecutions = activeExecutions;
        this.executionAnnotationRepository = executionAnnotationRepository;
        this.annotationTagMappingRepository = annotationTagMappingRepository;
        this.executionRepository = executionRepository;
        this.executionPersistence = executionPersistence;
        this.workflowHistoryRepository = workflowHistoryRepository;
        this.workflowRepository = workflowRepository;
        this.nodeTypes = nodeTypes;
        this.waitTracker = waitTracker;
        this.workflowRunner = workflowRunner;
        this.concurrencyControl = concurrencyControl;
        this.license = license;
        this.workflowSharingService = workflowSharingService;
        this.eventService = eventService;
        this.executionRedactionServiceProxy = executionRedactionServiceProxy;
    }
    async findOne(req, sharedWorkflowIds) {
        if (!sharedWorkflowIds.length)
            return undefined;
        const { id: executionId } = req.params;
        const execution = await this.executionRepository.findIfSharedUnflatten(executionId, sharedWorkflowIds);
        if (!execution) {
            this.logger.info('Attempt to read execution was blocked due to insufficient permissions', {
                userId: req.user.id,
                executionId,
            });
            return undefined;
        }
        let redactExecutionData;
        const redactQuery = api_types_1.ExecutionRedactionQueryDtoSchema.safeParse(req.query);
        if (redactQuery.success) {
            redactExecutionData = redactQuery.data.redactExecutionData;
        }
        const processedExecution = await this.executionRedactionServiceProxy.processExecution(execution, {
            user: req.user,
            redactExecutionData,
            ipAddress: req.ip ?? '',
            userAgent: req.headers['user-agent'] ?? '',
        });
        return {
            ...execution,
            data: (0, flatted_1.stringify)(processedExecution.data),
        };
    }
    async getLastSuccessfulExecution(workflowId, user, redactExecutionData) {
        const executions = await this.executionRepository.findMultipleExecutions({
            select: ['id', 'mode', 'startedAt', 'stoppedAt', 'workflowId'],
            where: {
                workflowId,
                status: 'success',
            },
            order: { id: 'DESC' },
            take: 1,
        }, {
            includeData: true,
            unflattenData: true,
        });
        const execution = executions[0];
        if (!execution)
            return undefined;
        await this.executionRedactionServiceProxy.processExecution(execution, {
            user,
            redactExecutionData,
        });
        return execution;
    }
    async retry(req, sharedWorkflowIds) {
        const { id: executionId } = req.params;
        const execution = await this.executionRepository.findWithUnflattenedData(executionId, sharedWorkflowIds);
        if (!execution) {
            this.logger.info('Attempt to retry an execution was blocked due to insufficient permissions', {
                userId: req.user.id,
                executionId,
            });
            throw new not_found_error_1.NotFoundError(`The execution with the ID "${executionId}" does not exist.`);
        }
        if (execution.status === 'new')
            throw new queued_execution_retry_error_1.QueuedExecutionRetryError();
        if (!execution.data.executionData)
            throw new aborted_execution_retry_error_1.AbortedExecutionRetryError();
        if (execution.finished) {
            throw new conflict_error_1.ConflictError('The execution succeeded, so it cannot be retried.');
        }
        const executionMode = 'retry';
        execution.workflowData.active = false;
        execution.workflowData.activeVersionId = null;
        const data = {
            executionMode,
            executionData: execution.data,
            retryOf: req.params.id,
            workflowData: execution.workflowData,
            userId: req.user.id,
        };
        const { lastNodeExecuted } = data.executionData.resultData;
        if (lastNodeExecuted) {
            delete data.executionData.resultData.error;
            const { length } = data.executionData.resultData.runData[lastNodeExecuted];
            if (length > 0 &&
                data.executionData.resultData.runData[lastNodeExecuted][length - 1].error !== undefined) {
                data.executionData.resultData.runData[lastNodeExecuted].pop();
            }
        }
        if (req.body.loadWorkflow) {
            const workflowId = execution.workflowData.id;
            const workflowData = (await this.workflowRepository.findOneBy({
                id: workflowId,
            }));
            if (workflowData === undefined) {
                throw new n8n_workflow_1.UserError('Workflow could not be found and so the data not be loaded for the retry.', { extra: { workflowId } });
            }
            data.workflowData = workflowData;
            const workflowInstance = new n8n_workflow_1.Workflow({
                id: workflowData.id,
                name: workflowData.name,
                nodes: workflowData.nodes,
                connections: workflowData.connections,
                active: false,
                nodeTypes: this.nodeTypes,
                staticData: undefined,
                settings: workflowData.settings,
            });
            for (const stack of data.executionData.executionData.nodeExecutionStack) {
                const node = workflowInstance.getNode(stack.node.name);
                if (node === null) {
                    this.logger.error('Failed to retry an execution because a node could not be found', {
                        userId: req.user.id,
                        executionId,
                        nodeName: stack.node.name,
                    });
                    throw new n8n_workflow_1.WorkflowOperationError(`Could not find the node "${stack.node.name}" in workflow. It probably got deleted or renamed. Without it the workflow can sadly not be retried.`);
                }
                stack.node = node;
            }
        }
        const retriedExecutionId = await this.workflowRunner.run(data);
        const executionData = await this.activeExecutions.getPostExecutePromise(retriedExecutionId);
        if (!executionData) {
            throw new n8n_workflow_1.UnexpectedError('The retry did not start for an unknown reason.');
        }
        this.eventService.emit('workflow-executed', {
            user: {
                id: req.user.id,
                email: req.user.email,
                firstName: req.user.firstName,
                lastName: req.user.lastName,
                role: req.user.role,
            },
            workflowId: execution.workflowId,
            workflowName: execution.workflowData.name,
            executionId: retriedExecutionId,
            source: 'user-retry',
        });
        const response = {
            id: retriedExecutionId,
            mode: executionData.mode,
            startedAt: executionData.startedAt,
            workflowId: execution.workflowId,
            finished: executionData.finished ?? false,
            retryOf: executionId,
            status: executionData.status,
            waitTill: executionData.waitTill,
            data: executionData.data,
            workflowData: execution.workflowData,
            customData: execution.customData,
            annotation: execution.annotation,
            storedAt: execution.storedAt,
        };
        const redactQuery = api_types_1.ExecutionRedactionQueryDtoSchema.safeParse(req.query);
        const redactExecutionData = redactQuery.success
            ? redactQuery.data.redactExecutionData
            : undefined;
        await this.executionRedactionServiceProxy.processExecution(response, {
            user: req.user,
            redactExecutionData,
        });
        return response;
    }
    async delete(req, sharedWorkflowIds) {
        const { deleteBefore, ids, filters: requestFiltersRaw } = req.body;
        let requestFilters;
        if (requestFiltersRaw) {
            try {
                Object.keys(requestFiltersRaw).map((key) => {
                    if (!exports.allowedExecutionsQueryFilterFields.includes(key))
                        delete requestFiltersRaw[key];
                });
                if ((0, jsonschema_1.validate)(requestFiltersRaw, exports.schemaGetExecutionsQueryFilter).valid) {
                    requestFilters = requestFiltersRaw;
                }
            }
            catch (error) {
                throw new internal_server_error_1.InternalServerError('Parameter "filter" contained invalid JSON string.', error);
            }
        }
        if (requestFilters?.metadata && !this.license.isAdvancedExecutionFiltersEnabled()) {
            delete requestFilters.metadata;
        }
        await this.executionPersistence.hardDeleteBy({
            filters: requestFilters,
            accessibleWorkflowIds: sharedWorkflowIds,
            deleteConditions: { deleteBefore, ids },
        });
        this.eventService.emit('execution-deleted', {
            user: {
                id: req.user.id,
                email: req.user.email,
                firstName: req.user.firstName,
                lastName: req.user.lastName,
                role: req.user.role,
            },
            executionIds: ids ?? [],
            deleteBefore,
        });
    }
    async createErrorExecution(error, node, workflowData, workflow, mode) {
        const saveDataErrorExecutionDisabled = workflowData?.settings?.saveDataErrorExecution === 'none';
        if (saveDataErrorExecutionDisabled)
            return;
        const executionData = (0, n8n_workflow_1.createErrorExecutionData)(node, error);
        const fullExecutionData = {
            data: executionData,
            mode,
            finished: false,
            workflowData,
            workflowId: workflow.id,
            stoppedAt: new Date(),
            status: 'error',
        };
        await this.executionPersistence.create(fullExecutionData);
    }
    async findRangeWithCount(query) {
        const results = await this.executionRepository.findManyByRangeQuery(query);
        const { range: _, ...countQuery } = query;
        const executionCount = await this.getExecutionsCountForQuery({ ...countQuery, kind: 'count' });
        return { results, ...executionCount };
    }
    async findLatestCurrentAndCompleted(query) {
        const currentStatuses = ['new', 'running'];
        const completedStatuses = n8n_workflow_1.ExecutionStatusList.filter((s) => !currentStatuses.includes(s));
        const completedQuery = {
            ...query,
            status: completedStatuses,
            order: { startedAt: 'DESC' },
        };
        const { range: _, ...countQuery } = completedQuery;
        const currentQuery = {
            ...query,
            status: currentStatuses,
            order: { top: 'running' },
        };
        const [current, completed, completedCount] = await Promise.all([
            this.executionRepository.findManyByRangeQuery(currentQuery),
            this.executionRepository.findManyByRangeQuery(completedQuery),
            this.getExecutionsCountForQuery({ ...countQuery, kind: 'count' }),
        ]);
        return {
            results: current.concat(completed),
            count: completedCount.count,
            estimated: completedCount.estimated,
        };
    }
    async getConcurrentExecutionsCount() {
        if (!this.isConcurrentExecutionsCountSupported()) {
            return -1;
        }
        return await this.executionRepository.getConcurrentExecutionsCount();
    }
    isConcurrentExecutionsCountSupported() {
        const isConcurrencyEnabled = this.globalConfig.executions.concurrency.productionLimit !== -1;
        const isInRegularMode = this.globalConfig.executions.mode === 'regular';
        if (!isConcurrencyEnabled || !isInRegularMode) {
            return false;
        }
        return true;
    }
    async getExecutionsCountForQuery(countQuery) {
        if (this.globalConfig.database.type === 'postgresdb') {
            const liveRows = await this.executionRepository.getLiveExecutionRowsOnPostgres();
            if (liveRows === -1)
                return { count: -1, estimated: false };
            if (liveRows > 100_000) {
                return { count: liveRows, estimated: true };
            }
        }
        const count = await this.executionRepository.fetchCount(countQuery);
        return { count, estimated: false };
    }
    async findAllEnqueuedExecutions() {
        return await this.executionRepository.findMultipleExecutions({
            select: ['id', 'mode'],
            where: { status: 'new' },
            order: { id: 'ASC' },
        }, { includeData: true, unflattenData: true });
    }
    async stop(executionId, sharedWorkflowIds) {
        const execution = await this.executionRepository.findWithUnflattenedData(executionId, sharedWorkflowIds);
        if (!execution) {
            this.logger.info(`Unable to stop execution "${executionId}" as it was not found or not accessible`, {
                executionId,
            });
            throw new missing_execution_stop_error_1.MissingExecutionStopError(executionId);
        }
        this.assertStoppable(execution);
        const { mode, startedAt, stoppedAt, finished, status } = this.globalConfig.executions.mode === 'regular'
            ? await this.stopInRegularMode(execution)
            : await this.stopInScalingMode(execution);
        return {
            mode,
            startedAt: new Date(startedAt),
            stoppedAt: stoppedAt ? new Date(stoppedAt) : undefined,
            finished,
            status,
        };
    }
    async stopMany(query, sharedWorkflowIds) {
        const executions = await this.executionRepository.findByStopExecutionsFilter(query);
        let stopped = 0;
        for (const { id } of executions) {
            try {
                await this.stop(id, sharedWorkflowIds);
                this.logger.debug(`Stopped execution ${id}`);
                stopped++;
            }
            catch (e) {
                if (!(e instanceof missing_execution_stop_error_1.MissingExecutionStopError)) {
                    this.logger.warn(`Unexpected error while attempting to stop execution ${id}: ${(0, n8n_workflow_1.ensureError)(e).message}`);
                }
            }
        }
        return stopped;
    }
    assertStoppable(execution) {
        const STOPPABLE_STATUSES = ['new', 'unknown', 'waiting', 'running'];
        if (!STOPPABLE_STATUSES.includes(execution.status)) {
            throw new n8n_workflow_1.WorkflowOperationError(`Only running or waiting executions can be stopped and ${execution.id} is currently ${execution.status}`);
        }
    }
    async stopInRegularMode(execution) {
        if (this.concurrencyControl.has(execution.id)) {
            this.concurrencyControl.remove({ mode: execution.mode, executionId: execution.id });
            return await this.executionRepository.stopBeforeRun(execution);
        }
        if (this.activeExecutions.has(execution.id)) {
            this.activeExecutions.stopExecution(execution.id, new n8n_workflow_1.ManualExecutionCancelledError(execution.id));
        }
        if (this.waitTracker.has(execution.id)) {
            this.waitTracker.stopExecution(execution.id);
        }
        return await this.executionRepository.stopDuringRun(execution);
    }
    async stopInScalingMode(execution) {
        if (this.activeExecutions.has(execution.id)) {
            this.activeExecutions.stopExecution(execution.id, new n8n_workflow_1.ManualExecutionCancelledError(execution.id));
        }
        if (this.waitTracker.has(execution.id)) {
            this.waitTracker.stopExecution(execution.id);
        }
        return await this.executionRepository.stopDuringRun(execution);
    }
    async addScopes(user, summaries) {
        const workflowIds = [...new Set(summaries.map((s) => s.workflowId))];
        const scopes = Object.fromEntries(await this.workflowSharingService.getSharedWorkflowScopes(workflowIds, user));
        for (const s of summaries) {
            s.scopes = scopes[s.workflowId] ?? [];
        }
    }
    async annotate(executionId, updateData, sharedWorkflowIds) {
        const execution = await this.executionRepository.findIfAccessible(executionId, sharedWorkflowIds);
        if (!execution) {
            this.logger.info('Attempt to read execution was blocked due to insufficient permissions', {
                executionId,
            });
            throw new not_found_error_1.NotFoundError('Execution not found');
        }
        await this.executionAnnotationRepository.upsert({ execution: { id: executionId }, vote: updateData.vote }, ['execution']);
        const annotation = await this.executionAnnotationRepository.findOneOrFail({
            where: {
                execution: { id: executionId },
            },
        });
        if (updateData.tags) {
            await this.annotationTagMappingRepository.overwriteTags(annotation.id, updateData.tags);
        }
    }
    async getExecutedVersions(workflowId) {
        const versionIds = await this.executionRepository.getDistinctVersionIds(workflowId);
        if (versionIds.length === 0)
            return [];
        const versions = await this.workflowHistoryRepository.find({
            where: { workflowId, versionId: (0, db_1.In)(versionIds) },
            select: ['versionId', 'name', 'createdAt'],
            order: { createdAt: 'DESC' },
        });
        return versions.map((v) => ({
            versionId: v.versionId,
            name: v.name,
            createdAt: v.createdAt,
        }));
    }
};
exports.ExecutionService = ExecutionService;
exports.ExecutionService = ExecutionService = __decorate([
    (0, di_1.Service)(),
    __metadata("design:paramtypes", [config_1.GlobalConfig,
        backend_common_1.Logger,
        active_executions_1.ActiveExecutions,
        db_1.ExecutionAnnotationRepository,
        db_1.AnnotationTagMappingRepository,
        db_1.ExecutionRepository,
        execution_persistence_1.ExecutionPersistence,
        db_1.WorkflowHistoryRepository,
        db_1.WorkflowRepository,
        node_types_1.NodeTypes,
        wait_tracker_1.WaitTracker,
        workflow_runner_1.WorkflowRunner,
        concurrency_control_service_1.ConcurrencyControlService,
        license_1.License,
        workflow_sharing_service_1.WorkflowSharingService,
        event_service_1.EventService,
        execution_redaction_proxy_service_1.ExecutionRedactionServiceProxy])
], ExecutionService);
//# sourceMappingURL=execution.service.js.map