n8n
Version:
n8n Workflow Automation Tool
329 lines • 15.9 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);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EvaluationCollectionService = void 0;
const db_1 = require("@n8n/db");
const di_1 = require("@n8n/di");
const typeorm_1 = require("@n8n/typeorm");
const nanoid_1 = require("nanoid");
const bad_request_error_1 = require("../errors/response-errors/bad-request.error");
const not_found_error_1 = require("../errors/response-errors/not-found.error");
const test_runner_service_ee_1 = require("../evaluation.ee/test-runner/test-runner.service.ee");
const telemetry_1 = require("../telemetry");
const workflow_history_service_1 = require("../workflows/workflow-history/workflow-history.service");
let EvaluationCollectionService = class EvaluationCollectionService {
constructor(collectionRepo, testRunRepo, evalConfigRepo, workflowHistoryRepo, publishedVersionRepo, workflowHistoryService, testRunnerService, telemetry) {
this.collectionRepo = collectionRepo;
this.testRunRepo = testRunRepo;
this.evalConfigRepo = evalConfigRepo;
this.workflowHistoryRepo = workflowHistoryRepo;
this.publishedVersionRepo = publishedVersionRepo;
this.workflowHistoryService = workflowHistoryService;
this.testRunnerService = testRunnerService;
this.telemetry = telemetry;
}
async createCollection(user, workflowId, input) {
const config = await this.evalConfigRepo.findByIdAndWorkflowId(input.evaluationConfigId, workflowId);
if (!config) {
throw new not_found_error_1.NotFoundError('EvaluationConfig not found for this workflow');
}
for (const [index, v] of input.versions.entries()) {
if (v.workflowVersionId) {
const exists = await this.workflowHistoryService.findVersion(workflowId, v.workflowVersionId);
if (!exists) {
throw new bad_request_error_1.BadRequestError(`versions[${index}]: workflow version "${v.workflowVersionId}" not found`);
}
}
if (v.existingTestRunId) {
const run = await this.testRunRepo.findOneBy({ id: v.existingTestRunId });
if (!run ||
run.workflowId !== workflowId ||
run.evaluationConfigId !== input.evaluationConfigId) {
throw new bad_request_error_1.BadRequestError(`versions[${index}]: test run "${v.existingTestRunId}" is not compatible with this collection`);
}
if (v.workflowVersionId && run.workflowVersionId !== v.workflowVersionId) {
throw new bad_request_error_1.BadRequestError(`versions[${index}]: test run "${v.existingTestRunId}" was executed against version "${run.workflowVersionId ?? '(unpinned)'}", not the requested "${v.workflowVersionId}"`);
}
if (!run.workflowVersionId) {
throw new bad_request_error_1.BadRequestError(`versions[${index}]: test run "${v.existingTestRunId}" has no pinned workflow version and cannot be reused in a collection`);
}
}
}
const collection = await this.collectionRepo.createCollection({
id: (0, nanoid_1.nanoid)(),
name: input.name,
description: input.description ?? null,
workflowId,
evaluationConfigId: input.evaluationConfigId,
createdById: user.id,
});
const existingRunIds = input.versions
.filter((v) => v.existingTestRunId)
.map((v) => v.existingTestRunId);
if (existingRunIds.length > 0) {
await this.collectionRepo.addRunsToCollection(collection.id, existingRunIds);
}
const configSnapshot = {
...config,
createdAt: config.createdAt.toISOString(),
updatedAt: config.updatedAt.toISOString(),
};
const runsStartedIds = [];
for (const v of input.versions) {
if (v.existingTestRunId)
continue;
const versionId = v.workflowVersionId ??
(await this.workflowHistoryService.snapshotCurrent(workflowId)).versionId;
const { testRun } = await this.testRunnerService.startTestRun(user, workflowId, input.concurrency ?? 1, {
collectionId: collection.id,
workflowVersionId: versionId,
evaluationConfigId: input.evaluationConfigId,
evaluationConfigSnapshot: configSnapshot,
});
runsStartedIds.push(testRun.id);
}
this.telemetry.track('Eval collection created', {
user_id: user.id,
workflow_id: workflowId,
collection_id: collection.id,
evaluation_config_id: input.evaluationConfigId,
version_count: input.versions.length,
existing_run_count: existingRunIds.length,
new_run_count: runsStartedIds.length,
dataset_id: this.extractDatasetId(config),
});
const record = this.toRecord(collection, existingRunIds.length + runsStartedIds.length);
return { record, runsStartedIds };
}
async listCollections(workflowId) {
const items = await this.collectionRepo.listByWorkflowId(workflowId);
return items.map(({ runCount, ...c }) => this.toRecord(c, runCount));
}
async getCollectionDetail(workflowId, collectionId) {
const detail = await this.collectionRepo.getDetailByIdAndWorkflowId(collectionId, workflowId);
if (!detail)
throw new not_found_error_1.NotFoundError('Collection not found');
const runs = detail.runs.map((r) => this.toRunSummary(r));
return { ...this.toRecord(detail.collection, runs.length), runs };
}
async updateCollectionMeta(workflowId, collectionId, input) {
const updated = await this.collectionRepo.updateMeta(collectionId, workflowId, input);
if (!updated)
throw new not_found_error_1.NotFoundError('Collection not found');
const runCount = await this.testRunRepo.count({ where: { collectionId } });
return this.toRecord(updated, runCount);
}
async deleteCollection(user, workflowId, collectionId) {
const owned = await this.collectionRepo.findByIdAndWorkflowId(collectionId, workflowId);
if (!owned)
throw new not_found_error_1.NotFoundError('Collection not found');
const active = await this.testRunRepo.find({
where: [
{ collectionId, status: 'running' },
{ collectionId, status: 'new' },
],
select: ['id'],
});
if (active.length > 0) {
await this.testRunnerService.cancelCollection(collectionId);
}
const { deleted, runsUnlinked } = await this.collectionRepo.deleteByIdAndWorkflowId(collectionId, workflowId);
if (!deleted)
throw new not_found_error_1.NotFoundError('Collection not found');
this.telemetry.track('Eval collection deleted', {
user_id: user.id,
workflow_id: workflowId,
collection_id: collectionId,
runs_unlinked: runsUnlinked,
});
return { runsUnlinked };
}
async addRunToCollection(workflowId, collectionId, testRunId) {
const collection = await this.collectionRepo.findByIdAndWorkflowId(collectionId, workflowId);
if (!collection)
throw new not_found_error_1.NotFoundError('Collection not found');
const run = await this.testRunRepo.findOneBy({ id: testRunId });
if (!run || run.workflowId !== workflowId) {
throw new not_found_error_1.NotFoundError('Test run not found for this workflow');
}
if (run.evaluationConfigId !== collection.evaluationConfigId) {
throw new bad_request_error_1.BadRequestError('Test run is not compatible with this collection (different evaluation config)');
}
if (!run.workflowVersionId) {
throw new bad_request_error_1.BadRequestError('Test run has no pinned workflow version and cannot be added to a collection');
}
await this.collectionRepo.addRunsToCollection(collectionId, [testRunId]);
await this.collectionRepo.updateInsightsCache(collectionId, null);
return await this.getCollectionDetail(workflowId, collectionId);
}
async removeRunFromCollection(workflowId, collectionId, testRunId) {
const collection = await this.collectionRepo.findByIdAndWorkflowId(collectionId, workflowId);
if (!collection)
throw new not_found_error_1.NotFoundError('Collection not found');
const affected = await this.collectionRepo.removeRunFromCollection(collectionId, testRunId);
if (affected === 0) {
throw new not_found_error_1.NotFoundError('Test run is not part of this collection');
}
await this.collectionRepo.updateInsightsCache(collectionId, null);
return await this.getCollectionDetail(workflowId, collectionId);
}
async getEvalVersions(workflowId, evaluationConfigId) {
const config = await this.evalConfigRepo.findByIdAndWorkflowId(evaluationConfigId, workflowId);
if (!config) {
throw new not_found_error_1.NotFoundError('EvaluationConfig not found for this workflow');
}
const history = await this.workflowHistoryRepo.find({
where: { workflowId },
order: { createdAt: 'DESC' },
select: ['versionId', 'name', 'autosaved', 'createdAt'],
});
const lastRuns = history.length === 0
? []
: await this.testRunRepo.find({
where: {
evaluationConfigId,
workflowVersionId: (0, typeorm_1.In)(history.map((h) => h.versionId)),
},
order: { createdAt: 'DESC' },
});
const latestRunByVersion = new Map();
for (const run of lastRuns) {
if (run.workflowVersionId && !latestRunByVersion.has(run.workflowVersionId)) {
latestRunByVersion.set(run.workflowVersionId, run);
}
}
const publishedRow = await this.publishedVersionRepo.findOneBy({ workflowId });
const publishedVersionId = publishedRow?.publishedVersionId ?? null;
const versions = [];
versions.push({
workflowVersionId: null,
label: 'Current draft',
sourceLabel: 'Live workflow',
isCurrent: true,
lastRun: null,
});
for (const h of history) {
const lastRun = latestRunByVersion.get(h.versionId) ?? null;
versions.push({
workflowVersionId: h.versionId,
label: h.name ?? this.shortVersionLabel(h.versionId),
sourceLabel: this.formatSourceLabel(h, h.versionId === publishedVersionId),
isCurrent: false,
lastRun: lastRun
? {
testRunId: lastRun.id,
runAt: (lastRun.runAt ?? lastRun.createdAt).toISOString(),
status: lastRun.status,
avgScore: this.computeAvgScore(lastRun),
isBest: false,
isCritical: false,
}
: null,
});
}
const scored = versions.filter((v) => v.lastRun?.avgScore !== null && v.lastRun !== null);
if (scored.length > 0) {
const best = scored.reduce((acc, v) => (v.lastRun.avgScore ?? -Infinity) > (acc.lastRun.avgScore ?? -Infinity) ? v : acc);
if (best.lastRun)
best.lastRun.isBest = true;
for (const v of scored) {
if (v.lastRun && v.lastRun.avgScore !== null && v.lastRun.avgScore < 0.6) {
v.lastRun.isCritical = true;
}
}
}
return { evaluationConfigId, versions };
}
toRecord(collection, runCount) {
return {
id: collection.id,
name: collection.name,
description: collection.description,
workflowId: collection.workflowId,
evaluationConfigId: collection.evaluationConfigId,
createdById: collection.createdById,
createdAt: collection.createdAt.toISOString(),
updatedAt: collection.updatedAt.toISOString(),
runCount,
};
}
toRunSummary(run) {
return {
testRunId: run.id,
workflowVersionId: run.workflowVersionId,
status: run.status,
runAt: run.runAt ? run.runAt.toISOString() : null,
completedAt: run.completedAt ? run.completedAt.toISOString() : null,
avgScore: this.computeAvgScore(run),
metrics: this.coerceMetrics(run.metrics),
};
}
computeAvgScore(run) {
const coerced = this.coerceMetrics(run.metrics);
if (!coerced)
return null;
const values = Object.values(coerced);
if (values.length === 0)
return null;
const sum = values.reduce((acc, v) => acc + v, 0);
return sum / values.length;
}
coerceMetrics(metrics) {
if (!metrics)
return null;
const out = {};
for (const [k, v] of Object.entries(metrics)) {
if (typeof v === 'number')
out[k] = v;
else if (typeof v === 'boolean')
out[k] = v ? 1 : 0;
}
return Object.keys(out).length > 0 ? out : null;
}
extractDatasetId(config) {
if (config.datasetSource === 'data_table' &&
typeof config.datasetRef === 'object' &&
config.datasetRef !== null &&
'dataTableId' in config.datasetRef) {
return String(config.datasetRef.dataTableId);
}
return null;
}
shortVersionLabel(versionId) {
return `v ${versionId.slice(0, 8)}`;
}
formatDateLabel(date) {
return date.toISOString().slice(0, 10);
}
formatSourceLabel(h, isPublished) {
const date = this.formatDateLabel(h.createdAt);
if (isPublished)
return `Published · ${date}`;
if (h.name)
return `Named · ${date}`;
if (h.autosaved)
return `Autosaved · ${date}`;
return `Snapshot · ${date}`;
}
};
exports.EvaluationCollectionService = EvaluationCollectionService;
exports.EvaluationCollectionService = EvaluationCollectionService = __decorate([
(0, di_1.Service)(),
__metadata("design:paramtypes", [db_1.EvaluationCollectionRepository,
db_1.TestRunRepository,
db_1.EvaluationConfigRepository,
db_1.WorkflowHistoryRepository,
db_1.WorkflowPublishedVersionRepository,
workflow_history_service_1.WorkflowHistoryService,
test_runner_service_ee_1.TestRunnerService,
telemetry_1.Telemetry])
], EvaluationCollectionService);
//# sourceMappingURL=evaluation-collection.service.js.map