UNPKG

n8n

Version:

n8n Workflow Automation Tool

225 lines • 11.9 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.WorkflowReviewInboxService = void 0; const db_1 = require("@n8n/db"); const di_1 = require("@n8n/di"); const permissions_1 = require("@n8n/permissions"); const bad_request_error_1 = require("../../errors/response-errors/bad-request.error"); const not_found_error_1 = require("../../errors/response-errors/not-found.error"); const project_service_ee_1 = require("../../services/project.service.ee"); const workflow_finder_service_1 = require("../../workflows/workflow-finder.service"); const workflow_history_service_1 = require("../../workflows/workflow-history/workflow-history.service"); const workflow_review_decision_eligibility_service_1 = require("./workflow-review-decision-eligibility.service"); const workflow_review_feature_gate_service_1 = require("./workflow-review-feature-gate.service"); const workflow_review_mapper_1 = require("./workflow-review.mapper"); let WorkflowReviewInboxService = class WorkflowReviewInboxService { constructor(featureGate, workflowFinderService, workflowHistoryService, workflowPublishedVersionRepository, workflowReviewRequestRepository, workflowReviewRequestWorkflowRepository, workflowReviewRequestReviewerRepository, userRepository, projectService, decisionEligibilityService) { this.featureGate = featureGate; this.workflowFinderService = workflowFinderService; this.workflowHistoryService = workflowHistoryService; this.workflowPublishedVersionRepository = workflowPublishedVersionRepository; this.workflowReviewRequestRepository = workflowReviewRequestRepository; this.workflowReviewRequestWorkflowRepository = workflowReviewRequestWorkflowRepository; this.workflowReviewRequestReviewerRepository = workflowReviewRequestReviewerRepository; this.userRepository = userRepository; this.projectService = projectService; this.decisionEligibilityService = decisionEligibilityService; } async listForInbox(user, query) { await this.featureGate.assertAvailable(); const projectIds = await this.resolveAccessibleProjectIds(user); const { limit } = query; const rows = await this.workflowReviewRequestRepository.findManyForInbox({ projectIds, requesterId: user.id, state: query.state ?? 'open', limit: limit + 1, cursor: query.cursor ? this.decodeInboxCursor(query.cursor) : undefined, }); const hasMore = rows.length > limit; const data = rows.slice(0, limit); const lastRow = data.at(-1); const nextCursor = hasMore && lastRow ? this.encodeInboxCursor(lastRow) : null; const requestIds = data.map((row) => row.id); const [linkedWorkflowByRequestId, reviewerRows] = await Promise.all([ this.workflowReviewRequestWorkflowRepository.findLinkedWorkflowsByRequestIds(requestIds), this.workflowReviewRequestReviewerRepository.findByRequestIds(requestIds), ]); const participantsByRequestId = await this.hydrateParticipants(data, reviewerRows); return { data: data.map((row) => { const { requester, reviewers } = participantsByRequestId.get(row.id) ?? { requester: null, reviewers: [], }; return this.toInboxItem(row, linkedWorkflowByRequestId.get(row.id) ?? null, requester, reviewers); }), nextCursor, hasMore, }; } async getInboxSummaryForUser(user) { await this.featureGate.assertAvailable(); const projectIds = await this.resolveAccessibleProjectIds(user); return await this.workflowReviewRequestRepository.countByStateForInbox({ projectIds, requesterId: user.id, }); } async getDetail(user, workflowReviewRequestId) { await this.featureGate.assertAvailable(); const request = await this.workflowReviewRequestRepository.findById(workflowReviewRequestId, {}); if (!request || !(await this.canAccessRequest(user, request))) { throw new not_found_error_1.NotFoundError('Could not find review request'); } const [workflowRows, reviewerRows] = await Promise.all([ this.workflowReviewRequestWorkflowRepository.findLinkedWorkflowDetailsByRequestId(request.id), this.workflowReviewRequestReviewerRepository.findByRequestIds([request.id]), ]); const readableRows = await this.filterReadableWorkflowRows(user, workflowRows); if (request.createdById !== user.id && workflowRows.length > 0 && readableRows.length === 0) { throw new not_found_error_1.NotFoundError('Could not find review request'); } const [workflows, participantsByRequestId, eligibility] = await Promise.all([ Promise.all(readableRows.map(async (row) => await this.toWorkflowDetail(row))), this.hydrateParticipants([request], reviewerRows), this.decisionEligibilityService.resolveViewerEligibility(user, request, workflowRows.at(0)?.workflowId ?? null), ]); const { requester, reviewers } = participantsByRequestId.get(request.id) ?? { requester: null, reviewers: [], }; return { ...this.toInboxItem(request, workflows.at(0) ?? null, requester, reviewers), description: request.description, workflows, viewerCanDecide: eligibility.canDecide, viewerDecisionIneligibilityReason: eligibility.reason, }; } async resolveAccessibleProjectIds(user) { if ((0, permissions_1.hasGlobalScope)(user, 'workflow:publish')) { return null; } return await this.projectService.getProjectIdsWithScope(user, ['workflow:publish']); } async canAccessRequest(user, request) { if (request.createdById === user.id) { return true; } const projectIds = await this.resolveAccessibleProjectIds(user); return projectIds === null || projectIds.includes(request.projectId); } async filterReadableWorkflowRows(user, rows) { const readable = await Promise.all(rows.map(async (row) => (await this.workflowFinderService.findWorkflowForUser(row.workflowId, user, [ 'workflow:read', ])) ? row : null)); return readable.filter((row) => row !== null); } async toWorkflowDetail(row) { const publishedVersionId = await this.workflowPublishedVersionRepository.getPublishedVersionId(row.workflowId); const [pinnedVersion, baselineVersion] = await Promise.all([ this.findVersionSnapshot(row.workflowId, row.workflowVersionId), this.findVersionSnapshot(row.workflowId, publishedVersionId), ]); return { workflowId: row.workflowId, workflowName: row.workflowName, workflowVersionId: row.workflowVersionId, pinnedVersion, baselineVersion, }; } async findVersionSnapshot(workflowId, versionId) { if (!versionId) { return null; } const version = await this.workflowHistoryService.findVersion(workflowId, versionId); return version ? this.toVersionSnapshot(version) : null; } toVersionSnapshot(version) { return { versionId: version.versionId, name: version.name, nodes: version.nodes, connections: version.connections, nodeGroups: version.nodeGroups, createdAt: version.createdAt.toISOString(), }; } async hydrateParticipants(rows, reviewerRows) { const reviewerIdsByRequestId = new Map(); for (const { workflowReviewRequestId, userId } of reviewerRows) { const ids = reviewerIdsByRequestId.get(workflowReviewRequestId) ?? []; ids.push(userId); reviewerIdsByRequestId.set(workflowReviewRequestId, ids); } const userIds = new Set([ ...rows.map((row) => row.createdById).filter((id) => id !== null), ...reviewerRows.map((row) => row.userId), ]); const usersById = new Map(); if (userIds.size > 0) { for (const user of await this.userRepository.findManyByIds([...userIds])) { usersById.set(user.id, (0, workflow_review_mapper_1.toEligibleReviewer)(user)); } } return new Map(rows.map((row) => [ row.id, { requester: row.createdById ? (usersById.get(row.createdById) ?? null) : null, reviewers: (reviewerIdsByRequestId.get(row.id) ?? []) .map((userId) => usersById.get(userId)) .filter((reviewer) => reviewer !== undefined), }, ])); } encodeInboxCursor(row) { return Buffer.from(`${row.createdAt.toISOString()}|${row.id}`, 'utf8').toString('base64url'); } decodeInboxCursor(cursor) { const decoded = Buffer.from(cursor, 'base64url').toString('utf8'); const separatorIndex = decoded.indexOf('|'); if (separatorIndex === -1) { throw new bad_request_error_1.BadRequestError('Invalid pagination cursor'); } const createdAt = new Date(decoded.slice(0, separatorIndex)); const id = decoded.slice(separatorIndex + 1); if (id.length === 0 || Number.isNaN(createdAt.getTime())) { throw new bad_request_error_1.BadRequestError('Invalid pagination cursor'); } return { createdAt, id }; } toInboxItem(entity, linkedWorkflow, requester, reviewers) { return { id: entity.id, projectId: entity.projectId, title: entity.title, workflowName: linkedWorkflow?.workflowName ?? null, workflowVersionId: linkedWorkflow?.workflowVersionId ?? null, decision: entity.decision, state: entity.state, createdAt: entity.createdAt.toISOString(), updatedAt: entity.updatedAt.toISOString(), requester, reviewers, }; } }; exports.WorkflowReviewInboxService = WorkflowReviewInboxService; exports.WorkflowReviewInboxService = WorkflowReviewInboxService = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [workflow_review_feature_gate_service_1.WorkflowReviewFeatureGate, workflow_finder_service_1.WorkflowFinderService, workflow_history_service_1.WorkflowHistoryService, db_1.WorkflowPublishedVersionRepository, db_1.WorkflowReviewRequestRepository, db_1.WorkflowReviewRequestWorkflowRepository, db_1.WorkflowReviewRequestReviewerRepository, db_1.UserRepository, project_service_ee_1.ProjectService, workflow_review_decision_eligibility_service_1.WorkflowReviewDecisionEligibilityService]) ], WorkflowReviewInboxService); //# sourceMappingURL=workflow-review-inbox.service.js.map