n8n
Version:
n8n Workflow Automation Tool
586 lines • 29.5 kB
JavaScript
"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