first commit
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

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,168 @@
import { SerpAPI } from '@langchain/community/tools/serpapi';
import { mock } from 'jest-mock-extended';
import type {
IExecuteFunctions,
INode,
INodeExecutionData,
ISupplyDataFunctions,
} from 'n8n-workflow';
import { ToolSerpApi } from './ToolSerpApi.node';
describe('ToolSerpApi', () => {
describe('supplyData', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should return SerpAPI tool instance', async () => {
const node = new ToolSerpApi();
const supplyDataResult = await node.supplyData.call(
mock<ISupplyDataFunctions>({
getNode: jest.fn(() => mock<INode>({ name: 'test serpapi' })),
getCredentials: jest.fn().mockResolvedValue({ apiKey: 'test-api-key' }),
getNodeParameter: jest.fn().mockReturnValue({}),
}),
0,
);
expect(supplyDataResult.response).toBeInstanceOf(SerpAPI);
});
});
describe('execute', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('should execute SerpAPI search and return result', async () => {
const node = new ToolSerpApi();
const inputData: INodeExecutionData[] = [
{
json: { input: 'artificial intelligence news' },
},
];
const mockExecute = mock<IExecuteFunctions>({
getInputData: jest.fn(() => inputData),
getNode: jest.fn(() => mock<INode>({ name: 'test serpapi' })),
getCredentials: jest.fn().mockResolvedValue({ apiKey: 'test-api-key' }),
getNodeParameter: jest.fn().mockReturnValue({}),
});
// Mock the SerpAPI.invoke method
const mockResult = 'Latest news about artificial intelligence...';
SerpAPI.prototype.invoke = jest.fn().mockResolvedValue(mockResult);
const result = await node.execute.call(mockExecute);
expect(result).toEqual([
[
{
json: {
response: mockResult,
},
pairedItem: {
item: 0,
},
},
],
]);
expect(SerpAPI.prototype.invoke).toHaveBeenCalledWith(inputData[0].json);
});
it('should handle multiple input items', async () => {
const node = new ToolSerpApi();
const inputData: INodeExecutionData[] = [
{
json: { input: 'machine learning' },
},
{
json: { input: 'deep learning' },
},
];
const mockExecute = mock<IExecuteFunctions>({
getInputData: jest.fn(() => inputData),
getNode: jest.fn(() => mock<INode>({ name: 'test serpapi' })),
getCredentials: jest.fn().mockResolvedValue({ apiKey: 'test-api-key' }),
getNodeParameter: jest.fn().mockReturnValue({}),
});
// Mock the SerpAPI.invoke method
SerpAPI.prototype.invoke = jest
.fn()
.mockResolvedValueOnce('Machine learning search results')
.mockResolvedValueOnce('Deep learning search results');
const result = await node.execute.call(mockExecute);
expect(result).toEqual([
[
{
json: {
response: 'Machine learning search results',
},
pairedItem: {
item: 0,
},
},
{
json: {
response: 'Deep learning search results',
},
pairedItem: {
item: 1,
},
},
],
]);
expect(SerpAPI.prototype.invoke).toHaveBeenCalledTimes(2);
});
it('should handle credentials and options correctly', async () => {
const node = new ToolSerpApi();
const inputData: INodeExecutionData[] = [
{
json: { input: 'test query' },
},
];
const testOptions = { engine: 'google', location: 'US' };
const mockExecute = mock<IExecuteFunctions>({
getInputData: jest.fn(() => inputData),
getNode: jest.fn(() => mock<INode>({ name: 'test serpapi' })),
getCredentials: jest.fn().mockResolvedValue({ apiKey: 'secret-api-key' }),
getNodeParameter: jest.fn().mockReturnValue(testOptions),
});
SerpAPI.prototype.invoke = jest.fn().mockResolvedValue('test result');
await node.execute.call(mockExecute);
expect(mockExecute.getCredentials).toHaveBeenCalledWith('serpApi');
expect(mockExecute.getNodeParameter).toHaveBeenCalledWith('options', 0);
});
it('should fail gracefully if input is missing', async () => {
const node = new ToolSerpApi();
const inputData: INodeExecutionData[] = [
{
json: {},
},
];
const mockExecute = mock<IExecuteFunctions>({
getInputData: jest.fn(() => inputData),
getNode: jest.fn(() => mock<INode>({ name: 'test serpapi' })),
getCredentials: jest.fn().mockResolvedValue({ apiKey: 'test-api-key' }),
getNodeParameter: jest.fn().mockReturnValue({}),
});
await expect(node.execute.call(mockExecute)).rejects.toThrow(
'Missing search query input at itemIndex 0',
);
});
});
});
@@ -0,0 +1,158 @@
import { SerpAPI } from '@langchain/community/tools/serpapi';
import { logWrapper, getConnectionHintNoticeField } from '@n8n/ai-utilities';
import {
type IExecuteFunctions,
NodeConnectionTypes,
type INodeType,
type INodeTypeDescription,
type ISupplyDataFunctions,
type SupplyData,
type INodeExecutionData,
NodeOperationError,
} from 'n8n-workflow';
async function getTool(ctx: ISupplyDataFunctions | IExecuteFunctions, itemIndex: number) {
const credentials = await ctx.getCredentials('serpApi');
const options = ctx.getNodeParameter('options', itemIndex) as object;
return new SerpAPI(credentials.apiKey as string, options);
}
export class ToolSerpApi implements INodeType {
description: INodeTypeDescription = {
displayName: 'SerpApi (Google Search)',
name: 'toolSerpApi',
icon: 'file:serpApi.svg',
group: ['transform'],
version: 1,
description: 'Search in Google using SerpAPI',
defaults: {
name: 'SerpAPI',
},
codex: {
categories: ['AI'],
subcategories: {
AI: ['Tools'],
Tools: ['Other Tools'],
},
resources: {
primaryDocumentation: [
{
url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.toolserpapi/',
},
],
},
},
inputs: [],
outputs: [NodeConnectionTypes.AiTool],
outputNames: ['Tool'],
credentials: [
{
name: 'serpApi',
required: true,
},
],
properties: [
getConnectionHintNoticeField([NodeConnectionTypes.AiAgent]),
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Country',
name: 'gl',
type: 'string',
default: 'us',
description:
'Defines the country to use for search. Head to <a href="https://serpapi.com/google-countries">Google countries page</a> for a full list of supported countries.',
},
{
displayName: 'Device',
name: 'device',
type: 'options',
options: [
{
name: 'Desktop',
value: 'desktop',
},
{
name: 'Mobile',
value: 'mobile',
},
{
name: 'Tablet',
value: 'tablet',
},
],
default: 'desktop',
description: 'Device to use to get the results',
},
{
displayName: 'Explicit Array',
name: 'no_cache',
type: 'boolean',
default: false,
description:
'Whether to force SerpApi to fetch the Google results even if a cached version is already present. Cache expires after 1h. Cached searches are free, and are not counted towards your searches per month.',
},
{
displayName: 'Google Domain',
name: 'google_domain',
type: 'string',
default: 'google.com',
description:
'Defines the domain to use for search. Head to <a href="https://serpapi.com/google-domains">Google domains page</a> for a full list of supported domains.',
},
{
displayName: 'Language',
name: 'hl',
type: 'string',
default: 'en',
description:
'Defines the language to use. It\'s a two-letter language code. (e.g., `en` for English, `es` for Spanish, or `fr` for French). Head to <a href="https://serpapi.com/google-languages">Google languages page</a> for a full list of supported languages.',
},
],
},
],
};
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
return {
response: logWrapper(await getTool(this, itemIndex), this),
};
}
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const inputData = this.getInputData();
const returnData: INodeExecutionData[] = [];
for (let itemIndex = 0; itemIndex < inputData.length; itemIndex++) {
const tool = await getTool(this, itemIndex);
const item = inputData[itemIndex].json;
if (typeof item.input !== 'string' || !item.input) {
throw new NodeOperationError(
this.getNode(),
`Missing search query input at itemIndex ${itemIndex}`,
);
}
const result = (await tool.invoke(item)) as string;
returnData.push({
json: {
response: result,
},
pairedItem: { item: itemIndex },
});
}
return [returnData];
}
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 4680 1340"><path fill="#7D7D87" d="M4463 121v110h207V11h-207zM300.5 47.6c-2.7.2-12.2.8-21 1.4-68.2 4.6-123.8 18.1-161 39.2C60.6 121 31.8 174.2 24 263c-2.7 31.5-2.2 98.3 1.1 135C35 509.4 72.8 555.1 185 591.5c24 7.7 55.1 15.8 98.2 25.5 20 4.5 47.1 10.9 60.3 14.2 71.8 18 95 32.5 102.5 63.9 5.2 21.9 5.2 74.3.1 97.9-9 40.9-36.1 55.8-105.8 57.8-91.6 2.8-177-9.3-279.6-39.5-10.4-3-19.2-5.1-19.7-4.7-.9 1.1-36.3 165.4-35.7 166 1.3 1.3 34.4 14.3 49.7 19.5 59.9 20.5 123.2 33.3 197.5 40 19.2 1.7 92.1 2.3 117 1 133.6-7.2 210-36.4 253.1-96.8 34.3-48 46.8-117.2 42.5-235.3-2.3-62.8-7.5-92.1-21.6-121.5-27.5-57.2-89-90.1-235.5-126.1-92.8-22.8-104.9-26.1-123.3-33.4-25.7-10.3-37.5-22-42.7-42.5-3.7-14.5-5.2-55.9-2.9-79 4-39.9 20.1-56.3 61.9-63 33.7-5.4 114.6-3.6 184.5 4.1 27 3 74.5 9.7 110.9 15.7 16.3 2.7 26 3.9 26.2 3.2.6-1.9 21.5-179.8 21.1-180.1-.7-.8-40.5-8.5-63.6-12.3C531.7 58 489.4 53 433.5 48.9c-14.1-1-122.1-2.1-133-1.3m2699 14.6c-1 2.9-291.5 956.8-291.5 957.3 0 .3 50.5.4 112.2.3l112.3-.3 25.2-91c13.8-50.1 25.4-92 25.8-93.3l.5-2.2 160.8.2 160.7.3 25.5 92c14.1 50.6 25.8 92.6 26 93.3.4 1 23.4 1.2 112.6 1l112-.3-146.6-479-146.7-479-144.2-.3c-114.8-.2-144.3 0-144.6 1m199.3 390c29.8 109.6 54.2 199.6 54.2 200 0 .5-49.5.8-110.1.8-91.3 0-110-.2-109.6-1.3.3-.8 25.2-90.8 55.3-200 30.1-109.3 55.1-198.7 55.4-198.7s25 89.7 54.8 199.2M1062 304.6c-17.4.9-38.6 2.7-48.5 4-129.8 17.5-205.8 85.9-226.5 204.1-5.2 29.7-5.3 30.9-5.7 142.8-.5 110.1-.1 126.7 3.8 154.4 7.1 50.8 25 95.7 51.4 128.6 37.3 46.6 94.8 76.9 168.9 88.9 34.1 5.6 57.4 7 102.1 6.3 53.4-.9 98.5-5.9 153.5-17.2 39.2-8 97-24.6 97-27.7 0-1.2-28.9-150.6-29.6-152.9-.4-1.5-.9-1.5-5.2-.3-40.8 12.4-110.5 23.9-166.9 27.5-24.8 1.6-77.4.7-91.1-1.5-41-6.7-60.5-20.4-70.3-49.7-4.1-12.4-5.9-25.2-6.6-49.2l-.6-20.7H1366v-73.8c0-76.6-.9-109.4-3.6-133.5-12.2-110.7-60.5-178.8-149.6-210.8-22.5-8-53.6-14.6-82.8-17.3-15.5-1.5-55.7-2.7-68-2m39 163.4c21.7 2.7 35.6 8.6 47.1 20 16.3 16.3 21.2 33.3 21.3 74.5l.1 24h-181l-.3-15c-.8-41.2 6.3-64.7 24.6-81.7 11.3-10.4 25.3-17.1 42.3-20.2 14.6-2.7 32-3.3 45.9-1.6m1296.5-162.9c-39.3 2.7-85 14.6-130.7 34-17.8 7.6-44.4 21-58.1 29.2l-10.8 6.5-1.2-3.1c-.7-1.8-5.6-14.3-10.8-27.9l-9.4-24.8H2014v506.5c0 464.7.1 506.5 1.6 506.5.9 0 47-6.3 102.4-14 55.5-7.7 101.3-14 101.9-14 .8 0 1.1-42.4 1.1-144.8v-144.9l8.3 1.8c35.6 7.9 82 14 128.5 17 24.2 1.5 72.6.7 88.7-1.5 67.9-9.5 115.3-36.5 146.9-83.6 7.6-11.3 19.5-35.7 24.4-50 7.6-22.2 12.5-46.1 15.9-76.5 1.3-11.6 1.6-34.9 2-136 .5-123.9 0-151.4-3.2-177.5-10-82.2-41.6-139.7-94.7-172.6-28.8-17.9-62.8-27.7-105.3-30.4-16.5-1.1-19-1.1-35 .1M2364 485c19.9 1.9 32.4 6.8 43.1 16.8s16.6 22 19.4 39.5c2.2 13.7 2.2 242.2 0 255-4.8 27.3-17.9 44.5-40.4 52.7-12 4.4-21.5 5.4-46.1 4.7-22.5-.6-44.9-2.9-71-7.1-15.7-2.6-37.1-6.8-44.2-8.6l-3.8-1.1V539.2l4.3-3.7c6.6-5.7 26.2-18.6 37.8-24.8 30.2-16.2 57.7-24.4 88.4-26.6 1.1 0 6.7.4 12.5.9m1714.5-180.4c-59 3.5-134.4 28.3-193.7 63.7l-10.7 6.4-4.9-12.6c-2.7-6.9-7.5-19.5-10.7-27.9l-5.9-15.2H3690v506.5c0 468 .1 506.5 1.6 506.5.9 0 47.1-6.3 102.6-14s101.3-14 101.8-14c.6 0 1-53.3 1-144.8v-144.8l18.3 3.7c30.2 6.1 56.5 9.7 97.7 13.6 23.5 2.2 88.9 2.5 105 .5 31-3.9 59.5-12 82.9-23.9 19.2-9.6 30.9-18.1 46.1-33.3 29.2-29.2 47-65.1 57-115 7.4-37 8-51.3 8-191 0-138.9-.6-152.8-8-190-20.3-102-80.6-160.8-177-172.4-13-1.5-38.1-2.6-48.5-2M4041 485c12.6 1.2 19.8 3.2 29.7 8 15.3 7.5 25.2 20.8 30.6 41l2.2 8.5v253l-2.2 8.4c-6.2 23.1-20.2 39-40.1 45.5-13.1 4.3-19.7 4.9-44.2 4.3-22.2-.6-36.1-1.9-63.5-5.8-13.7-2-45.3-8-52.7-10l-3.8-1V539l5.8-4.5c32.7-26.1 78.8-46 114.5-49.4 11.6-1.2 13.1-1.2 23.7-.1M1879 307.6c-45.1 11.8-115.7 42.6-162.5 70.9-7.8 4.8-12.2 6.9-12.6 6.2-.3-.7-3.7-15.7-7.4-33.5l-6.8-32.2H1517v701h207V592.2l10.8-7c35.7-23.1 97.7-53.7 158.4-78.2 8.4-3.4 15.6-6.5 16-6.8.4-.4-3.1-43.7-7.8-96.2-4.6-52.5-8.4-96.3-8.4-97.3 0-2.3-2.4-2.2-14 .9m2584 361.9V1020h207V319h-207z"/></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB