UNPKG

n8n

Version:

n8n Workflow Automation Tool

489 lines 25.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); 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 __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); 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.SourceControlService = void 0; const backend_common_1 = require("@n8n/backend-common"); const decorators_1 = require("@n8n/decorators"); const di_1 = require("@n8n/di"); const fs_1 = require("fs"); const n8n_workflow_1 = require("n8n-workflow"); const path = __importStar(require("path")); const constants_1 = require("./constants"); const source_control_export_service_ee_1 = require("./source-control-export.service.ee"); const source_control_git_service_ee_1 = require("./source-control-git.service.ee"); const source_control_helper_ee_1 = require("./source-control-helper.ee"); const source_control_import_service_ee_1 = require("./source-control-import.service.ee"); const source_control_preferences_service_ee_1 = require("./source-control-preferences.service.ee"); const source_control_resource_helper_1 = require("./source-control-resource-helper"); const source_control_context_factory_1 = require("./source-control-context.factory"); const source_control_scoped_service_1 = require("./source-control-scoped.service"); const source_control_status_service_ee_1 = require("./source-control-status.service.ee"); const bad_request_error_1 = require("../../errors/response-errors/bad-request.error"); const forbidden_error_1 = require("../../errors/response-errors/forbidden.error"); const event_service_1 = require("../../events/event.service"); let SourceControlService = class SourceControlService { constructor(logger, gitService, sourceControlPreferencesService, sourceControlExportService, sourceControlImportService, sourceControlContextFactory, sourceControlScopedService, eventService, sourceControlStatusService) { this.logger = logger; this.gitService = gitService; this.sourceControlPreferencesService = sourceControlPreferencesService; this.sourceControlExportService = sourceControlExportService; this.sourceControlImportService = sourceControlImportService; this.sourceControlContextFactory = sourceControlContextFactory; this.sourceControlScopedService = sourceControlScopedService; this.eventService = eventService; this.sourceControlStatusService = sourceControlStatusService; this.isReloading = false; const { gitFolder, sshFolder, sshKeyName } = sourceControlPreferencesService; this.gitFolder = gitFolder; this.sshFolder = sshFolder; this.sshKeyName = sshKeyName; } async start() { await this.refreshServiceState(); } isHostKeyVerificationError(error) { const message = error.message.toLowerCase(); return (message.includes('host key verification failed') || message.includes('host identification has changed') || message.includes('offending key')); } async refreshServiceState() { this.gitService.resetService(); (0, source_control_helper_ee_1.sourceControlFoldersExistCheck)([this.gitFolder, this.sshFolder]); await this.sourceControlPreferencesService.loadFromDbAndApplySourceControlPreferences(); if (this.sourceControlPreferencesService.isSourceControlLicensedAndEnabled()) { await this.initGitService(); } } async reloadConfiguration() { if (this.isReloading) { this.logger.warn('Source control configuration reload already in progress'); return; } this.isReloading = true; try { this.logger.debug('Source control configuration changed, reloading from database'); const wasConnected = this.sourceControlPreferencesService.isSourceControlConnected(); await this.refreshServiceState(); const isNowConnected = this.sourceControlPreferencesService.isSourceControlConnected(); if (wasConnected && !isNowConnected) { await this.sourceControlExportService.deleteRepositoryFolder(); this.logger.info('Cleaned up git repository folder after source control disconnect'); } } finally { this.isReloading = false; } } async initGitService() { await this.gitService.initService({ sourceControlPreferences: this.sourceControlPreferencesService.getPreferences(), gitFolder: this.gitFolder, sshKeyName: this.sshKeyName, sshFolder: this.sshFolder, }); } async sanityCheck() { try { const foldersExisted = (0, source_control_helper_ee_1.sourceControlFoldersExistCheck)([this.gitFolder, this.sshFolder], false); if (!foldersExisted) { throw new n8n_workflow_1.UserError('No folders exist'); } if (!this.gitService.git) { await this.initGitService(); } const branches = await this.gitService.getCurrentBranch(); if (branches.current === '' || branches.current !== this.sourceControlPreferencesService.sourceControlPreferences.branchName) { throw new n8n_workflow_1.UserError('Branch is not set up correctly'); } } catch (error) { throw new bad_request_error_1.BadRequestError('Source control is not properly set up, please disconnect and reconnect.'); } } async disconnect(options = {}) { try { const preferences = this.sourceControlPreferencesService.getPreferences(); await this.sourceControlPreferencesService.setPreferences({ connected: false, branchName: '', repositoryUrl: '', connectionType: preferences.connectionType, }); await this.sourceControlExportService.deleteRepositoryFolder(); if (preferences.connectionType === 'https') { await this.sourceControlPreferencesService.deleteHttpsCredentials(); } else if (!options.keepKeyPair) { await this.sourceControlPreferencesService.deleteKeyPair(); } await this.sourceControlPreferencesService.resetKnownHosts(); this.gitService.resetService(); return this.sourceControlPreferencesService.sourceControlPreferences; } catch (error) { throw new n8n_workflow_1.UnexpectedError('Failed to disconnect from source control', { cause: error }); } } async initializeRepository(preferences, user) { if (!this.gitService.git) { await this.initGitService(); } this.logger.debug('Initializing repository...'); await this.gitService.initRepository(preferences, user); let getBranchesResult; try { getBranchesResult = await this.getBranches(); } catch (error) { if (error.message.includes('Warning: Permanently added')) { this.logger.debug('Added repository host to the list of known hosts. Retrying...'); getBranchesResult = await this.getBranches(); } else if (this.isHostKeyVerificationError(error)) { throw new n8n_workflow_1.UserError("SSH host key verification failed. The remote server's key may have changed. " + 'If this is expected (e.g., server migration), disconnect and reconnect from Source Control settings to reset the known hosts.'); } else { throw error; } } if (getBranchesResult.branches.includes(preferences.branchName)) { await this.gitService.setBranch(preferences.branchName); } else { if (getBranchesResult.branches?.length === 0) { try { (0, fs_1.writeFileSync)(path.join(this.gitFolder, '/README.md'), constants_1.SOURCE_CONTROL_README); await this.gitService.stage(new Set(['README.md'])); await this.gitService.commit('Initial commit'); await this.gitService.push({ branch: preferences.branchName, force: true, }); getBranchesResult = await this.getBranches(); await this.gitService.setBranch(preferences.branchName); } catch (fileError) { this.logger.error(`Failed to create initial commit: ${fileError.message}`); } } } await this.sourceControlPreferencesService.setPreferences({ branchName: getBranchesResult.currentBranch, connected: true, }); return getBranchesResult; } async getBranches() { if (!this.gitService.git) { await this.initGitService(); } await this.gitService.fetch(); return await this.gitService.getBranches(); } async setBranch(branch) { if (!this.gitService.git) { await this.initGitService(); } await this.sourceControlPreferencesService.setPreferences({ branchName: branch, connected: branch?.length > 0, }); return await this.gitService.setBranch(branch); } async resetWorkfolder() { if (!this.gitService.git) { await this.initGitService(); } try { await this.gitService.resetBranch(); await this.gitService.pull(); } catch (error) { this.logger.error(`Failed to reset workfolder: ${error.message}`); throw new n8n_workflow_1.UserError('Unable to fetch updates from git - your folder might be out of sync. Try reconnecting from the Source Control settings page.'); } return; } async pushWorkfolder(user, options) { await this.sanityCheck(); if (this.sourceControlPreferencesService.isBranchReadOnly()) { throw new bad_request_error_1.BadRequestError('Cannot push onto read-only branch.'); } const context = await this.sourceControlContextFactory.createContext(user); let filesToPush = options.fileNames.map((file) => { const normalizedPath = (0, source_control_helper_ee_1.normalizeAndValidateSourceControlledFilePath)(this.gitFolder, file.file); return { ...file, file: normalizedPath, }; }); const allowedResources = await this.sourceControlStatusService.getStatus(user, { direction: 'push', verbose: false, preferLocalVersion: true, }); if (!filesToPush.length) { filesToPush = allowedResources; } if (filesToPush !== allowedResources && filesToPush.some((file) => !allowedResources.some((allowed) => { return allowed.id === file.id && allowed.type === file.type; }))) { throw new forbidden_error_1.ForbiddenError('You are not allowed to push these changes'); } let statusResult = filesToPush; if (!options.force) { const possibleConflicts = filesToPush?.filter((file) => file.conflict); if (possibleConflicts?.length > 0) { return { statusCode: 409, pushResult: undefined, statusResult: filesToPush, }; } } try { const filesToBePushed = new Set(); const filesToBeDeleted = new Set(); filesToPush .filter((f) => ['workflow', 'credential', 'project', 'datatable'].includes(f.type)) .forEach((e) => { if (e.status !== 'deleted') { filesToBePushed.add(e.file); } else { filesToBeDeleted.add(e.file); } }); await this.sourceControlExportService.rmFilesFromExportFolder(filesToBeDeleted); const workflowsToBeExported = (0, source_control_resource_helper_1.getNonDeletedResources)(filesToPush, 'workflow'); await this.sourceControlExportService.exportWorkflowsToWorkFolder(workflowsToBeExported); const credentialsToBeExported = (0, source_control_resource_helper_1.getNonDeletedResources)(filesToPush, 'credential'); const credentialExportResult = await this.sourceControlExportService.exportCredentialsToWorkFolder(credentialsToBeExported); if (credentialExportResult.missingIds && credentialExportResult.missingIds.length > 0) { credentialExportResult.missingIds.forEach((id) => { filesToBePushed.delete(this.sourceControlExportService.getCredentialsPath(id)); statusResult = statusResult.filter((e) => e.file !== this.sourceControlExportService.getCredentialsPath(id)); }); } const projectsToBeExported = (0, source_control_resource_helper_1.getNonDeletedResources)(filesToPush, 'project'); await this.sourceControlExportService.exportTeamProjectsToWorkFolder(projectsToBeExported); filesToBePushed.add((0, source_control_helper_ee_1.getTagsPath)(this.gitFolder)); await this.sourceControlExportService.exportTagsToWorkFolder(context); const folderChanges = (0, source_control_resource_helper_1.filterByType)(filesToPush, 'folders')[0]; if (folderChanges) { filesToBePushed.add(folderChanges.file); await this.sourceControlExportService.exportFoldersToWorkFolder(context); } const variablesChanges = (0, source_control_resource_helper_1.filterByType)(filesToPush, 'variables')[0]; if (variablesChanges) { filesToBePushed.add(variablesChanges.file); await this.sourceControlExportService.exportGlobalVariablesToWorkFolder(); } const dataTableCandidates = (0, source_control_resource_helper_1.filterByType)(filesToPush, 'datatable'); if (dataTableCandidates.length > 0) { await this.sourceControlExportService.exportDataTablesToWorkFolder(dataTableCandidates, context); } await this.gitService.stage(filesToBePushed, filesToBeDeleted); await this.gitService.commit(options.commitMessage ?? 'Updated Workfolder'); } catch (error) { this.logger.error('Failed to export or commit changes', { error }); try { await this.gitService.resetBranch({ hard: true, target: 'HEAD' }); } catch (resetError) { this.logger.error('Failed to reset branch after export/commit error', { error: resetError, }); } throw error; } const branchName = this.sourceControlPreferencesService.getBranchName(); let pushResult; try { pushResult = await this.gitService.push({ branch: branchName, force: options.force ?? false, }); statusResult.forEach((result) => (result.pushed = true)); } catch (error) { this.logger.error('Failed to push changes', { error }); try { await this.gitService.resetBranch({ hard: true, target: `origin/${branchName}` }); } catch (resetError) { this.logger.error('Failed to reset branch after push error', { error: resetError }); } throw error; } this.eventService.emit('source-control-user-finished-push-ui', (0, source_control_helper_ee_1.getTrackingInformationFromPostPushResult)(user.id, statusResult)); return { statusCode: 200, pushResult, statusResult, }; } async pullWorkfolder(user, options) { await this.sanityCheck(); const statusResult = await this.sourceControlStatusService.getStatus(user, { direction: 'pull', verbose: false, preferLocalVersion: false, }); if (options.force !== true) { const possibleConflicts = statusResult.filter((file) => file.conflict || file.status === 'modified'); if (possibleConflicts?.length > 0) { await this.gitService.resetBranch(); return { statusCode: 409, statusResult, }; } } const projectsToBeImported = (0, source_control_resource_helper_1.getNonDeletedResources)(statusResult, 'project'); this.logger.debug(`[Project Debug] Found ${projectsToBeImported.length} projects to import: ${JSON.stringify(projectsToBeImported.map((p) => ({ id: p.id, name: p.name })))}`); await this.sourceControlImportService.importTeamProjectsFromWorkFolder(projectsToBeImported, user.id); const foldersToBeImported = (0, source_control_resource_helper_1.getNonDeletedResources)(statusResult, 'folders')[0]; if (foldersToBeImported) { await this.sourceControlImportService.importFoldersFromWorkFolder(user, foldersToBeImported); } const workflowsToBeImported = (0, source_control_resource_helper_1.getNonDeletedResources)(statusResult, 'workflow'); const workflowImportResults = await this.sourceControlImportService.importWorkflowFromWorkFolder(workflowsToBeImported, user.id, options.autoPublish); const statusByWorkflowId = new Map(statusResult.filter((item) => item.type === 'workflow').map((item) => [item.id, item])); for (const { id, publishingError } of workflowImportResults) { if (!publishingError) continue; const statusItem = statusByWorkflowId.get(id); if (statusItem) { statusItem.publishingError = publishingError; } } const workflowsToBeDeleted = (0, source_control_resource_helper_1.getDeletedResources)(statusResult, 'workflow'); await this.sourceControlImportService.deleteWorkflowsNotInWorkfolder(user, workflowsToBeDeleted); const credentialsToBeImported = (0, source_control_resource_helper_1.getNonDeletedResources)(statusResult, 'credential'); await this.sourceControlImportService.importCredentialsFromWorkFolder(credentialsToBeImported, user.id); const credentialsToBeDeleted = (0, source_control_resource_helper_1.getDeletedResources)(statusResult, 'credential'); await this.sourceControlImportService.deleteCredentialsNotInWorkfolder(user, credentialsToBeDeleted); const tagsToBeImported = (0, source_control_resource_helper_1.getNonDeletedResources)(statusResult, 'tags')[0]; if (tagsToBeImported) { await this.sourceControlImportService.importTagsFromWorkFolder(tagsToBeImported, user); } const tagsToBeDeleted = (0, source_control_resource_helper_1.getDeletedResources)(statusResult, 'tags'); await this.sourceControlImportService.deleteTagsNotInWorkfolder(tagsToBeDeleted); const variablesToBeImported = (0, source_control_resource_helper_1.getNonDeletedResources)(statusResult, 'variables')[0]; if (variablesToBeImported) { await this.sourceControlImportService.importVariablesFromWorkFolder(variablesToBeImported); } const variablesToBeDeleted = (0, source_control_resource_helper_1.getDeletedResources)(statusResult, 'variables'); await this.sourceControlImportService.deleteVariablesNotInWorkfolder(variablesToBeDeleted); const dataTableCandidates = (0, source_control_resource_helper_1.getNonDeletedResources)(statusResult, 'datatable'); if (dataTableCandidates.length > 0) { await this.sourceControlImportService.importDataTablesFromWorkFolder(dataTableCandidates, user.id); } const dataTablesToBeDeleted = (0, source_control_resource_helper_1.getDeletedResources)(statusResult, 'datatable'); await this.sourceControlImportService.deleteDataTablesNotInWorkFolder(dataTablesToBeDeleted); const foldersToBeDeleted = (0, source_control_resource_helper_1.getDeletedResources)(statusResult, 'folders'); await this.sourceControlImportService.deleteFoldersNotInWorkfolder(foldersToBeDeleted); const projectsToBeDeleted = (0, source_control_resource_helper_1.getDeletedResources)(statusResult, 'project'); await this.sourceControlImportService.deleteTeamProjectsNotInWorkfolder(projectsToBeDeleted); this.eventService.emit('source-control-user-finished-pull-ui', (0, source_control_helper_ee_1.getTrackingInformationFromPullResult)(user.id, statusResult)); return { statusCode: 200, statusResult, }; } async getStatus(user, options) { await this.sanityCheck(); return await this.sourceControlStatusService.getStatus(user, options); } async setGitUserDetails(name = constants_1.SOURCE_CONTROL_DEFAULT_NAME, email = constants_1.SOURCE_CONTROL_DEFAULT_EMAIL) { await this.sanityCheck(); await this.gitService.setGitUserDetails(name, email); } async getRemoteFileEntity({ user, type, id, commit = 'HEAD', }) { await this.sanityCheck(); const context = await this.sourceControlContextFactory.createContext(user); switch (type) { case 'workflow': { if (typeof id === 'undefined') { throw new bad_request_error_1.BadRequestError('Workflow ID is required to fetch workflow content'); } const authorizedWorkflows = await this.sourceControlScopedService.getWorkflowsInAdminProjectsFromContext(context, id); if (authorizedWorkflows && authorizedWorkflows.length === 0) { throw new forbidden_error_1.ForbiddenError(`You are not allowed to access workflow with id ${id}`); } const content = await this.gitService.getFileContent(`${constants_1.SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER}/${id}.json`, commit); return (0, n8n_workflow_1.jsonParse)(content); } default: throw new bad_request_error_1.BadRequestError(`Unsupported file type: ${type}`); } } }; exports.SourceControlService = SourceControlService; __decorate([ (0, decorators_1.OnPubSubEvent)('reload-source-control-config', { instanceType: 'main' }), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], SourceControlService.prototype, "reloadConfiguration", null); exports.SourceControlService = SourceControlService = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [backend_common_1.Logger, source_control_git_service_ee_1.SourceControlGitService, source_control_preferences_service_ee_1.SourceControlPreferencesService, source_control_export_service_ee_1.SourceControlExportService, source_control_import_service_ee_1.SourceControlImportService, source_control_context_factory_1.SourceControlContextFactory, source_control_scoped_service_1.SourceControlScopedService, event_service_1.EventService, source_control_status_service_ee_1.SourceControlStatusService]) ], SourceControlService); //# sourceMappingURL=source-control.service.ee.js.map