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,112 @@
import * as getMany from '../../../../v2/actions/base/getMany.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction } from '../helpers';
const bases = [
{
id: 'appXXX',
name: 'base 1',
permissionLevel: 'create',
},
{
id: 'appYYY',
name: 'base 2',
permissionLevel: 'edit',
},
{
id: 'appZZZ',
name: 'base 3',
permissionLevel: 'create',
},
];
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
apiRequest: jest.fn(async function () {
return { bases };
}),
};
});
describe('Test AirtableV2, base => getMany', () => {
it('should return all bases', async () => {
const nodeParameters = {
resource: 'base',
returnAll: true,
options: {},
};
const response = await getMany.execute.call(createMockExecuteFunction(nodeParameters));
expect(transport.apiRequest).toHaveBeenCalledWith('GET', 'meta/bases');
expect(response).toEqual([
{
json: {
id: 'appXXX',
name: 'base 1',
permissionLevel: 'create',
},
pairedItem: [
{
item: 0,
},
],
},
{
json: {
id: 'appYYY',
name: 'base 2',
permissionLevel: 'edit',
},
pairedItem: [
{
item: 0,
},
],
},
{
json: {
id: 'appZZZ',
name: 'base 3',
permissionLevel: 'create',
},
pairedItem: [
{
item: 0,
},
],
},
]);
});
it('should return one base with edit permission', async () => {
const nodeParameters = {
resource: 'base',
returnAll: false,
limit: 2,
options: { permissionLevel: ['edit'] },
};
const response = await getMany.execute.call(createMockExecuteFunction(nodeParameters));
expect(transport.apiRequest).toHaveBeenCalledWith('GET', 'meta/bases');
expect(response).toEqual([
{
json: {
id: 'appYYY',
name: 'base 2',
permissionLevel: 'edit',
},
pairedItem: [
{
item: 0,
},
],
},
]);
});
});
@@ -0,0 +1,55 @@
import { mockDeep } from 'jest-mock-extended';
import get from 'lodash/get';
import type { IExecuteFunctions } from 'n8n-workflow';
import * as getSchema from '../../../../v2/actions/base/getSchema.operation';
import * as transport from '../../../../v2/transport';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
apiRequest: jest.fn(async function () {
return { tables: [] };
}),
};
});
describe('Test AirtableV2, base => getSchema', () => {
it('should return all bases', async () => {
const nodeParameters = {
resource: 'base',
operation: 'getSchema',
base: {
value: '={{$json.id}}',
},
};
const items = [
{
json: { id: 'appYobase1' },
},
{
json: { id: 'appYobase2' },
},
];
await getSchema.execute.call(
mockDeep<IExecuteFunctions>({
getInputData: jest.fn(() => items),
getNodeParameter: jest.fn((param: string, itemIndex: number) => {
if (param === 'base') {
return items[itemIndex].json.id;
}
return get(nodeParameters, param);
}),
}),
items,
);
expect(transport.apiRequest).toBeCalledTimes(2);
expect(transport.apiRequest).toHaveBeenCalledWith('GET', 'meta/bases/appYobase1/tables');
expect(transport.apiRequest).toHaveBeenCalledWith('GET', 'meta/bases/appYobase2/tables');
});
});
@@ -0,0 +1,39 @@
import get from 'lodash/get';
import { constructExecutionMetaData } from 'n8n-core';
import type { IDataObject, IExecuteFunctions, IGetNodeParameterOptions, INode } from 'n8n-workflow';
export const node: INode = {
id: '11',
name: 'Airtable node',
typeVersion: 2,
type: 'n8n-nodes-base.airtable',
position: [42, 42],
parameters: {
operation: 'create',
},
};
export const createMockExecuteFunction = (nodeParameters: IDataObject) => {
const fakeExecuteFunction = {
getInputData: jest.fn(() => {
return [{ json: {} }];
}),
getNodeParameter: jest.fn(
(
parameterName: string,
_itemIndex: number,
fallbackValue?: IDataObject,
options?: IGetNodeParameterOptions,
) => {
const parameter = options?.extractValue ? `${parameterName}.value` : parameterName;
return get(nodeParameters, parameter, fallbackValue);
},
),
getNode: jest.fn(() => {
return node;
}),
helpers: { constructExecutionMetaData: jest.fn(constructExecutionMetaData) },
continueOnFail: jest.fn(() => false),
} as unknown as IExecuteFunctions;
return fakeExecuteFunction;
};
@@ -0,0 +1,208 @@
import * as create from '../../../../v2/actions/record/create.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
apiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('Test AirtableV2, create operation', () => {
beforeEach(() => jest.clearAllMocks());
it('should create a record, autoMapInputData', async () => {
const nodeParameters = {
operation: 'create',
columns: {
mappingMode: 'autoMapInputData',
value: {
bar: 'bar 1',
foo: 'foo 1',
spam: 'eggs',
},
matchingColumns: [],
schema: [
{
id: 'foo',
displayName: 'foo',
required: false,
defaultMatch: false,
display: true,
type: 'string',
},
{
id: 'bar',
displayName: 'bar',
required: false,
defaultMatch: false,
display: true,
type: 'string',
},
{
id: 'spam',
displayName: 'spam',
required: false,
defaultMatch: false,
display: true,
type: 'string',
},
],
},
options: {
typecast: true,
ignoreFields: 'spam',
},
};
const items = [
{
json: {
foo: 'foo 1',
spam: 'eggs',
bar: 'bar 1',
},
},
{
json: {
foo: 'foo 2',
spam: 'eggs',
bar: 'bar 2',
},
},
];
await create.execute.call(
createMockExecuteFunction(nodeParameters),
items,
'appYoLbase',
'tblltable',
);
expect(transport.apiRequest).toHaveBeenCalledTimes(2);
expect(transport.apiRequest).toHaveBeenCalledWith('POST', 'appYoLbase/tblltable', {
fields: {
foo: 'foo 1',
bar: 'bar 1',
},
typecast: true,
});
expect(transport.apiRequest).toHaveBeenCalledWith('POST', 'appYoLbase/tblltable', {
fields: {
foo: 'foo 2',
bar: 'bar 2',
},
typecast: true,
});
});
it('should create a record, defineBelow', async () => {
const nodeParameters = {
operation: 'create',
columns: {
mappingMode: 'defineBelow',
value: {
bar: 'bar 1',
foo: 'foo 1',
},
matchingColumns: [],
schema: [
{
id: 'foo',
displayName: 'foo',
required: false,
defaultMatch: false,
display: true,
type: 'string',
},
{
id: 'bar',
displayName: 'bar',
required: false,
defaultMatch: false,
display: true,
type: 'string',
},
],
},
options: {},
};
const items = [
{
json: {},
},
];
await create.execute.call(
createMockExecuteFunction(nodeParameters),
items,
'appYoLbase',
'tblltable',
);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith('POST', 'appYoLbase/tblltable', {
fields: {
foo: 'foo 1',
bar: 'bar 1',
},
typecast: false,
});
});
it('should skip validation if typecast option is true', async () => {
const nodeParameters = {
operation: 'create',
columns: {
mappingMode: 'defineBelow',
value: {
bar: 'bar 1',
foo: 'foo 1',
},
matchingColumns: [],
schema: [
{
id: 'foo',
displayName: 'foo',
required: false,
defaultMatch: false,
display: true,
type: 'string',
},
{
id: 'bar',
displayName: 'bar',
required: false,
defaultMatch: false,
display: true,
type: 'string',
},
],
},
options: {
typecast: true,
},
};
const mockExecuteFunctions = createMockExecuteFunction(nodeParameters);
await create.execute.call(mockExecuteFunctions, [{ json: {} }], 'appYoLbase', 'tblltable');
expect(mockExecuteFunctions.getNodeParameter).toHaveBeenCalledWith('columns.value', 0, [], {
skipValidation: true,
});
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith('POST', 'appYoLbase/tblltable', {
fields: {
foo: 'foo 1',
bar: 'bar 1',
},
typecast: true,
});
});
});
@@ -0,0 +1,38 @@
import * as deleteRecord from '../../../../v2/actions/record/deleteRecord.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
apiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('Test AirtableV2, deleteRecord operation', () => {
it('should delete a record', async () => {
const nodeParameters = {
operation: 'deleteRecord',
id: 'recXXX',
};
const items = [
{
json: {},
},
];
await deleteRecord.execute.call(
createMockExecuteFunction(nodeParameters),
items,
'appYoLbase',
'tblltable',
);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith('DELETE', 'appYoLbase/tblltable/recXXX');
});
});
@@ -0,0 +1,60 @@
import * as get from '../../../../v2/actions/record/get.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
apiRequest: jest.fn(async function (method: string) {
if (method === 'GET') {
return {
id: 'recXXX',
fields: {
foo: 'foo 1',
bar: 'bar 1',
},
};
}
}),
};
});
describe('Test AirtableV2, create operation', () => {
it('should create a record, autoMapInputData', async () => {
const nodeParameters = {
operation: 'get',
id: 'recXXX',
options: {},
};
const items = [
{
json: {},
},
];
const responce = await get.execute.call(
createMockExecuteFunction(nodeParameters),
items,
'appYoLbase',
'tblltable',
);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith('GET', 'appYoLbase/tblltable/recXXX');
expect(responce).toEqual([
{
json: {
id: 'recXXX',
foo: 'foo 1',
bar: 'bar 1',
},
pairedItem: {
item: 0,
},
},
]);
});
});
@@ -0,0 +1,152 @@
import * as search from '../../../../v2/actions/record/search.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
apiRequest: jest.fn(async function (method: string) {
if (method === 'GET') {
return {
records: [
{
id: 'recYYY',
fields: {
foo: 'foo 2',
bar: 'bar 2',
},
},
],
};
}
}),
apiRequestAllItems: jest.fn(async function (method: string) {
if (method === 'GET') {
return {
records: [
{
id: 'recYYY',
fields: {
foo: 'foo 2',
bar: 'bar 2',
},
},
{
id: 'recXXX',
fields: {
foo: 'foo 1',
bar: 'bar 1',
},
},
],
};
}
}),
};
});
describe('Test AirtableV2, search operation', () => {
it('should return all records', async () => {
const nodeParameters = {
operation: 'search',
filterByFormula: 'foo',
returnAll: true,
options: {
fields: ['foo', 'bar'],
view: {
value: 'viwView',
mode: 'list',
},
},
sort: {
property: [
{
field: 'bar',
direction: 'desc',
},
],
},
};
const items = [
{
json: {},
},
];
const result = await search.execute.call(
createMockExecuteFunction(nodeParameters),
items,
'appYoLbase',
'tblltable',
);
expect(transport.apiRequestAllItems).toHaveBeenCalledTimes(1);
expect(transport.apiRequestAllItems).toHaveBeenCalledWith(
'GET',
'appYoLbase/tblltable',
{},
{
fields: ['foo', 'bar'],
filterByFormula: 'foo',
sort: [{ direction: 'desc', field: 'bar' }],
view: 'viwView',
},
);
expect(result).toHaveLength(2);
expect(result[0]).toEqual({
json: { id: 'recYYY', foo: 'foo 2', bar: 'bar 2' },
pairedItem: [
{
item: 0,
},
],
});
});
it('should return all records', async () => {
const nodeParameters = {
operation: 'search',
filterByFormula: 'foo',
returnAll: false,
limit: 1,
options: {
fields: ['foo', 'bar'],
},
sort: {},
};
const items = [
{
json: {},
},
];
const result = await search.execute.call(
createMockExecuteFunction(nodeParameters),
items,
'appYoLbase',
'tblltable',
);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'GET',
'appYoLbase/tblltable',
{},
{ fields: ['foo', 'bar'], filterByFormula: 'foo', maxRecords: 1 },
);
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
json: { id: 'recYYY', foo: 'foo 2', bar: 'bar 2' },
pairedItem: [
{
item: 0,
},
],
});
});
});
@@ -0,0 +1,150 @@
import type { MockProxy } from 'jest-mock-extended';
import { mock } from 'jest-mock-extended';
import type { IExecuteFunctions } from 'n8n-workflow';
import * as update from '../../../../v2/actions/record/update.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
apiRequest: jest.fn(async function () {
return {};
}),
batchUpdate: jest.fn(async function () {
return {};
}),
apiRequestAllItems: jest.fn(async function (method: string) {
if (method === 'GET') {
return {
records: [
{
id: 'recYYY',
fields: {
foo: 'foo 2',
bar: 'bar 2',
},
},
{
id: 'recXXX',
fields: {
foo: 'foo 1',
bar: 'bar 1',
},
},
],
};
}
}),
};
});
describe('Test AirtableV2, update operation', () => {
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
afterEach(() => {
jest.clearAllMocks();
});
it('should skip validation if typecast option is true', async () => {
mockExecuteFunctions = mock<IExecuteFunctions>();
mockExecuteFunctions.helpers.constructExecutionMetaData = jest.fn(() => []);
mockExecuteFunctions.getNodeParameter.mockImplementation((key: string) => {
if (key === 'columns.mappingMode') {
return 'defineBelow';
}
if (key === 'columns.matchingColumns') {
return ['id'];
}
if (key === 'options') {
return {
typecast: true,
};
}
if (key === 'columns.value') {
return {
id: 'recXXX',
field1: 'foo 1',
field2: 'bar 1',
};
}
return undefined;
});
await update.execute.call(mockExecuteFunctions, [{ json: {} }], 'base', 'table');
expect(mockExecuteFunctions.getNodeParameter).toHaveBeenCalledWith('columns.value', 0, [], {
skipValidation: true,
});
});
it('should update a record by id, autoMapInputData', async () => {
const nodeParameters = {
operation: 'update',
columns: {
mappingMode: 'autoMapInputData',
matchingColumns: ['id'],
},
options: {},
};
const items = [
{
json: {
id: 'recXXX',
foo: 'foo 1',
bar: 'bar 1',
},
},
];
await update.execute.call(
createMockExecuteFunction(nodeParameters),
items,
'appYoLbase',
'tblltable',
);
expect(transport.batchUpdate).toHaveBeenCalledWith(
'appYoLbase/tblltable',
{ typecast: false },
[{ fields: { bar: 'bar 1', foo: 'foo 1' }, id: 'recXXX' }],
);
});
it('should update a record by field name, autoMapInputData', async () => {
const nodeParameters = {
operation: 'update',
columns: {
mappingMode: 'autoMapInputData',
matchingColumns: ['foo'],
},
options: {},
};
const items = [
{
json: {
id: 'recXXX',
foo: 'foo 1',
bar: 'bar 1',
},
},
];
await update.execute.call(
createMockExecuteFunction(nodeParameters),
items,
'appYoLbase',
'tblltable',
);
expect(transport.batchUpdate).toHaveBeenCalledWith(
'appYoLbase/tblltable',
{ typecast: false },
[{ fields: { bar: 'bar 1', foo: 'foo 1', id: 'recXXX' }, id: 'recXXX' }],
);
});
});