n8n
Version:
n8n Workflow Automation Tool
967 lines • 47.2 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.SourceControlStatusService = 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 pick_1 = __importDefault(require("lodash/pick"));
const n8n_workflow_1 = require("n8n-workflow");
const constants_1 = require("./constants");
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_context_factory_1 = require("./source-control-context.factory");
const forbidden_error_1 = require("../../errors/response-errors/forbidden.error");
const event_service_1 = require("../../events/event.service");
let SourceControlStatusService = class SourceControlStatusService {
constructor(logger, gitService, sourceControlImportService, sourceControlPreferencesService, sourceControlContextFactory, tagRepository, folderRepository, workflowRepository, eventService) {
this.logger = logger;
this.gitService = gitService;
this.sourceControlImportService = sourceControlImportService;
this.sourceControlPreferencesService = sourceControlPreferencesService;
this.sourceControlContextFactory = sourceControlContextFactory;
this.tagRepository = tagRepository;
this.folderRepository = folderRepository;
this.workflowRepository = workflowRepository;
this.eventService = eventService;
}
get gitFolder() {
return this.sourceControlPreferencesService.gitFolder;
}
get dataTableExportFolder() {
return `${this.gitFolder}/${constants_1.SOURCE_CONTROL_DATATABLES_EXPORT_FOLDER}`;
}
convertToStatusResourceOwner(owner) {
if (!owner) {
return;
}
if (owner.type === 'personal') {
return;
}
if ('teamId' in owner && 'teamName' in owner) {
return {
type: 'team',
projectId: owner.teamId,
projectName: owner.teamName,
};
}
if ('projectId' in owner) {
return owner;
}
return;
}
isSameDataTableProject(localOwner, remoteOwner) {
if (!localOwner && !remoteOwner) {
return true;
}
if (localOwner?.type === 'team' && remoteOwner?.type === 'team') {
return localOwner.projectId === remoteOwner.teamId;
}
if (localOwner?.type === 'personal' || remoteOwner?.type === 'personal') {
return false;
}
return false;
}
buildFolderPath(parentFolderId, foldersById) {
if (!parentFolderId) {
return [];
}
const pathSegments = [];
const visited = new Set();
let currentFolderId = parentFolderId;
while (currentFolderId && !visited.has(currentFolderId)) {
visited.add(currentFolderId);
const folder = foldersById.get(currentFolderId);
if (!folder) {
break;
}
pathSegments.unshift(folder.name);
currentFolderId = folder.parentFolderId;
}
return pathSegments;
}
async getStatus(user, options) {
const context = await this.sourceControlContextFactory.createContext(user);
const collectVerbose = options?.verbose ?? false;
if (options.direction === 'pull' && !(0, permissions_1.hasGlobalScope)(user, 'sourceControl:pull')) {
throw new forbidden_error_1.ForbiddenError('You do not have permission to pull from source control');
}
await this.resetWorkfolder();
const remoteFolders = await this.sourceControlImportService.getRemoteFoldersAndMappingsFromFile(context);
const [workflowsResult, credentialsResult, variablesResult, dataTablesResult, tagsMappingsResult, foldersMappingResult, projectsResult,] = await Promise.all([
this.getStatusWorkflows(options, context, collectVerbose, remoteFolders),
this.getStatusCredentials(options, context, collectVerbose),
this.getStatusVariables(options, collectVerbose),
this.getStatusDataTables(options, collectVerbose),
this.getStatusTagsMappings(options, context, collectVerbose),
this.getStatusFoldersMapping(options, context, collectVerbose, remoteFolders),
this.getStatusProjects(options, context, collectVerbose),
]);
const sourceControlledFiles = [
...workflowsResult.files,
...credentialsResult.files,
...variablesResult.files,
...dataTablesResult.files,
...tagsMappingsResult.files,
...foldersMappingResult.files,
...projectsResult.files,
];
if (options.direction === 'push') {
this.eventService.emit('source-control-user-started-push-ui', (0, source_control_helper_ee_1.getTrackingInformationFromPrePushResult)(user.id, sourceControlledFiles));
}
else if (options.direction === 'pull') {
this.eventService.emit('source-control-user-started-pull-ui', (0, source_control_helper_ee_1.getTrackingInformationFromPullResult)(user.id, sourceControlledFiles));
}
if (collectVerbose) {
return {
...workflowsResult.verbose,
...credentialsResult.verbose,
...variablesResult.verbose,
...dataTablesResult.verbose,
...tagsMappingsResult.verbose,
...foldersMappingResult.verbose,
...projectsResult.verbose,
sourceControlledFiles,
};
}
return sourceControlledFiles;
}
async resetWorkfolder() {
try {
if (!this.gitService.git) {
throw new Error('Git service not initialized');
}
await this.gitService.resetBranch();
await this.gitService.pull();
}
catch (error) {
this.logger.error(`Failed to reset workfolder: ${error instanceof Error ? error.message : String(error)}`);
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. Git error message: ${error instanceof Error ? error.message : String(error)}`);
}
}
async populateMissingLocalFolderPathNodes(localFoldersById, localWorkflows) {
const getMissingFolderIds = (folderIds) => {
const missingFolderIds = folderIds.filter((id) => id !== null && !localFoldersById.has(id));
return new Set(missingFolderIds);
};
const initialParentFolderIds = localWorkflows.map((workflow) => workflow.parentFolderId);
const folderIdsToProcessQueue = getMissingFolderIds(initialParentFolderIds);
while (folderIdsToProcessQueue.size > 0) {
const currentBatchFolderIds = Array.from(folderIdsToProcessQueue);
folderIdsToProcessQueue.clear();
const loadedFolders = await this.folderRepository.find({
where: { id: (0, typeorm_1.In)(currentBatchFolderIds) },
relations: ['parentFolder'],
select: {
id: true,
name: true,
parentFolder: {
id: true,
},
},
});
loadedFolders.forEach((folder) => {
localFoldersById.set(folder.id, {
name: folder.name,
parentFolderId: folder.parentFolder?.id ?? null,
});
});
const parentFolderIds = loadedFolders.map((folder) => folder.parentFolder?.id ?? null);
const missingParentFolderIds = getMissingFolderIds(parentFolderIds);
missingParentFolderIds.forEach((folderId) => {
folderIdsToProcessQueue.add(folderId);
});
}
}
async getStatusWorkflows(options, context, collectVerbose, prefetchedRemoteFolders) {
const sourceControlledFiles = [];
const [wfRemoteVersionIds, wfLocalVersionIds, foldersMappingsLocal] = await Promise.all([
this.sourceControlImportService.getRemoteVersionIdsFromFiles(context),
this.sourceControlImportService.getLocalVersionIdsFromDb(context),
this.sourceControlImportService.getLocalFoldersAndMappingsFromDb(context),
]);
const remoteFoldersById = new Map(prefetchedRemoteFolders.folders.map((folder) => [
folder.id,
(0, pick_1.default)(folder, ['name', 'parentFolderId']),
]));
const localFoldersById = new Map(foldersMappingsLocal.folders.map((folder) => [
folder.id,
(0, pick_1.default)(folder, ['name', 'parentFolderId']),
]));
await this.populateMissingLocalFolderPathNodes(localFoldersById, wfLocalVersionIds);
const wfRemoteById = new Map(wfRemoteVersionIds.map((w) => [w.id, w]));
const wfRemoteIds = new Set(wfRemoteVersionIds.map((w) => w.id));
const wfLocalIds = new Set(wfLocalVersionIds.map((w) => w.id));
const candidateIds = [...new Set([...wfLocalIds, ...wfRemoteIds])];
const localWorkflowsWithStatus = await this.workflowRepository.findByIds(candidateIds, {
fields: ['id', 'activeVersionId'],
});
const publishedWorkflowIds = new Set(localWorkflowsWithStatus.filter((w) => !!w.activeVersionId).map((w) => w.id));
const archivedWorkflowIds = new Map(wfRemoteVersionIds.filter((w) => w.isRemoteArchived).map((w) => [w.id, true]));
let outOfScopeWF = [];
if (!context.hasAccessToAllProjects()) {
outOfScopeWF = await this.sourceControlImportService.getAllLocalVersionIdsFromDb();
outOfScopeWF = outOfScopeWF.filter((wf) => !wfLocalIds.has(wf.id));
}
const outOfScopeIds = new Set(outOfScopeWF.map((wf) => wf.id));
const wfMissingInLocal = [];
const wfMissingInRemote = [];
const wfModifiedInEither = [];
for (const remoteWorkflow of wfRemoteVersionIds) {
if (!wfLocalIds.has(remoteWorkflow.id) && !outOfScopeIds.has(remoteWorkflow.id)) {
if (collectVerbose) {
wfMissingInLocal.push(remoteWorkflow);
}
sourceControlledFiles.push({
id: remoteWorkflow.id,
name: remoteWorkflow.name ?? 'Workflow',
type: 'workflow',
status: options.direction === 'push' ? 'deleted' : 'created',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: false,
file: remoteWorkflow.filename,
updatedAt: remoteWorkflow.updatedAt ?? new Date().toISOString(),
isLocalPublished: false,
isRemoteArchived: archivedWorkflowIds.get(remoteWorkflow.id) ?? false,
parentFolderId: remoteWorkflow.parentFolderId,
folderPath: this.buildFolderPath(remoteWorkflow.parentFolderId, remoteFoldersById),
owner: remoteWorkflow.owner,
});
}
}
for (const localWorkflow of wfLocalVersionIds) {
const remoteWorkflowWithSameId = wfRemoteById.get(localWorkflow.id);
if (!remoteWorkflowWithSameId) {
if (collectVerbose) {
wfMissingInRemote.push(localWorkflow);
}
sourceControlledFiles.push({
id: localWorkflow.id,
name: localWorkflow.name ?? 'Workflow',
type: 'workflow',
status: options.direction === 'push' ? 'created' : 'deleted',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: options.direction === 'push' ? false : true,
file: localWorkflow.filename,
updatedAt: localWorkflow.updatedAt ?? new Date().toISOString(),
isLocalPublished: publishedWorkflowIds.has(localWorkflow.id),
isRemoteArchived: false,
parentFolderId: localWorkflow.parentFolderId,
folderPath: this.buildFolderPath(localWorkflow.parentFolderId, localFoldersById),
owner: localWorkflow.owner,
});
continue;
}
if (!(0, source_control_helper_ee_1.isWorkflowModified)(localWorkflow, remoteWorkflowWithSameId)) {
continue;
}
let name = (options?.preferLocalVersion ? localWorkflow?.name : remoteWorkflowWithSameId?.name) ??
'Workflow';
if (localWorkflow.name &&
remoteWorkflowWithSameId?.name &&
localWorkflow.name !== remoteWorkflowWithSameId.name) {
name = options?.preferLocalVersion
? `${localWorkflow.name} (Remote: ${remoteWorkflowWithSameId.name})`
: (name = `${remoteWorkflowWithSameId.name} (Local: ${localWorkflow.name})`);
}
const preferredParentFolderId = options.preferLocalVersion
? localWorkflow.parentFolderId
: remoteWorkflowWithSameId.parentFolderId;
const preferredFolderPath = this.buildFolderPath(preferredParentFolderId, options.preferLocalVersion ? localFoldersById : remoteFoldersById);
const wfModified = {
...localWorkflow,
name,
parentFolderId: preferredParentFolderId,
versionId: options.preferLocalVersion
? localWorkflow.versionId
: remoteWorkflowWithSameId.versionId,
localId: localWorkflow.versionId,
remoteId: remoteWorkflowWithSameId.versionId,
};
if (collectVerbose) {
wfModifiedInEither.push(wfModified);
}
sourceControlledFiles.push({
id: wfModified.id,
name: wfModified.name ?? 'Workflow',
type: 'workflow',
status: 'modified',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: true,
file: wfModified.filename,
updatedAt: wfModified.updatedAt ?? new Date().toISOString(),
isLocalPublished: publishedWorkflowIds.has(wfModified.id),
isRemoteArchived: archivedWorkflowIds.get(wfModified.id) ?? false,
parentFolderId: preferredParentFolderId,
folderPath: preferredFolderPath,
owner: wfModified.owner,
});
}
return {
files: sourceControlledFiles,
verbose: {
wfRemoteVersionIds,
wfLocalVersionIds,
wfMissingInLocal,
wfMissingInRemote,
wfModifiedInEither,
},
};
}
async getStatusCredentials(options, context, collectVerbose) {
const sourceControlledFiles = [];
const credRemoteIds = await this.sourceControlImportService.getRemoteCredentialsFromFiles(context);
const credLocalIds = await this.sourceControlImportService.getLocalCredentialsFromDb(context);
const credRemoteById = new Map(credRemoteIds.map((cred) => [cred.id, cred]));
const credLocalIdsSet = new Set(credLocalIds.map((cred) => cred.id));
const credMissingInLocal = [];
const credMissingInRemote = [];
const credModifiedInEither = [];
for (const remoteCredential of credRemoteIds) {
if (!credLocalIdsSet.has(remoteCredential.id)) {
if (collectVerbose) {
credMissingInLocal.push(remoteCredential);
}
sourceControlledFiles.push({
id: remoteCredential.id,
name: remoteCredential.name ?? 'Credential',
type: 'credential',
status: options.direction === 'push' ? 'deleted' : 'created',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: false,
file: remoteCredential.filename,
updatedAt: new Date().toISOString(),
owner: remoteCredential.ownedBy,
});
}
}
for (const localCredential of credLocalIds) {
const credRemote = credRemoteById.get(localCredential.id);
if (credRemote) {
if ((0, source_control_helper_ee_1.areSameCredentials)(localCredential, credRemote)) {
continue;
}
const modifiedCredential = {
...localCredential,
name: options?.preferLocalVersion ? localCredential.name : credRemote.name,
};
if (collectVerbose) {
credModifiedInEither.push(modifiedCredential);
}
sourceControlledFiles.push({
id: modifiedCredential.id,
name: modifiedCredential.name ?? 'Credential',
type: 'credential',
status: 'modified',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: true,
file: modifiedCredential.filename,
updatedAt: new Date().toISOString(),
owner: modifiedCredential.ownedBy,
});
}
else {
if (collectVerbose) {
credMissingInRemote.push(localCredential);
}
sourceControlledFiles.push({
id: localCredential.id,
name: localCredential.name ?? 'Credential',
type: 'credential',
status: options.direction === 'push' ? 'created' : 'deleted',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: options.direction === 'push' ? false : true,
file: localCredential.filename,
updatedAt: new Date().toISOString(),
owner: localCredential.ownedBy,
});
}
}
return {
files: sourceControlledFiles,
verbose: {
credMissingInLocal,
credMissingInRemote,
credModifiedInEither,
},
};
}
async getStatusVariables(options, collectVerbose) {
const sourceControlledFiles = [];
const varRemoteIds = await this.sourceControlImportService.getRemoteVariablesFromFile();
const varLocalIds = await this.sourceControlImportService.getLocalGlobalVariablesFromDb();
const varRemoteIdsSet = new Set(varRemoteIds.map((remote) => remote.id));
const varLocalIdsSet = new Set(varLocalIds.map((local) => local.id));
const varMissingInLocal = [];
const varMissingInRemote = [];
const varModifiedInEither = [];
for (const remoteVariable of varRemoteIds) {
if (!varLocalIdsSet.has(remoteVariable.id)) {
if (collectVerbose) {
varMissingInLocal.push(remoteVariable);
}
sourceControlledFiles.push({
id: remoteVariable.id,
name: remoteVariable.key,
type: 'variables',
status: options.direction === 'push' ? 'deleted' : 'created',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: false,
file: (0, source_control_helper_ee_1.getVariablesPath)(this.gitFolder),
updatedAt: new Date().toISOString(),
});
}
}
for (const localVariable of varLocalIds) {
if (!varRemoteIdsSet.has(localVariable.id)) {
if (collectVerbose) {
varMissingInRemote.push(localVariable);
}
sourceControlledFiles.push({
id: localVariable.id,
name: localVariable.key,
type: 'variables',
status: options.direction === 'push' ? 'created' : 'deleted',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: options.direction === 'push' ? false : true,
file: (0, source_control_helper_ee_1.getVariablesPath)(this.gitFolder),
updatedAt: new Date().toISOString(),
});
}
}
for (const localVariable of varLocalIds) {
const mismatchingIds = varRemoteIds.find((remote) => (remote.id === localVariable.id && remote.key !== localVariable.key) ||
(remote.id !== localVariable.id && remote.key === localVariable.key));
if (!mismatchingIds) {
continue;
}
const modified = options.preferLocalVersion ? localVariable : mismatchingIds;
if (collectVerbose) {
varModifiedInEither.push(modified);
}
sourceControlledFiles.push({
id: modified.id,
name: modified.key,
type: 'variables',
status: 'modified',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: true,
file: (0, source_control_helper_ee_1.getVariablesPath)(this.gitFolder),
updatedAt: new Date().toISOString(),
});
}
return {
files: sourceControlledFiles,
verbose: {
varMissingInLocal,
varMissingInRemote,
varModifiedInEither,
},
};
}
async getStatusDataTables(options, collectVerbose) {
const sourceControlledFiles = [];
const dataTablesRemote = (await this.sourceControlImportService.getRemoteDataTablesFromFiles()) ?? [];
const dataTablesLocal = (await this.sourceControlImportService.getLocalDataTablesFromDb()) ?? [];
const localById = new Map(dataTablesLocal.map((dt) => [dt.id, dt]));
const remoteById = new Map(dataTablesRemote.map((dt) => [dt.id, dt]));
const remoteByName = new Map(dataTablesRemote.map((dt) => [dt.name, dt]));
const historicallyTrackedFiles = await this.gitService.getHistoricallyTrackedFiles(constants_1.SOURCE_CONTROL_DATATABLES_EXPORT_FOLDER);
const previouslySyncedIds = new Set();
for (const filePath of historicallyTrackedFiles) {
const match = filePath.match(/([^/]+)\.json$/);
if (match) {
previouslySyncedIds.add(match[1]);
}
}
const dtMissingInLocal = [];
const dtMissingInRemote = [];
const dtModifiedInEither = [];
for (const remote of dataTablesRemote) {
if (!localById.has(remote.id)) {
if (options.direction === 'push' && !previouslySyncedIds.has(remote.id)) {
continue;
}
if (collectVerbose) {
dtMissingInLocal.push(remote);
}
sourceControlledFiles.push({
id: remote.id,
name: remote.name,
type: 'datatable',
status: options.direction === 'push' ? 'deleted' : 'created',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: false,
file: (0, source_control_helper_ee_1.getDataTableExportPath)(remote.id, this.dataTableExportFolder),
updatedAt: new Date().toISOString(),
owner: this.convertToStatusResourceOwner(remote.ownedBy),
});
}
}
for (const local of dataTablesLocal) {
const remote = remoteById.get(local.id);
if (!remote) {
const nameCandidate = remoteByName.get(local.name);
const nameCollision = nameCandidate &&
nameCandidate.id !== local.id &&
this.isSameDataTableProject(local.ownedBy, nameCandidate.ownedBy)
? nameCandidate
: undefined;
if (nameCollision) {
const modified = options.preferLocalVersion ? local : nameCollision;
if (collectVerbose) {
dtModifiedInEither.push(modified);
}
sourceControlledFiles.push({
id: modified.id,
name: modified.name,
type: 'datatable',
status: 'modified',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: true,
file: (0, source_control_helper_ee_1.getDataTableExportPath)(modified.id, this.dataTableExportFolder),
updatedAt: new Date().toISOString(),
owner: this.convertToStatusResourceOwner(modified.ownedBy),
});
}
if (options.direction === 'pull' && !previouslySyncedIds.has(local.id)) {
continue;
}
if (collectVerbose) {
dtMissingInRemote.push(local);
}
sourceControlledFiles.push({
id: local.id,
name: local.name,
type: 'datatable',
status: options.direction === 'push' ? 'created' : 'deleted',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: options.direction !== 'push',
file: (0, source_control_helper_ee_1.getDataTableExportPath)(local.id, this.dataTableExportFolder),
updatedAt: new Date().toISOString(),
owner: local.ownedBy ?? undefined,
});
continue;
}
const isModified = (0, source_control_helper_ee_1.isDataTableModified)(local, remote);
if (isModified) {
const modified = options.preferLocalVersion ? local : remote;
if (collectVerbose) {
dtModifiedInEither.push(modified);
}
sourceControlledFiles.push({
id: modified.id,
name: modified.name,
type: 'datatable',
status: 'modified',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: true,
file: (0, source_control_helper_ee_1.getDataTableExportPath)(modified.id, this.dataTableExportFolder),
updatedAt: new Date().toISOString(),
owner: this.convertToStatusResourceOwner(modified.ownedBy),
});
}
}
return {
files: sourceControlledFiles,
verbose: {
dtMissingInLocal,
dtMissingInRemote,
dtModifiedInEither,
},
};
}
async getStatusTagsMappings(options, context, collectVerbose) {
const sourceControlledFiles = [];
const lastUpdatedTag = await this.tagRepository.find({
order: { updatedAt: 'DESC' },
take: 1,
select: ['updatedAt'],
});
const lastUpdatedDate = lastUpdatedTag[0]?.updatedAt ?? new Date();
const tagMappingsRemote = await this.sourceControlImportService.getRemoteTagsAndMappingsFromFile(context);
const tagMappingsLocal = await this.sourceControlImportService.getLocalTagsAndMappingsFromDb(context);
const tagsMissingInLocal = [];
const tagsMissingInRemote = [];
const tagsModifiedInEither = [];
const mappingsMissingInLocal = [];
const mappingsMissingInRemote = [];
let tagsMissingInLocalCount = 0;
let tagsMissingInRemoteCount = 0;
let tagsModifiedInEitherCount = 0;
let mappingsMissingInLocalCount = 0;
let mappingsMissingInRemoteCount = 0;
for (const remote of tagMappingsRemote.tags) {
if (tagMappingsLocal.tags.findIndex((local) => local.id === remote.id) === -1) {
tagsMissingInLocalCount += 1;
if (collectVerbose) {
tagsMissingInLocal.push(remote);
}
sourceControlledFiles.push({
id: remote.id,
name: remote.name,
type: 'tags',
status: options.direction === 'push' ? 'deleted' : 'created',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: false,
file: (0, source_control_helper_ee_1.getTagsPath)(this.gitFolder),
updatedAt: lastUpdatedDate.toISOString(),
});
}
}
for (const localTag of tagMappingsLocal.tags) {
if (tagMappingsRemote.tags.findIndex((remote) => remote.id === localTag.id) === -1) {
tagsMissingInRemoteCount += 1;
if (collectVerbose) {
tagsMissingInRemote.push(localTag);
}
sourceControlledFiles.push({
id: localTag.id,
name: localTag.name,
type: 'tags',
status: options.direction === 'push' ? 'created' : 'deleted',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: options.direction === 'push' ? false : true,
file: (0, source_control_helper_ee_1.getTagsPath)(this.gitFolder),
updatedAt: lastUpdatedDate.toISOString(),
});
}
}
for (const localTag of tagMappingsLocal.tags) {
const mismatchingIds = tagMappingsRemote.tags.find((remote) => remote.id === localTag.id && remote.name !== localTag.name);
if (!mismatchingIds) {
continue;
}
tagsModifiedInEitherCount += 1;
const modified = options.preferLocalVersion ? localTag : mismatchingIds;
if (collectVerbose) {
tagsModifiedInEither.push(modified);
}
sourceControlledFiles.push({
id: modified.id,
name: modified.name,
type: 'tags',
status: 'modified',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: true,
file: (0, source_control_helper_ee_1.getTagsPath)(this.gitFolder),
updatedAt: lastUpdatedDate.toISOString(),
});
}
for (const remoteTagMapping of tagMappingsRemote.mappings) {
const isMissing = tagMappingsLocal.mappings.findIndex((local) => local.tagId === remoteTagMapping.tagId &&
local.workflowId === remoteTagMapping.workflowId);
if (isMissing === -1) {
mappingsMissingInLocalCount += 1;
if (collectVerbose) {
mappingsMissingInLocal.push(remoteTagMapping);
}
}
}
for (const localTagMapping of tagMappingsLocal.mappings) {
const isMissing = tagMappingsRemote.mappings.findIndex((remote) => remote.tagId === localTagMapping.tagId &&
remote.workflowId === localTagMapping.workflowId);
if (isMissing === -1) {
mappingsMissingInRemoteCount += 1;
if (collectVerbose) {
mappingsMissingInRemote.push(localTagMapping);
}
}
}
const hasMappingChanges = mappingsMissingInLocalCount > 0 || mappingsMissingInRemoteCount > 0;
const hasTagChanges = tagsMissingInLocalCount > 0 || tagsMissingInRemoteCount > 0 || tagsModifiedInEitherCount > 0;
if (hasMappingChanges && !hasTagChanges) {
const isConflict = options.direction === 'pull' && mappingsMissingInRemoteCount > 0;
sourceControlledFiles.push({
id: 'tags',
name: 'Workflow Tags',
type: 'tags',
status: 'modified',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: isConflict,
file: (0, source_control_helper_ee_1.getTagsPath)(this.gitFolder),
updatedAt: lastUpdatedDate.toISOString(),
});
}
return {
files: sourceControlledFiles,
verbose: {
tagsMissingInLocal,
tagsMissingInRemote,
tagsModifiedInEither,
mappingsMissingInLocal,
mappingsMissingInRemote,
},
};
}
async getStatusFoldersMapping(options, context, collectVerbose, prefetchedRemoteFolders) {
const sourceControlledFiles = [];
const lastUpdatedFolder = await this.folderRepository.find({
order: { updatedAt: 'DESC' },
take: 1,
select: ['updatedAt'],
});
const lastUpdatedDate = lastUpdatedFolder[0]?.updatedAt ?? new Date();
const foldersMappingsRemote = prefetchedRemoteFolders;
const foldersMappingsLocal = await this.sourceControlImportService.getLocalFoldersAndMappingsFromDb(context);
const foldersMissingInLocal = [];
const foldersMissingInRemote = [];
const allTeamProjects = await this.sourceControlImportService.getLocalTeamProjectsFromDb();
const foldersModifiedInEither = [];
for (const remoteFolder of foldersMappingsRemote.folders) {
if (foldersMappingsLocal.folders.findIndex((local) => local.id === remoteFolder.id) === -1) {
if (collectVerbose) {
foldersMissingInLocal.push(remoteFolder);
}
sourceControlledFiles.push({
id: remoteFolder.id,
name: remoteFolder.name,
type: 'folders',
status: options.direction === 'push' ? 'deleted' : 'created',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: false,
file: (0, source_control_helper_ee_1.getFoldersPath)(this.gitFolder),
updatedAt: lastUpdatedDate.toISOString(),
});
}
}
for (const localFolder of foldersMappingsLocal.folders) {
if (foldersMappingsRemote.folders.findIndex((remote) => remote.id === localFolder.id) === -1) {
if (collectVerbose) {
foldersMissingInRemote.push(localFolder);
}
sourceControlledFiles.push({
id: localFolder.id,
name: localFolder.name,
type: 'folders',
status: options.direction === 'push' ? 'created' : 'deleted',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: options.direction === 'push' ? false : true,
file: (0, source_control_helper_ee_1.getFoldersPath)(this.gitFolder),
updatedAt: lastUpdatedDate.toISOString(),
});
}
}
const teamProjectsById = new Map(allTeamProjects.map((project) => [project.id, project]));
for (const localFolder of foldersMappingsLocal.folders) {
const localHomeProject = teamProjectsById.get(localFolder.homeProjectId);
const mismatchingIds = foldersMappingsRemote.folders.find((remote) => {
const remoteHomeProject = teamProjectsById.get(remote.homeProjectId);
const localOwner = localHomeProject
? {
type: 'team',
projectId: localHomeProject.id,
projectName: localHomeProject.name,
}
: undefined;
const remoteOwner = remoteHomeProject
? {
type: 'team',
projectId: remoteHomeProject?.id,
projectName: remoteHomeProject?.name,
}
: undefined;
const ownerChanged = (0, source_control_helper_ee_1.hasOwnerChanged)(localOwner, remoteOwner);
return (remote.id === localFolder.id &&
(remote.name !== localFolder.name ||
remote.parentFolderId !== localFolder.parentFolderId ||
ownerChanged));
});
if (!mismatchingIds) {
continue;
}
const modified = options.preferLocalVersion ? localFolder : mismatchingIds;
if (collectVerbose) {
foldersModifiedInEither.push(modified);
}
sourceControlledFiles.push({
id: modified.id,
name: modified.name,
type: 'folders',
status: 'modified',
location: options.direction === 'push' ? 'local' : 'remote',
conflict: true,
file: (0, source_control_helper_ee_1.getFoldersPath)(this.gitFolder),
updatedAt: lastUpdatedDate.toISOString(),
});
}
return {
files: sourceControlledFiles,
verbose: {
foldersMissingInLocal,
foldersMissingInRemote,
foldersModifiedInEither,
},
};
}
async getStatusProjects(options, context, collectVerbose) {
const sourceControlledFiles = [];
const projectsRemote = await this.sourceControlImportService.getRemoteProjectsFromFiles(context);
const projectsLocal = await this.sourceControlImportService.getLocalTeamProjectsFromDb(context);
let outOfScopeProjects = [];
if (!context.hasAccessToAllProjects()) {
outOfScopeProjects = await this.sourceControlImportService.getLocalTeamProjectsFromDb();
outOfScopeProjects = outOfScopeProjects.filter((project) => !projectsLocal.some((local) => local.id === project.id));
}
const projectsMissingInLocal = [];
const areRemoteProjectsEmpty = projectsRemote.length === 0;
const projectsMissingInRemote = [];
const projectsModifiedInEither = [];
const projectLocalIds = new Set(projectsLocal.map((localProject) => localProject.id));
const projectRemoteById = new Map(projectsRemote.map((remoteProject) => [remoteProject.id, remoteProject]));
const outOfScopeProjectIds = new Set(outOfScopeProjects.map((outOfScope) => outOfScope.id));
const mapExportableProjectWithFileNameToSourceControlledFile = ({ project, status, conflict, }) => {
return {
id: project.id,
name: project.name ?? 'Project',
type: 'project',
status,
location: options.direction === 'push' ? 'local' : 'remote',
conflict,
file: project.filename,
updatedAt: new Date().toISOString(),
owner: {
type: project.owner.type,
projectId: project.owner.teamId,
projectName: project.owner.teamName,
},
};
};
for (const remoteProject of projectsRemote) {
if (!projectLocalIds.has(remoteProject.id) && !outOfScopeProjectIds.has(remoteProject.id)) {
if (collectVerbose) {
projectsMissingInLocal.push(remoteProject);
}
sourceControlledFiles.push(mapExportableProjectWithFileNameToSourceControlledFile({
project: remoteProject,
status: options.direction === 'push' ? 'deleted' : 'created',
conflict: false,
}));
}
}
if (!(options.direction === 'pull' && areRemoteProjectsEmpty)) {
for (const localProject of projectsLocal) {
if (!projectRemoteById.has(localProject.id)) {
if (collectVerbose) {
projectsMissingInRemote.push(localProject);
}
sourceControlledFiles.push(mapExportableProjectWithFileNameToSourceControlledFile({
project: localProject,
status: options.direction === 'push' ? 'created' : 'deleted',
conflict: options.direction === 'push' ? false : true,
}));
}
}
}
projectsLocal.forEach((localProject) => {
const remoteProjectWithSameId = projectRemoteById.get(localProject.id);
if (!remoteProjectWithSameId) {
return;
}
if (this.isProjectModified(localProject, remoteProjectWithSameId)) {
let name = (options?.preferLocalVersion ? localProject?.name : remoteProjectWithSameId?.name) ??
'Project';
if (localProject.name &&
remoteProjectWithSameId?.name &&
localProject.name !== remoteProjectWithSameId.name) {
name = options?.preferLocalVersion
? `${localProject.name} (Remote: ${remoteProjectWithSameId.name})`
: `${remoteProjectWithSameId.name} (Local: ${localProject.name})`;
}
const modified = {
...localProject,
name,
description: options.preferLocalVersion
? localProject.description
: remoteProjectWithSameId.description,
icon: options.preferLocalVersion ? localProject.icon : remoteProjectWithSameId.icon,
variableStubs: options.preferLocalVersion
? localProject.variableStubs
: remoteProjectWithSameId.variableStubs,
};
if (collectVerbose) {
projectsModifiedInEither.push(modified);
}
sourceControlledFiles.push(mapExportableProjectWithFileNameToSourceControlledFile({
project: modified,
status: 'modified',
conflict: true,
}));
}
});
return {
files: sourceControlledFiles,
verbose: {
projectsRemote,
projectsLocal,
projectsMissingInLocal,
projectsMissingInRemote,
projectsModifiedInEither,
},
};
}
areVariablesEqual(localVariables, remoteVariables) {
if (Array.isArray(localVariables) !== Array.isArray(remoteVariables)) {
return false;
}
if (localVariables?.length !== remoteVariables?.length) {
return false;
}
const sortedLocalVars = [...(localVariables ?? [])].sort((a, b) => a.key.localeCompare(b.key));
const sortedRemoteVars = [...(remoteVariables ?? [])].sort((a, b) => a.key.localeCompare(b.key));
return sortedLocalVars.every((localVar, index) => {
const remoteVar = sortedRemoteVars[index];
return localVar.key === remoteVar.key && localVar.type === remoteVar.type;
});
}
isProjectModified(local, remote) {
const isIconModified = this.isProjectIconModified({
localIcon: local.icon,
remoteIcon: remote.icon,
});
return (isIconModified ||
remote.type !== local.type ||
remote.name !== local.name ||
remote.description !== local.description ||
!this.areVariablesEqual(local.variableStubs, remote.variableStubs));
}
isProjectIconModified({ localIcon, remoteIcon, }) {
if (!remoteIcon && !!localIcon)
return true;
if (!!remoteIcon && !localIcon)
return true;
if (!!remoteIcon && !!localIcon) {
return remoteIcon.type !== localIcon.type || remoteIcon.value !== localIcon.value;
}
return false;
}
};
exports.SourceControlStatusService = SourceControlStatusService;
exports.SourceControlStatusService = SourceControlStatusService = __decorate([
(0, di_1.Service)(),
__metadata("design:paramtypes", [backend_common_1.Logger,
source_control_git_service_ee_1.SourceControlGitService,
source_control_import_service_ee_1.SourceControlImportService,
source_control_preferences_service_ee_1.SourceControlPreferencesService,
source_control_context_factory_1.SourceControlContextFactory,
db_1.TagRepository,
db_1.FolderRepository,
db_1.WorkflowRepository,
event_service_1.EventService])
], SourceControlStatusService);
//# sourceMappingURL=source-control-status.service.ee.js.map