UNPKG

n8n

Version:

n8n Workflow Automation Tool

586 lines 29.5 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 (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __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); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SourceControlService = void 0; const typedi_1 = __importStar(require("typedi")); const path_1 = __importDefault(require("path")); const sourceControlHelper_ee_1 = require("./sourceControlHelper.ee"); const constants_1 = require("./constants"); const sourceControlGit_service_ee_1 = require("./sourceControlGit.service.ee"); const sourceControlExport_service_ee_1 = require("./sourceControlExport.service.ee"); const sourceControlPreferences_service_ee_1 = require("./sourceControlPreferences.service.ee"); const fs_1 = require("fs"); const sourceControlImport_service_ee_1 = require("./sourceControlImport.service.ee"); const InternalHooks_1 = require("../../InternalHooks"); const tag_repository_1 = require("../../databases/repositories/tag.repository"); const Logger_1 = require("../../Logger"); const bad_request_error_1 = require("../../errors/response-errors/bad-request.error"); const n8n_workflow_1 = require("n8n-workflow"); let SourceControlService = class SourceControlService { constructor(logger, gitService, sourceControlPreferencesService, sourceControlExportService, sourceControlImportService, tagRepository) { this.logger = logger; this.gitService = gitService; this.sourceControlPreferencesService = sourceControlPreferencesService; this.sourceControlExportService = sourceControlExportService; this.sourceControlImportService = sourceControlImportService; this.tagRepository = tagRepository; const { gitFolder, sshFolder, sshKeyName } = sourceControlPreferencesService; this.gitFolder = gitFolder; this.sshFolder = sshFolder; this.sshKeyName = sshKeyName; } async init() { this.gitService.resetService(); (0, sourceControlHelper_ee_1.sourceControlFoldersExistCheck)([this.gitFolder, this.sshFolder]); await this.sourceControlPreferencesService.loadFromDbAndApplySourceControlPreferences(); if (this.sourceControlPreferencesService.isSourceControlLicensedAndEnabled()) { await this.initGitService(); } } 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, sourceControlHelper_ee_1.sourceControlFoldersExistCheck)([this.gitFolder, this.sshFolder], false); if (!foldersExisted) { throw new n8n_workflow_1.ApplicationError('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.ApplicationError('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 { await this.sourceControlPreferencesService.setPreferences({ connected: false, branchName: '', }); await this.sourceControlExportService.deleteRepositoryFolder(); if (!options.keepKeyPair) { await this.sourceControlPreferencesService.deleteKeyPair(); } this.gitService.resetService(); return this.sourceControlPreferencesService.sourceControlPreferences; } catch (error) { throw new n8n_workflow_1.ApplicationError('Failed to disconnect from source control', { cause: error }); } } async initializeRepository(preferences, user) { var _a; 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 { throw error; } } if (getBranchesResult.branches.includes(preferences.branchName)) { await this.gitService.setBranch(preferences.branchName); } else { if (((_a = getBranchesResult.branches) === null || _a === void 0 ? void 0 : _a.length) === 0) { try { (0, fs_1.writeFileSync)(path_1.default.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 === null || branch === void 0 ? void 0 : 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.ApplicationError('Unable to fetch updates from git - your folder might be out of sync. Try reconnecting from the Source Control settings page.'); } return; } async pushWorkfolder(options) { var _a, _b; await this.sanityCheck(); if (this.sourceControlPreferencesService.isBranchReadOnly()) { throw new bad_request_error_1.BadRequestError('Cannot push onto read-only branch.'); } let statusResult = options.fileNames; if (statusResult.length === 0) { statusResult = (await this.getStatus({ direction: 'push', verbose: false, preferLocalVersion: true, })); } if (!options.force) { const possibleConflicts = statusResult === null || statusResult === void 0 ? void 0 : statusResult.filter((file) => file.conflict); if ((possibleConflicts === null || possibleConflicts === void 0 ? void 0 : possibleConflicts.length) > 0) { return { statusCode: 409, pushResult: undefined, statusResult, }; } } const filesToBePushed = new Set(); const filesToBeDeleted = new Set(); options.fileNames.forEach((e) => { if (e.status !== 'deleted') { filesToBePushed.add(e.file); } else { filesToBeDeleted.add(e.file); } }); this.sourceControlExportService.rmFilesFromExportFolder(filesToBeDeleted); const workflowsToBeExported = options.fileNames.filter((e) => e.type === 'workflow' && e.status !== 'deleted'); await this.sourceControlExportService.exportWorkflowsToWorkFolder(workflowsToBeExported); const credentialsToBeExported = options.fileNames.filter((e) => e.type === 'credential' && e.status !== 'deleted'); 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)); }); } if (options.fileNames.find((e) => e.type === 'tags')) { await this.sourceControlExportService.exportTagsToWorkFolder(); } if (options.fileNames.find((e) => e.type === 'variables')) { await this.sourceControlExportService.exportVariablesToWorkFolder(); } await this.gitService.stage(filesToBePushed, filesToBeDeleted); for (let i = 0; i < statusResult.length; i++) { if (options.fileNames.find((file) => file.file === statusResult[i].file)) { statusResult[i].pushed = true; } } await this.gitService.commit((_a = options.message) !== null && _a !== void 0 ? _a : 'Updated Workfolder'); const pushResult = await this.gitService.push({ branch: this.sourceControlPreferencesService.getBranchName(), force: (_b = options.force) !== null && _b !== void 0 ? _b : false, }); void typedi_1.default.get(InternalHooks_1.InternalHooks).onSourceControlUserFinishedPushUI((0, sourceControlHelper_ee_1.getTrackingInformationFromPostPushResult)(statusResult)); return { statusCode: 200, pushResult, statusResult, }; } async pullWorkfolder(options) { await this.sanityCheck(); const statusResult = (await this.getStatus({ direction: 'pull', verbose: false, preferLocalVersion: false, })); const filteredResult = statusResult.filter((e) => { if (e.status === 'created' && e.location === 'local') { return false; } if (e.type === 'credential' && e.status === 'deleted') { return false; } return true; }); if (options.force !== true) { const possibleConflicts = filteredResult === null || filteredResult === void 0 ? void 0 : filteredResult.filter((file) => (file.conflict || file.status === 'modified') && file.type === 'workflow'); if ((possibleConflicts === null || possibleConflicts === void 0 ? void 0 : possibleConflicts.length) > 0) { await this.gitService.resetBranch(); return { statusCode: 409, statusResult: filteredResult, }; } } const workflowsToBeImported = statusResult.filter((e) => e.type === 'workflow' && e.status !== 'deleted'); await this.sourceControlImportService.importWorkflowFromWorkFolder(workflowsToBeImported, options.userId); const credentialsToBeImported = statusResult.filter((e) => e.type === 'credential' && e.status !== 'deleted'); await this.sourceControlImportService.importCredentialsFromWorkFolder(credentialsToBeImported, options.userId); const tagsToBeImported = statusResult.find((e) => e.type === 'tags'); if (tagsToBeImported) { await this.sourceControlImportService.importTagsFromWorkFolder(tagsToBeImported); } const variablesToBeImported = statusResult.find((e) => e.type === 'variables'); if (variablesToBeImported) { await this.sourceControlImportService.importVariablesFromWorkFolder(variablesToBeImported); } void typedi_1.default.get(InternalHooks_1.InternalHooks).onSourceControlUserFinishedPullUI((0, sourceControlHelper_ee_1.getTrackingInformationFromPullResult)(statusResult)); return { statusCode: 200, statusResult: filteredResult, }; } async getStatus(options) { await this.sanityCheck(); const sourceControlledFiles = []; await this.resetWorkfolder(); const { wfRemoteVersionIds, wfLocalVersionIds, wfMissingInLocal, wfMissingInRemote, wfModifiedInEither, } = await this.getStatusWorkflows(options, sourceControlledFiles); const { credMissingInLocal, credMissingInRemote, credModifiedInEither } = await this.getStatusCredentials(options, sourceControlledFiles); const { varMissingInLocal, varMissingInRemote, varModifiedInEither } = await this.getStatusVariables(options, sourceControlledFiles); const { tagsMissingInLocal, tagsMissingInRemote, tagsModifiedInEither, mappingsMissingInLocal, mappingsMissingInRemote, } = await this.getStatusTagsMappings(options, sourceControlledFiles); if (options.direction === 'push') { void typedi_1.default.get(InternalHooks_1.InternalHooks).onSourceControlUserStartedPushUI((0, sourceControlHelper_ee_1.getTrackingInformationFromPrePushResult)(sourceControlledFiles)); } else if (options.direction === 'pull') { void typedi_1.default.get(InternalHooks_1.InternalHooks).onSourceControlUserStartedPullUI((0, sourceControlHelper_ee_1.getTrackingInformationFromPullResult)(sourceControlledFiles)); } if (options === null || options === void 0 ? void 0 : options.verbose) { return { wfRemoteVersionIds, wfLocalVersionIds, wfMissingInLocal, wfMissingInRemote, wfModifiedInEither, credMissingInLocal, credMissingInRemote, credModifiedInEither, varMissingInLocal, varMissingInRemote, varModifiedInEither, tagsMissingInLocal, tagsMissingInRemote, tagsModifiedInEither, mappingsMissingInLocal, mappingsMissingInRemote, sourceControlledFiles, }; } else { return sourceControlledFiles; } } async getStatusWorkflows(options, sourceControlledFiles) { const wfRemoteVersionIds = await this.sourceControlImportService.getRemoteVersionIdsFromFiles(); const wfLocalVersionIds = await this.sourceControlImportService.getLocalVersionIdsFromDb(); const wfMissingInLocal = wfRemoteVersionIds.filter((remote) => wfLocalVersionIds.findIndex((local) => local.id === remote.id) === -1); const wfMissingInRemote = wfLocalVersionIds.filter((local) => wfRemoteVersionIds.findIndex((remote) => remote.id === local.id) === -1); const wfModifiedInEither = []; wfLocalVersionIds.forEach((local) => { var _a; const mismatchingIds = wfRemoteVersionIds.find((remote) => remote.id === local.id && remote.versionId !== local.versionId); let name = (_a = ((options === null || options === void 0 ? void 0 : options.preferLocalVersion) ? local === null || local === void 0 ? void 0 : local.name : mismatchingIds === null || mismatchingIds === void 0 ? void 0 : mismatchingIds.name)) !== null && _a !== void 0 ? _a : 'Workflow'; if (local.name && (mismatchingIds === null || mismatchingIds === void 0 ? void 0 : mismatchingIds.name) && local.name !== mismatchingIds.name) { name = (options === null || options === void 0 ? void 0 : options.preferLocalVersion) ? `${local.name} (Remote: ${mismatchingIds.name})` : (name = `${mismatchingIds.name} (Local: ${local.name})`); } if (mismatchingIds) { wfModifiedInEither.push({ ...local, name, versionId: options.preferLocalVersion ? local.versionId : mismatchingIds.versionId, localId: local.versionId, remoteId: mismatchingIds.versionId, }); } }); wfMissingInLocal.forEach((item) => { var _a, _b; sourceControlledFiles.push({ id: item.id, name: (_a = item.name) !== null && _a !== void 0 ? _a : 'Workflow', type: 'workflow', status: options.direction === 'push' ? 'deleted' : 'created', location: options.direction === 'push' ? 'local' : 'remote', conflict: false, file: item.filename, updatedAt: (_b = item.updatedAt) !== null && _b !== void 0 ? _b : new Date().toISOString(), }); }); wfMissingInRemote.forEach((item) => { var _a, _b; sourceControlledFiles.push({ id: item.id, name: (_a = item.name) !== null && _a !== void 0 ? _a : 'Workflow', type: 'workflow', status: options.direction === 'push' ? 'created' : 'deleted', location: options.direction === 'push' ? 'local' : 'remote', conflict: false, file: item.filename, updatedAt: (_b = item.updatedAt) !== null && _b !== void 0 ? _b : new Date().toISOString(), }); }); wfModifiedInEither.forEach((item) => { var _a, _b; sourceControlledFiles.push({ id: item.id, name: (_a = item.name) !== null && _a !== void 0 ? _a : 'Workflow', type: 'workflow', status: 'modified', location: options.direction === 'push' ? 'local' : 'remote', conflict: true, file: item.filename, updatedAt: (_b = item.updatedAt) !== null && _b !== void 0 ? _b : new Date().toISOString(), }); }); return { wfRemoteVersionIds, wfLocalVersionIds, wfMissingInLocal, wfMissingInRemote, wfModifiedInEither, }; } async getStatusCredentials(options, sourceControlledFiles) { const credRemoteIds = await this.sourceControlImportService.getRemoteCredentialsFromFiles(); const credLocalIds = await this.sourceControlImportService.getLocalCredentialsFromDb(); const credMissingInLocal = credRemoteIds.filter((remote) => credLocalIds.findIndex((local) => local.id === remote.id) === -1); const credMissingInRemote = credLocalIds.filter((local) => credRemoteIds.findIndex((remote) => remote.id === local.id) === -1); const credModifiedInEither = []; credLocalIds.forEach((local) => { const mismatchingCreds = credRemoteIds.find((remote) => { return remote.id === local.id && (remote.name !== local.name || remote.type !== local.type); }); if (mismatchingCreds) { credModifiedInEither.push({ ...local, name: (options === null || options === void 0 ? void 0 : options.preferLocalVersion) ? local.name : mismatchingCreds.name, }); } }); credMissingInLocal.forEach((item) => { var _a; sourceControlledFiles.push({ id: item.id, name: (_a = item.name) !== null && _a !== void 0 ? _a : 'Credential', type: 'credential', status: options.direction === 'push' ? 'deleted' : 'created', location: options.direction === 'push' ? 'local' : 'remote', conflict: false, file: item.filename, updatedAt: new Date().toISOString(), }); }); credMissingInRemote.forEach((item) => { var _a; sourceControlledFiles.push({ id: item.id, name: (_a = item.name) !== null && _a !== void 0 ? _a : 'Credential', type: 'credential', status: options.direction === 'push' ? 'created' : 'deleted', location: options.direction === 'push' ? 'local' : 'remote', conflict: false, file: item.filename, updatedAt: new Date().toISOString(), }); }); credModifiedInEither.forEach((item) => { var _a; sourceControlledFiles.push({ id: item.id, name: (_a = item.name) !== null && _a !== void 0 ? _a : 'Credential', type: 'credential', status: 'modified', location: options.direction === 'push' ? 'local' : 'remote', conflict: true, file: item.filename, updatedAt: new Date().toISOString(), }); }); return { credMissingInLocal, credMissingInRemote, credModifiedInEither, }; } async getStatusVariables(options, sourceControlledFiles) { const varRemoteIds = await this.sourceControlImportService.getRemoteVariablesFromFile(); const varLocalIds = await this.sourceControlImportService.getLocalVariablesFromDb(); const varMissingInLocal = varRemoteIds.filter((remote) => varLocalIds.findIndex((local) => local.id === remote.id) === -1); const varMissingInRemote = varLocalIds.filter((local) => varRemoteIds.findIndex((remote) => remote.id === local.id) === -1); const varModifiedInEither = []; varLocalIds.forEach((local) => { const mismatchingIds = varRemoteIds.find((remote) => (remote.id === local.id && remote.key !== local.key) || (remote.id !== local.id && remote.key === local.key)); if (mismatchingIds) { varModifiedInEither.push(options.preferLocalVersion ? local : mismatchingIds); } }); if (varMissingInLocal.length > 0 || varMissingInRemote.length > 0 || varModifiedInEither.length > 0) { if (options.direction === 'pull' && varRemoteIds.length === 0) { } else { sourceControlledFiles.push({ id: 'variables', name: 'variables', type: 'variables', status: 'modified', location: options.direction === 'push' ? 'local' : 'remote', conflict: false, file: (0, sourceControlHelper_ee_1.getVariablesPath)(this.gitFolder), updatedAt: new Date().toISOString(), }); } } return { varMissingInLocal, varMissingInRemote, varModifiedInEither, }; } async getStatusTagsMappings(options, sourceControlledFiles) { var _a; const lastUpdatedTag = await this.tagRepository.find({ order: { updatedAt: 'DESC' }, take: 1, select: ['updatedAt'], }); const tagMappingsRemote = await this.sourceControlImportService.getRemoteTagsAndMappingsFromFile(); const tagMappingsLocal = await this.sourceControlImportService.getLocalTagsAndMappingsFromDb(); const tagsMissingInLocal = tagMappingsRemote.tags.filter((remote) => tagMappingsLocal.tags.findIndex((local) => local.id === remote.id) === -1); const tagsMissingInRemote = tagMappingsLocal.tags.filter((local) => tagMappingsRemote.tags.findIndex((remote) => remote.id === local.id) === -1); const tagsModifiedInEither = []; tagMappingsLocal.tags.forEach((local) => { const mismatchingIds = tagMappingsRemote.tags.find((remote) => remote.id === local.id && remote.name !== local.name); if (!mismatchingIds) { return; } tagsModifiedInEither.push(options.preferLocalVersion ? local : mismatchingIds); }); const mappingsMissingInLocal = tagMappingsRemote.mappings.filter((remote) => tagMappingsLocal.mappings.findIndex((local) => local.tagId === remote.tagId && local.workflowId === remote.workflowId) === -1); const mappingsMissingInRemote = tagMappingsLocal.mappings.filter((local) => tagMappingsRemote.mappings.findIndex((remote) => remote.tagId === local.tagId && remote.workflowId === remote.workflowId) === -1); if (tagsMissingInLocal.length > 0 || tagsMissingInRemote.length > 0 || tagsModifiedInEither.length > 0 || mappingsMissingInLocal.length > 0 || mappingsMissingInRemote.length > 0) { if (options.direction === 'pull' && tagMappingsRemote.tags.length === 0 && tagMappingsRemote.mappings.length === 0) { } else { sourceControlledFiles.push({ id: 'mappings', name: 'tags', type: 'tags', status: 'modified', location: options.direction === 'push' ? 'local' : 'remote', conflict: false, file: (0, sourceControlHelper_ee_1.getTagsPath)(this.gitFolder), updatedAt: (_a = lastUpdatedTag[0]) === null || _a === void 0 ? void 0 : _a.updatedAt.toISOString(), }); } } return { tagsMissingInLocal, tagsMissingInRemote, tagsModifiedInEither, mappingsMissingInLocal, mappingsMissingInRemote, }; } 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); } }; exports.SourceControlService = SourceControlService; exports.SourceControlService = SourceControlService = __decorate([ (0, typedi_1.Service)(), __metadata("design:paramtypes", [Logger_1.Logger, sourceControlGit_service_ee_1.SourceControlGitService, sourceControlPreferences_service_ee_1.SourceControlPreferencesService, sourceControlExport_service_ee_1.SourceControlExportService, sourceControlImport_service_ee_1.SourceControlImportService, tag_repository_1.TagRepository]) ], SourceControlService); //# sourceMappingURL=sourceControl.service.ee.js.map