Files
alighasami 3d5eaf9445
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
first commit
2026-03-17 16:22:57 +03:30

388 lines
10 KiB
TypeScript

/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import type {
INodeType,
INodeTypeDescription,
IExecuteFunctions,
INodeExecutionData,
NodeExecutionWithMetadata,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import {
getSelectFields as dataTableFilters,
getSelectFilter as getDataTableFilter,
} from '../../DataTable/common/selectMany';
import { document, sheet } from '../../Google/Sheet/GoogleSheetsTrigger.node';
import { readFilter as googleSheetFilters } from '../../Google/Sheet/v2/actions/sheet/read.operation';
import { authentication } from '../../Google/Sheet/v2/actions/versionDescription';
import type { ILookupValues } from '../../Google/Sheet/v2/helpers/GoogleSheets.types';
import { sourcePicker } from '../Evaluation/Description.node';
import { listSearch, loadOptions, credentialTest } from '../methods';
import {
getGoogleSheet,
getResults,
getRowsLeft,
getNumberOfRowsLeftFiltered,
getSheet,
} from '../utils/evaluationTriggerUtils';
export const DEFAULT_STARTING_ROW = 2;
const MAX_ROWS = 1000;
export class EvaluationTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Evaluation Trigger',
icon: 'fa:check-double',
name: 'evaluationTrigger',
group: ['trigger'],
version: [4.6, 4.7],
description: 'Run a test dataset through your workflow to check performance',
eventTriggerDescription: '',
defaults: {
name: 'When fetching a dataset row',
color: '#c3c9d5',
},
inputs: [],
outputs: [NodeConnectionTypes.Main],
properties: [
{
...sourcePicker,
default: 'dataTable',
displayOptions: { show: { '@version': [{ _cnd: { gte: 4.7 } }] } },
},
{
...sourcePicker,
default: 'googleSheets',
displayOptions: { show: { '@version': [{ _cnd: { lte: 4.6 } }] } },
},
{
displayName:
'Pulls a test dataset from a Google Sheet. The workflow will run once for each row, in sequence. Tips for wiring this node up <a href="https://docs.n8n.io/advanced-ai/evaluations/tips-and-common-issues/#combining-multiple-triggers">here</a>.',
name: 'notice',
type: 'notice',
default: '',
displayOptions: { hide: { source: ['dataTable'] } },
},
{
displayName: 'Credentials',
name: 'credentials',
type: 'credentials',
default: '',
displayOptions: { hide: { source: ['dataTable'] } },
},
{
...authentication,
displayOptions: {
hide: {
source: ['dataTable'],
},
},
},
{
...document,
displayName: 'Document Containing Dataset',
hint: 'Example dataset format <a href="https://docs.google.com/spreadsheets/d/1vD_IdeFUg7sHsK9okL6Doy1rGOkWTnPJV3Dro4FBUsY/edit?gid=0#gid=0">here</a>',
displayOptions: { hide: { source: ['dataTable'] } },
},
{
...sheet,
displayName: 'Sheet Containing Dataset',
displayOptions: { hide: { source: ['dataTable'] } },
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
displayName: 'Data table',
name: 'dataTableId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'dataTableSearch',
searchable: true,
skipCredentialsCheckInRLC: true,
},
},
{
displayName: 'ID',
name: 'id',
type: 'string',
},
],
displayOptions: { show: { source: ['dataTable'] } },
},
{
displayName: 'Limit Rows',
name: 'limitRows',
type: 'boolean',
default: false,
noDataExpression: true,
description: 'Whether to limit number of rows to process',
},
{
displayName: 'Max Rows to Process',
name: 'maxRows',
type: 'number',
default: 10,
description: 'Maximum number of rows to process',
noDataExpression: false,
displayOptions: { show: { limitRows: [true] } },
},
{ ...googleSheetFilters, displayOptions: { hide: { source: ['dataTable'] } } },
{
displayName: 'Filter Rows',
name: 'filterRows',
type: 'boolean',
default: false,
noDataExpression: true,
description: 'Whether to filter rows to process',
displayOptions: { show: { source: ['dataTable'] } },
},
...dataTableFilters({ show: { filterRows: [true] } }),
],
codex: {
alias: ['Test', 'Metrics', 'Evals', 'Set Output', 'Set Metrics'],
},
credentials: [
{
name: 'googleApi',
required: true,
displayOptions: {
show: {
authentication: ['serviceAccount'],
},
},
testedBy: 'googleApiCredentialTest',
},
{
name: 'googleSheetsOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: ['oAuth2'],
},
},
},
],
};
methods = {
loadOptions,
listSearch,
credentialTest,
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const inputData = this.getInputData();
const source = this.getNodeParameter('source', 0) as string;
const previousRunRowNumber = inputData?.[0]?.json?.row_number;
const previousRunRowsLeft = inputData?.[0]?.json?._rowsLeft;
if (source === 'dataTable') {
const maxRows = this.getNodeParameter('limitRows', 0, false)
? (this.getNodeParameter('maxRows', 0, MAX_ROWS) as number)
: MAX_ROWS;
if (this.helpers.getDataTableProxy === undefined) {
throw new NodeOperationError(
this.getNode(),
'Attempted to use Data table node but the module is disabled',
);
}
const currentIndex =
typeof previousRunRowNumber === 'number' && previousRunRowsLeft !== 0
? previousRunRowNumber + 1
: 0;
const dataTableId = this.getNodeParameter('dataTableId', 0, undefined, {
extractValue: true,
}) as string;
const dataTableProxy = await this.helpers.getDataTableProxy(dataTableId);
const filter = await getDataTableFilter(this, 0);
const previousRunRowId = inputData?.[0]?.json?.row_id;
let effectiveFilter = filter;
if (typeof previousRunRowId === 'number' && previousRunRowsLeft !== 0) {
effectiveFilter = {
type: 'and',
filters: [
...filter.filters,
{
columnName: 'id',
condition: 'gt',
value: previousRunRowId,
},
],
};
}
const { data, count } = await dataTableProxy.getManyRowsAndCount({
skip: 0,
take: 1,
filter: effectiveFilter,
});
if (data.length === 0) {
throw new NodeOperationError(this.getNode(), 'No row found');
}
const effectiveTotal = Math.min(count, maxRows);
const rowsLeft = Math.max(0, effectiveTotal - 1);
const currentRow = {
json: {
...data[0],
row_number: currentIndex,
row_id: data[0].id,
_rowsLeft: rowsLeft,
},
} satisfies INodeExecutionData;
return [[currentRow]];
} else if (source === 'googleSheets') {
const maxRows = this.getNodeParameter('limitRows', 0, false)
? (this.getNodeParameter('maxRows', 0, MAX_ROWS) as number) + 1
: MAX_ROWS;
const firstDataRow =
typeof previousRunRowNumber === 'number' && previousRunRowsLeft !== 0
? previousRunRowNumber + 1
: DEFAULT_STARTING_ROW;
const rangeOptions = {
rangeDefinition: 'specifyRange',
headerRow: 1,
firstDataRow,
};
const googleSheetInstance = getGoogleSheet.call(this);
const googleSheet = await getSheet.call(this, googleSheetInstance);
const allRows = await getResults.call(
this,
[],
googleSheetInstance,
googleSheet,
rangeOptions,
);
const hasFilter = this.getNodeParameter('filtersUI.values', 0, []) as ILookupValues[];
if (hasFilter.length > 0) {
const currentRow = allRows[0];
const currentRowNumber = currentRow.json?.row_number as number;
if (currentRow === undefined) {
throw new NodeOperationError(this.getNode(), 'No row found');
}
const rowsLeft = await getNumberOfRowsLeftFiltered.call(
this,
googleSheetInstance,
googleSheet.title,
currentRowNumber + 1,
maxRows,
);
currentRow.json._rowsLeft = rowsLeft;
return [[currentRow]];
} else {
const currentRow = allRows.find(
(row) => (row?.json?.row_number as number) === firstDataRow,
);
const rowsLeft = await getRowsLeft.call(
this,
googleSheetInstance,
googleSheet.title,
`${googleSheet.title}!${firstDataRow}:${maxRows}`,
);
if (currentRow === undefined) {
throw new NodeOperationError(this.getNode(), 'No row found');
}
currentRow.json._rowsLeft = rowsLeft;
return [[currentRow]];
}
}
throw new NodeOperationError(this.getNode(), `Unknown source "${source}"`);
}
customOperations = {
dataset: {
async getRows(
this: IExecuteFunctions,
): Promise<INodeExecutionData[][] | NodeExecutionWithMetadata[][] | null> {
try {
const source = this.getNodeParameter('source', 0) as string;
if (source === 'dataTable') {
const maxRows = this.getNodeParameter('limitRows', 0, false)
? (this.getNodeParameter('maxRows', 0, MAX_ROWS) as number)
: MAX_ROWS;
if (this.helpers.getDataTableProxy === undefined) {
throw new NodeOperationError(
this.getNode(),
'Attempted to use Data table node but the module is disabled',
);
}
const dataTableId = this.getNodeParameter('dataTableId', 0, undefined, {
extractValue: true,
}) as string;
const dataTableProxy = await this.helpers.getDataTableProxy(dataTableId);
const filter = await getDataTableFilter(this, 0);
const { data } = await dataTableProxy.getManyRowsAndCount({
skip: 0,
take: maxRows,
filter,
});
const result: INodeExecutionData[] = data.map((row, i) => ({
json: {
...row,
row_id: row.id,
row_number: i,
},
pairedItem: { item: 0 },
}));
return [result];
} else if (source === 'googleSheets') {
const maxRows = this.getNodeParameter('limitRows', 0, false)
? (this.getNodeParameter('maxRows', 0, MAX_ROWS) as number) + 1
: MAX_ROWS;
const googleSheetInstance = getGoogleSheet.call(this);
const googleSheet = await getSheet.call(this, googleSheetInstance);
const results = await getResults.call(this, [], googleSheetInstance, googleSheet, {});
const result = results.slice(0, maxRows - 1);
return [result];
}
throw new NodeOperationError(this.getNode(), `Unknown source "${source}"`);
} catch (error) {
throw new NodeOperationError(this.getNode(), error);
}
},
},
};
}