n8n
Version:
n8n Workflow Automation Tool
285 lines • 13.6 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 BreakingChangeService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.BreakingChangeService = void 0;
const backend_common_1 = require("@n8n/backend-common");
const constants_1 = require("@n8n/constants");
const db_1 = require("@n8n/db");
const di_1 = require("@n8n/di");
const n8n_core_1 = require("n8n-core");
const breaking_changes_rule_registry_service_1 = require("./breaking-changes.rule-registry.service");
const rules_1 = require("./rules");
const constants_2 = require("../../constants");
const cache_service_1 = require("../../services/cache/cache.service");
let BreakingChangeService = BreakingChangeService_1 = class BreakingChangeService {
constructor(ruleRegistry, workflowRepository, cacheService, logger, errorReporter) {
this.ruleRegistry = ruleRegistry;
this.workflowRepository = workflowRepository;
this.cacheService = cacheService;
this.logger = logger;
this.errorReporter = errorReporter;
this.batchSize = 100;
this.ongoingDetections = new Map();
this.logger = logger.scoped('breaking-changes');
this.registerRules();
}
registerRules() {
const rulesServices = rules_1.allRules.map((rule) => di_1.Container.get(rule));
this.ruleRegistry.registerAll(rulesServices);
}
async getAllInstanceRulesResults(instanceLevelRules) {
const instanceLevelResults = [];
for (const rule of instanceLevelRules) {
try {
const ruleResult = await rule.detect();
if (ruleResult.isAffected) {
instanceLevelResults.push({
ruleId: rule.id,
ruleTitle: rule.getMetadata().title,
ruleDescription: rule.getMetadata().description,
ruleSeverity: rule.getMetadata().severity,
ruleDocumentationUrl: rule.getMetadata().documentationUrl,
instanceIssues: ruleResult.instanceIssues,
recommendations: ruleResult.recommendations,
});
}
}
catch (error) {
console.log('error', error);
this.errorReporter.error(error, { shouldBeLogged: true });
}
}
return instanceLevelResults;
}
groupNodesByType(nodes) {
const nodesGroupedByType = new Map();
for (const node of nodes) {
if (!nodesGroupedByType.has(node.type)) {
nodesGroupedByType.set(node.type, []);
}
nodesGroupedByType.get(node.type).push(node);
}
return nodesGroupedByType;
}
async aggregateRegularRuleResults(workflowLevelRules, allAffectedWorkflowsByRule) {
const results = [];
for (const rule of workflowLevelRules) {
const workflowResults = allAffectedWorkflowsByRule.get(rule.id) ?? [];
const isAffected = workflowResults.some((wr) => wr.issues.length > 0);
if (isAffected) {
results.push({
ruleId: rule.id,
ruleTitle: rule.getMetadata().title,
ruleDescription: rule.getMetadata().description,
ruleSeverity: rule.getMetadata().severity,
ruleDocumentationUrl: rule.getMetadata().documentationUrl,
affectedWorkflows: workflowResults,
recommendations: await rule.getRecommendations(workflowResults),
});
}
}
return results;
}
async aggregateBatchRuleResults(batchRules, workflowMetadataMap) {
const results = [];
for (const rule of batchRules) {
const batchReport = await rule.produceReport();
if (batchReport.affectedWorkflows.length === 0) {
continue;
}
const affectedWorkflows = [];
for (const affected of batchReport.affectedWorkflows) {
const metadata = workflowMetadataMap.get(affected.workflowId);
if (!metadata) {
this.logger.warn('Workflow metadata not found for batch rule result', {
workflowId: affected.workflowId,
ruleId: rule.id,
});
continue;
}
affectedWorkflows.push({
id: affected.workflowId,
name: metadata.name,
active: metadata.active,
issues: affected.issues,
numberOfExecutions: metadata.numberOfExecutions,
lastExecutedAt: metadata.lastExecutedAt,
lastUpdatedAt: metadata.lastUpdatedAt,
});
}
if (affectedWorkflows.length > 0) {
results.push({
ruleId: rule.id,
ruleTitle: rule.getMetadata().title,
ruleDescription: rule.getMetadata().description,
ruleSeverity: rule.getMetadata().severity,
ruleDocumentationUrl: rule.getMetadata().documentationUrl,
affectedWorkflows,
recommendations: await rule.getRecommendations(affectedWorkflows),
});
}
}
return results;
}
async getAllWorkflowRulesResults(workflowLevelRules, batchRules, totalWorkflows) {
const allAffectedWorkflowsByRule = new Map();
const workflowMetadataMap = new Map();
batchRules.forEach((rule) => rule.reset());
this.logger.debug('Processing workflows in batches', {
totalWorkflows,
batchSize: this.batchSize,
regularRulesCount: workflowLevelRules.length,
batchRulesCount: batchRules.length,
});
for (let skip = 0; skip < totalWorkflows; skip += this.batchSize) {
const workflows = await this.workflowRepository.find({
select: ['id', 'name', 'active', 'activeVersionId', 'nodes', 'updatedAt', 'statistics'],
skip,
take: this.batchSize,
order: { id: 'ASC' },
relations: { statistics: true },
});
this.logger.debug('Processing batch', { skip, workflowsInBatch: workflows.length });
for (const workflow of workflows) {
const nodesGroupedByType = this.groupNodesByType(workflow.nodes);
const workflowMetadata = {
name: workflow.name,
active: !!workflow.activeVersionId,
numberOfExecutions: workflow.statistics.reduce((acc, cur) => acc + (cur.count || 0), 0),
lastExecutedAt: workflow.statistics.sort((a, b) => b.latestEvent.getTime() - a.latestEvent.getTime())[0]?.latestEvent,
lastUpdatedAt: workflow.updatedAt,
};
workflowMetadataMap.set(workflow.id, workflowMetadata);
for (const rule of workflowLevelRules) {
const result = await rule.detectWorkflow(workflow, nodesGroupedByType);
if (result.isAffected) {
const affectedWorkflow = {
id: workflow.id,
issues: result.issues,
...workflowMetadata,
};
const existing = allAffectedWorkflowsByRule.get(rule.id);
if (existing) {
existing.push(affectedWorkflow);
}
else {
allAffectedWorkflowsByRule.set(rule.id, [affectedWorkflow]);
}
}
}
for (const rule of batchRules) {
await rule.collectWorkflowData(workflow, nodesGroupedByType);
}
}
}
const regularResults = await this.aggregateRegularRuleResults(workflowLevelRules, allAffectedWorkflowsByRule);
const batchResults = await this.aggregateBatchRuleResults(batchRules, workflowMetadataMap);
return regularResults.concat(batchResults);
}
async refreshDetectionResults(targetVersion) {
await this.cacheService.delete(`${BreakingChangeService_1.CACHE_KEY_PREFIX}_${targetVersion}`);
return await this.getDetectionResults(targetVersion);
}
async getDetectionResults(targetVersion) {
const existingDetection = this.ongoingDetections.get(targetVersion);
if (existingDetection) {
this.logger.debug('Reusing ongoing detection', { targetVersion });
return await existingDetection;
}
const cacheKey = `${BreakingChangeService_1.CACHE_KEY_PREFIX}_${targetVersion}`;
const detectionPromise = new Promise((resolve) => {
void (async () => {
const cachedResult = await this.cacheService.get(cacheKey);
if (cachedResult) {
this.logger.debug('Using cached breaking change detection results', {
targetVersion,
});
return resolve(cachedResult);
}
const detectionResult = await this.detect(targetVersion);
return resolve(detectionResult);
})();
});
this.ongoingDetections.set(targetVersion, detectionPromise);
try {
const result = await detectionPromise;
if (result.shouldCache) {
await this.cacheService.set(cacheKey, result);
}
return result;
}
finally {
this.ongoingDetections.delete(targetVersion);
}
}
shouldCacheDetection(durationMs) {
return durationMs > BreakingChangeService_1.REPORT_DURATION_CACHE_THRESHOLD;
}
async detect(targetVersion) {
const startTime = Date.now();
this.logger.debug('Starting breaking change detection', { targetVersion });
const rules = this.ruleRegistry.getRules(targetVersion);
const workflowLevelRules = rules.filter((rule) => 'detectWorkflow' in rule);
const batchWorkflowRules = rules.filter((rule) => 'collectWorkflowData' in rule);
const instanceLevelRules = rules.filter((rule) => 'detect' in rule);
const totalWorkflows = await this.workflowRepository.count();
const [instanceLevelResults, workflowLevelResults] = await Promise.all([
this.getAllInstanceRulesResults(instanceLevelRules),
this.getAllWorkflowRulesResults(workflowLevelRules, batchWorkflowRules, totalWorkflows),
]);
const report = this.createDetectionReport(targetVersion, instanceLevelResults, workflowLevelResults);
const duration = Date.now() - startTime;
this.logger.debug('Breaking change detection completed', {
duration,
});
return {
report,
totalWorkflows,
shouldCache: this.shouldCacheDetection(duration),
};
}
async getDetectionReportForRule(ruleId) {
const rule = this.ruleRegistry.getRule(ruleId);
if (!rule) {
return undefined;
}
const totalWorkflows = await this.workflowRepository.count();
if ('detectWorkflow' in rule) {
return (await this.getAllWorkflowRulesResults([rule], [], totalWorkflows))[0];
}
if ('collectWorkflowData' in rule) {
return (await this.getAllWorkflowRulesResults([], [rule], totalWorkflows))[0];
}
return (await this.getAllInstanceRulesResults([rule]))[0];
}
createDetectionReport(targetVersion, instanceResults, workflowResults) {
return {
generatedAt: new Date(),
targetVersion,
currentVersion: constants_2.N8N_VERSION,
workflowResults,
instanceResults,
};
}
};
exports.BreakingChangeService = BreakingChangeService;
BreakingChangeService.REPORT_DURATION_CACHE_THRESHOLD = constants_1.Time.seconds.toMilliseconds * 2;
BreakingChangeService.CACHE_KEY_PREFIX = 'breaking-changes:results:';
exports.BreakingChangeService = BreakingChangeService = BreakingChangeService_1 = __decorate([
(0, di_1.Service)(),
__metadata("design:paramtypes", [breaking_changes_rule_registry_service_1.RuleRegistry,
db_1.WorkflowRepository,
cache_service_1.CacheService,
backend_common_1.Logger,
n8n_core_1.ErrorReporter])
], BreakingChangeService);
//# sourceMappingURL=breaking-changes.service.js.map