mirror of
https://github.com/n8n-io/n8n
synced 2026-04-21 15:47:20 +00:00
fix(HubSpot Trigger Node): Add missing property selectors (#28595)
This commit is contained in:
parent
5b376cb12d
commit
d179f667c0
4 changed files with 150 additions and 42 deletions
|
|
@ -7,6 +7,7 @@ const scopes = [
|
|||
'crm.schemas.companies.read',
|
||||
'crm.objects.deals.read',
|
||||
'crm.schemas.deals.read',
|
||||
'conversations.read',
|
||||
'tickets',
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,32 @@ import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
|
|||
|
||||
import { hubspotApiRequest, propertyEvents } from './V1/GenericFunctions';
|
||||
|
||||
export async function getEntityProperties(
|
||||
this: ILoadOptionsFunctions,
|
||||
endpoint: string,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const properties = await hubspotApiRequest.call(this, 'GET', endpoint, {});
|
||||
|
||||
if (!Array.isArray(properties)) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`HubSpot returned an unexpected response while loading properties from "${endpoint}". Expected an array of properties.`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const property of properties) {
|
||||
if (typeof property?.label === 'string' && typeof property?.name === 'string') {
|
||||
returnData.push({
|
||||
name: property.label,
|
||||
value: property.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export class HubspotTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'HubSpot Trigger',
|
||||
|
|
@ -224,6 +250,52 @@ export class HubspotTrigger implements INodeType {
|
|||
default: '',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Property Name or ID',
|
||||
name: 'property',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['ticket.propertyChange'],
|
||||
loadOptionsMethod: 'getTicketProperties',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
name: ['ticket.propertyChange'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Property Name or ID',
|
||||
name: 'property',
|
||||
type: 'options',
|
||||
description:
|
||||
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
|
||||
options: [
|
||||
{
|
||||
name: 'Assigned To',
|
||||
value: 'assignedTo',
|
||||
},
|
||||
{
|
||||
name: 'Is Archived',
|
||||
value: 'isArchived',
|
||||
},
|
||||
{
|
||||
name: 'Status',
|
||||
value: 'status',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
name: ['conversation.propertyChange'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
@ -251,53 +323,17 @@ export class HubspotTrigger implements INodeType {
|
|||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
// Get all the available contacts to display them to user so that they can
|
||||
// select them easily
|
||||
async getContactProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const endpoint = '/properties/v2/contacts/properties';
|
||||
const properties = await hubspotApiRequest.call(this, 'GET', endpoint, {});
|
||||
for (const property of properties) {
|
||||
const propertyName = property.label;
|
||||
const propertyId = property.name;
|
||||
returnData.push({
|
||||
name: propertyName,
|
||||
value: propertyId,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
return await getEntityProperties.call(this, '/properties/v2/contacts/properties');
|
||||
},
|
||||
// Get all the available companies to display them to user so that they can
|
||||
// select them easily
|
||||
async getCompanyProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const endpoint = '/properties/v2/companies/properties';
|
||||
const properties = await hubspotApiRequest.call(this, 'GET', endpoint, {});
|
||||
for (const property of properties) {
|
||||
const propertyName = property.label;
|
||||
const propertyId = property.name;
|
||||
returnData.push({
|
||||
name: propertyName,
|
||||
value: propertyId,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
return await getEntityProperties.call(this, '/properties/v2/companies/properties');
|
||||
},
|
||||
// Get all the available deals to display them to user so that they can
|
||||
// select them easily
|
||||
async getDealProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const endpoint = '/properties/v2/deals/properties';
|
||||
const properties = await hubspotApiRequest.call(this, 'GET', endpoint, {});
|
||||
for (const property of properties) {
|
||||
const propertyName = property.label;
|
||||
const propertyId = property.name;
|
||||
returnData.push({
|
||||
name: propertyName,
|
||||
value: propertyId,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
return await getEntityProperties.call(this, '/properties/v2/deals/properties');
|
||||
},
|
||||
async getTicketProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
return await getEntityProperties.call(this, '/properties/v2/tickets/properties');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -448,6 +484,9 @@ export class HubspotTrigger implements INodeType {
|
|||
if (subscriptionType.includes('ticket')) {
|
||||
bodyData[i].ticketId = bodyData[i].objectId;
|
||||
}
|
||||
if (subscriptionType.includes('conversation')) {
|
||||
bodyData[i].conversationId = bodyData[i].objectId;
|
||||
}
|
||||
delete bodyData[i].objectId;
|
||||
}
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -135,6 +135,8 @@ export const propertyEvents = [
|
|||
'contact.propertyChange',
|
||||
'company.propertyChange',
|
||||
'deal.propertyChange',
|
||||
'ticket.propertyChange',
|
||||
'conversation.propertyChange',
|
||||
];
|
||||
|
||||
export const contactFields = [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
import type { ILoadOptionsFunctions } from 'n8n-workflow';
|
||||
|
||||
import { getEntityProperties } from '../HubspotTrigger.node';
|
||||
import { hubspotApiRequest } from '../V1/GenericFunctions';
|
||||
|
||||
jest.mock('../V1/GenericFunctions', () => ({
|
||||
hubspotApiRequest: jest.fn(),
|
||||
propertyEvents: [],
|
||||
}));
|
||||
|
||||
const mockedHubspotApiRequest = jest.mocked(hubspotApiRequest);
|
||||
|
||||
describe('HubspotTrigger getEntityProperties', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('maps HubSpot properties into options', async () => {
|
||||
mockedHubspotApiRequest.mockResolvedValueOnce([
|
||||
{ label: 'Email', name: 'email' },
|
||||
{ label: 'First Name', name: 'firstname' },
|
||||
]);
|
||||
|
||||
const context = {} as ILoadOptionsFunctions;
|
||||
const result = await getEntityProperties.call(context, '/properties/v2/contacts/properties');
|
||||
|
||||
expect(result).toEqual([
|
||||
{ name: 'Email', value: 'email' },
|
||||
{ name: 'First Name', value: 'firstname' },
|
||||
]);
|
||||
expect(mockedHubspotApiRequest).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'/properties/v2/contacts/properties',
|
||||
{},
|
||||
);
|
||||
expect(mockedHubspotApiRequest.mock.contexts[0]).toBe(context);
|
||||
});
|
||||
|
||||
it('throws for non-array responses', async () => {
|
||||
mockedHubspotApiRequest.mockResolvedValueOnce({ results: [] });
|
||||
const endpoint = '/properties/v2/contacts/properties';
|
||||
const context = {
|
||||
getNode: jest.fn().mockReturnValue({}),
|
||||
} as unknown as ILoadOptionsFunctions;
|
||||
|
||||
await expect(getEntityProperties.call(context, endpoint)).rejects.toThrow(
|
||||
`HubSpot returned an unexpected response while loading properties from "${endpoint}". Expected an array of properties.`,
|
||||
);
|
||||
});
|
||||
|
||||
it('filters invalid property entries', async () => {
|
||||
mockedHubspotApiRequest.mockResolvedValueOnce([
|
||||
{ label: 'Valid', name: 'valid_name' },
|
||||
{ label: 'Missing Name' },
|
||||
{ name: 'missing_label' },
|
||||
{ label: 123, name: 'bad_label_type' },
|
||||
]);
|
||||
|
||||
const result = await getEntityProperties.call(
|
||||
{} as ILoadOptionsFunctions,
|
||||
'/properties/v2/contacts/properties',
|
||||
);
|
||||
|
||||
expect(result).toEqual([{ name: 'Valid', value: 'valid_name' }]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue