feat(Airtable Node): Overhaul (#6200)

This commit is contained in:
Michael Kret
2023-07-17 19:42:30 +03:00
committed by GitHub
parent fc8ed55c0d
commit b69d20c12e
42 changed files with 3989 additions and 871 deletions

View File

@@ -0,0 +1,116 @@
import nock from 'nock';
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', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
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,
},
},
]);
});
});

View File

@@ -0,0 +1,48 @@
import nock from 'nock';
import * as getSchema from '../../../../v2/actions/base/getSchema.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, base => getSchema', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('should return all bases', async () => {
const nodeParameters = {
resource: 'base',
operation: 'getSchema',
base: {
value: 'appYobase',
},
};
const items = [
{
json: {},
},
];
await getSchema.execute.call(createMockExecuteFunction(nodeParameters), items);
expect(transport.apiRequest).toBeCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith('GET', 'meta/bases/appYobase/tables');
});
});

View File

@@ -0,0 +1,35 @@
import type { IDataObject, IExecuteFunctions, IGetNodeParameterOptions, INode } from 'n8n-workflow';
import { get } from 'lodash';
import { constructExecutionMetaData } from 'n8n-core';
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 = {
getNodeParameter(
parameterName: string,
_itemIndex: number,
fallbackValue?: IDataObject | undefined,
options?: IGetNodeParameterOptions | undefined,
) {
const parameter = options?.extractValue ? `${parameterName}.value` : parameterName;
return get(nodeParameters, parameter, fallbackValue);
},
getNode() {
return node;
},
helpers: { constructExecutionMetaData },
continueOnFail: () => false,
} as unknown as IExecuteFunctions;
return fakeExecuteFunction;
};

View File

@@ -0,0 +1,171 @@
import nock from 'nock';
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', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
afterEach(() => {
jest.restoreAllMocks();
});
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,
});
});
});

View File

@@ -0,0 +1,54 @@
import nock from 'nock';
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', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
afterEach(() => {
jest.restoreAllMocks();
});
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');
});
});

View File

@@ -0,0 +1,76 @@
import nock from 'nock';
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', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
afterEach(() => {
jest.restoreAllMocks();
});
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,
},
},
]);
});
});

View File

@@ -0,0 +1,160 @@
import nock from 'nock';
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', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
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,
},
});
});
});

View File

@@ -0,0 +1,120 @@
import nock from 'nock';
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', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
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' }],
);
});
});