n8n
Version:
n8n Workflow Automation Tool
395 lines • 20.8 kB
JavaScript
"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.SourceControlService = void 0;
const backend_common_1 = require("@n8n/backend-common");
const di_1 = require("@n8n/di");
const fs_1 = require("fs");
const n8n_workflow_1 = require("n8n-workflow");
const path_1 = __importDefault(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_scoped_service_1 = require("./source-control-scoped.service");
const source_control_status_service_ee_1 = require("./source-control-status.service.ee");
const source_control_context_1 = require("./types/source-control-context");
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, sourceControlScopedService, eventService, sourceControlStatusService) {
this.logger = logger;
this.gitService = gitService;
this.sourceControlPreferencesService = sourceControlPreferencesService;
this.sourceControlExportService = sourceControlExportService;
this.sourceControlImportService = sourceControlImportService;
this.sourceControlScopedService = sourceControlScopedService;
this.eventService = eventService;
this.sourceControlStatusService = sourceControlStatusService;
const { gitFolder, sshFolder, sshKeyName } = sourceControlPreferencesService;
this.gitFolder = gitFolder;
this.sshFolder = sshFolder;
this.sshKeyName = sshKeyName;
}
async init() {
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 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();
}
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 {
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_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?.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 = new source_control_context_1.SourceControlContext(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'].includes(f.type))
.forEach((e) => {
if (e.status !== 'deleted') {
filesToBePushed.add(e.file);
}
else {
filesToBeDeleted.add(e.file);
}
});
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();
}
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');
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');
await this.sourceControlImportService.importWorkflowFromWorkFolder(workflowsToBeImported, user.id);
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);
}
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 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 = new source_control_context_1.SourceControlContext(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;
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_scoped_service_1.SourceControlScopedService,
event_service_1.EventService,
source_control_status_service_ee_1.SourceControlStatusService])
], SourceControlService);
//# sourceMappingURL=source-control.service.ee.js.map