fix(Pipedrive Node): Map user_id to owner_id for v2 deal and activity requests (#30890)

This commit is contained in:
Alexander Gekov 2026-05-28 18:18:48 +03:00 committed by GitHub
parent 1c3901576b
commit e9631b336f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 235 additions and 7 deletions

View File

@ -15,6 +15,7 @@ describe('Test PipedriveV2, activity => create', () => {
deal_id: 8,
person_id: 10,
org_id: 7,
owner_id: 25455458,
due_date: '2026-04-01',
})
.reply(200, {

View File

@ -21,6 +21,7 @@
"deal_id": 8,
"person_id": 10,
"org_id": 7,
"user_id": 25455458,
"due_date": "2026-04-01T00:00:00.000Z"
}
},

View File

@ -9,7 +9,7 @@ describe('Test PipedriveV2, activity => getAll', () => {
nock('https://api.pipedrive.com/api/v2')
.get('/activities')
.query({ limit: 2 })
.query({ limit: 2, owner_id: 25455458 })
.reply(200, {
success: true,
data: [

View File

@ -16,7 +16,9 @@
"operation": "getAll",
"returnAll": false,
"limit": 2,
"filters": {}
"filters": {
"user_id": 25455458
}
},
"id": "e5e5e5e5-f6f6-a7a7-b8b8-c9c9c9c9c9c9",
"name": "Pipedrive",

View File

@ -11,6 +11,7 @@ describe('Test PipedriveV2, activity => update', () => {
.patch('/activities/10', {
subject: 'Updated call',
done: true,
owner_id: 25455458,
due_date: '2026-04-02',
})
.reply(200, {

View File

@ -18,6 +18,7 @@
"updateFields": {
"subject": "Updated call",
"done": true,
"user_id": 25455458,
"due_date": "2026-04-02T00:00:00.000Z"
}
},

View File

@ -15,6 +15,7 @@ describe('Test PipedriveV2, deal => create', () => {
value: 5000,
currency: 'USD',
status: 'open',
owner_id: 25455458,
})
.reply(200, {
success: true,

View File

@ -21,7 +21,8 @@
"person_id": 10,
"value": 5000,
"currency": "USD",
"status": "open"
"status": "open",
"user_id": 25455458
}
},
"id": "bb222222-cc33-dd44-ee55-ff6666666666",

View File

@ -9,7 +9,7 @@ describe('Test PipedriveV2, deal => getAll', () => {
nock('https://api.pipedrive.com/api/v2')
.get('/deals')
.query({ limit: 2 })
.query({ limit: 2, owner_id: 25455458 })
.reply(200, {
success: true,
data: [

View File

@ -16,7 +16,9 @@
"operation": "getAll",
"returnAll": false,
"limit": 2,
"filters": {}
"filters": {
"user_id": 25455458
}
},
"id": "b2b2b2b2-1111-2222-3333-444444444444",
"name": "Pipedrive",

View File

@ -11,6 +11,7 @@ describe('Test PipedriveV2, deal => update', () => {
.patch('/deals/10', {
title: 'Updated Deal',
value: 7500,
owner_id: 25455458,
expected_close_date: '2026-04-13',
})
.reply(200, {

View File

@ -18,6 +18,7 @@
"updateFields": {
"title": "Updated Deal",
"value": 7500,
"user_id": 25455458,
"expected_close_date": "2026-04-13T00:00:00.000Z"
}
},

View File

@ -0,0 +1,184 @@
import type { IDataObject, IExecuteFunctions } from 'n8n-workflow';
import { execute as activityCreateExecute } from '../../v2/actions/activity/create.operation';
import { execute as activityGetAllExecute } from '../../v2/actions/activity/getAll.operation';
import { execute as activityUpdateExecute } from '../../v2/actions/activity/update.operation';
import { execute as dealCreateExecute } from '../../v2/actions/deal/create.operation';
import { execute as dealGetAllExecute } from '../../v2/actions/deal/getAll.operation';
import { execute as dealUpdateExecute } from '../../v2/actions/deal/update.operation';
import {
pipedriveApiRequest,
pipedriveApiRequestAllItemsCursor,
pipedriveGetCustomProperties,
} from '../../v2/transport';
jest.mock('../../v2/transport', () => ({
pipedriveApiRequest: { call: jest.fn() },
pipedriveApiRequestAllItemsCursor: { call: jest.fn() },
pipedriveApiRequestAllItemsOffset: { call: jest.fn() },
pipedriveGetCustomProperties: { call: jest.fn() },
}));
const mockApiRequest = pipedriveApiRequest as unknown as { call: jest.Mock };
const mockApiRequestAllItemsCursor = pipedriveApiRequestAllItemsCursor as unknown as {
call: jest.Mock;
};
const mockGetCustomProperties = pipedriveGetCustomProperties as unknown as { call: jest.Mock };
function buildContext(params: Record<string, unknown>): IExecuteFunctions {
return {
getInputData: jest.fn(() => [{ json: {} }]),
getNodeParameter: jest.fn((name: string, _i?: number, defaultValue?: unknown) => {
if (Object.prototype.hasOwnProperty.call(params, name)) return params[name];
return defaultValue;
}),
continueOnFail: jest.fn(() => false),
helpers: {
returnJsonArray: jest.fn((data: unknown) => (Array.isArray(data) ? data : [data])),
constructExecutionMetaData: jest.fn((items: unknown) => items),
},
getNode: jest.fn(() => ({})),
} as unknown as IExecuteFunctions;
}
describe('Pipedrive v2 maps user_id parameter to owner_id on outgoing requests', () => {
beforeEach(() => {
mockApiRequest.call.mockReset().mockResolvedValue({ data: {}, additionalData: {} });
mockApiRequestAllItemsCursor.call.mockReset().mockResolvedValue({ data: [] });
mockGetCustomProperties.call.mockReset().mockResolvedValue({});
});
it('deal/create sends owner_id in body', async () => {
const ctx = buildContext({
rawCustomFieldKeys: true,
title: 'Test Deal',
associateWith: 'organization',
org_id: 7,
additionalFields: { user_id: 25455458 },
});
await dealCreateExecute.call(ctx);
const [, method, endpoint, body] = mockApiRequest.call.mock.calls[0] as [
unknown,
string,
string,
IDataObject,
];
expect(method).toBe('POST');
expect(endpoint).toBe('/deals');
expect(body.owner_id).toBe(25455458);
expect(body).not.toHaveProperty('user_id');
});
it('deal/update sends owner_id in body', async () => {
const ctx = buildContext({
rawCustomFieldKeys: true,
dealId: 10,
updateFields: { user_id: 25455458 },
});
await dealUpdateExecute.call(ctx);
const [, method, endpoint, body] = mockApiRequest.call.mock.calls[0] as [
unknown,
string,
string,
IDataObject,
];
expect(method).toBe('PATCH');
expect(endpoint).toBe('/deals/10');
expect(body.owner_id).toBe(25455458);
expect(body).not.toHaveProperty('user_id');
});
it('deal/getAll sends owner_id as query filter', async () => {
const ctx = buildContext({
rawCustomFieldOutput: true,
returnAll: false,
limit: 50,
filters: { user_id: 25455458 },
});
await dealGetAllExecute.call(ctx);
const [, method, endpoint, , qs] = mockApiRequest.call.mock.calls[0] as [
unknown,
string,
string,
IDataObject,
IDataObject,
];
expect(method).toBe('GET');
expect(endpoint).toBe('/deals');
expect(qs.owner_id).toBe(25455458);
expect(qs).not.toHaveProperty('user_id');
});
it('activity/create sends owner_id in body', async () => {
const ctx = buildContext({
rawCustomFieldKeys: true,
subject: 'Call client',
done: false,
type: 'call',
additionalFields: { user_id: 25455458 },
});
await activityCreateExecute.call(ctx);
const [, method, endpoint, body] = mockApiRequest.call.mock.calls[0] as [
unknown,
string,
string,
IDataObject,
];
expect(method).toBe('POST');
expect(endpoint).toBe('/activities');
expect(body.owner_id).toBe(25455458);
expect(body).not.toHaveProperty('user_id');
});
it('activity/update sends owner_id in body', async () => {
const ctx = buildContext({
rawCustomFieldKeys: true,
activityId: 10,
updateFields: { user_id: 25455458 },
});
await activityUpdateExecute.call(ctx);
const [, method, endpoint, body] = mockApiRequest.call.mock.calls[0] as [
unknown,
string,
string,
IDataObject,
];
expect(method).toBe('PATCH');
expect(endpoint).toBe('/activities/10');
expect(body.owner_id).toBe(25455458);
expect(body).not.toHaveProperty('user_id');
});
it('activity/getAll sends owner_id as query filter', async () => {
const ctx = buildContext({
rawCustomFieldOutput: true,
returnAll: false,
limit: 50,
filters: { user_id: 25455458 },
});
await activityGetAllExecute.call(ctx);
const [, method, endpoint, , qs] = mockApiRequest.call.mock.calls[0] as [
unknown,
string,
string,
IDataObject,
IDataObject,
];
expect(method).toBe('GET');
expect(endpoint).toBe('/activities');
expect(qs.owner_id).toBe(25455458);
expect(qs).not.toHaveProperty('user_id');
});
});

View File

@ -137,6 +137,13 @@ export async function execute(this: IExecuteFunctions): Promise<INodeExecutionDa
const additionalFields = this.getNodeParameter('additionalFields', i);
addFieldsToBody(body, additionalFields);
// Pipedrive v2 activities API renamed `user_id` to `owner_id`; remap so the existing
// `user_id` parameter (kept for backward compatibility with saved workflows) is accepted.
if (body.user_id !== undefined) {
body.owner_id = body.user_id;
delete body.user_id;
}
if (body.due_date) {
body.due_date = toDateOnly(body.due_date as string);
}

View File

@ -152,8 +152,10 @@ export async function execute(this: IExecuteFunctions): Promise<INodeExecutionDa
qs.type = (filters.type as string[]).join(',');
}
// Pipedrive v2 activities filter param was renamed from `user_id` to `owner_id`;
// keep the UI field key as `user_id` for backward compatibility with saved workflows.
if (filters.user_id) {
qs.user_id = filters.user_id;
qs.owner_id = filters.user_id;
}
if (filters.done !== undefined) {

View File

@ -155,6 +155,13 @@ export async function execute(this: IExecuteFunctions): Promise<INodeExecutionDa
const updateFields = this.getNodeParameter('updateFields', i);
addFieldsToBody(body, updateFields);
// Pipedrive v2 activities API renamed `user_id` to `owner_id`; remap so the existing
// `user_id` parameter (kept for backward compatibility with saved workflows) is accepted.
if (body.user_id !== undefined) {
body.owner_id = body.user_id;
delete body.user_id;
}
// Coerce done to boolean for v2 API
if (body.done !== undefined) {
body.done = coerceToBoolean(body.done);

View File

@ -246,6 +246,13 @@ export async function execute(this: IExecuteFunctions): Promise<INodeExecutionDa
const additionalFields = this.getNodeParameter('additionalFields', i);
addFieldsToBody(body, additionalFields);
// Pipedrive v2 deals API renamed `user_id` to `owner_id`; remap so the existing
// `user_id` parameter (kept for backward compatibility with saved workflows) is accepted.
if (body.user_id !== undefined) {
body.owner_id = body.user_id;
delete body.user_id;
}
if (body.expected_close_date) {
body.expected_close_date = toDateOnly(body.expected_close_date as string);
}

View File

@ -154,8 +154,10 @@ export async function execute(this: IExecuteFunctions): Promise<INodeExecutionDa
qs.status = filters.status;
}
// Pipedrive v2 deals filter param was renamed from `user_id` to `owner_id`;
// keep the UI field key as `user_id` for backward compatibility with saved workflows.
if (filters.user_id) {
qs.user_id = filters.user_id;
qs.owner_id = filters.user_id;
}
let responseData;

View File

@ -194,6 +194,13 @@ export async function execute(this: IExecuteFunctions): Promise<INodeExecutionDa
const updateFields = this.getNodeParameter('updateFields', i);
addFieldsToBody(body, updateFields);
// Pipedrive v2 deals API renamed `user_id` to `owner_id`; remap so the existing
// `user_id` parameter (kept for backward compatibility with saved workflows) is accepted.
if (body.user_id !== undefined) {
body.owner_id = body.user_id;
delete body.user_id;
}
// Clear label when set to 'null' string
if (body.label === 'null') {
body.label = null;