mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-28 17:22:01 +08:00
fix(Airtable Node): Support matching array field values in update operation (#31517)
Co-authored-by: RomanDavydchuk <roman.davydchuk@n8n.io>
This commit is contained in:
@@ -215,4 +215,115 @@ describe('Test AirtableV2, update operation', () => {
|
||||
[{ fields: { bar: 'bar 1', foo: 'foo 1', id: 'recXXX' }, id: 'recXXX' }],
|
||||
);
|
||||
});
|
||||
|
||||
it('should update a record by lookup field with array value, autoMapInputData', async () => {
|
||||
vi.mocked(transport.apiRequestAllItems).mockResolvedValueOnce({
|
||||
records: [
|
||||
{
|
||||
id: 'recAAA',
|
||||
fields: {
|
||||
lookupField: ['lookup_val_1'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'recBBB',
|
||||
fields: {
|
||||
lookupField: ['lookup_val_2'],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const nodeParameters = {
|
||||
operation: 'update',
|
||||
columns: {
|
||||
mappingMode: 'autoMapInputData',
|
||||
matchingColumns: ['lookupField'],
|
||||
},
|
||||
options: {},
|
||||
};
|
||||
|
||||
const items = [
|
||||
{
|
||||
json: {
|
||||
lookupField: 'lookup_val_1',
|
||||
name: 'updated name',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await update.execute.call(
|
||||
createMockExecuteFunction(nodeParameters),
|
||||
items,
|
||||
'appYoLbase',
|
||||
'tblltable',
|
||||
);
|
||||
|
||||
expect(transport.batchUpdate).toHaveBeenCalledWith(
|
||||
'appYoLbase/tblltable',
|
||||
{ typecast: false },
|
||||
[{ fields: { lookupField: 'lookup_val_1', name: 'updated name' }, id: 'recAAA' }],
|
||||
);
|
||||
});
|
||||
|
||||
it('should update all matches by lookup field with array value, autoMapInputData', async () => {
|
||||
vi.mocked(transport.apiRequestAllItems).mockResolvedValueOnce({
|
||||
records: [
|
||||
{
|
||||
id: 'recAAA',
|
||||
fields: {
|
||||
lookupField: ['shared_val'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'recBBB',
|
||||
fields: {
|
||||
lookupField: ['other_val'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'recCCC',
|
||||
fields: {
|
||||
lookupField: ['shared_val'],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const nodeParameters = {
|
||||
operation: 'update',
|
||||
columns: {
|
||||
mappingMode: 'autoMapInputData',
|
||||
matchingColumns: ['lookupField'],
|
||||
},
|
||||
options: {
|
||||
updateAllMatches: true,
|
||||
},
|
||||
};
|
||||
|
||||
const items = [
|
||||
{
|
||||
json: {
|
||||
lookupField: 'shared_val',
|
||||
name: 'updated name',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await update.execute.call(
|
||||
createMockExecuteFunction(nodeParameters),
|
||||
items,
|
||||
'appYoLbase',
|
||||
'tblltable',
|
||||
);
|
||||
|
||||
expect(transport.batchUpdate).toHaveBeenCalledWith(
|
||||
'appYoLbase/tblltable',
|
||||
{ typecast: false },
|
||||
[
|
||||
{ fields: { lookupField: 'shared_val', name: 'updated name' }, id: 'recAAA' },
|
||||
{ fields: { lookupField: 'shared_val', name: 'updated name' }, id: 'recCCC' },
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { coerceArrayTypeFields, findMatches, removeIgnored } from '../../v2/helpers/utils';
|
||||
import {
|
||||
coerceArrayTypeFields,
|
||||
findMatches,
|
||||
removeIgnored,
|
||||
valuesMatch,
|
||||
} from '../../v2/helpers/utils';
|
||||
|
||||
const makeColumnsParam = (fields: Array<{ id: string; type: string }>) => ({
|
||||
mappingMode: 'defineBelow',
|
||||
@@ -179,4 +184,118 @@ describe('test AirtableV2, findMatches', () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should find match when record field value is an array and input is a scalar', () => {
|
||||
const data = [
|
||||
{
|
||||
fields: {
|
||||
lookupField: ['value1'],
|
||||
data: 'data 1',
|
||||
},
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
lookupField: ['value2'],
|
||||
data: 'data 2',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = findMatches(data, ['lookupField'], {
|
||||
lookupField: 'value1',
|
||||
data: 'updated',
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
fields: {
|
||||
lookupField: ['value1'],
|
||||
data: 'data 1',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should find all matches when record field values are arrays and input is a scalar', () => {
|
||||
const data = [
|
||||
{
|
||||
fields: {
|
||||
lookupField: ['shared'],
|
||||
data: 'data 1',
|
||||
},
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
lookupField: ['other'],
|
||||
data: 'data 2',
|
||||
},
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
lookupField: ['shared'],
|
||||
data: 'data 3',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = findMatches(
|
||||
data,
|
||||
['lookupField'],
|
||||
{
|
||||
lookupField: 'shared',
|
||||
data: 'updated',
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].fields.lookupField).toEqual(['shared']);
|
||||
expect(result[1].fields.lookupField).toEqual(['shared']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('test AirtableV2, valuesMatch', () => {
|
||||
it('should match identical scalar values', () => {
|
||||
expect(valuesMatch('foo', 'foo')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not match different scalar values', () => {
|
||||
expect(valuesMatch('foo', 'bar')).toBe(false);
|
||||
});
|
||||
|
||||
it('should match when record value is an array containing the input value', () => {
|
||||
expect(valuesMatch(['foo'], 'foo')).toBe(true);
|
||||
});
|
||||
|
||||
it('should match when record value is a multi-element array containing the input value', () => {
|
||||
expect(valuesMatch(['foo', 'bar'], 'foo')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not match when record value is an array not containing the input value', () => {
|
||||
expect(valuesMatch(['foo'], 'bar')).toBe(false);
|
||||
});
|
||||
|
||||
it('should match identical arrays', () => {
|
||||
expect(valuesMatch(['foo', 'bar'], ['foo', 'bar'])).toBe(true);
|
||||
});
|
||||
|
||||
it('should not match arrays with different lengths', () => {
|
||||
expect(valuesMatch(['foo'], ['foo', 'bar'])).toBe(false);
|
||||
});
|
||||
|
||||
it('should not match arrays with different elements', () => {
|
||||
expect(valuesMatch(['foo', 'baz'], ['foo', 'bar'])).toBe(false);
|
||||
});
|
||||
|
||||
it('should match identical numbers', () => {
|
||||
expect(valuesMatch(42, 42)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not match undefined values to a string', () => {
|
||||
expect(valuesMatch(undefined, 'foo')).toBe(false);
|
||||
});
|
||||
|
||||
it('should match when both are undefined', () => {
|
||||
expect(valuesMatch(undefined, undefined)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,6 +32,27 @@ export function removeIgnored(data: IDataObject, ignore: string | string[]) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare a value from the Airtable API response with a value from the user input.
|
||||
* Airtable returns array values for Lookup and Linked Record fields (e.g. ["value"]),
|
||||
* while user input typically provides scalar values (e.g. "value"). This helper
|
||||
* handles that mismatch so that matching by such fields works correctly.
|
||||
*/
|
||||
export function valuesMatch(recordValue: unknown, inputValue: unknown): boolean {
|
||||
if (recordValue === inputValue) return true;
|
||||
|
||||
if (Array.isArray(recordValue) && !Array.isArray(inputValue)) {
|
||||
return recordValue.includes(inputValue);
|
||||
}
|
||||
|
||||
if (Array.isArray(recordValue) && Array.isArray(inputValue)) {
|
||||
if (recordValue.length !== inputValue.length) return false;
|
||||
return recordValue.every((v, i) => v === inputValue[i]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function findMatches(
|
||||
data: UpdateRecord[],
|
||||
keys: string[],
|
||||
@@ -41,7 +62,7 @@ export function findMatches(
|
||||
if (updateAll) {
|
||||
const matches = data.filter((record) => {
|
||||
for (const key of keys) {
|
||||
if (record.fields[key] !== fields[key]) {
|
||||
if (!valuesMatch(record.fields[key], fields[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -56,7 +77,7 @@ export function findMatches(
|
||||
} else {
|
||||
const match = data.find((record) => {
|
||||
for (const key of keys) {
|
||||
if (record.fields[key] !== fields[key]) {
|
||||
if (!valuesMatch(record.fields[key], fields[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user