UNPKG

n8n

Version:

n8n Workflow Automation Tool

516 lines 24.8 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); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SourceControlExportService = void 0; const backend_common_1 = require("@n8n/backend-common"); const db_1 = require("@n8n/db"); const di_1 = require("@n8n/di"); const permissions_1 = require("@n8n/permissions"); const typeorm_1 = require("@n8n/typeorm"); const chunk_1 = __importDefault(require("lodash/chunk")); const n8n_core_1 = require("n8n-core"); const n8n_workflow_1 = require("n8n-workflow"); const promises_1 = require("node:fs/promises"); const path_1 = __importDefault(require("path")); const constants_1 = require("./constants"); const source_control_helper_ee_1 = require("./source-control-helper.ee"); const source_control_scoped_service_1 = require("./source-control-scoped.service"); const variables_service_ee_1 = require("../../environments.ee/variables/variables.service.ee"); const data_table_repository_1 = require("../../modules/data-table/data-table.repository"); const workflow_formatter_1 = require("../../workflows/workflow.formatter"); let SourceControlExportService = class SourceControlExportService { constructor(logger, variablesService, tagRepository, projectRepository, sharedCredentialsRepository, sharedWorkflowRepository, workflowRepository, workflowTagMappingRepository, folderRepository, sourceControlScopedService, instanceSettings, dataTableRepository) { this.logger = logger; this.variablesService = variablesService; this.tagRepository = tagRepository; this.projectRepository = projectRepository; this.sharedCredentialsRepository = sharedCredentialsRepository; this.sharedWorkflowRepository = sharedWorkflowRepository; this.workflowRepository = workflowRepository; this.workflowTagMappingRepository = workflowTagMappingRepository; this.folderRepository = folderRepository; this.sourceControlScopedService = sourceControlScopedService; this.dataTableRepository = dataTableRepository; this.gitFolder = path_1.default.join(instanceSettings.n8nFolder, constants_1.SOURCE_CONTROL_GIT_FOLDER); this.workflowExportFolder = path_1.default.join(this.gitFolder, constants_1.SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER); this.projectExportFolder = path_1.default.join(this.gitFolder, constants_1.SOURCE_CONTROL_PROJECT_EXPORT_FOLDER); this.credentialExportFolder = path_1.default.join(this.gitFolder, constants_1.SOURCE_CONTROL_CREDENTIAL_EXPORT_FOLDER); this.dataTableExportFolder = path_1.default.join(this.gitFolder, constants_1.SOURCE_CONTROL_DATATABLES_EXPORT_FOLDER); } getWorkflowPath(workflowId) { return (0, source_control_helper_ee_1.getWorkflowExportPath)(workflowId, this.workflowExportFolder); } getCredentialsPath(credentialsId) { return (0, source_control_helper_ee_1.getCredentialExportPath)(credentialsId, this.credentialExportFolder); } getDataTablePath(dataTableId) { return (0, source_control_helper_ee_1.getDataTableExportPath)(dataTableId, this.dataTableExportFolder); } async deleteRepositoryFolder() { try { await (0, promises_1.rm)(this.gitFolder, { recursive: true }); } catch (error) { this.logger.error(`Failed to delete work folder: ${error.message}`); } } async rmFilesFromExportFolder(filesToBeDeleted) { try { await Promise.all([...filesToBeDeleted].map(async (e) => await (0, promises_1.rm)(e))); } catch (error) { this.logger.error(`Failed to delete workflows from work folder: ${error.message}`); } return filesToBeDeleted; } async writeExportableWorkflowsToExportFolder(workflowsToBeExported, owners) { const workflowChunks = (0, chunk_1.default)(workflowsToBeExported, constants_1.SOURCE_CONTROL_WRITE_FILE_BATCH_SIZE); for (const workflowChunk of workflowChunks) { await Promise.all(workflowChunk.map(async (workflow) => { const fileName = this.getWorkflowPath(workflow.id); const sanitizedWorkflow = { id: workflow.id, name: workflow.name, nodes: workflow.nodes, connections: workflow.connections, settings: workflow.settings, triggerCount: workflow.triggerCount, versionId: workflow.versionId, owner: owners[workflow.id], parentFolderId: workflow.parentFolder?.id ?? null, isArchived: workflow.isArchived, }; this.logger.debug(`Writing workflow ${workflow.id} to ${fileName}`); return await (0, promises_1.writeFile)(fileName, JSON.stringify(sanitizedWorkflow, null, 2)); })); } } async exportWorkflowsToWorkFolder(candidates) { try { (0, source_control_helper_ee_1.sourceControlFoldersExistCheck)([this.workflowExportFolder]); const workflowIds = candidates.map((e) => e.id); const sharedWorkflows = await this.sharedWorkflowRepository.findByWorkflowIds(workflowIds); const workflows = await this.workflowRepository.find({ where: { id: (0, typeorm_1.In)(workflowIds) }, relations: ['parentFolder'], }); const owners = {}; sharedWorkflows.forEach((sharedWorkflow) => { const project = sharedWorkflow.project; if (!project) { throw new n8n_workflow_1.UnexpectedError(`Workflow ${(0, workflow_formatter_1.formatWorkflow)(sharedWorkflow.workflow)} has no owner`); } if (project.type === 'personal') { const ownerRelation = project.projectRelations.find((pr) => pr.role.slug === permissions_1.PROJECT_OWNER_ROLE_SLUG); if (!ownerRelation) { throw new n8n_workflow_1.UnexpectedError(`Workflow ${(0, workflow_formatter_1.formatWorkflow)(sharedWorkflow.workflow)} has no owner`); } owners[sharedWorkflow.workflowId] = { type: 'personal', projectId: project.id, projectName: project.name, personalEmail: ownerRelation.user.email, }; } else if (project.type === 'team') { owners[sharedWorkflow.workflowId] = { type: 'team', teamId: project.id, teamName: project.name, }; } else { throw new n8n_workflow_1.UnexpectedError(`Workflow belongs to unknown project type: ${project.type}`); } }); await this.writeExportableWorkflowsToExportFolder(workflows, owners); return { count: sharedWorkflows.length, folder: this.workflowExportFolder, files: workflows.map((e) => ({ id: e?.id, name: this.getWorkflowPath(e?.name), })), }; } catch (error) { if (error instanceof n8n_workflow_1.UnexpectedError) throw error; throw new n8n_workflow_1.UnexpectedError('Failed to export workflows to work folder', { cause: error }); } } async exportGlobalVariablesToWorkFolder() { try { (0, source_control_helper_ee_1.sourceControlFoldersExistCheck)([this.gitFolder]); const variables = await this.variablesService.getAllCached({ globalOnly: true }); if (variables.length === 0) { return { count: 0, folder: this.gitFolder, files: [], }; } const fileName = (0, source_control_helper_ee_1.getVariablesPath)(this.gitFolder); const sanitizedVariables = variables.map((e) => ({ id: e.id, key: e.key, type: e.type, value: '', })); await (0, promises_1.writeFile)(fileName, JSON.stringify(sanitizedVariables, null, 2)); return { count: sanitizedVariables.length, folder: this.gitFolder, files: [ { id: '', name: fileName, }, ], }; } catch (error) { this.logger.error('Failed to export variables to work folder', { error }); throw new n8n_workflow_1.UnexpectedError('Failed to export variables to work folder', { cause: error, }); } } async exportDataTablesToWorkFolder(candidates, _context) { try { (0, source_control_helper_ee_1.sourceControlFoldersExistCheck)([this.gitFolder, this.dataTableExportFolder]); if (candidates.length === 0) { return { count: 0, folder: this.dataTableExportFolder, files: [], }; } const candidateIds = candidates.map((candidate) => candidate.id); const dataTables = await this.dataTableRepository.find({ where: { id: (0, typeorm_1.In)(candidateIds), }, relations: [ 'columns', 'project', 'project.projectRelations', 'project.projectRelations.role', 'project.projectRelations.user', ], select: { id: true, name: true, projectId: true, createdAt: true, updatedAt: true, columns: { id: true, name: true, type: true, index: true, }, project: { id: true, name: true, type: true, projectRelations: { userId: true, role: { slug: true, }, user: { email: true, }, }, }, }, }); const exportedFiles = []; for (const table of dataTables) { let owner = null; if (table.project?.type === 'personal') { const ownerRelation = table.project.projectRelations?.find((pr) => pr.role.slug === permissions_1.PROJECT_OWNER_ROLE_SLUG); if (ownerRelation) { owner = { type: 'personal', projectId: table.project.id, projectName: table.project.name, personalEmail: ownerRelation.user.email, }; } } else if (table.project?.type === 'team') { owner = { type: 'team', teamId: table.project.id, teamName: table.project.name, }; } const exportableDataTable = { id: table.id, name: table.name, columns: table.columns .sort((a, b) => a.index - b.index) .map((col) => ({ id: col.id, name: col.name, type: col.type, index: col.index, })), ownedBy: owner, createdAt: table.createdAt.toISOString(), updatedAt: table.updatedAt.toISOString(), }; const filePath = this.getDataTablePath(table.id); await (0, promises_1.writeFile)(filePath, JSON.stringify(exportableDataTable, null, 2)); exportedFiles.push({ id: table.id, name: filePath, }); } return { count: dataTables.length, folder: this.dataTableExportFolder, files: exportedFiles, }; } catch (error) { this.logger.error('Failed to export data tables to work folder', { error }); throw new n8n_workflow_1.UnexpectedError('Failed to export data tables to work folder', { cause: error, }); } } async exportFoldersToWorkFolder(context) { try { (0, source_control_helper_ee_1.sourceControlFoldersExistCheck)([this.gitFolder]); const folders = await this.folderRepository.find({ relations: ['parentFolder', 'homeProject'], select: { id: true, name: true, createdAt: true, updatedAt: true, parentFolder: { id: true, }, homeProject: { id: true, }, }, where: this.sourceControlScopedService.getFoldersInAdminProjectsFromContextFilter(context), }); if (folders.length === 0) { return { count: 0, folder: this.gitFolder, files: [], }; } const fileName = (0, source_control_helper_ee_1.getFoldersPath)(this.gitFolder); let foldersToKeepUnchanged = []; if (!context.hasAccessToAllProjects()) { const existingFolders = await (0, source_control_helper_ee_1.readFoldersFromSourceControlFile)(fileName); foldersToKeepUnchanged = existingFolders.folders.filter((folder) => !context.canAccessProject(folder.homeProjectId)); } const newFolders = foldersToKeepUnchanged.concat(...folders.map((f) => ({ id: f.id, name: f.name, parentFolderId: f.parentFolder?.id ?? null, homeProjectId: f.homeProject.id, createdAt: f.createdAt.toISOString(), updatedAt: f.updatedAt.toISOString(), }))); await (0, promises_1.writeFile)(fileName, JSON.stringify({ folders: newFolders, }, null, 2)); return { count: folders.length, folder: this.gitFolder, files: [ { id: '', name: fileName, }, ], }; } catch (error) { this.logger.error('Failed to export folders to work folder', { error }); throw new n8n_workflow_1.UnexpectedError('Failed to export folders to work folder', { cause: error }); } } async exportTagsToWorkFolder(context) { try { const fileName = path_1.default.join(this.gitFolder, constants_1.SOURCE_CONTROL_TAGS_EXPORT_FILE); (0, source_control_helper_ee_1.sourceControlFoldersExistCheck)([this.gitFolder]); const tags = await this.tagRepository.find(); if (tags.length === 0) { await (0, promises_1.writeFile)(fileName, JSON.stringify({ tags: [], mappings: [] }, null, 2)); return { count: 0, folder: this.gitFolder, files: [{ id: '', name: fileName }], }; } const mappingsOfAllowedWorkflows = await this.workflowTagMappingRepository.find({ where: this.sourceControlScopedService.getWorkflowTagMappingInAdminProjectsFromContextFilter(context), }); const allowedWorkflows = await this.workflowRepository.find({ where: this.sourceControlScopedService.getWorkflowsInAdminProjectsFromContextFilter(context), }); const existingTagsAndMapping = await (0, source_control_helper_ee_1.readTagAndMappingsFromSourceControlFile)(fileName); const mappingsToKeep = existingTagsAndMapping.mappings.filter((mapping) => { return !allowedWorkflows.some((allowedWorkflow) => allowedWorkflow.id === mapping.workflowId); }); await (0, promises_1.writeFile)(fileName, JSON.stringify({ tags: tags.map((tag) => ({ id: tag.id, name: tag.name })), mappings: mappingsToKeep.concat(mappingsOfAllowedWorkflows), }, null, 2)); return { count: tags.length, folder: this.gitFolder, files: [{ id: '', name: fileName }], }; } catch (error) { this.logger.error('Failed to export tags to work folder', { error }); throw new n8n_workflow_1.UnexpectedError('Failed to export tags to work folder', { cause: error }); } } async exportCredentialsToWorkFolder(candidates) { try { (0, source_control_helper_ee_1.sourceControlFoldersExistCheck)([this.credentialExportFolder]); const credentialIds = candidates.map((e) => e.id); const credentialsToBeExported = await this.sharedCredentialsRepository.findByCredentialIds(credentialIds, 'credential:owner'); let missingIds = []; if (credentialsToBeExported.length !== credentialIds.length) { const foundCredentialIds = credentialsToBeExported.map((e) => e.credentialsId); missingIds = credentialIds.filter((remote) => foundCredentialIds.findIndex((local) => local === remote) === -1); } await Promise.all(credentialsToBeExported.map(async (sharing) => { const { name, type, data, id, isGlobal = false } = sharing.credentials; const credentials = new n8n_core_1.Credentials({ id, name }, type, data); let owner = null; if (sharing.project.type === 'personal') { const ownerRelation = sharing.project.projectRelations.find((pr) => pr.role.slug === permissions_1.PROJECT_OWNER_ROLE_SLUG); if (ownerRelation) { owner = { type: 'personal', projectId: sharing.project.id, projectName: sharing.project.name, personalEmail: ownerRelation.user.email, }; } } else if (sharing.project.type === 'team') { owner = { type: 'team', teamId: sharing.project.id, teamName: sharing.project.name, }; } const sanitizedData = (0, source_control_helper_ee_1.sanitizeCredentialData)(await credentials.getData()); const stub = { id, name, type, data: sanitizedData, ownedBy: owner, isGlobal, }; const filePath = this.getCredentialsPath(id); this.logger.debug(`Writing credentials stub "${name}" (ID ${id}) to: ${filePath}`); return await (0, promises_1.writeFile)(filePath, JSON.stringify(stub, null, 2)); })); return { count: credentialsToBeExported.length, folder: this.credentialExportFolder, files: credentialsToBeExported.map((e) => ({ id: e.credentials.id, name: path_1.default.join(this.credentialExportFolder, `${e.credentials.name}.json`), })), missingIds, }; } catch (error) { this.logger.error('Failed to export credentials to work folder', { error }); throw new n8n_workflow_1.UnexpectedError('Failed to export credentials to work folder', { cause: error }); } } async exportTeamProjectsToWorkFolder(candidates) { try { (0, source_control_helper_ee_1.sourceControlFoldersExistCheck)([this.projectExportFolder], true); const projectIds = candidates.map((e) => e.id); const projects = await this.projectRepository.find({ where: { id: (0, typeorm_1.In)(projectIds), type: 'team' }, relations: ['variables'], }); await Promise.all(projects.map(async (project) => { const fileName = (0, source_control_helper_ee_1.getProjectExportPath)(project.id, this.projectExportFolder); const sanitizedProject = { id: project.id, name: project.name, icon: project.icon, description: project.description, type: 'team', owner: { type: 'team', teamId: project.id, teamName: project.name, }, variableStubs: project.variables.map((variable) => ({ id: variable.id, key: variable.key, type: variable.type, value: '', })), }; this.logger.debug(`Writing project ${project.id} to ${fileName}`); return await (0, promises_1.writeFile)(fileName, JSON.stringify(sanitizedProject, null, 2)); })); return { count: projects.length, folder: this.projectExportFolder, files: projects.map((project) => ({ id: project.id, name: (0, source_control_helper_ee_1.getProjectExportPath)(project.id, this.projectExportFolder), })), }; } catch (error) { if (error instanceof n8n_workflow_1.UnexpectedError) throw error; throw new n8n_workflow_1.UnexpectedError('Failed to export projects to work folder', { cause: error }); } } }; exports.SourceControlExportService = SourceControlExportService; exports.SourceControlExportService = SourceControlExportService = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [backend_common_1.Logger, variables_service_ee_1.VariablesService, db_1.TagRepository, db_1.ProjectRepository, db_1.SharedCredentialsRepository, db_1.SharedWorkflowRepository, db_1.WorkflowRepository, db_1.WorkflowTagMappingRepository, db_1.FolderRepository, source_control_scoped_service_1.SourceControlScopedService, n8n_core_1.InstanceSettings, data_table_repository_1.DataTableRepository]) ], SourceControlExportService); //# sourceMappingURL=source-control-export.service.ee.js.map