Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,82 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { ownerIdProp } from '../common/props';
import { pipedriveApiCall } from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
export const addFollowerAction = createAction({
auth: pipedriveAuth,
name: 'add-follower',
displayName: 'Add Follower',
description: 'Adds a follower to a deal, person, organization or product.',
props: {
followerId: ownerIdProp('Follower', true),
entity: Property.StaticDropdown({
displayName: 'Target Object',
description: 'Type of object to add the follower to.',
required: true,
options: {
disabled: false,
options: [
{
label: 'Deal',
value: 'deal',
},
{
label: 'Person',
value: 'person',
},
{
label: 'Organization',
value: 'organization',
},
{
label: 'Product',
value: 'product',
},
],
},
}),
entityId: Property.ShortText({
displayName: 'Target Object ID',
description: 'ID of the object to add the follower to.',
required:true
}),
},
async run(context) {
const { followerId, entity, entityId } = context.propsValue;
let endpoint = '';
switch (entity) {
case 'deal':
endpoint = `/deals/${entityId}/followers`;
break;
case 'organization':
endpoint = `/organizations/${entityId}/followers`;
break;
case 'person':
endpoint = `/persons/${entityId}/followers`;
break;
case 'product':
endpoint = `/products/${entityId}/followers`;
break;
}
if (!endpoint) {
throw new Error(`Invalid object type: ${entity}`);
}
const response = await pipedriveApiCall({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.POST,
resourceUri: `/v2${endpoint}`,
body: {
user_id: followerId,
},
});
return response;
},
});

View File

@@ -0,0 +1,58 @@
import { pipedriveAuth } from '../../index';
import { createAction } from '@activepieces/pieces-framework';
import { labelIdsProp, personIdProp } from '../common/props';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedriveTransformCustomFields,
} from '../common';
import { GetField, GetPersonResponse } from '../common/types';
import { HttpMethod } from '@activepieces/pieces-common';
export const addLabelToPersonAction = createAction({
auth: pipedriveAuth,
name: 'add-labels-to-person',
displayName: 'Add Labels to Person',
description: 'Adds existing labels to an existing person.',
props: {
personId: personIdProp(true),
labelIds: labelIdsProp('person', 'label_ids', true),
},
async run(context) {
const { personId } = context.propsValue;
const labelIds = (context.propsValue.labelIds as number[]) ?? [];
const personUpdatePayload: Record<string, any> = {};
if (labelIds.length > 0) {
personUpdatePayload.label_ids = labelIds;
}
const updatedPersonResponse = await pipedriveApiCall<GetPersonResponse>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.PATCH,
resourceUri: `/v2/persons/${personId}`,
body: {
...personUpdatePayload,
},
});
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/personFields',
});
const transformedPersonData = pipedriveTransformCustomFields(
customFieldsResponse,
updatedPersonResponse.data,
);
return {
...updatedPersonResponse,
data: transformedPersonData,
};
},
});

View File

@@ -0,0 +1,113 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { dealIdProp, productIdProp } from '../common/props';
import { pipedriveApiCall } from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
export const addProductToDealAction = createAction({
auth: pipedriveAuth,
name: 'add-product-to-deal',
displayName: 'Add Product to Deal',
description: 'Adds a product to a deal.',
props: {
dealId: dealIdProp(true),
productId: productIdProp(true),
price: Property.Number({
displayName: 'Price',
required: true,
}),
quantity: Property.Number({
displayName: 'Quantity',
required: true,
}),
discount: Property.Number({
displayName: 'Discount',
required: false,
}),
discountType: Property.StaticDropdown({
displayName: 'Discount Type',
required: false,
options: {
disabled: false,
options: [
{
label: 'Percentage',
value: 'percentage',
},
{
label: 'Amount',
value: 'amount',
},
],
},
}),
comments: Property.LongText({
displayName: 'Comments',
required: false,
}),
enableProduct: Property.Checkbox({
displayName: 'Enable Product?',
required: false,
defaultValue: true,
}),
taxMethod: Property.StaticDropdown({
displayName: 'Tax Method',
required: false,
options: {
disabled: false,
options: [
{
label: 'Exclusive',
value: 'exclusive',
},
{
label: 'Inclusive',
value: 'inclusive',
},
{
label: 'None',
value: 'none',
},
],
},
}),
taxPercentage: Property.Number({
displayName: 'Tax Percentage',
required: false,
}),
},
async run(context) {
const {
productId,
dealId,
price,
quantity,
discountType,
discount,
comments,
enableProduct,
taxPercentage,
taxMethod,
} = context.propsValue;
const response = await pipedriveApiCall({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.POST,
resourceUri: `/v2/deals/${dealId}/products`,
body: {
product_id: productId,
item_price: price,
quantity,
discount_type: discountType,
discount,
comments: comments ?? '',
is_enabled: enableProduct,
tax: taxPercentage,
tax_method: taxMethod,
},
});
return response;
},
});

View File

@@ -0,0 +1,58 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { dealIdProp, organizationIdProp, personIdProp, productIdProp } from '../common/props';
import FormData from 'form-data';
import { AuthenticationType, httpClient, HttpMethod } from '@activepieces/pieces-common';
export const attachFileAction = createAction({
auth: pipedriveAuth,
name: 'attach-file',
displayName: 'Attach File',
description: 'Uploads a file and attaches it to a deal,person,organization,activity or product.',
props: {
file: Property.File({
displayName: 'File',
required: true,
}),
fileName: Property.ShortText({
displayName: 'File Name',
required: true,
}),
dealId: dealIdProp(false),
personId: personIdProp(false),
organizationId: organizationIdProp(false),
productId: productIdProp(false),
activityId: Property.Number({
displayName: 'Activity ID',
required: false,
}),
},
async run(context) {
const { file, fileName, dealId, personId, organizationId, productId, activityId } =
context.propsValue;
const formatData = new FormData();
formatData.append('file', file.data, fileName);
if (dealId) formatData.append('deal_id', dealId);
if (personId) formatData.append('person_id', personId);
if (organizationId) formatData.append('org_id', organizationId);
if (productId) formatData.append('product_id', productId);
if (activityId) formatData.append('activity_id', activityId);
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `${context.auth.data['api_domain']}/api/v1/files`,
body: formatData,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
headers: {
...formatData.getHeaders(),
},
});
return response.body;
},
});

View File

@@ -0,0 +1,74 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { activityCommonProps } from '../common/props';
import { pipedriveApiCall } from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import dayjs from 'dayjs';
export const createActivityAction = createAction({
auth: pipedriveAuth,
name: 'create-activity',
displayName: 'Create Activity',
description: 'Creates a new activity.',
props: {
subject: Property.ShortText({
displayName: 'Subject',
required: true,
}),
...activityCommonProps,
},
async run(context) {
const {
subject,
organizationId,
personId,
dealId,
leadId,
assignTo,
type,
dueDate,
dueTime,
duration,
isDone,
busy,
note,
publicDescription,
} = context.propsValue;
const activityPayload: Record<string, any> = {
subject,
org_id: organizationId,
deal_id: dealId,
lead_id: leadId,
public_description: publicDescription,
type,
owner_id: assignTo,
due_time: dueTime,
duration,
done: isDone,
note
};
if (personId) {
activityPayload.participants = [{ person_id: personId, primary: true }];
}
if (busy) {
activityPayload.busy = busy === 'busy' ? true : false;
}
if (dueDate) {
activityPayload.due_date = dayjs(dueDate).format('YYYY-MM-DD');
}
const response = await pipedriveApiCall({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.POST,
resourceUri: '/v2/activities',
body: activityPayload,
});
return response;
},
});

View File

@@ -0,0 +1,101 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { dealCommonProps } from '../common/props';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedriveParseCustomFields,
pipedriveTransformCustomFields,
} from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import { GetField, GetDealResponse } from '../common/types';
import dayjs from 'dayjs';
import { isEmpty } from '@activepieces/shared';
export const createDealAction = createAction({
auth: pipedriveAuth,
name: 'create-deal',
displayName: 'Create Deal',
description: 'Creates a new deal.',
props: {
title: Property.ShortText({
displayName: 'Title',
required: true,
}),
...dealCommonProps,
},
async run(context) {
const {
title,
dealValue,
dealValueCurrency,
expectedCloseDate,
visibleTo,
probability,
stageId,
status,
pipelineId,
ownerId,
organizationId,
personId,
creationTime,
} = context.propsValue;
const labelIds = (context.propsValue.labelIds as number[]) ?? [];
const customFields = context.propsValue.customfields ?? {};
const dealPayload: Record<string, any> = {
title,
pipeline_id: pipelineId,
stage_id: stageId,
status,
add_time: creationTime ? dayjs(creationTime).format('YYYY-MM-DDTHH:mm:ss[Z]') : undefined,
probability,
visible_to: visibleTo,
owner_id: ownerId,
org_id: organizationId,
person_id: personId,
value: dealValue,
currency: dealValueCurrency,
};
if (labelIds.length > 0) {
dealPayload.label_ids = labelIds;
}
if (expectedCloseDate) {
dealPayload.expected_close_date = dayjs(expectedCloseDate).format('YYYY-MM-DD');
}
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/dealFields',
});
const dealCustomFields = pipedriveParseCustomFields(customFieldsResponse, customFields);
if (!isEmpty(dealCustomFields)) {
dealPayload.custom_fields = dealCustomFields;
}
const createdDealResponse = await pipedriveApiCall<GetDealResponse>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.POST,
resourceUri: '/v2/deals',
body: dealPayload,
});
const transformedPersonProperties = pipedriveTransformCustomFields(
customFieldsResponse,
createdDealResponse.data,
);
return {
...createdDealResponse,
data: transformedPersonProperties,
};
},
});

View File

@@ -0,0 +1,113 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { leadCommonProps } from '../common/props';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedriveTransformCustomFields,
pipedriveTransformV1CustomFields,
} from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import { GetField, GetLeadResponse } from '../common/types';
import dayjs from 'dayjs';
export const createLeadAction = createAction({
auth: pipedriveAuth,
name: 'create-lead',
displayName: 'Create Lead',
description: 'Creates a new lead.',
props: {
title: Property.ShortText({
displayName: 'Title',
required: true,
}),
...leadCommonProps,
},
async run(context) {
const {
title,
leadValue,
leadValueCurrency,
expectedCloseDate,
visibleTo,
ownerId,
organizationId,
personId,
channel,
} = context.propsValue;
if (!personId && !organizationId) {
throw new Error(
'Neither an Organization nor a Person were provided. One of them must be provided in order to create a lead.',
);
}
const labelIds = (context.propsValue.labelIds as string[]) ?? [];
const customFields = context.propsValue.customfields ?? {};
const leadDefaultFields: Record<string, any> = {
title,
owner_id: ownerId,
organization_id: organizationId,
person_id: personId,
channel: channel,
visible_to: visibleTo,
};
if (labelIds.length > 0) {
leadDefaultFields.label_ids = labelIds;
}
if(expectedCloseDate)
{
leadDefaultFields.expected_close_date= dayjs(expectedCloseDate).format('YYYY-MM-DD')
}
if (leadValue) {
if (!leadValueCurrency) {
throw new Error('lead Value Currency is required when lead Value is provided');
}
leadDefaultFields.value = {
amount: leadValue,
currency: leadValueCurrency,
};
}
const leadCustomFields: Record<string, any> = {};
Object.entries(customFields).forEach(([key, value]) => {
// Format values if they are arrays
leadCustomFields[key] = Array.isArray(value) ? value.join(',') : value;
});
const createdLeadResponse = await pipedriveApiCall<GetLeadResponse>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.POST,
resourceUri: '/v1/leads',
body: {
...leadDefaultFields,
...leadCustomFields,
},
});
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/dealFields',
});
const updatedLeadProperties = pipedriveTransformV1CustomFields(
customFieldsResponse,
createdLeadResponse.data,
);
return {
...createdLeadResponse,
data: updatedLeadProperties,
};
},
});

View File

@@ -0,0 +1,83 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { dealIdProp, leadIdProp, organizationIdProp, personIdProp } from '../common/props';
import { pipedriveApiCall } from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
export const createNoteAction = createAction({
auth: pipedriveAuth,
name: 'create-note',
displayName: 'Create Note',
description: 'Creates a new note.',
props: {
content: Property.LongText({
displayName: 'Content',
required: true,
}),
dealId: dealIdProp(false),
pinnedToDeal: Property.Checkbox({
displayName: 'Pin note to deal?',
required: false,
defaultValue: false,
}),
personId: personIdProp(false),
pinnedToPerson: Property.Checkbox({
displayName: 'Pin note to person?',
required: false,
defaultValue: false,
}),
organizationId: organizationIdProp(false),
pinnedToOrganization: Property.Checkbox({
displayName: 'Pin note to organization?',
required: false,
defaultValue: false,
}),
leadId: leadIdProp(false),
pinnedToLead: Property.Checkbox({
displayName: 'Pin note to lead?',
required: false,
defaultValue: false,
}),
},
async run(context) {
const {
content,
dealId,
personId,
organizationId,
leadId,
pinnedToDeal,
pinnedToPerson,
pinnedToOrganization,
pinnedToLead,
} = context.propsValue;
if (!dealId && !personId && !organizationId && !leadId) {
throw new Error(
'Note must be associated with at least one organization, person, deal, lead or project.',
);
}
const notePayload: Record<string, any> = {
content,
pinned_to_deal_flag: pinnedToDeal ? 1 : 0,
pinned_to_person_flag: pinnedToPerson ? 1 : 0,
pinned_to_organization_flag: pinnedToOrganization ? 1 : 0,
pinned_to_lead_flag: pinnedToLead ? 1 : 0,
lead_id: leadId,
person_id: personId,
org_id: organizationId,
deal_id: dealId,
};
const response = await pipedriveApiCall({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.POST,
resourceUri: '/v1/notes',
body: notePayload,
});
return response;
},
});

View File

@@ -0,0 +1,79 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { organizationCommonProps } from '../common/props';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedriveParseCustomFields,
pipedriveTransformCustomFields,
} from '../common';
import { GetField, GetOrganizationResponse } from '../common/types';
import { HttpMethod } from '@activepieces/pieces-common';
import { isEmpty } from '@activepieces/shared';
export const createOrganizationAction = createAction({
auth: pipedriveAuth,
name: 'create-organization',
displayName: 'Create Organization',
description: 'Creates a new organization.',
props: {
name: Property.ShortText({
displayName: 'Name',
required: true,
}),
...organizationCommonProps,
},
async run(context) {
const { name, ownerId, address, visibleTo } = context.propsValue;
const labelIds = (context.propsValue.labelIds as number[]) ?? [];
const customFields = context.propsValue.customfields ?? {};
const organizationPayload: Record<string, any> = {
name: name,
owner_id: ownerId,
visible_to: visibleTo,
};
if (address) {
if (typeof address === 'string') {
organizationPayload.address = { value: address };
}
}
if (labelIds.length > 0) {
organizationPayload.label_ids = labelIds;
}
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/organizationFields',
});
const orgCustomFields = pipedriveParseCustomFields(customFieldsResponse, customFields);
if (!isEmpty(orgCustomFields)) {
organizationPayload.custom_fields = orgCustomFields;
}
const createdOrganizationResponse = await pipedriveApiCall<GetOrganizationResponse>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.POST,
resourceUri: '/v2/organizations',
body: organizationPayload,
});
const transformedPersonProperties = pipedriveTransformCustomFields(
customFieldsResponse,
createdOrganizationResponse.data,
);
return {
...createdOrganizationResponse,
data: transformedPersonProperties,
};
},
});

View File

@@ -0,0 +1,106 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { personCommonProps } from '../common/props';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedriveParseCustomFields,
pipedriveTransformCustomFields,
} from '../common';
import { GetField, GetPersonResponse } from '../common/types';
import { HttpMethod } from '@activepieces/pieces-common';
import { isEmpty } from '@activepieces/shared';
export const createPersonAction = createAction({
auth: pipedriveAuth,
name: 'create-person',
displayName: 'Create Person',
description: 'Creates a new person.',
props: {
name: Property.ShortText({
displayName: 'Name',
required: false,
}),
...personCommonProps,
},
async run(context) {
const { name, ownerId, organizationId, marketing_status, visibleTo, firstName, lastName } =
context.propsValue;
const rawPhones = (context.propsValue.phone as string[]) ?? [];
const rawEmails = (context.propsValue.email as string[]) ?? [];
const labelIds = (context.propsValue.labelIds as number[]) ?? [];
const customFields = context.propsValue.customfields ?? {};
// https://pipedrive.readme.io/docs/pipedrive-api-v2-migration-guide#post-apiv1persons-to-post-apiv2persons
if (name && (firstName || lastName)) {
throw new Error('Provide either Name OR First Name/Last Name, not both.');
}
if (!name && !firstName && !lastName) {
throw new Error('Provide Name or at least one of First Name / Last Name.');
}
if (!name && ((firstName && !lastName) || (!firstName && lastName))) {
throw new Error('If First Name is provided, Last Name must be provided as well.');
}
const phones = rawPhones.map((value, index) => ({
value,
label: 'work',
primary: index === 0,
}));
const emails = rawEmails.map((value, index) => ({
value,
label: 'work',
primary: index === 0,
}));
const personPayload: Record<string, any> = {
name,
owner_id: ownerId,
org_id: organizationId,
visible_to: visibleTo,
first_name: firstName,
last_name: lastName,
marketing_status: marketing_status,
};
// Phones and emails
if (phones.length) personPayload.phones = phones;
if (emails.length) personPayload.emails = emails;
if (labelIds.length) personPayload.label_ids = labelIds;
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/personFields',
});
const personCustomFields = pipedriveParseCustomFields(customFieldsResponse, customFields);
if (!isEmpty(personCustomFields)) {
personPayload.custom_fields = personCustomFields;
}
const createdPersonResponse = await pipedriveApiCall<GetPersonResponse>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.POST,
resourceUri: '/v2/persons',
body: personPayload,
});
const transformedPersonProperties = pipedriveTransformCustomFields(
customFieldsResponse,
createdPersonResponse.data,
);
return {
...createdPersonResponse,
data: transformedPersonProperties,
};
},
});

View File

@@ -0,0 +1,137 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { customFieldsProp, ownerIdProp, visibleToProp } from '../common/props';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedriveParseCustomFields,
pipedriveTransformCustomFields,
} from '../common';
import { GetField, GetProductResponse } from '../common/types';
import { HttpMethod } from '@activepieces/pieces-common';
import { isEmpty } from '@activepieces/shared';
export const createProductAction = createAction({
auth: pipedriveAuth,
name: 'create-product',
displayName: 'Create Product',
description: 'Creates a new product.',
props: {
name: Property.ShortText({
displayName: 'Name',
required: true,
}),
code: Property.ShortText({
displayName: 'Code',
required: false,
}),
description: Property.LongText({
displayName: 'Description',
required: false,
}),
unit: Property.ShortText({
displayName: 'Unit',
required: false,
}),
tax: Property.Number({
displayName: 'Tax percentage',
required: false,
}),
isActive: Property.Checkbox({
displayName: 'Is Active ?',
required: false,
defaultValue: true,
}),
ownerId: ownerIdProp('Owner', false),
currency: Property.ShortText({
displayName: 'Currency',
required: false,
description: 'Please enter currency code (e.g., "USD", "EUR").',
}),
price: Property.Number({
displayName: 'Price',
required: false,
}),
cost: Property.Number({
displayName: 'Cost',
required: false,
}),
overheadCost: Property.Number({
displayName: 'Overhead Cost',
required: false,
}),
visibleTo: visibleToProp,
customfields: customFieldsProp('product'),
},
async run(context) {
const {
name,
code,
description,
unit,
tax,
isActive,
ownerId,
currency,
price,
cost,
overheadCost,
visibleTo,
} = context.propsValue;
const customFields = context.propsValue.customfields ?? {};
const productPayload: Record<string, any> = {
name,
code,
description,
unit,
tax,
is_deleted: !isActive,
prices: [
{
price: price ?? 0,
currency: currency ?? 'USD',
cost: cost ?? 0,
direct_cost: overheadCost ?? 0,
},
],
visible_to: visibleTo,
};
if (ownerId) {
productPayload.owner_id = ownerId;
}
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/productFields',
});
const productCustomFields = pipedriveParseCustomFields(customFieldsResponse, customFields);
if (!isEmpty(productCustomFields)) {
productPayload.custom_fields = productCustomFields;
}
const createdProductResponse = await pipedriveApiCall<GetProductResponse>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.POST,
resourceUri: '/v2/products',
body: productPayload,
});
const updatedProductProperties = pipedriveTransformCustomFields(
customFieldsResponse,
createdProductResponse.data,
);
return {
...createdProductResponse,
data: updatedProductProperties,
};
},
});

View File

@@ -0,0 +1,102 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { activityTypeIdProp, filterIdProp, ownerIdProp } from '../common/props';
import { pipedrivePaginatedV2ApiCall } from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import { isNil } from '@activepieces/shared';
export const findActivityAction = createAction({
auth: pipedriveAuth,
name: 'find-activity',
displayName: 'Find Activity',
description: 'Finds an activity by subject.',
props: {
subject: Property.ShortText({
displayName: 'Subject',
required: true,
}),
exactMatch: Property.Checkbox({
displayName: 'Exact Match',
required: false,
defaultValue: true,
}),
assignTo: ownerIdProp('Assign To', false),
type: activityTypeIdProp(false),
filterId: filterIdProp('activity', false),
status: Property.StaticDropdown({
displayName: 'Status',
required: false,
options: {
disabled: false,
options: [
{
label: 'Done',
value: 1,
},
{
label: 'Not Done',
value: 0,
},
],
},
}),
},
async run(context) {
const { subject, assignTo, type, filterId, status, exactMatch } = context.propsValue;
const response = await pipedrivePaginatedV2ApiCall<{
id: number;
subject: string;
type: string;
}>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/activities',
query: {
owner_id: assignTo,
done: status != null ? status === 1 : undefined,
sort_by: 'update_time',
sort_direction: 'desc',
filter_id: filterId,
},
});
if (isNil(response) || response.length === 0) {
return {
found: false,
data: [],
};
}
const result = [];
for (const activity of response) {
let matched = false;
if (activity.subject) {
if (exactMatch && activity.subject === subject) {
matched = true;
} else if (!exactMatch && activity.subject.toLowerCase().includes(subject.toLowerCase())) {
matched = true;
}
}
// If type is provided, require both subject & type match
if (type) {
if (matched && activity.type === type) {
result.push(activity);
}
} else {
if (matched) {
result.push(activity);
}
}
}
return {
found: result.length > 0,
data: result,
};
},
});

View File

@@ -0,0 +1,117 @@
import { createAction } from '@activepieces/pieces-framework';
import { pipedriveAuth } from '../../index';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedriveTransformCustomFields,
} from '../common';
import { GetField } from '../common/types';
import { HttpMethod } from '@activepieces/pieces-common';
import { isNil } from '@activepieces/shared';
import { searchFieldProp, searchFieldValueProp } from '../common/props';
import { DEAL_OPTIONAL_FIELDS } from '../common/constants';
export const findDealAction = createAction({
auth: pipedriveAuth,
name: 'find-deal',
displayName: 'Find Deal',
description: 'Finds a deal by any field.',
props: {
searchField: searchFieldProp('deal'),
searchFieldValue: searchFieldValueProp('deal'),
},
async run(context) {
const { searchField } = context.propsValue;
const fieldValue = context.propsValue.searchFieldValue['field_value'];
if (isNil(fieldValue)) {
throw new Error('Please enter a value for the field');
}
const filter = await pipedriveApiCall<{ data: { id: number } }>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.POST,
resourceUri: '/v1/filters',
body: {
name: `Activepieces Find Deal Filter`,
type: 'deals',
conditions: {
glue: 'and',
conditions: [
{
glue: 'and',
conditions: [
{
object: 'deal',
field_id: searchField,
operator: '=',
value: fieldValue,
},
],
},
{
glue: 'or',
conditions: [
{
object: 'deal',
field_id: searchField,
operator: 'IS NOT NULL',
value: null,
},
],
},
],
},
},
});
// Search for deals using the created filter
const deals = await pipedriveApiCall<{ data: { id: number }[] }>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/deals',
query: {
filter_id: filter.data.id,
limit: 1,
sort_by: 'update_time',
sort_direction: 'desc',
include_fields: DEAL_OPTIONAL_FIELDS.join(','),
},
});
// Delete the temporary filter
await pipedriveApiCall({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.DELETE,
resourceUri: `/v1/filters/${filter.data.id}`,
});
if (isNil(deals.data) || deals.data.length === 0) {
return {
found: false,
data: [],
};
}
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/dealFields',
});
const updatedDealProperties = pipedriveTransformCustomFields(
customFieldsResponse,
deals.data[0],
);
return {
found: true,
data: [updatedDealProperties],
};
},
});

View File

@@ -0,0 +1,63 @@
import { pipedriveAuth } from '../../index';
import { createAction } from '@activepieces/pieces-framework';
import { personIdProp } from '../common/props';
import {
pipedrivePaginatedV1ApiCall,
pipedrivePaginatedV2ApiCall,
pipedriveTransformCustomFields,
} from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import { isNil } from '@activepieces/shared';
import { GetField } from '../common/types';
import { DEAL_OPTIONAL_FIELDS } from '../common/constants';
export const findDealsAssociatedWithPersonAction = createAction({
auth: pipedriveAuth,
name: 'find-deals-associated-with-person',
displayName: 'Find Deals Associated With Person',
description: 'Finds multiple deals related to a specific person.',
props: {
personId: personIdProp(true),
},
async run(context) {
const { personId } = context.propsValue;
const deals = await pipedrivePaginatedV2ApiCall<Record<string, any>>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: `/v2/deals`,
query: {
person_id: personId,
sort_by: 'update_time',
sort_direction: 'desc',
include_fields: DEAL_OPTIONAL_FIELDS.join(','),
},
});
if (isNil(deals) || deals.length === 0) {
return {
found: false,
data: [],
};
}
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/dealFields',
});
const result = [];
for (const deal of deals) {
const updatedDealProperties = pipedriveTransformCustomFields(customFieldsResponse, deal);
result.push(updatedDealProperties);
}
return {
found: true,
data: result,
};
},
});

View File

@@ -0,0 +1,67 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { pipedrivePaginatedV1ApiCall } from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import { isNil } from '@activepieces/shared';
export const findNotesAction = createAction({
auth: pipedriveAuth,
name: 'find-notes',
displayName: 'Find Notes',
description: 'Finds notes by Deal, Lead, Person, or Organization ID.',
props: {
objectType: Property.StaticDropdown({
displayName: 'Search By',
required: true,
options: {
disabled: false,
options: [
{
label: 'Deal',
value: 'deal_id',
},
{
label: 'Lead',
value: 'lead_id',
},
{
label: 'Person',
value: 'person_id',
},
{
label: 'Organization',
value: 'org_id',
},
],
},
}),
objectId: Property.ShortText({
displayName: 'ID',
required: true,
}),
},
async run(context) {
const response = await pipedrivePaginatedV1ApiCall({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: `/v1/notes`,
query: {
sort: 'update_time DESC',
[context.propsValue.objectType]:context.propsValue.objectId
},
});
if (isNil(response) || response.length === 0) {
return {
found: false,
data: [],
};
}
return {
found: response.length > 0,
data: response,
};
},
});

View File

@@ -0,0 +1,116 @@
import { createAction } from '@activepieces/pieces-framework';
import { pipedriveAuth } from '../../index';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedriveTransformCustomFields,
} from '../common';
import { GetField } from '../common/types';
import { HttpMethod } from '@activepieces/pieces-common';
import { isNil } from '@activepieces/shared';
import { searchFieldProp, searchFieldValueProp } from '../common/props';
import { ORGANIZATION_OPTIONAL_FIELDS } from '../common/constants';
export const findOrganizationAction = createAction({
auth: pipedriveAuth,
name: 'find-organization',
displayName: 'Find Organization',
description: 'Finds an organization.',
props: {
searchField: searchFieldProp('organization'),
searchFieldValue: searchFieldValueProp('organization'),
},
async run(context) {
const { searchField } = context.propsValue;
const fieldValue = context.propsValue.searchFieldValue['field_value'];
if (isNil(fieldValue)) {
throw new Error('Please enter a value for the field');
}
// create Filter
const filter = await pipedriveApiCall<{ data: { id: number } }>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.POST,
resourceUri: '/v1/filters',
body: {
name: 'Activepieces Find Organization Filter',
type: 'org',
conditions: {
glue: 'and',
conditions: [
{
glue: 'and',
conditions: [
{
object: 'organization',
field_id: searchField,
operator: '=',
value: fieldValue,
},
],
},
{
glue: 'or',
conditions: [
{
object: 'organization',
field_id: searchField,
operator: 'IS NOT NULL',
value: null,
},
],
},
],
},
},
});
const organizations = await pipedriveApiCall<{ data: { id: number }[] }>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/organizations',
query: {
filter_id: filter.data.id,
limit: 1,
sort_by: 'update_time',
sort_direction: 'desc',
include_fields: ORGANIZATION_OPTIONAL_FIELDS.join(','),
},
});
// delete the filter
await pipedriveApiCall({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.DELETE,
resourceUri: `/v1/filters/${filter.data.id}`,
});
if (isNil(organizations.data) || organizations.data.length === 0) {
return {
found: false,
data: [],
};
}
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/organizationFields',
});
const updatedOrganizationProperties = pipedriveTransformCustomFields(
customFieldsResponse,
organizations.data[0],
);
return {
found: true,
data: [updatedOrganizationProperties],
};
},
});

View File

@@ -0,0 +1,117 @@
import { createAction } from '@activepieces/pieces-framework';
import { pipedriveAuth } from '../../index';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedriveTransformCustomFields,
} from '../common';
import { GetField } from '../common/types';
import { HttpMethod } from '@activepieces/pieces-common';
import { isNil } from '@activepieces/shared';
import { searchFieldProp, searchFieldValueProp } from '../common/props';
import { PERSON_OPTIONAL_FIELDS } from '../common/constants';
export const findPersonAction = createAction({
auth: pipedriveAuth,
name: 'find-person',
displayName: 'Find Person',
description: 'Finds a person.',
props: {
searchField: searchFieldProp('person'),
searchFieldValue: searchFieldValueProp('person'),
},
async run(context) {
const { searchField } = context.propsValue;
const fieldValue = context.propsValue.searchFieldValue['field_value'];
if (isNil(fieldValue)) {
throw new Error('Please enter a value for the field');
}
// create Filter
const filter = await pipedriveApiCall<{ data: { id: number } }>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.POST,
resourceUri: '/v1/filters',
body: {
name: 'Activepieces Find Person Filter',
type: 'people',
conditions: {
glue: 'and',
conditions: [
{
glue: 'and',
conditions: [
{
object: 'person',
field_id: searchField,
operator: '=',
value: fieldValue,
},
],
},
{
glue: 'or',
conditions: [
{
object: 'person',
field_id: searchField,
operator: 'IS NOT NULL',
value: null,
},
],
},
],
},
},
});
// search for persons using the filter
const persons = await pipedriveApiCall<{ data: { id: number }[] }>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/persons',
query: {
filter_id: filter.data.id,
limit: 1,
sort_by:'update_time',
sort_direction:'desc',
include_fields:PERSON_OPTIONAL_FIELDS.join(',')
},
});
// delete the filter
await pipedriveApiCall({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.DELETE,
resourceUri: `/v1/filters/${filter.data.id}`,
});
if (isNil(persons.data) || persons.data.length === 0) {
return {
found: false,
data: [],
};
}
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/personFields',
});
const updatedPersonProperties = pipedriveTransformCustomFields(
customFieldsResponse,
persons.data[0],
);
return {
found: true,
data: [updatedPersonProperties],
};
},
});

View File

@@ -0,0 +1,76 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedriveTransformCustomFields,
} from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import { GetField } from '../common/types';
import { isNil } from '@activepieces/shared';
export const findProductAction = createAction({
auth: pipedriveAuth,
name: 'find-product',
displayName: 'Find Product',
description: 'Finds a product by name ',
props: {
searchTerm: Property.ShortText({
displayName: 'Search Term',
required: true,
}),
},
async run(context) {
const { searchTerm } = context.propsValue;
const searchResponse = await pipedriveApiCall<{
success: boolean;
data: { items: Array<{ item: { id: number; name: string; } }> };
}>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/products/search',
query: {
term: searchTerm,
fields: 'name',
limit: 1,
include_fields:'product.price'
},
});
if (isNil(searchResponse.data) || isNil(searchResponse.data.items) || searchResponse.data.items.length === 0) {
return {
found: false,
data: [],
};
}
const productDetailsResponse = await pipedriveApiCall<Record<string, any>>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: `/v2/products/${searchResponse.data.items[0].item.id}`,
});
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/productFields',
});
// Transform custom fields in the response data
const updatedProductProperties = pipedriveTransformCustomFields(
customFieldsResponse,
productDetailsResponse.data,
);
return {
found: true,
data: [updatedProductProperties],
};
},
});

View File

@@ -0,0 +1,91 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedrivePaginatedV2ApiCall,
pipedriveTransformCustomFields,
} from '../common';
import { HttpMethod, QueryParams } from '@activepieces/pieces-common';
import { isNil } from '@activepieces/shared';
import { GetField } from '../common/types';
export const findProductsAction = createAction({
auth: pipedriveAuth,
name: 'find-products',
displayName: 'Find Products',
description: 'Finds a product or products by name or product code.',
props: {
field: Property.StaticDropdown({
displayName: 'Field to search by',
required: true,
defaultValue: 'name',
options: {
disabled: false,
options: [
{
label: 'Name',
value: 'name',
},
{
label: 'Product Code',
value: 'code',
},
],
},
}),
fieldValue: Property.ShortText({
displayName: 'Field Value',
required: true,
}),
},
async run(context) {
const qs: QueryParams = {
term: context.propsValue.fieldValue,
fields: context.propsValue.field,
exact_match: 'true',
limit: '100',
};
let cursor: string | undefined = undefined;
let hasMoreItems = true;
const products = [];
do {
if (cursor) {
qs.cursor = cursor;
}
const response = await pipedriveApiCall<{
success: boolean;
data: { items: Array<{ item: { id: number } }> };
additional_data: {
next_cursor?: string;
};
}>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/products/search',
query: {
term: context.propsValue.fieldValue,
fields: context.propsValue.field,
exact_match: 'true',
},
});
if (isNil(response.data.items)) break;
for (const product of response.data.items) {
products.push(product.item);
}
hasMoreItems = response.additional_data?.next_cursor != null;
cursor = response.additional_data?.next_cursor;
} while (hasMoreItems);
return {
found: products.length > 0,
data: products,
};
},
});

View File

@@ -0,0 +1,55 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { pipedriveApiCall } from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
export const findUserAction = createAction({
auth: pipedriveAuth,
name: 'find-user',
displayName: 'Find User',
description: 'Finds a user by name or email.',
props: {
field: Property.StaticDropdown({
displayName: 'Field to search by',
required: true,
options: {
disabled: false,
options: [
{
label: 'Name',
value: 'name',
},
{
label: 'Email',
value: 'email',
},
],
},
}),
fieldValue: Property.ShortText({
displayName: 'Field Value',
required: true,
}),
},
async run(context) {
const { field, fieldValue } = context.propsValue;
const response = await pipedriveApiCall<{ success: boolean; data: Array<Record<string, any>> }>(
{
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/users/find',
query: {
term: fieldValue,
search_by_email: field == 'email' ? 1 : 0,
},
},
);
return {
found: response.data && response.data.length > 0,
data: response.data,
};
},
});

View File

@@ -0,0 +1,33 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { pipedriveApiCall } from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
export const getNoteAction = createAction({
auth: pipedriveAuth,
name: 'get-note',
displayName: 'Retrieve a Note',
description: 'Finds a note by ID.',
props: {
noteId: Property.Number({
displayName: 'Note ID',
required: true,
}),
},
async run(context) {
try {
const response = await pipedriveApiCall({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: `/v1/notes/${context.propsValue.noteId}`,
});
return response;
} catch (error) {
return {
success: false,
data:{}
};
}
},
});

View File

@@ -0,0 +1,54 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedriveTransformCustomFields,
} from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import { GetField } from '../common/types';
export const getProductAction = createAction({
auth: pipedriveAuth,
name: 'get-product',
displayName: 'Retrieve a Product',
description: 'Finds a product by ID.',
props: {
productId: Property.Number({
displayName: 'Product ID',
required: true,
}),
},
async run(context) {
try {
const response = await pipedriveApiCall<Record<string, any>>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: `/v2/products/${context.propsValue.productId}`,
});
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/productFields',
});
const updatedProductProperties = pipedriveTransformCustomFields(
customFieldsResponse,
response.data,
);
return {
found: true,
data: [updatedProductProperties],
};
} catch (error) {
return {
found: false,
data: [],
};
}
},
});

View File

@@ -0,0 +1,82 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { activityCommonProps } from '../common/props';
import { pipedriveApiCall } from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import dayjs from 'dayjs';
export const updateActivityAction = createAction({
auth: pipedriveAuth,
name: 'update-activity',
displayName: 'Update Activity',
description: 'Updates an existing activity.',
props: {
activityId: Property.Number({
displayName: 'Activity ID',
required: true,
}),
subject: Property.ShortText({
displayName: 'Subject',
required: false,
}),
...activityCommonProps,
},
async run(context) {
const {
activityId,
subject,
organizationId,
personId,
dealId,
leadId,
assignTo, // This is the user ID for the assignee
type,
dueDate,
dueTime,
duration,
isDone,
busy,
note,
publicDescription,
} = context.propsValue;
const activityPayload: Record<string, any> = {
subject,
org_id: organizationId,
deal_id: dealId,
lead_id: leadId,
note,
public_description: publicDescription,
type,
owner_id: assignTo,
due_time: dueTime,
duration,
done: isDone,
};
if (personId) {
activityPayload.participants = [{ person_id: personId, primary: true }];
}
if (busy) {
activityPayload.busy = busy === 'busy' ? true : false;
}
if (dueDate) {
activityPayload.due_date = dayjs(dueDate).format('YYYY-MM-DD');
}
const response = await pipedriveApiCall({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.PATCH,
resourceUri: `/v2/activities/${activityId}`,
body: activityPayload,
});
return response;
},
});

View File

@@ -0,0 +1,101 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { dealCommonProps, dealIdProp } from '../common/props';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedriveParseCustomFields,
pipedriveTransformCustomFields,
} from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import { GetField, GetDealResponse } from '../common/types';
import dayjs from 'dayjs';
import { isEmpty } from '@activepieces/shared';
export const updateDealAction = createAction({
auth: pipedriveAuth,
name: 'update-deal',
displayName: 'Update Deal',
description: 'Updates an existing deal.',
props: {
dealId: dealIdProp(true),
title: Property.ShortText({
displayName: 'Title',
required: false,
}),
...dealCommonProps,
},
async run(context) {
const {
dealId,
title,
dealValue,
dealValueCurrency,
expectedCloseDate,
visibleTo,
probability,
stageId,
status,
pipelineId,
ownerId,
organizationId,
personId,
} = context.propsValue;
const labelIds = (context.propsValue.labelIds as number[]) ?? [];
const customFields = context.propsValue.customfields ?? {};
const dealPayload: Record<string, any> = {
title,
pipeline_id: pipelineId,
stage_id: stageId,
status,
probability,
visible_to: visibleTo,
owner_id: ownerId,
org_id: organizationId,
person_id: personId,
value: dealValue,
currency: dealValueCurrency,
};
if (labelIds.length > 0) {
dealPayload.label_ids = labelIds;
}
if (expectedCloseDate) {
dealPayload.expected_close_date = dayjs(expectedCloseDate).format('YYYY-MM-DD');
}
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/dealFields',
});
const personCustomFields = pipedriveParseCustomFields(customFieldsResponse, customFields);
if (!isEmpty(personCustomFields)) {
dealPayload.custom_fields = personCustomFields;
}
const updatedDealResponse = await pipedriveApiCall<GetDealResponse>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.PATCH,
resourceUri: `/v2/deals/${dealId}`,
body: dealPayload,
});
const transformedDealProperties = pipedriveTransformCustomFields(
customFieldsResponse,
updatedDealResponse.data,
);
return {
...updatedDealResponse,
data: transformedDealProperties,
};
},
});

View File

@@ -0,0 +1,105 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { leadCommonProps, leadIdProp } from '../common/props';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedriveTransformV1CustomFields,
} from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import { GetField, GetLeadResponse } from '../common/types';
import dayjs from 'dayjs';
export const updateLeadAction = createAction({
auth: pipedriveAuth,
name: 'update-lead',
displayName: 'Update Lead',
description: 'Updates an existing lead.',
props: {
leadId: leadIdProp(true),
title: Property.ShortText({
displayName: 'Title',
required: false,
}),
...leadCommonProps,
},
async run(context) {
const {
title,
ownerId,
leadId,
channel,
organizationId,
personId,
expectedCloseDate,
visibleTo,
leadValue,
leadValueCurrency,
} = context.propsValue;
const labelIds = (context.propsValue.labelIds as string[]) ?? [];
const customFields = context.propsValue.customfields ?? {};
const leadDefaultFields: Record<string, any> = {
title,
owner_id: ownerId,
organization_id: organizationId,
person_id: personId,
channel: channel,
visible_to: visibleTo,
};
if (labelIds.length > 0) {
leadDefaultFields.label_ids = labelIds;
}
if (expectedCloseDate) {
leadDefaultFields.expected_close_date = dayjs(expectedCloseDate).format('YYYY-MM-DD');
}
if (leadValue) {
if (!leadValueCurrency) {
throw new Error('lead Value Currency is required when lead Value is provided');
}
leadDefaultFields.value = {
amount: leadValue,
currency: leadValueCurrency,
};
}
const leadCustomFields: Record<string, any> = {};
Object.entries(customFields).forEach(([key, value]) => {
// Format values if they are arrays
leadCustomFields[key] = Array.isArray(value) && value.length > 0 ? value.join(',') : value;
});
const updatedLeadResponse = await pipedriveApiCall<GetLeadResponse>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.PATCH,
resourceUri: `/v1/leads/${leadId}`,
body: {
...leadDefaultFields,
...leadCustomFields,
},
});
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/dealFields',
});
const transformedLeadProperties = pipedriveTransformV1CustomFields(
customFieldsResponse,
updatedLeadResponse.data,
);
return {
...updatedLeadResponse,
data: transformedLeadProperties,
};
},
});

View File

@@ -0,0 +1,80 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { organizationCommonProps, organizationIdProp } from '../common/props';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedriveParseCustomFields,
pipedriveTransformCustomFields,
} from '../common';
import { GetField, GetOrganizationResponse } from '../common/types';
import { HttpMethod } from '@activepieces/pieces-common';
import { isEmpty } from '@activepieces/shared';
export const updateOrganizationAction = createAction({
auth: pipedriveAuth,
name: 'update-organization',
displayName: 'Update Organization',
description: 'Updates an existing organization.',
props: {
organizationId: organizationIdProp(true),
name: Property.ShortText({
displayName: 'Name',
required: false,
}),
...organizationCommonProps,
},
async run(context) {
const { name, ownerId, address, visibleTo, organizationId } = context.propsValue;
const labelIds = (context.propsValue.labelIds as number[]) ?? [];
const customFields = context.propsValue.customfields ?? {};
const organizationPayload: Record<string, any> = {
name: name,
owner_id: ownerId,
visible_to: visibleTo,
};
if (address) {
if (typeof address === 'string') {
organizationPayload.address = { value: address };
}
}
if (labelIds.length > 0) {
organizationPayload.label_ids = labelIds;
}
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/organizationFields',
});
const orgCustomFields = pipedriveParseCustomFields(customFieldsResponse, customFields);
if (!isEmpty(orgCustomFields)) {
organizationPayload.custom_fields = orgCustomFields;
}
const updatedOrganizationResponse = await pipedriveApiCall<GetOrganizationResponse>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.PATCH,
resourceUri: `/v2/organizations/${organizationId}`,
body: organizationPayload,
});
const transformedOrganizationProperties = pipedriveTransformCustomFields(
customFieldsResponse,
updatedOrganizationResponse.data,
);
return {
...updatedOrganizationResponse,
data: transformedOrganizationProperties,
};
},
});

View File

@@ -0,0 +1,108 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { personCommonProps, personIdProp, customFieldsProp } from '../common/props';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedriveParseCustomFields,
pipedriveTransformCustomFields,
} from '../common';
import { GetField, GetPersonResponse } from '../common/types';
import { HttpMethod } from '@activepieces/pieces-common';
import { isEmpty } from '@activepieces/shared';
export const updatePersonAction = createAction({
auth: pipedriveAuth,
name: 'update-person',
displayName: 'Update Person',
description: 'Updates an existing person.',
props: {
personId: personIdProp(true),
name: Property.ShortText({
displayName: 'Name',
required: false,
}),
...personCommonProps,
},
async run(context) {
const {
name,
ownerId,
personId,
organizationId,
marketing_status,
visibleTo,
firstName,
lastName,
} = context.propsValue;
const rawPhones = (context.propsValue.phone as string[]) ?? [];
const rawEmails = (context.propsValue.email as string[]) ?? [];
const labelIds = (context.propsValue.labelIds as number[]) ?? [];
const customFields = context.propsValue.customfields ?? {};
if ((firstName && !lastName) || (!firstName && lastName)) {
throw new Error('If First Name is provided, Last Name must be provided as well.');
}
const phones = rawPhones.map((value, index) => ({
value,
label: 'work',
primary: index === 0,
}));
const emails = rawEmails.map((value, index) => ({
value,
label: 'work',
primary: index === 0,
}));
const personPayload: Record<string, any> = {
name: name,
owner_id: ownerId,
org_id: organizationId,
marketing_status: marketing_status,
visible_to: visibleTo,
first_name: firstName,
last_name: lastName,
};
// Phones and emails
if (phones.length) personPayload.phones = phones;
if (emails.length) personPayload.emails = emails;
if (labelIds.length) personPayload.label_ids = labelIds;
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/personFields',
});
const personCustomFields = pipedriveParseCustomFields(customFieldsResponse, customFields);
if (!isEmpty(personCustomFields)) {
personPayload.custom_fields = personCustomFields;
}
const updatedPersonResponse = await pipedriveApiCall<GetPersonResponse>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.PATCH,
resourceUri: `/v2/persons/${personId}`,
body: personPayload,
});
const transformedPersonProperties = pipedriveTransformCustomFields(
customFieldsResponse,
updatedPersonResponse.data,
);
return {
...updatedPersonResponse,
data: transformedPersonProperties,
};
},
});

View File

@@ -0,0 +1,142 @@
import { pipedriveAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { customFieldsProp, ownerIdProp, visibleToProp } from '../common/props';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedriveParseCustomFields,
pipedriveTransformCustomFields,
} from '../common';
import { GetField, GetProductResponse } from '../common/types';
import { HttpMethod } from '@activepieces/pieces-common';
import { isEmpty } from '@activepieces/shared';
export const updateProductAction = createAction({
auth: pipedriveAuth,
name: 'update-product',
displayName: 'Update Product',
description: 'Updates an existing product.',
props: {
id: Property.ShortText({
displayName: 'Product ID',
required: true,
}),
name: Property.ShortText({
displayName: 'Name',
required: false,
}),
code: Property.ShortText({
displayName: 'Code',
required: false,
}),
description: Property.LongText({
displayName: 'Description',
required: false,
}),
unit: Property.ShortText({
displayName: 'Unit',
required: false,
}),
tax: Property.Number({
displayName: 'Tax percentage',
required: false,
}),
isActive: Property.Checkbox({
displayName: 'Is Active ?',
required: false,
defaultValue: true,
}),
ownerId: ownerIdProp('Owner', false),
currency: Property.ShortText({
displayName: 'Currency',
required: false,
description: 'Please enter currency code (e.g., "USD", "EUR").',
}),
price: Property.Number({
displayName: 'Price',
required: false,
}),
cost: Property.Number({
displayName: 'Cost',
required: false,
}),
overheadCost: Property.Number({
displayName: 'Overhead Cost',
required: false,
}),
visibleTo: visibleToProp,
customfields: customFieldsProp('product'),
},
async run(context) {
const {
id,
name,
code,
description,
unit,
tax,
isActive,
ownerId,
currency,
price,
cost,
overheadCost,
visibleTo,
} = context.propsValue;
const customFields = context.propsValue.customfields ?? {};
const productPayload: Record<string, any> = {
name,
code,
description,
unit,
tax,
is_deleted: !isActive,
prices: [
{
price: price ?? 0,
currency: currency ?? 'USD',
cost: cost ?? 0,
direct_cost: overheadCost ?? 0,
},
],
visible_to: visibleTo,
};
if (ownerId) {
productPayload.owner_id = ownerId;
}
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/productFields',
});
const productCustomFields = pipedriveParseCustomFields(customFieldsResponse, customFields);
if (!isEmpty(productCustomFields)) {
productPayload.custom_fields = productCustomFields;
}
const updatedProductResponse = await pipedriveApiCall<GetProductResponse>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.PATCH,
resourceUri: '/v2/products/' + id,
body: productPayload,
});
const updatedProductProperties = pipedriveTransformCustomFields(
customFieldsResponse,
updatedProductResponse.data,
);
return {
...updatedProductResponse,
data: updatedProductProperties,
};
},
});

View File

@@ -0,0 +1,62 @@
export const PERSON_OPTIONAL_FIELDS = [
'next_activity_id',
'last_activity_id',
'open_deals_count',
'related_open_deals_count',
'closed_deals_count',
'related_closed_deals_count',
'participant_open_deals_count',
'participant_closed_deals_count',
'email_messages_count',
'activities_count',
'done_activities_count',
'undone_activities_count',
'files_count',
'notes_count',
'followers_count',
'related_won_deals_count',
'lost_deals_count',
'related_lost_deals_count',
'last_incoming_mail_time',
'last_outgoing_mail_time',
'won_deals_count',
];
export const DEAL_OPTIONAL_FIELDS = [
'next_activity_id',
'last_activity_id',
'first_won_time',
'products_count',
'files_count',
'notes_count',
'followers_count',
'email_messages_count',
'activities_count',
'done_activities_count',
'undone_activities_count',
'participants_count',
'last_incoming_mail_time',
'last_outgoing_mail_time',
'smart_bcc_email',
];
export const ORGANIZATION_OPTIONAL_FIELDS = [
'next_activity_id',
'last_activity_id',
'open_deals_count',
'related_open_deals_count',
'closed_deals_count',
'related_closed_deals_count',
'email_messages_count',
'people_count',
'activities_count',
'done_activities_count',
'undone_activities_count',
'files_count',
'notes_count',
'followers_count',
'won_deals_count',
'related_won_deals_count',
'lost_deals_count',
'related_lost_deals_count',
];

View File

@@ -0,0 +1,341 @@
import {
AuthenticationType,
httpClient,
HttpError,
HttpMessageBody,
HttpMethod,
HttpRequest,
} from '@activepieces/pieces-common';
import { GetField, PaginatedV2Response, PaginatedV1Response, RequestParams } from './types';
import { isEmpty, isNil } from '@activepieces/shared';
import dayjs from 'dayjs';
type FlexibleQueryParams = Record<
string,
string | number | boolean | string[] | number[] | null | undefined
>;
export const pipedriveCommon = {
subscribeWebhook: async (
object: string,
action: string,
webhookUrl: string,
apiDomain: string,
accessToken: string,
) => {
const request: HttpRequest = {
method: HttpMethod.POST,
url: `${apiDomain}/api/v1/webhooks`,
body: {
event_object: object,
event_action: action,
subscription_url: webhookUrl,
version: '2.0',
},
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
queryParams: {},
};
const { body: webhook } = await httpClient.sendRequest<{
data: { id: string };
}>(request);
return webhook;
},
unsubscribeWebhook: async (webhookId: string, apiDomain: string, accessToken: string) => {
const request: HttpRequest = {
method: HttpMethod.DELETE,
url: `${apiDomain}/api/v1/webhooks/${webhookId}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
};
return await httpClient.sendRequest(request);
},
};
export type PipedriveApiCallParams = {
accessToken: string;
apiDomain: string;
method: HttpMethod;
resourceUri: string;
query?: RequestParams;
body?: any;
};
export async function pipedriveApiCall<T extends HttpMessageBody>({
accessToken,
apiDomain,
method,
resourceUri,
query,
body,
}: PipedriveApiCallParams): Promise<T> {
const url = `${apiDomain}/api${resourceUri}`;
const qs: Record<string, string> = {};
if (query) {
for (const [key, value] of Object.entries(query)) {
if (value !== null && value !== undefined) {
qs[key] = Array.isArray(value) ? value.map(String).join(',') : String(value);
}
}
}
let requestBody: any;
if (body) {
requestBody = Object.entries(body).reduce((acc, [key, value]) => {
if (!isNil(value)) {
acc[key] = value;
}
return acc;
}, {} as Record<string, any>);
}
const request: HttpRequest = {
method,
url,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
queryParams: qs,
body: requestBody,
};
try {
const response = await httpClient.sendRequest<T>(request);
return response.body;
} catch (error) {
if (error instanceof HttpError) {
if (error.response.status === 403) {
throw new Error('Please reconnect your Pipedrive account.');
}
}
throw error;
}
}
export async function pipedrivePaginatedV1ApiCall<T extends HttpMessageBody>({
accessToken,
apiDomain,
method,
resourceUri,
query,
body,
}: PipedriveApiCallParams): Promise<T[]> {
const qs = query ? query : {};
qs.start = 0;
qs.limit = 500;
const resultData: T[] = [];
let hasMoreItems = true;
do {
const response = await pipedriveApiCall<PaginatedV1Response<T>>({
accessToken,
apiDomain,
method,
resourceUri,
query: qs,
body,
});
if (isNil(response.data)) {
break;
}
resultData.push(...response.data);
qs.start = response.additional_data.pagination.next_start;
hasMoreItems = response.additional_data.pagination.more_items_in_collection;
} while (hasMoreItems);
return resultData;
}
export async function pipedrivePaginatedV2ApiCall<T extends HttpMessageBody>({
accessToken,
apiDomain,
method,
resourceUri,
query,
body,
}: PipedriveApiCallParams): Promise<T[]> {
const qs: FlexibleQueryParams = query ? { ...query } : {};
let cursor: string | undefined = undefined;
const resultData: T[] = [];
let hasMoreItems = true;
do {
const currentQuery: FlexibleQueryParams = { ...qs };
if (cursor) {
currentQuery.cursor = cursor;
}
currentQuery.limit = 500;
const response = await pipedriveApiCall<PaginatedV2Response<T>>({
accessToken,
apiDomain,
method,
resourceUri,
query: currentQuery as RequestParams,
body,
});
if (isNil(response.data)) {
break;
}
if (Array.isArray(response.data)) {
resultData.push(...response.data);
} else {
resultData.push(response.data);
}
hasMoreItems = response.additional_data?.next_cursor != null;
cursor = response.additional_data?.next_cursor;
} while (hasMoreItems && cursor);
return resultData;
}
function formatDateIfValid(val: any) {
return dayjs(val).isValid() ? dayjs(val).format('YYYY-MM-DD') : val;
}
export function pipedriveParseCustomFields(
customFieldsDefinitions: GetField[],
inputData: Record<string, any>,
): Record<string, any> {
const fieldTypeMap: Record<string, GetField> = customFieldsDefinitions.reduce((acc, field) => {
acc[field.key] = field;
return acc;
}, {} as Record<string, GetField>);
const parsedFields: Record<string, any> = {};
for (const [key, value] of Object.entries(inputData)) {
if (isEmpty(value)) continue;
const matchedField = fieldTypeMap[key];
if (!matchedField) continue;
// https://pipedrive.readme.io/docs/pipedrive-api-v2-migration-guide#custom-fields
if (matchedField.is_subfield) {
const parentField = key.split('_')[0];
parsedFields[parentField] = {
...parsedFields[parentField],
[matchedField.id_suffix]: matchedField.field_type === "date" ? formatDateIfValid(value) : value,
};
continue;
}
switch (matchedField.field_type) {
case 'monetary':
parsedFields[key] = { ...parsedFields[key], value: Number(value) };
break;
case 'address':
case 'daterange':
case 'timerange':
case 'time':
parsedFields[key] = { ...parsedFields[key], value: formatDateIfValid(value) };
break;
case 'date':
parsedFields[key] = formatDateIfValid(value);
break;
default:
parsedFields[key] = value;
break;
}
}
return parsedFields;
}
export function pipedriveTransformCustomFields(
customFieldsDefinitions: GetField[],
responseData: Record<string, any>,
): Record<string, any> {
const updatedResponseData = { ...responseData };
const rawCustomFields = responseData.custom_fields || {};
for (const field of customFieldsDefinitions) {
if (!field.edit_flag) {
continue;
}
const oldKey = field.key;
const newKey = field.name;
const fieldType = field.field_type;
if (oldKey in rawCustomFields) {
const value = rawCustomFields[oldKey];
if (isNil(value)) {
updatedResponseData[newKey] = null;
} else if (fieldType === 'enum') {
updatedResponseData[newKey] =
field.options?.find((option) => option.id === value)?.label || null;
} else if (fieldType === 'set') {
if (Array.isArray(value)) {
updatedResponseData[newKey] = value.map(
(item) => field.options?.find((option) => option.id === item)?.label || null,
);
} else {
updatedResponseData[newKey] = value;
}
} else {
updatedResponseData[newKey] = value;
}
}
}
delete updatedResponseData.custom_fields;
return updatedResponseData;
}
export function pipedriveTransformV1CustomFields(
CustomFields: GetField[],
responseData: Record<string, any>,
): Record<string, any> {
const updatedResponseData = { ...responseData };
for (const field of CustomFields) {
if (!field.edit_flag) {
continue;
}
const oldKey = field.key;
const newKey = field.name;
const fieldType = field.field_type;
if (oldKey in responseData) {
if (responseData[oldKey] === null || responseData[oldKey] === undefined) {
updatedResponseData[newKey] = null;
} else if (fieldType === 'enum') {
updatedResponseData[newKey] =
field.options?.find((option) => option.id.toString() === responseData[oldKey])?.label ||
null;
} else if (fieldType === 'set') {
const values: string[] = responseData[oldKey].split(',');
updatedResponseData[newKey] = values.map(
(item) => field.options?.find((option) => option.id.toString() === item)?.label || null,
);
} else {
updatedResponseData[newKey] = responseData[oldKey];
}
delete updatedResponseData[oldKey];
}
}
return updatedResponseData;
}

View File

@@ -0,0 +1,963 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { pipedriveApiCall, pipedrivePaginatedV1ApiCall, pipedrivePaginatedV2ApiCall } from '.';
import { pipedriveAuth } from '../../index';
import {
DropdownOption,
DynamicPropsValue,
PiecePropValueSchema,
Property,
} from '@activepieces/pieces-framework';
import { GetField, StageWithPipelineInfo } from './types';
import { isNil } from '@activepieces/shared';
import { context } from '@opentelemetry/api';
/**
* Fetches options for Pipedrive filters.
* @param auth Pipedrive authentication.
* @param type The type of object for the filter (e.g., 'deals', 'people', 'org').
* @returns Dropdown options for filters.
*/
export async function fetchFiltersOptions(
auth: PiecePropValueSchema<typeof pipedriveAuth>,
type: string,
): Promise<DropdownOption<number>[]> {
const filters = await pipedriveApiCall<{ data: Array<{ id: number; name: string }> }>({
accessToken: auth.access_token,
apiDomain: auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/filters',
query: {
type: type,
},
});
const options: DropdownOption<number>[] = [];
for (const filter of filters.data) {
options.push({
label: filter.name,
value: filter.id,
});
}
return options;
}
/**
* Fetches options for Pipedrive activity types.
* @param auth Pipedrive authentication.
* @returns Dropdown options for activity types.
*/
export async function fetchActivityTypesOptions(
auth: PiecePropValueSchema<typeof pipedriveAuth>,
): Promise<DropdownOption<string>[]> {
const activityTypes = await pipedriveApiCall<{
data: Array<{ key_string: string; name: string }>;
}>({
accessToken: auth.access_token,
apiDomain: auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/activityTypes',
});
const options: DropdownOption<string>[] = [];
for (const type of activityTypes.data) {
options.push({
label: type.name,
value: type.key_string,
});
}
return options;
}
/**
* Fetches options for Pipedrive pipelines.
* @param auth Pipedrive authentication.
* @returns Dropdown options for pipelines.
*/
export async function fetchPipelinesOptions(
auth: PiecePropValueSchema<typeof pipedriveAuth>,
): Promise<DropdownOption<number>[]> {
const pipelines = await pipedrivePaginatedV2ApiCall<{ id: number; name: string }>({
accessToken: auth.access_token,
apiDomain: auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/pipelines',
});
const options: DropdownOption<number>[] = [];
for (const pipeline of pipelines) {
options.push({
label: pipeline.name,
value: pipeline.id,
});
}
return options;
}
/**
* Fetches options for Pipedrive persons.
* @param auth Pipedrive authentication.
* @returns Dropdown options for persons.
*/
export async function fetchPersonsOptions(
auth: PiecePropValueSchema<typeof pipedriveAuth>,
): Promise<DropdownOption<number>[]> {
const persons = await pipedriveApiCall<{ data: Array<{ id: number; name: string }> }>({
accessToken: auth.access_token,
apiDomain: auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/persons',
query: {
sort_by: 'update_time',
sort_direction: 'desc',
},
});
const options: DropdownOption<number>[] = [];
for (const person of persons.data) {
options.push({
label: person.name,
value: person.id,
});
}
return options;
}
/**
* Fetches options for Pipedrive users (owners).
* @param auth Pipedrive authentication.
* @returns Dropdown options for owners.
*/
export async function fetchOwnersOptions(
auth: PiecePropValueSchema<typeof pipedriveAuth>,
): Promise<DropdownOption<number>[]> {
const users = await pipedriveApiCall<{ data: Array<{ id: number; email: string }> }>({
accessToken: auth.access_token,
apiDomain: auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/users:(id,email)',
});
const options: DropdownOption<number>[] = [];
for (const user of users.data) {
options.push({
label: user.email,
value: user.id,
});
}
return options;
}
/**
* Creates a property definition for a custom field based on its Pipedrive field type.
* @param property The Pipedrive custom field definition.
* @returns A Property.ShortText, Property.LongText, Property.StaticDropdown, Property.StaticMultiSelectDropdown, or Property.Number.
*/
export function createPropertyDefinition(property: GetField) {
switch (property.field_type) {
case 'varchar':
case 'varchar_auto':
case 'phone':
return Property.ShortText({
displayName: property.name,
required: false,
});
case 'text':
case 'address':
return Property.LongText({
displayName: property.name,
required: false,
});
case 'enum':
return Property.StaticDropdown({
displayName: property.name,
required: false,
options: {
disabled: false,
options: property.options
? property.options.map((option) => {
return {
label: option.label,
value: option.id,
};
})
: [],
},
});
case 'set':
return Property.StaticMultiSelectDropdown({
displayName: property.name,
required: false,
options: {
disabled: false,
options: property.options
? property.options.map((option) => {
return {
label: option.label,
value: option.id,
};
})
: [],
},
});
case 'double':
case 'monetary':
return Property.Number({
displayName: property.name,
required: false,
});
case 'time':
case 'timerange':
return Property.ShortText({
displayName: property.name,
description: 'Please enter time in HH:mm:ss format.',
required: false,
});
case 'int':
return Property.Number({
displayName: property.name,
required: false,
});
case 'date':
case 'daterange':
return Property.DateTime({
displayName: property.name,
description: 'Please enter date in YYYY-MM-DD format.',
required: false,
});
default:
return null;
}
}
/**
* Retrieves custom properties for a given Pipedrive object type.
* @param auth Pipedrive authentication.
* @param objectType The type of object (e.g., 'person', 'deal', 'organization', 'product', 'lead').
* @returns Dynamic properties for custom fields.
*/
export async function retrieveObjectCustomProperties(
auth: PiecePropValueSchema<typeof pipedriveAuth>,
objectType: string,
) {
let endpoint = '';
switch (objectType) {
case 'person':
endpoint = '/v1/personFields';
break;
case 'deal':
case 'lead':
endpoint = '/v1/dealFields';
break;
case 'organization':
endpoint = '/v1/organizationFields';
break;
case 'product':
endpoint = '/v1/productFields';
break;
}
const customFields = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: auth.access_token,
apiDomain: auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: endpoint,
});
const props: DynamicPropsValue = {};
for (const field of customFields) {
if (!field.edit_flag) {
continue;
}
const propertyDefinition = createPropertyDefinition(field);
if (propertyDefinition) {
props[field.key] = propertyDefinition;
}
}
return props;
}
/**
* Property definition for selecting a search field for an object type.
* @param objectType The type of object (e.g., 'deal', 'person').
*/
export const searchFieldProp = (objectType: string) =>
Property.Dropdown({
auth: pipedriveAuth,
displayName: 'Field to search by',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account.',
};
}
const authValue = auth as PiecePropValueSchema<typeof pipedriveAuth>;
let endpoint = '';
switch (objectType) {
case 'person':
endpoint = '/v1/personFields';
break;
case 'deal':
case 'lead':
endpoint = '/v1/dealFields';
break;
case 'organization':
endpoint = '/v1/organizationFields';
break;
case 'product':
endpoint = '/v1/productFields';
break;
}
const response = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: authValue.access_token,
apiDomain: authValue.data['api_domain'],
method: HttpMethod.GET,
resourceUri: endpoint,
});
const options: DropdownOption<string>[] = [];
for (const field of response) {
if (!isNil(field.id)) {
options.push({
label: field.name,
value: field.id.toString(),
});
}
}
return {
disabled: false,
options,
};
},
});
/**
* Property definition for the value of a search field.
* Dynamically renders based on the selected searchField's type.
* @param objectType The type of object (e.g., 'deal', 'person').
*/
export const searchFieldValueProp = (objectType: string) =>
Property.DynamicProperties({
auth: pipedriveAuth,
displayName: 'Field Value',
required: true,
refreshers: ['searchField'],
props: async ({ auth, searchField }) => {
if (!auth || !searchField) return {};
const authValue = auth as PiecePropValueSchema<typeof pipedriveAuth>;
const props: DynamicPropsValue = {};
let endpoint = '';
switch (objectType) {
case 'person':
endpoint = '/v1/personFields';
break;
case 'deal':
case 'lead':
endpoint = '/v1/dealFields';
break;
case 'organization':
endpoint = '/v1/organizationFields';
break;
case 'product':
endpoint = '/v1/productFields';
break;
}
const response = await pipedriveApiCall<{ data: GetField }>({
accessToken: authValue.access_token,
apiDomain: authValue.data['api_domain'],
method: HttpMethod.GET,
resourceUri: `${endpoint}/${searchField}`,
});
const propertyDefinition =
response.data.field_type === 'set'
? Property.StaticDropdown({
displayName: response.data.name,
required: false,
options: {
disabled: false,
options: response.data.options
? response.data.options.map((option) => {
return {
label: option.label,
value: option.id.toString(),
};
})
: [],
},
})
: createPropertyDefinition(response.data);
if (propertyDefinition) {
props['field_value'] = propertyDefinition;
} else {
props['field_value'] = Property.ShortText({
displayName: response.data.name,
required: false,
});
}
return props;
},
});
export const ownerIdProp = (displayName: string, required = false) =>
Property.Dropdown({
auth: pipedriveAuth,
displayName,
refreshers: [],
required,
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account.',
};
}
const authValue = auth as PiecePropValueSchema<typeof pipedriveAuth>;
const options = await fetchOwnersOptions(authValue);
return {
disabled: false,
options,
};
},
});
export const filterIdProp = (type: string, required = false) =>
Property.Dropdown({
auth: pipedriveAuth,
displayName: 'Filter',
refreshers: [],
required,
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account.',
};
}
const authValue = auth as PiecePropValueSchema<typeof pipedriveAuth>;
const options = await fetchFiltersOptions(authValue, type);
return {
disabled: false,
options,
};
},
});
export const organizationIdProp = (required = false) =>
Property.Number({
displayName: 'Organization ID',
description: 'You can use Find Organization action to retrieve org ID.',
required,
});
export const dealPipelineIdProp = (required = false) =>
Property.Dropdown({
auth: pipedriveAuth,
displayName: 'Pipeline',
refreshers: [],
required,
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account.',
};
}
const authValue = auth as PiecePropValueSchema<typeof pipedriveAuth>;
const options = await fetchPipelinesOptions(authValue);
return {
disabled: false,
options,
};
},
});
export const dealStageIdProp = (required = false) =>
Property.Dropdown({
auth: pipedriveAuth,
displayName: 'Stage',
description: 'If a stage is chosen above, the pipeline field will be ignored.',
required,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
placeholder: 'Please connect your account.',
disabled: true,
options: [],
};
}
const authValue = auth as PiecePropValueSchema<typeof pipedriveAuth>;
const response = await pipedrivePaginatedV2ApiCall<StageWithPipelineInfo>({
accessToken: authValue.access_token,
apiDomain: authValue.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/stages',
});
const options: DropdownOption<number>[] = [];
for (const stage of response) {
options.push({
label: `${stage.name}`,
value: stage.id,
});
}
return {
disabled: false,
options,
};
},
});
export const personIdProp = (required = false) =>
Property.Number({
displayName: 'Person ID',
description: 'You can use Find Person action to retrieve person ID.',
required,
});
export const labelIdsProp = (objectType: string, labelFieldName: string, required = false) =>
Property.MultiSelectDropdown({
auth: pipedriveAuth,
displayName: 'Label',
required,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account.',
};
}
const authValue = auth as PiecePropValueSchema<typeof pipedriveAuth>;
let endpoint = '';
switch (objectType) {
case 'person':
endpoint = '/v1/personFields';
break;
case 'deal':
endpoint = '/v1/dealFields';
break;
case 'organization':
endpoint = '/v1/organizationFields';
break;
}
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: authValue.access_token,
apiDomain: authValue.data['api_domain'],
method: HttpMethod.GET,
resourceUri: endpoint,
});
// Find the specific label field within the custom fields response
const labelField = customFieldsResponse.find((field) => field.key === labelFieldName);
const options: DropdownOption<number>[] = [];
if (labelField && labelField.options) {
for (const option of labelField.options) {
options.push({
label: option.label,
value: option.id,
});
}
}
return {
disabled: false,
options,
};
},
});
export const leadLabelIdsProp = (required = false) =>
Property.MultiSelectDropdown({
auth: pipedriveAuth,
displayName: 'Label',
required,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account.',
};
}
const authValue = auth as PiecePropValueSchema<typeof pipedriveAuth>;
const leadLabelsResponse = await pipedriveApiCall<{
data: Array<{ id: string; name: string; color: string }>;
}>({
accessToken: authValue.access_token,
apiDomain: authValue.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/leadLabels',
});
const options: DropdownOption<string>[] = [];
for (const option of leadLabelsResponse.data) {
options.push({
label: `${option.name} (${option.color})`,
value: option.id,
});
}
return {
disabled: false,
options,
};
},
});
export const productIdProp = (required = false) =>
Property.Number({
displayName: 'Product ID',
description: 'You can use Find Product action to retrieve product ID.',
required,
});
/**
* Property definition for 'Visible To' setting.
*/
export const visibleToProp = Property.StaticDropdown({
displayName: 'Visible To',
required: false,
options: {
disabled: false,
options: [
{
label: 'Owner & followers',
value: 1,
},
{
label: 'Entire company',
value: 3,
},
],
},
});
export const leadIdProp = (required = false) =>
Property.ShortText({
displayName: 'Lead ID',
required,
});
export const activityTypeIdProp = (required = false) =>
Property.Dropdown({
auth: pipedriveAuth,
displayName: 'Activity Type',
refreshers: [],
required,
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account.',
};
}
const authValue = auth as PiecePropValueSchema<typeof pipedriveAuth>;
const options = await fetchActivityTypesOptions(authValue);
return {
disabled: false,
options,
};
},
});
export const dealIdProp = (required = false) =>
Property.Number({
displayName: 'Deal ID',
description: 'You can use Find Deal action to retrieve deal ID.',
required,
});
export const dealCommonProps = {
creationTime: Property.DateTime({
displayName: 'Creation Time',
required: false,
}),
status: Property.StaticDropdown({
displayName: 'Status',
required: false,
options: {
disabled: false,
options: [
{
label: 'Open',
value: 'open',
},
{
label: 'Won',
value: 'won',
},
{
label: 'Lost',
value: 'lost',
},
{
label: 'Deleted',
value: 'deleted',
},
],
},
}),
stageId: dealStageIdProp(false),
pipelineId: dealPipelineIdProp(false),
ownerId: ownerIdProp('Owner', false),
organizationId: organizationIdProp(false),
personId: personIdProp(false),
labelIds: labelIdsProp('deal', 'label', false),
probability: Property.Number({
displayName: 'Probability',
required: false,
}),
expectedCloseDate: Property.DateTime({
displayName: 'Expected Close Date',
required: false,
description: 'Please enter date in YYYY-MM-DD format.',
}),
dealValue: Property.Number({
displayName: 'Value',
required: false,
}),
dealValueCurrency: Property.ShortText({
displayName: 'Currency',
required: false,
description: 'Please enter currency code (e.g., "USD", "EUR").',
}),
visibleTo: visibleToProp,
customfields: customFieldsProp('deal'),
};
/**
* Common properties for Lead actions.
*/
export const leadCommonProps = {
ownerId: ownerIdProp('Owner', false),
organizationId: organizationIdProp(false),
personId: personIdProp(false),
labelIds: leadLabelIdsProp(false),
expectedCloseDate: Property.DateTime({
displayName: 'Expected Close Date',
required: false,
description: 'Please enter date in YYYY-MM-DD format.',
}),
visibleTo: visibleToProp,
channel: Property.Dropdown({
auth: pipedriveAuth,
displayName: 'Channel',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account.',
};
}
const authValue = auth as PiecePropValueSchema<typeof pipedriveAuth>;
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: authValue.access_token,
apiDomain: authValue.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/dealFields:(key,name,options)',
});
const channelField = customFieldsResponse.find((field) => field.key === 'channel');
const options: DropdownOption<number>[] = [];
if (channelField) {
for (const option of channelField.options ?? []) {
options.push({
label: option.label,
value: option.id,
});
}
}
return {
disabled: false,
options,
};
},
}),
leadValue: Property.Number({
displayName: 'Lead Value Amount',
required: false,
}),
leadValueCurrency: Property.ShortText({
displayName: 'Lead Value Currency',
required: false,
description: 'The currency of the lead value (e.g., "USD", "EUR").',
}),
customfields: customFieldsProp('lead'),
};
/**
* Common properties for Organization actions.
*/
export const organizationCommonProps = {
ownerId: ownerIdProp('Owner', false),
visibleTo: visibleToProp,
labelIds: labelIdsProp('organization', 'label_ids', false),
address: Property.LongText({
displayName: 'Address',
required: false,
}),
customfields: customFieldsProp('organization'),
};
export const personCommonProps = {
ownerId: ownerIdProp('Owner', false),
organizationId: organizationIdProp(false),
email: Property.Array({
displayName: 'Email',
required: false,
}),
phone: Property.Array({
displayName: 'Phone',
required: false,
}),
labelIds: labelIdsProp('person', 'label_ids', false),
firstName: Property.ShortText({
displayName: 'First Name',
required: false,
}),
lastName: Property.ShortText({
displayName: 'Last Name',
required: false,
}),
visibleTo: visibleToProp,
marketing_status: Property.StaticDropdown<string>({
displayName: 'Marketing Status',
description: 'Marketing opt-in status',
required: false,
options: {
disabled: false,
options: [
{
label: 'No Consent',
value: 'no_consent',
},
{
label: 'Unsubscribed',
value: 'unsubscribed',
},
{
label: 'Subscribed',
value: 'subscribed',
},
{
label: 'Archived',
value: 'archived',
},
],
},
}),
customfields: customFieldsProp('person'),
};
export const activityCommonProps = {
organizationId: organizationIdProp(false),
personId: personIdProp(false),
dealId: dealIdProp(false),
leadId: leadIdProp(false),
assignTo: ownerIdProp('Assign To', false),
type: activityTypeIdProp(false),
dueDate: Property.DateTime({
displayName: 'Due Date',
required: false,
description: 'Please enter date in YYYY-MM-DD format.',
}),
dueTime: Property.ShortText({
displayName: 'Due Time',
required: false,
description: 'Please enter time in HH:MM format.',
}),
duration: Property.ShortText({
displayName: 'Duration',
required: false,
description: 'Please enter time in HH:MM format (e.g., "01:30" for 1 hour 30 minutes).',
}),
isDone: Property.Checkbox({
displayName: 'Mark as Done?',
required: false,
defaultValue: false,
}),
busy: Property.StaticDropdown({
displayName: 'Free or Busy',
required: false,
options: {
disabled: false,
options: [
{
label: 'Free',
value: 'free',
},
{
label: 'Busy',
value: 'busy',
},
],
},
}),
note: Property.LongText({
displayName: 'Note',
required: false,
}),
publicDescription: Property.LongText({
displayName: 'Public Description',
required: false,
}),
};
// Helper function for custom fields property definition
export function customFieldsProp(objectType: string) {
return Property.DynamicProperties({
auth: pipedriveAuth,
displayName: 'Custom Fields',
required: false,
refreshers: [],
props: async ({ auth }) => {
if (!auth) return {};
const authValue = auth;
return await retrieveObjectCustomProperties(authValue, objectType);
},
});
}

View File

@@ -0,0 +1,410 @@
export interface PipedriveDealV2 {
id: number;
title: string;
creator_user_id: number; // No longer an object, just the ID
owner_id: number;
person_id: number | null; // No longer an object, just the ID
org_id: number | null; // No longer an object, just the ID
stage_id: number;
pipeline_id: number;
value: number;
currency: string;
add_time: string; // RFC 3339 format
update_time: string; // RFC 3339 format
stage_change_time: string; // RFC 3339 format
is_deleted: boolean; // Replaces 'active' and 'deleted' flags, is negation of old 'active'
status: 'open' | 'won' | 'lost';
probability: number | null;
lost_reason: string | null;
visible_to: number; // Is an integer now
close_time: string | null; // RFC 3339 format
won_time: string | null; // RFC 3339 format
first_won_time?: string; // RFC 3339 format, included only when using `include_fields` parameter
lost_time: string | null; // RFC 3339 format
products_count?: number; // Included only when using `include_fields` parameter
files_count?: number; // Included only when using `include_fields` parameter
notes_count?: number; // Included only when using `include_fields` parameter
followers_count?: number; // Included only when using `include_fields` parameter
email_messages_count?: number; // Included only when using `include_fields` parameter
activities_count?: number;
done_activities_count?: number;
undone_activities_count?: number;
participants_count?: number;
expected_close_date: string | null; // YYYY-MM-DD
last_incoming_mail_time?: string; // RFC 3339 format
last_outgoing_mail_time?: string; // RFC 3339 format
label_ids: number[]; // Replaces 'label' (array of IDs)
rotten_time: string | null; // RFC 3339 format
smart_bcc_email?: string;
acv?: number;
arr?: number;
mrr?: number;
custom_fields: Record<string, unknown>;
}
export interface PipedriveActivityV2 {
id: number;
subject: string;
owner_id: number;
type: string;
is_deleted: boolean;
done: boolean;
conference_meeting_client: string | null;
conference_meeting_url: string | null;
conference_meeting_id: string | null;
due_date: string; // YYYY-MM-DD
due_time: string; // HH:MM
duration: string; // HH:MM
busy: boolean;
add_time: string; // RFC 3339 format
update_time: string; // RFC 3339 format
marked_as_done_time: string | null; // RFC 3339 format, null if not done
public_description: string | null;
location: { // Nested object now
value: string | null;
street_number: string | null;
route: string | null;
sublocality: string | null;
locality: string | null;
admin_area_level_1: string | null;
admin_area_level_2: string | null;
country: string | null;
postal_code: string | null;
formatted_address: string | null;
} | null;
org_id: number | null;
person_id: number | null;
deal_id: number | null;
lead_id: string | null;
project_id: number | null;
private: boolean;
priority: number;
note: string | null;
creator_user_id: number;
attendees?: { // Included only when using include_fields parameter
email_address: string;
name: string;
status: string;
is_organizer: number;
person_id: number | null;
user_id: number | null;
}[];
participants?: {
person_id: number;
primary: boolean;
}[];
}
export interface PipedrivePersonV2 {
id: number;
name: string;
first_name: string | null;
last_name: string | null;
owner_id: number;
org_id: number | null;
picture_id: number | null;
add_time: string; // RFC 3339 format
update_time: string; // RFC 3339 format
is_deleted: boolean;
visible_to: number; // Is an integer now (e.g., 1, 3, 5, 7)
phones: {
value: string;
primary: boolean;
label: string;
}[];
emails: {
value: string;
primary: boolean;
label: string;
}[];
label_ids: number[];
custom_fields: Record<string, unknown>;
next_activity_id?: number | null;
last_activity_id?: number | null;
open_deals_count?: number;
related_open_deals_count?: number;
closed_deals_count?: number;
related_closed_deals_count?: number;
participant_open_deals_count?: number;
participant_closed_deals_count?: number;
email_messages_count?: number;
activities_count?: number;
done_activities_count?: number;
undone_activities_count?: number;
files_count?: number;
notes_count?: number;
followers_count?: number;
won_deals_count?: number;
related_won_deals_count?: number;
lost_deals_count?: number;
related_lost_deals_count?: number;
last_incoming_mail_time?: string | null; // RFC 3339 format
last_outgoing_mail_time?: string | null; // RFC 3339 format
marketing_status?: string;
doi_status?: string;
}
export interface PipedriveOrganizationV2 {
id: number;
name: string;
owner_id: number; // No longer an object, just the ID
add_time: string; // RFC 3339 format
update_time: string; // RFC 3339 format
is_deleted: boolean; // Replaces active_flag, is negation of old value
visible_to: number; // Is an integer now (e.g., 1, 3, 5, 7)
picture_id: number | null;
label_ids: number[]; // Replaces 'label' (array of IDs)
address: { // Is a nested object now
value: string | null;
street_number: string | null;
route: string | null;
sublocality: string | null;
locality: string | null;
admin_area_level_1: string | null;
admin_area_level_2: string | null;
country: string | null;
postal_code: string | null;
formatted_address: string | null;
} | null;
custom_fields: Record<string, unknown>;
next_activity_id?: number | null;
last_activity_id?: number | null;
open_deals_count?: number;
related_open_deals_count?: number;
closed_deals_count?: number;
related_closed_deals_count?: number;
participant_open_deals_count?: number;
participant_closed_deals_count?: number;
email_messages_count?: number;
activities_count?: number;
done_activities_count?: number;
undone_activities_count?: number;
files_count?: number;
notes_count?: number;
followers_count?: number;
won_deals_count?: number;
related_won_deals_count?: number;
lost_deals_count?: number;
related_lost_deals_count?: number;
last_incoming_mail_time?: string | null; // RFC 3339 format
last_outgoing_mail_time?: string | null; // RFC 3339 format
marketing_status?: string;
doi_status?: string;
}
export interface PipedriveLeadV2 {
id: string; // Lead IDs are UUIDs (strings)
title: string;
owner_id: number; // No longer an object, just the ID
creator_id: number; // No longer an object, just the ID
label_ids: string[]; // Array of string UUIDs for labels
value: number | null;
expected_close_date: string | null; // YYYY-MM-DD
person_id: number | null; // No longer an object, just the ID
organization_id: number | null; // No longer an object, just the ID
is_archived: boolean;
source_name: string;
origin: string;
origin_id: string | null;
channel: number | null;
channel_id: string | null;
was_seen: boolean;
next_activity_id: number | null;
add_time: string; // RFC 3339 format
update_time: string; // RFC 3339 format
visible_to: number; // Is an integer now (e.g., 1, 3, 5, 7)
custom_fields?: Record<string, unknown>; // Custom fields are now nested here
}
export interface PipedriveNoteV2 {
id: number;
user_id: number; // The user who owns the note (owner_id in other contexts)
deal_id: number | null;
person_id: number | null;
org_id: number | null;
lead_id: string | null; // Lead IDs are UUIDs
content: string;
add_time: string; // RFC 3339 format
update_time: string; // RFC 3339 format
is_deleted: boolean; // Replaces active_flag, is negation of old value
last_update_user_id: number | null;
}
export interface PipedriveProductV2 {
id: number;
name: string;
code: string | null;
description: string | null;
unit: string | null;
tax: number | null;
prices: Array<{
id?: number;
product_id?: number;
price: number;
currency: string;
cost?: number;
direct_cost?: number;
overhead_cost?: number; // Keep for backward compatibility if needed, but direct_cost is preferred
}>;
owner_id: number;
add_time: string; // RFC 3339 format
update_time: string; // RFC 3339 format
is_deleted: boolean; // Replaces active_flag, is negation of old value
visible_to: number; // Is an integer now
custom_fields?: Record<string, unknown>;
}
// --- Common Types ---
export type GetField = {
id: number; // Field ID is a number
name: string;
key: string; // Field key is a string (e.g., hash for custom fields)
edit_flag: boolean;
is_subfield:boolean,
id_suffix:string,
field_type: "varchar" | "text" | "enum" | "set" | "varchar_auto" | "double" | "monetary" | "user" | "org" | "people" | "phone" | "time" | "int" | "timerange" | "date" | "daterange" | "address" | "lead_label"; // Added lead_label
options?: Array<{ id: number, label: string }>; // Option IDs are numbers
};
// --- Updated Pagination Types for v2 ---
type PaginationInfoV2 = {
start?: number; // Optional in response
limit?: number; // Optional in response
more_items_in_collection: boolean;
next_cursor?: string;
};
type AdditionalDataV2 = {
pagination?: PaginationInfoV2;
};
export type PaginatedV2Response<T> = {
success: boolean;
data: T[];
additional_data?: {
next_cursor?:string
};
};
export type PaginatedV1Response<T> =
{
success: boolean;
data: T[];
additional_data: {
pagination: {
start: number;
limit: number;
more_items_in_collection: boolean;
next_start: number;
};
};
}
export type FieldsResponse = {
success: boolean;
data: GetField[];
additional_data?: AdditionalDataV2;
};
export type StageWithPipelineInfo = {
id: number;
name: string;
pipeline_id: number;
};
export type GetStagesResponse = {
success: boolean;
data: StageWithPipelineInfo[];
additional_data?: AdditionalDataV2;
};
export type ListDealsResponse = {
success: boolean;
data: PipedriveDealV2[];
additional_data?: AdditionalDataV2;
};
export type GetDealResponse = {
success: boolean;
data: PipedriveDealV2;
additional_data?: AdditionalDataV2;
}
export type ListActivitiesResponse = {
success: boolean;
data: PipedriveActivityV2[];
additional_data?: AdditionalDataV2;
}
export type PersonListResponse = {
success: boolean;
data: PipedrivePersonV2[];
additional_data?: AdditionalDataV2;
}
export type GetPersonResponse = {
success: boolean;
data: PipedrivePersonV2;
additional_data?: AdditionalDataV2;
}
export type OrganizationListResponse = {
success: boolean;
data: PipedriveOrganizationV2[];
additional_data?: AdditionalDataV2;
}
export type GetOrganizationResponse = {
success: boolean;
data: PipedriveOrganizationV2;
additional_data?: AdditionalDataV2;
}
export type LeadListResponse = {
success: boolean;
data: PipedriveLeadV2[];
additional_data?: AdditionalDataV2;
}
export type GetLeadResponse = {
success: boolean;
data: PipedriveLeadV2;
additional_data?: AdditionalDataV2;
}
export type GetNoteResponse = {
success: boolean;
data: PipedriveNoteV2;
additional_data?: AdditionalDataV2;
}
export type WebhookCreateResponse = {
status?: string;
success: boolean;
data: {
id: string;
}
}
export type GetProductResponse = {
success: boolean;
data: PipedriveProductV2;
additional_data?: AdditionalDataV2;
}
export type RequestParams = Record<string, string | number | boolean | string[] | number[] | null | undefined>;

View File

@@ -0,0 +1,181 @@
import { pipedriveAuth } from '../../index';
import { HttpMethod } from '@activepieces/pieces-common';
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { filterIdProp } from '../common/props';
import { pipedriveApiCall, pipedrivePaginatedV2ApiCall } from '../common';
import { LeadListResponse } from '../common/types';
import { isNil } from '@activepieces/shared';
export const activityMatchingFilterTrigger = createTrigger({
auth: pipedriveAuth,
name: 'activity-matching-filter',
displayName: 'Activity Matching Filter',
description: 'Trigges when an activity newly matches a Pipedrive filter for the first time.',
type: TriggerStrategy.POLLING,
props: {
filterId: filterIdProp('activity', true),
},
async onEnable(context) {
const ids: number[] = [];
const response = await pipedrivePaginatedV2ApiCall<{ id: number }>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/activities',
query: { filter_id: context.propsValue.filterId },
});
if (!isNil(response)) {
response.forEach((activity) => {
ids.push(activity.id);
});
}
await context.store.put('activities', JSON.stringify(ids));
},
async onDisable(context) {
await context.store.delete('activities');
},
async test(context) {
const activities = [];
const response = await pipedriveApiCall<LeadListResponse>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/activities',
query: {
limit: 10,
filter_id: context.propsValue.filterId,
sort_by:'update_time',
sort_direction:'desc',
include_fields:'attendees'
},
});
if (isNil(response.data)) {
return [];
}
for (const activity of response.data) {
activities.push(activity);
}
return activities;
},
async run(context) {
const existingIds = (await context.store.get<string>('activities')) ?? '[]';
const parsedExistingIds = JSON.parse(existingIds) as number[];
const response = await pipedrivePaginatedV2ApiCall<{ id: number }>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/activities',
query: { filter_id: context.propsValue.filterId,include_fields:'attendees'},
});
if (isNil(response) || response.length === 0) {
return [];
}
// Filter valid activities
const newActivities = response.filter((activity) => !parsedExistingIds.includes(activity.id));
const newIds = newActivities.map((activity) => activity.id);
if (newIds.length === 0) {
return [];
}
// Store new IDs
await context.store.put('activities', JSON.stringify([...newIds, ...parsedExistingIds]));
return newActivities;
},
sampleData: {
id: 8,
company_id: 22122,
user_id: 1234,
done: false,
type: 'deadline',
reference_type: 'scheduler-service',
reference_id: 7,
conference_meeting_client: '871b8bc88d3a1202',
conference_meeting_url: 'https://pipedrive.zoom.us/link',
conference_meeting_id: '01758746701',
due_date: '2020-06-09',
due_time: '10:00',
duration: '01:00',
busy_flag: true,
add_time: '2020-06-08 12:37:56',
marked_as_done_time: '2020-08-08 08:08:38',
last_notification_time: '2020-08-08 12:37:56',
last_notification_user_id: 7655,
notification_language_id: 1,
subject: 'Deadline',
public_description: 'This is a description',
calendar_sync_include_context: '',
location: 'Mustamäe tee 3, Tallinn, Estonia',
org_id: 5,
person_id: 1101,
deal_id: 300,
lead_id: '46c3b0e1-db35-59ca-1828-4817378dff71',
active_flag: true,
update_time: '2020-08-08 12:37:56',
update_user_id: 5596,
gcal_event_id: '',
google_calendar_id: '',
google_calendar_etag: '',
source_timezone: '',
rec_rule: 'RRULE:FREQ=WEEKLY;BYDAY=WE',
rec_rule_extension: '',
rec_master_activity_id: 1,
series: [],
note: 'A note for the activity',
created_by_user_id: 1234,
location_subpremise: '',
location_street_number: '3',
location_route: 'Mustamäe tee',
location_sublocality: 'Kristiine',
location_locality: 'Tallinn',
location_admin_area_level_1: 'Harju maakond',
location_admin_area_level_2: '',
location_country: 'Estonia',
location_postal_code: '10616',
location_formatted_address: 'Mustamäe tee 3, 10616 Tallinn, Estonia',
attendees: [
{
email_address: 'attendee@pipedrivemail.com',
is_organizer: 0,
name: 'Attendee',
person_id: 25312,
status: 'noreply',
user_id: null,
},
],
participants: [
{
person_id: 17985,
primary_flag: false,
},
{
person_id: 1101,
primary_flag: true,
},
],
org_name: 'Organization',
person_name: 'Person',
deal_title: 'Deal',
owner_name: 'Creator',
person_dropbox_bcc: 'company@pipedrivemail.com',
deal_dropbox_bcc: 'company+deal300@pipedrivemail.com',
assigned_to_user_id: 1235,
file: {
id: '376892,',
clean_name: 'Audio 10:55:07.m4a',
url: 'https://pipedrive-files.s3-eu-west-1.amazonaws.com/Audio-recording.m4a',
},
},
});

View File

@@ -0,0 +1,257 @@
import { pipedriveAuth } from '../../index';
import { HttpMethod } from '@activepieces/pieces-common';
import { createTrigger, Property, TriggerStrategy } from '@activepieces/pieces-framework';
import { filterIdProp } from '../common/props';
import {
pipedriveApiCall,
pipedrivePaginatedV2ApiCall,
pipedrivePaginatedV1ApiCall,
pipedriveTransformCustomFields,
} from '../common';
import { GetField, LeadListResponse } from '../common/types';
import { isNil } from '@activepieces/shared';
import { DEAL_OPTIONAL_FIELDS } from '../common/constants';
export const dealMatchingFilterTrigger = createTrigger({
auth: pipedriveAuth,
name: 'deal-matching-filter',
displayName: 'Deal Matching Filter',
description: 'Trigges when a deal newly matches a Pipedrive filter for the first time.',
type: TriggerStrategy.POLLING,
props: {
filterId: filterIdProp('deals', true),
status: Property.StaticDropdown({
displayName: 'Status',
required: false,
defaultValue: 'all_not_deleted',
options: {
disabled: false,
options: [
{
label: 'Open',
value: 'open',
},
{
label: 'Won',
value: 'won',
},
{
label: 'Lost',
value: 'lost',
},
{
label: 'Deleted',
value: 'deleted',
},
{
label: 'All(Not Deleted)',
value: 'all_not_deleted',
},
],
},
}),
},
async onEnable(context) {
const ids: number[] = [];
const response = await pipedrivePaginatedV2ApiCall<{ id: number }>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/deals',
query: {
filter_id: context.propsValue.filterId,
status: context.propsValue.status,
},
});
if (!isNil(response)) {
response.forEach((deal) => {
ids.push(deal.id);
});
}
await context.store.put('deals', JSON.stringify(ids));
},
async onDisable(context) {
await context.store.delete('deals');
},
async test(context) {
const deals = [];
const response = await pipedriveApiCall<LeadListResponse>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/deals',
query: {
limit: 10,
filter_id: context.propsValue.filterId,
status: context.propsValue.status,
include_fields: DEAL_OPTIONAL_FIELDS.join(','),
},
});
if (isNil(response.data)) {
return [];
}
for (const deal of response.data) {
deals.push(deal);
}
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/dealFields',
});
const result = [];
for (const deal of deals) {
const updatedDealProperties = pipedriveTransformCustomFields(customFieldsResponse, deal);
result.push(updatedDealProperties);
}
return result;
},
async run(context) {
const existingIds = (await context.store.get<string>('deals')) ?? '[]';
const parsedExistingIds = JSON.parse(existingIds) as number[];
const response = await pipedrivePaginatedV2ApiCall<{ id: number }>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/deals',
query: {
filter_id: context.propsValue.filterId,
status: context.propsValue.status,
include_fields: DEAL_OPTIONAL_FIELDS.join(','),
},
});
if (isNil(response) || response.length === 0) {
return [];
}
// Filter valid deals
const newDeals = response.filter((deal) => !parsedExistingIds.includes(deal.id));
const newIds = newDeals.map((deal) => deal.id);
if (newIds.length === 0) {
return [];
}
// Store new IDs
await context.store.put('deals', JSON.stringify([...newIds, ...parsedExistingIds]));
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/dealFields',
});
const result = [];
// Transform valid deal fields
for (const deal of newDeals) {
const updatedDealProperties = pipedriveTransformCustomFields(customFieldsResponse, deal);
result.push(updatedDealProperties);
}
return result;
},
sampleData: {
id: 1,
creator_user_id:22701301,
person_id: 1101,
org_id: 5,
stage_id: 2,
title: 'Deal One',
value: 5000,
currency: 'EUR',
add_time: '2019-05-29 04:21:51',
update_time: '2019-11-28 16:19:50',
stage_change_time: '2019-11-28 15:41:22',
active: true,
deleted: false,
status: 'open',
probability: null,
next_activity_date: '2019-11-29',
next_activity_time: '11:30:00',
next_activity_id: 128,
last_activity_id: null,
last_activity_date: null,
lost_reason: null,
visible_to: '1',
close_time: null,
pipeline_id: 1,
won_time: '2019-11-27 11:40:36',
first_won_time: '2019-11-27 11:40:36',
lost_time: '',
products_count: 0,
files_count: 0,
notes_count: 2,
followers_count: 0,
email_messages_count: 4,
activities_count: 1,
done_activities_count: 0,
undone_activities_count: 1,
participants_count: 1,
expected_close_date: '2019-06-29',
last_incoming_mail_time: '2019-05-29 18:21:42',
last_outgoing_mail_time: '2019-05-30 03:45:35',
label: 11,
stage_order_nr: 2,
person_name: 'Person',
org_name: 'Organization',
next_activity_subject: 'Call',
next_activity_type: 'call',
next_activity_duration: '00:30:00',
next_activity_note: 'Note content',
formatted_value: '€5,000',
weighted_value: 5000,
formatted_weighted_value: '€5,000',
weighted_value_currency: 'EUR',
rotten_time: null,
owner_name: 'Creator',
cc_email: 'company+deal1@pipedrivemail.com',
org_hidden: false,
person_hidden: false,
average_time_to_won: {
y: 0,
m: 0,
d: 0,
h: 0,
i: 20,
s: 49,
total_seconds: 1249,
},
average_stage_progress: 4.99,
age: {
y: 0,
m: 6,
d: 14,
h: 8,
i: 57,
s: 26,
total_seconds: 17139446,
},
stay_in_pipeline_stages: {
times_in_stages: {
'1': 15721267,
'2': 1288449,
'3': 4368,
'4': 3315,
'5': 26460,
},
order_of_stages: [1, 2, 3, 4, 5],
},
last_activity: null,
next_activity: null,
},
});

View File

@@ -0,0 +1,204 @@
import { createTrigger } from '@activepieces/pieces-framework';
import { TriggerStrategy } from '@activepieces/pieces-framework';
import { pipedriveApiCall, pipedriveCommon } from '../common';
import { pipedriveAuth } from '../..';
import { HttpMethod } from '@activepieces/pieces-common';
import { LeadListResponse } from '../common/types';
import { isNil } from '@activepieces/shared';
interface PipedriveActivityV2 {
id: number;
subject: string;
owner_id: number;
type: string;
is_deleted: boolean;
done: boolean;
conference_meeting_client: string | null;
conference_meeting_url: string | null;
conference_meeting_id: string | null;
due_date: string;
due_time: string;
duration: string;
busy: boolean;
add_time: string;
update_time: string;
marked_as_done_time: string | null;
public_description: string | null;
location: {
value: string | null;
street_number: string | null;
route: string | null;
sublocality: string | null;
locality: string | null;
admin_area_level_1: string | null;
admin_area_level_2: string | null;
country: string | null;
postal_code: string | null;
formatted_address: string | null;
} | null;
org_id: number | null;
person_id: number | null;
deal_id: number | null;
lead_id: string | null;
project_id: number | null;
private: boolean;
priority: number;
note: string | null;
creator_user_id: number;
attendees?: {
email_address: string;
name: string;
status: string;
is_organizer: number;
person_id: number | null;
user_id: number | null;
}[];
participants?: {
person_id: number;
primary: boolean;
}[];
}
interface ListActivitiesResponse {
data: PipedriveActivityV2[];
}
export const newActivity = createTrigger({
auth: pipedriveAuth,
name: 'new_activity',
displayName: 'New Activity',
description: 'Triggers when a new activity is added',
props: {},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const webhook = await pipedriveCommon.subscribeWebhook(
'activity',
'create',
context.webhookUrl!,
context.auth.data['api_domain'],
context.auth.access_token,
);
await context.store?.put<WebhookInformation>('_new_activity_trigger', {
webhookId: webhook.data.id,
});
},
async onDisable(context) {
const response = await context.store?.get<WebhookInformation>('_new_activity_trigger');
if (response !== null && response !== undefined) {
await pipedriveCommon.unsubscribeWebhook(
response.webhookId,
context.auth.data['api_domain'],
context.auth.access_token,
);
}
},
async test(context) {
const activities = [];
const response = await pipedriveApiCall<LeadListResponse>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/activities',
query: {
limit: 10,
sort_by: 'update_time',
sort_direction: 'desc',
include_fields: 'attendees',
},
});
if (isNil(response.data)) {
return [];
}
for (const activity of response.data) {
activities.push(activity);
}
return activities;
},
async run(context) {
const payloadBody = context.payload.body as PayloadBody;
const response = await pipedriveApiCall<{ data: PipedriveActivityV2 }>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: `/v2/activities/${payloadBody.data.id}`,
query: {
include_fields: 'attendees',
},
});
return [response.data];
},
sampleData: {
id: 8,
owner_id: 1234,
done: false,
type: 'deadline',
due_date: '2020-06-09',
due_time: '10:00',
duration: '01:00',
busy: true,
add_time: '2020-06-08T12:37:56Z',
marked_as_done_time: '2020-08-08T08:08:38Z',
subject: 'Deadline',
public_description: 'This is a description',
location: {
// Nested object
value: 'Mustamäe tee 3, Tallinn, Estonia',
street_number: '3',
route: 'Mustamäe tee',
sublocality: 'Kristiine',
locality: 'Tallinn',
admin_area_level_1: 'Harju maakond',
admin_area_level_2: null,
country: 'Estonia',
postal_code: '10616',
formatted_address: 'Mustamäe tee 3, 10616 Tallinn, Estonia',
},
org_id: 5,
person_id: 1101,
deal_id: 300,
lead_id: '46c3b0e1-db35-59ca-1828-4817378dff71',
is_deleted: false,
update_time: '2020-08-08T12:37:56Z',
note: 'A note for the activity',
creator_user_id: 1234,
attendees: [
{
email_address: 'attendee@pipedrivemail.com',
is_organizer: 0,
name: 'Attendee',
person_id: 25312,
status: 'noreply',
user_id: null,
},
],
participants: [
{
person_id: 17985,
primary: false,
},
{
person_id: 1101,
primary: true,
},
],
},
});
interface WebhookInformation {
webhookId: string;
}
type PayloadBody = {
data: PipedriveActivityV2;
previous: PipedriveActivityV2;
meta: {
action: string;
entity: string;
};
};

View File

@@ -0,0 +1,220 @@
import { createTrigger } from '@activepieces/pieces-framework';
import { TriggerStrategy } from '@activepieces/pieces-framework';
import {
pipedriveApiCall,
pipedriveCommon,
pipedrivePaginatedV1ApiCall,
pipedriveTransformCustomFields,
} from '../common';
import { pipedriveAuth } from '../..';
import { HttpMethod } from '@activepieces/pieces-common';
import { GetField } from '../common/types';
import { isNil } from '@activepieces/shared';
import { DEAL_OPTIONAL_FIELDS } from '../common/constants';
interface PipedriveDealV2 {
id: number;
title: string;
creator_user_id: number;
owner_id: number;
person_id: number | null;
org_id: number | null;
stage_id: number;
pipeline_id: number;
value: number;
currency: string;
add_time: string;
update_time: string;
stage_change_time: string;
is_deleted: boolean;
status: 'open' | 'won' | 'lost';
probability: number | null;
lost_reason: string | null;
visible_to: number;
close_time: string | null;
won_time: string | null;
first_won_time?: string;
lost_time: string | null;
products_count?: number;
files_count?: number;
notes_count?: number;
followers_count?: number;
email_messages_count?: number;
activities_count?: number;
done_activities_count?: number;
undone_activities_count?: number;
participants_count?: number;
expected_close_date: string | null;
last_incoming_mail_time?: string;
last_outgoing_mail_time?: string;
label_ids: number[];
rotten_time: string | null;
smart_bcc_email?: string;
acv?: number;
arr?: number;
mrr?: number;
custom_fields: Record<string, unknown>;
}
interface ListDealsResponseV2 {
data: PipedriveDealV2[];
additional_data?: {
next_cursor?: string;
};
}
interface GetDealResponseV2 {
data: PipedriveDealV2;
}
export const newDeal = createTrigger({
auth: pipedriveAuth,
name: 'new_deal',
displayName: 'New Deal',
description: 'Triggers when a new deal is created.',
props: {},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const webhook = await pipedriveCommon.subscribeWebhook(
'deal',
'create',
context.webhookUrl!,
context.auth.data['api_domain'],
context.auth.access_token,
);
await context.store?.put<WebhookInformation>('_new_deal_trigger', {
webhookId: webhook.data.id,
});
},
async onDisable(context) {
const response = await context.store?.get<WebhookInformation>('_new_deal_trigger');
if (response !== null && response !== undefined) {
await pipedriveCommon.unsubscribeWebhook(
response.webhookId,
context.auth.data['api_domain'],
context.auth.access_token,
);
}
},
async test(context) {
const dealsResponse = await pipedriveApiCall<ListDealsResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/deals',
query: {
limit: 5,
sort_by: 'update_time',
sort_direction: 'desc',
include_fields: DEAL_OPTIONAL_FIELDS.join(','),
},
});
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/dealFields',
});
if (isNil(dealsResponse.data)) {
return [];
}
const result = [];
for (const deal of dealsResponse.data) {
const updatedDealProperties = pipedriveTransformCustomFields(customFieldsResponse, deal);
result.push(updatedDealProperties);
}
return result;
},
async run(context) {
const payloadBody = context.payload.body as PayloadBody;
const dealResponse = await pipedriveApiCall<GetDealResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: `/v2/deals/${payloadBody.data.id}`,
query: {
include_fields: DEAL_OPTIONAL_FIELDS.join(','),
},
});
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/dealFields',
});
const updatedDealProperties = pipedriveTransformCustomFields(
customFieldsResponse,
dealResponse.data,
);
return [updatedDealProperties];
},
sampleData: {
id: 1,
creator_user_id: 8877,
owner_id: 8877,
person_id: 1101,
org_id: 5,
stage_id: 2,
title: 'Deal One',
value: 5000,
currency: 'EUR',
add_time: '2019-05-29T04:21:51Z',
update_time: '2019-11-28T16:19:50Z',
stage_change_time: '2019-11-28T15:41:22Z',
is_deleted: false,
status: 'open',
probability: null,
next_activity_id: 128,
last_activity_id: null,
lost_reason: null,
visible_to: 1,
close_time: null,
pipeline_id: 1,
won_time: '2019-11-27T11:40:36Z',
first_won_time: '2019-11-27T11:40:36Z',
lost_time: null,
products_count: 0,
files_count: 0,
notes_count: 2,
followers_count: 0,
email_messages_count: 4,
activities_count: 1,
done_activities_count: 0,
undone_activities_count: 1,
participants_count: 1,
expected_close_date: '2019-06-29',
last_incoming_mail_time: '2019-05-29T18:21:42Z',
last_outgoing_mail_time: '2019-05-30T03:45:35Z',
label_ids: [11],
rotten_time: null,
smart_bcc_email: 'company+deal1@pipedrivemail.com',
custom_fields: {
d4de1c1518b4531717c676029a45911c340390a6: {
value: 2300,
currency: 'EUR',
},
},
},
});
interface WebhookInformation {
webhookId: string;
}
type PayloadBody = {
data: PipedriveDealV2;
previous: PipedriveDealV2;
meta: {
action: string;
entity: string;
};
};

View File

@@ -0,0 +1,181 @@
import { pipedriveAuth } from '../../';
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { AuthenticationType, httpClient, HttpMethod } from '@activepieces/pieces-common';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedriveTransformCustomFields,
pipedriveTransformV1CustomFields,
} from '../common';
import { GetField } from '../common/types';
import { isNil } from '@activepieces/shared';
interface PipedriveLeadV2 {
id: string;
title: string;
owner_id: number;
creator_id: number;
label_ids: string[];
value: number | null;
expected_close_date: string | null;
person_id: number | null;
organization_id: number | null;
is_archived: boolean;
source_name: string;
origin: string;
origin_id: string | null;
channel: number | null;
channel_id: string | null;
was_seen: boolean;
next_activity_id: number | null;
add_time: string;
update_time: string;
visible_to: number;
custom_fields?: Record<string, unknown>;
}
interface LeadListResponseV2 {
data: PipedriveLeadV2[];
additional_data?: {
pagination?: {
start: number;
limit: number;
more_items_in_collection: boolean;
next_cursor?: string;
};
};
}
interface GetLeadResponseV2 {
data: PipedriveLeadV2;
}
export const newLeadTrigger = createTrigger({
auth: pipedriveAuth,
name: 'new-lead',
displayName: 'New Lead',
description: 'Triggers when a new lead is created.',
props: {},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const response = await httpClient.sendRequest<{ data: { id: string } }>({
method: HttpMethod.POST,
url: `${context.auth.data['api_domain']}/api/v1/webhooks`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
body: {
event_object: 'lead',
event_action: 'create',
subscription_url: context.webhookUrl,
version: '2.0',
},
});
await context.store?.put<{
webhookId: string;
}>('_new_lead_trigger', {
webhookId: response.body.data.id,
});
},
async onDisable(context) {
const response = await context.store?.get<{
webhookId: string;
}>('_new_lead_trigger');
if (response !== null && !isNil(response.webhookId)) {
await httpClient.sendRequest({
method: HttpMethod.DELETE,
url: `${context.auth.data['api_domain']}/api/v1/webhooks/${response.webhookId}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
});
}
},
async test(context) {
const response = await pipedriveApiCall<LeadListResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/leads',
query: {
limit: 10,
sort: 'update_time DESC',
},
});
if (isNil(response.data)) {
return [];
}
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/dealFields',
});
const result = [];
for (const lead of response.data) {
const updatedLeadProperties = pipedriveTransformV1CustomFields(customFieldsResponse, lead);
result.push(updatedLeadProperties);
}
return result;
},
async run(context) {
const payloadBody = context.payload.body as {
data: PipedriveLeadV2;
previous: PipedriveLeadV2;
meta: {
action: string;
entity: string;
};
};
const leadResponse = await pipedriveApiCall<GetLeadResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: `/v1/leads/${payloadBody.data.id}`,
});
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/dealFields',
});
const updatedLeadProperties = pipedriveTransformV1CustomFields(
customFieldsResponse,
leadResponse.data,
);
return [updatedLeadProperties];
},
sampleData: {
id: 'f3c23480-c9b1-11ef-bc83-2b8218e028ef',
title: 'Test lead',
owner_id: 22701301,
creator_id: 22701301,
label_ids: ['a0e5f330-d2a7-4181-a6e3-a44d634b7bf7', '8a0e6918-1eee-4e56-a615-c81d712a6a77'],
value: null,
expected_close_date: null,
person_id: 2,
organization_id: 1,
is_archived: false,
source_name: 'Manually created',
origin: 'ManuallyCreated',
origin_id: null,
channel: 1,
channel_id: null,
was_seen: true,
next_activity_id: null,
add_time: '2025-01-03T09:06:00.776Z',
update_time: '2025-01-03T09:06:00.776Z',
visible_to: 3,
},
});

View File

@@ -0,0 +1,121 @@
import { pipedriveAuth } from '../../';
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import {
pipedriveApiCall,
pipedriveCommon,
} from '../common';
import { isNil } from '@activepieces/shared';
interface PipedriveNoteV2 {
id: number;
user_id: number;
deal_id: number | null;
person_id: number | null;
org_id: number | null;
lead_id: string | null;
content: string;
add_time: string;
update_time: string;
is_deleted: boolean;
last_update_user_id: number | null;
}
interface NoteListResponseV2 {
data: PipedriveNoteV2[];
additional_data?: {
pagination?: {
start: number;
limit: number;
more_items_in_collection: boolean;
next_cursor?: string;
};
};
}
interface GetNoteResponseV2 {
data: PipedriveNoteV2;
}
export const newNoteTrigger = createTrigger({
auth: pipedriveAuth,
name: 'new-note',
displayName: 'New Note',
description: 'Triggers when a new note is created.',
props: {},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const webhook = await pipedriveCommon.subscribeWebhook(
'note',
'create',
context.webhookUrl!,
context.auth.data['api_domain'],
context.auth.access_token,
);
await context.store?.put<{
webhookId: string;
}>('_new_note_trigger', {
webhookId: webhook.data.id,
});
},
async onDisable(context) {
const response = await context.store?.get<{
webhookId: string;
}>('_new_note_trigger');
if (response !== null && response !== undefined) {
await pipedriveCommon.unsubscribeWebhook(
response.webhookId,
context.auth.data['api_domain'],
context.auth.access_token,
);
}
},
async test(context) {
const response = await pipedriveApiCall<NoteListResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/notes',
query: {
limit: 10,
sort: 'update_time DESC',
},
});
if (isNil(response.data)) {
return [];
}
return response.data;
},
async run(context) {
const payloadBody = context.payload.body as {
data: PipedriveNoteV2;
previous: PipedriveNoteV2;
meta: {
action: string;
entity: string;
};
};
const noteResponse = await pipedriveApiCall<GetNoteResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: `/v1/notes/${payloadBody.data.id}`,
});
return [noteResponse.data];
},
sampleData: {
id: 1,
user_id: 22701301,
deal_id: null,
person_id: 1,
org_id: 1,
lead_id: null,
content: 'Note content for v2 API.',
add_time: '2024-12-04T06:48:26Z',
update_time: '2024-12-04T06:48:26Z',
is_deleted: false,
},
});

View File

@@ -0,0 +1,202 @@
import { pipedriveAuth } from '../../';
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import {
pipedriveApiCall,
pipedriveCommon,
pipedrivePaginatedV1ApiCall,
pipedriveTransformCustomFields,
} from '../common';
import { GetField } from '../common/types';
import { isNil } from '@activepieces/shared';
import { ORGANIZATION_OPTIONAL_FIELDS } from '../common/constants';
interface PipedriveOrganizationV2 {
id: number;
name: string;
owner_id: number;
add_time: string;
update_time: string;
is_deleted: boolean;
visible_to: number;
picture_id: number | null;
label_ids: number[];
address: {
value: string | null;
street_number: string | null;
route: string | null;
sublocality: string | null;
locality: string | null;
admin_area_level_1: string | null;
admin_area_level_2: string | null;
country: string | null;
postal_code: string | null;
formatted_address: string | null;
} | null;
custom_fields: Record<string, unknown>;
next_activity_id?: number | null;
last_activity_id?: number | null;
open_deals_count?: number;
related_open_deals_count?: number;
closed_deals_count?: number;
related_closed_deals_count?: number;
participant_open_deals_count?: number;
participant_closed_deals_count?: number;
email_messages_count?: number;
activities_count?: number;
done_activities_count?: number;
undone_activities_count?: number;
files_count?: number;
notes_count?: number;
followers_count?: number;
won_deals_count?: number;
related_won_deals_count?: number;
lost_deals_count?: number;
related_lost_deals_count?: number;
last_incoming_mail_time?: string | null;
last_outgoing_mail_time?: string | null;
marketing_status?: string;
doi_status?: string;
}
interface OrganizationListResponseV2 {
data: PipedriveOrganizationV2[];
additional_data?: {
pagination?: {
start: number;
limit: number;
more_items_in_collection: boolean;
next_cursor?: string;
};
};
}
interface GetOrganizationResponseV2 {
data: PipedriveOrganizationV2;
}
export const newOrganizationTrigger = createTrigger({
auth: pipedriveAuth,
name: 'new-organization',
displayName: 'New Organization',
description: 'Triggers when a new organization is created.',
props: {},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const webhook = await pipedriveCommon.subscribeWebhook(
'organization',
'create',
context.webhookUrl!,
context.auth.data['api_domain'],
context.auth.access_token,
);
await context.store?.put<{
webhookId: string;
}>('_new_organization_trigger', {
webhookId: webhook.data.id,
});
},
async onDisable(context) {
const response = await context.store?.get<{
webhookId: string;
}>('_new_organization_trigger');
if (response !== null && response !== undefined) {
await pipedriveCommon.unsubscribeWebhook(
response.webhookId,
context.auth.data['api_domain'],
context.auth.access_token,
);
}
},
async test(context) {
const response = await pipedriveApiCall<OrganizationListResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/organizations',
query: {
limit: 10,
sort_by: 'update_time',
sort_direction: 'desc',
include_fields: ORGANIZATION_OPTIONAL_FIELDS.join(','),
},
});
if (isNil(response.data)) {
return [];
}
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/organizationFields',
});
const result = [];
for (const org of response.data) {
const updatedOrgProperties = pipedriveTransformCustomFields(customFieldsResponse, org);
result.push(updatedOrgProperties);
}
return result;
},
async run(context) {
const payloadBody = context.payload.body as {
data: PipedriveOrganizationV2;
previous: PipedriveOrganizationV2;
meta: {
action: string;
entity: string;
};
};
const orgResponse = await pipedriveApiCall<GetOrganizationResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: `/v2/organizations/${payloadBody.data.id}`,
query: {
include_fields: ORGANIZATION_OPTIONAL_FIELDS.join(','),
},
});
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/organizationFields',
});
const updatedOrgProperties = pipedriveTransformCustomFields(
customFieldsResponse,
orgResponse.data,
);
return [updatedOrgProperties];
},
sampleData: {
id: 1,
owner_id: 22701301,
name: 'Pipedrive Sample Org',
add_time: '2024-12-04T03:49:06Z',
update_time: '2024-12-14T11:03:19Z',
is_deleted: false,
visible_to: 3,
picture_id: null,
label_ids: [],
address: {
value: 'Mustamäe tee 3, Tallinn, Estonia',
street_number: '3',
route: 'Mustamäe tee',
sublocality: 'Kristiine',
locality: 'Tallinn',
admin_area_level_1: 'Harju maakond',
admin_area_level_2: null,
country: 'Estonia',
postal_code: '10616',
formatted_address: 'Mustamäe tee 3, 10616 Tallinn, Estonia',
},
},
});

View File

@@ -0,0 +1,215 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { pipedriveAuth } from '../../';
import {
pipedriveApiCall,
pipedriveCommon,
pipedrivePaginatedV1ApiCall,
pipedriveTransformCustomFields,
} from '../common';
import { GetField } from '../common/types';
import { isNil } from '@activepieces/shared';
import { PERSON_OPTIONAL_FIELDS } from '../common/constants';
interface PipedrivePersonV2 {
id: number;
name: string;
first_name: string | null;
last_name: string | null;
owner_id: number;
org_id: number | null;
picture_id: number | null;
add_time: string;
update_time: string;
is_deleted: boolean;
visible_to: number;
phones: {
value: string;
primary: boolean;
label: string;
}[];
emails: {
value: string;
primary: boolean;
label: string;
}[];
label_ids: number[];
custom_fields: Record<string, unknown>;
next_activity_id?: number | null;
last_activity_id?: number | null;
open_deals_count?: number;
related_open_deals_count?: number;
closed_deals_count?: number;
related_closed_deals_count?: number;
participant_open_deals_count?: number;
participant_closed_deals_count?: number;
email_messages_count?: number;
activities_count?: number;
done_activities_count?: number;
undone_activities_count?: number;
files_count?: number;
notes_count?: number;
followers_count?: number;
won_deals_count?: number;
related_won_deals_count?: number;
lost_deals_count?: number;
related_lost_deals_count?: number;
last_incoming_mail_time?: string | null;
last_outgoing_mail_time?: string | null;
marketing_status?: string;
doi_status?: string;
}
interface PersonListResponseV2 {
data: PipedrivePersonV2[];
additional_data?: {
pagination?: {
start: number;
limit: number;
more_items_in_collection: boolean;
next_cursor?: string;
};
};
}
interface GetPersonResponseV2 {
data: PipedrivePersonV2;
}
export const newPerson = createTrigger({
auth: pipedriveAuth,
name: 'new_person',
displayName: 'New Person',
description: 'Triggers when a new person is created',
props: {},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const webhook = await pipedriveCommon.subscribeWebhook(
'person',
'create',
context.webhookUrl!,
context.auth.data['api_domain'],
context.auth.access_token,
);
await context.store?.put<WebhookInformation>('_new_person_trigger', {
webhookId: webhook.data.id,
});
},
async onDisable(context) {
const response = await context.store?.get<WebhookInformation>('_new_person_trigger');
if (response !== null && response !== undefined) {
await pipedriveCommon.unsubscribeWebhook(
response.webhookId,
context.auth.data['api_domain'],
context.auth.access_token,
);
}
},
async test(context) {
const personsResponse = await pipedriveApiCall<PersonListResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/persons',
query: {
limit: 5,
sort_by: 'update_time',
sort_direction: 'desc',
include_fields: PERSON_OPTIONAL_FIELDS.join(','),
},
});
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/personFields',
});
if (isNil(personsResponse.data)) {
return [];
}
const result = [];
for (const person of personsResponse.data) {
const updatedPersonProperties = pipedriveTransformCustomFields(customFieldsResponse, person);
result.push(updatedPersonProperties);
}
return result;
},
async run(context) {
const payloadBody = context.payload.body as PayloadBody;
const personResponse = await pipedriveApiCall<GetPersonResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: `/v2/persons/${payloadBody.data.id}`,
query: {
include_fields: PERSON_OPTIONAL_FIELDS.join(','),
},
});
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/personFields',
});
const updatedPersonProperties = pipedriveTransformCustomFields(
customFieldsResponse,
personResponse.data,
);
return [updatedPersonProperties];
},
sampleData: {
id: 1,
owner_id: 123,
org_id: 1234,
name: 'Will Smith',
first_name: 'Will',
last_name: 'Smith',
is_deleted: false,
phones: [
{
value: '12345',
primary: true,
label: 'work',
},
],
emails: [
{
value: 'will.smith@example.com',
primary: true,
label: 'work',
},
],
add_time: '2017-10-18T13:23:07Z',
update_time: '2020-05-08T05:30:20Z',
visible_to: 3,
picture_id: 4,
next_activity_id: 128,
last_activity_id: 34,
last_incoming_mail_time: '2019-05-29T18:21:42Z',
last_outgoing_mail_time: '2019-05-30T03:45:35Z',
label_ids: [1],
marketing_status: 'no_consent',
},
});
interface WebhookInformation {
webhookId: string;
}
type PayloadBody = {
data: PipedrivePersonV2;
previous: PipedrivePersonV2;
meta: {
action: string;
entity: string;
};
};

View File

@@ -0,0 +1,221 @@
import { pipedriveAuth } from '../../index';
import { HttpMethod } from '@activepieces/pieces-common';
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { filterIdProp } from '../common/props';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedrivePaginatedV2ApiCall,
pipedriveTransformCustomFields,
} from '../common';
import { GetField } from '../common/types';
import { isNil } from '@activepieces/shared';
import { ORGANIZATION_OPTIONAL_FIELDS } from '../common/constants';
interface PipedriveOrganizationV2 {
id: number;
name: string;
owner_id: number;
add_time: string;
update_time: string;
is_deleted: boolean;
visible_to: number;
picture_id: number | null;
label_ids: number[];
address: {
value: string | null;
street_number: string | null;
route: string | null;
sublocality: string | null;
locality: string | null;
admin_area_level_1: string | null;
admin_area_level_2: string | null;
country: string | null;
postal_code: string | null;
formatted_address: string | null;
} | null;
custom_fields: Record<string, unknown>;
next_activity_id?: number | null;
last_activity_id?: number | null;
open_deals_count?: number;
related_open_deals_count?: number;
closed_deals_count?: number;
related_closed_deals_count?: number;
participant_open_deals_count?: number;
participant_closed_deals_count?: number;
email_messages_count?: number;
activities_count?: number;
done_activities_count?: number;
undone_activities_count?: number;
files_count?: number;
notes_count?: number;
followers_count?: number;
won_deals_count?: number;
related_won_deals_count?: number;
lost_deals_count?: number;
related_lost_deals_count?: number;
last_incoming_mail_time?: string | null;
last_outgoing_mail_time?: string | null;
marketing_status?: string;
doi_status?: string;
}
interface OrganizationListResponseV2 {
data: PipedriveOrganizationV2[];
additional_data?: {
pagination?: {
start: number;
limit: number;
more_items_in_collection: boolean;
next_cursor?: string;
};
};
}
export const organizationMatchingFilterTrigger = createTrigger({
auth: pipedriveAuth,
name: 'organization-matching-filter',
displayName: 'Organization Matching Filter',
description: 'Triggers when an organization newly matches a Pipedrive filter for the first time.',
type: TriggerStrategy.POLLING,
props: {
filterId: filterIdProp('org', true),
},
async onEnable(context) {
const ids: number[] = [];
const response = await pipedrivePaginatedV2ApiCall<{ id: number }>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/organizations',
query: {
sort_by: 'update_time',
sort_direction: 'desc',
filter_id: context.propsValue.filterId,
},
});
if (!isNil(response)) {
response.forEach((organization) => {
ids.push(organization.id);
});
}
await context.store.put('organizations', JSON.stringify(ids));
},
async onDisable(context) {
await context.store.delete('organizations');
},
async test(context) {
const organizations = [];
const response = await pipedriveApiCall<OrganizationListResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/organizations',
query: {
limit: 10,
sort_by: 'update_time',
sort_direction: 'desc',
filter_id: context.propsValue.filterId,
include_fields: ORGANIZATION_OPTIONAL_FIELDS.join(','),
},
});
if (isNil(response.data)) {
return [];
}
for (const org of response.data) {
organizations.push(org);
}
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/organizationFields',
});
const result = [];
for (const org of organizations) {
const updatedOrgProperties = pipedriveTransformCustomFields(customFieldsResponse, org);
result.push(updatedOrgProperties);
}
return result;
},
async run(context) {
const existingIds = (await context.store.get<string>('organizations')) ?? '[]';
const parsedExistingIds = JSON.parse(existingIds) as number[];
const response = await pipedrivePaginatedV2ApiCall<{ id: number }>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/organizations',
query: {
sort_by: 'update_time',
sort_direction: 'desc',
filter_id: context.propsValue.filterId,
include_fields: ORGANIZATION_OPTIONAL_FIELDS.join(','),
},
});
if (isNil(response) || response.length === 0) {
return [];
}
const newOrganizations = response.filter(
(organization) => !parsedExistingIds.includes(organization.id),
);
const newIds = newOrganizations.map((organization) => organization.id);
if (newIds.length === 0) {
return [];
}
await context.store.put('organizations', JSON.stringify([...newIds, ...parsedExistingIds]));
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/organizationFields',
});
const result = [];
for (const org of newOrganizations) {
const updatedOrgProperties = pipedriveTransformCustomFields(customFieldsResponse, org);
result.push(updatedOrgProperties);
}
return result;
},
sampleData: {
id: 1,
owner_id: 22701301,
name: 'Pipedrive Sample Org',
add_time: '2024-12-04T03:49:06Z',
update_time: '2024-12-14T11:03:19Z',
is_deleted: false,
visible_to: 3,
picture_id: null,
label_ids: [],
address: {
value: 'Mustamäe tee 3, Tallinn, Estonia',
street_number: '3',
route: 'Mustamäe tee',
sublocality: 'Kristiine',
locality: 'Tallinn',
admin_area_level_1: 'Harju maakond',
admin_area_level_2: null,
country: 'Estonia',
postal_code: '10616',
formatted_address: 'Mustamäe tee 3, 10616 Tallinn, Estonia',
},
custom_fields: {
your_custom_field_key: 'your_custom_field_value',
},
},
});

View File

@@ -0,0 +1,219 @@
import { pipedriveAuth } from '../../index';
import { HttpMethod } from '@activepieces/pieces-common';
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { filterIdProp } from '../common/props';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedrivePaginatedV2ApiCall,
pipedriveTransformCustomFields,
} from '../common';
import { GetField } from '../common/types';
import { isNil } from '@activepieces/shared';
import { PERSON_OPTIONAL_FIELDS } from '../common/constants';
interface PipedrivePersonV2 {
id: number;
name: string;
first_name: string | null;
last_name: string | null;
owner_id: number;
org_id: number | null;
picture_id: number | null;
add_time: string;
update_time: string;
is_deleted: boolean;
visible_to: number;
phones: {
value: string;
primary: boolean;
label: string;
}[];
emails: {
value: string;
primary: boolean;
label: string;
}[];
label_ids: number[];
custom_fields: Record<string, unknown>;
next_activity_id?: number | null;
last_activity_id?: number | null;
open_deals_count?: number;
related_open_deals_count?: number;
closed_deals_count?: number;
related_closed_deals_count?: number;
participant_open_deals_count?: number;
participant_closed_deals_count?: number;
email_messages_count?: number;
activities_count?: number;
done_activities_count?: number;
undone_activities_count?: number;
files_count?: number;
notes_count?: number;
followers_count?: number;
won_deals_count?: number;
related_won_deals_count?: number;
lost_deals_count?: number;
related_lost_deals_count?: number;
last_incoming_mail_time?: string | null;
last_outgoing_mail_time?: string | null;
marketing_status?: string;
doi_status?: string;
}
interface PersonListResponseV2 {
data: PipedrivePersonV2[];
additional_data?: {
pagination?: {
start: number;
limit: number;
more_items_in_collection: boolean;
next_cursor?: string;
};
};
}
interface GetPersonResponseV2 {
data: PipedrivePersonV2;
}
export const personMatchingFilterTrigger = createTrigger({
auth: pipedriveAuth,
name: 'person-matching-filter',
displayName: 'Person Matching Filter',
description: 'Triggers when a person newly matches a Pipedrive filter for the first time.',
type: TriggerStrategy.POLLING,
props: {
filterId: filterIdProp('people', true),
},
async onEnable(context) {
const ids: number[] = [];
const response = await pipedrivePaginatedV2ApiCall<{ id: number }>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/persons',
query: {
sort_by: 'update_time',
sort_direction: 'desc',
filter_id: context.propsValue.filterId,
},
});
if (!isNil(response)) {
response.forEach((person) => {
ids.push(person.id);
});
}
await context.store.put('persons', JSON.stringify(ids));
},
async onDisable(context) {
await context.store.delete('persons');
},
async test(context) {
const persons = [];
const response = await pipedriveApiCall<PersonListResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/persons',
query: {
limit: 10,
sort_by: 'update_time',
sort_direction: 'desc',
filter_id: context.propsValue.filterId,
include_fields: PERSON_OPTIONAL_FIELDS.join(','),
},
});
if (isNil(response.data)) {
return [];
}
for (const person of response.data) {
persons.push(person);
}
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/personFields',
});
const result = [];
for (const person of persons) {
const updatedPersonProperties = pipedriveTransformCustomFields(customFieldsResponse, person);
result.push(updatedPersonProperties);
}
return result;
},
async run(context) {
const existingIds = (await context.store.get<string>('persons')) ?? '[]';
const parsedExistingIds = JSON.parse(existingIds) as number[];
const response = await pipedrivePaginatedV2ApiCall<{ id: number }>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/persons',
query: {
sort_by: 'update_time',
sort_direction: 'desc',
filter_id: context.propsValue.filterId,
include_fields: PERSON_OPTIONAL_FIELDS.join(','),
},
});
if (isNil(response) || response.length === 0) {
return [];
}
const newPersons = response.filter((person) => !parsedExistingIds.includes(person.id));
const newIds = newPersons.map((person) => person.id);
if (newIds.length === 0) {
return [];
}
await context.store.put('persons', JSON.stringify([...newIds, ...parsedExistingIds]));
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/personFields',
});
const result = [];
for (const person of newPersons) {
const updatedPersonProperties = pipedriveTransformCustomFields(customFieldsResponse, person);
result.push(updatedPersonProperties);
}
return result;
},
sampleData: {
id: 1,
owner_id: 123,
org_id: 1234,
name: 'Will Smith',
first_name: 'Will',
last_name: 'Smith',
is_deleted: false,
phones: [
{
value: '12345',
primary: true,
label: 'work',
},
],
emails: [
{
value: 'will.smith@example.com',
primary: true,
label: 'work',
},
],
add_time: '2017-10-18T13:23:07Z',
update_time: '2020-05-08T05:30:20Z',
visible_to: 3,
picture_id: 4,
next_activity_id: 128,
last_activity_id: 34,
last_incoming_mail_time: '2019-05-29T18:21:42Z',
last_outgoing_mail_time: '2019-05-30T03:45:35Z',
label_ids: [1],
marketing_status: 'no_consent',
},
});

View File

@@ -0,0 +1,331 @@
import { pipedriveAuth } from '../../index';
import {
createTrigger,
DropdownOption,
PiecePropValueSchema,
Property,
TriggerStrategy,
} from '@activepieces/pieces-framework';
import {
pipedriveApiCall,
pipedrivePaginatedV1ApiCall,
pipedrivePaginatedV2ApiCall,
pipedriveTransformCustomFields,
} from '../common';
import { GetField, RequestParams, WebhookCreateResponse } from '../common/types';
import { HttpMethod } from '@activepieces/pieces-common';
import { isNil } from '@activepieces/shared';
import { DEAL_OPTIONAL_FIELDS } from '../common/constants';
interface PipedriveDealV2 {
id: number;
title: string;
creator_user_id: number;
owner_id: number;
person_id: number | null;
org_id: number | null;
stage_id: number;
pipeline_id: number;
value: number;
currency: string;
add_time: string;
update_time: string;
stage_change_time: string;
is_deleted: boolean;
status: 'open' | 'won' | 'lost';
probability: number | null;
lost_reason: string | null;
visible_to: number;
close_time: string | null;
won_time: string | null;
first_won_time?: string;
lost_time: string | null;
products_count?: number;
files_count?: number;
notes_count?: number;
followers_count?: number;
email_messages_count?: number;
activities_count?: number;
done_activities_count?: number;
undone_activities_count?: number;
participants_count?: number;
expected_close_date: string | null;
last_incoming_mail_time?: string;
last_outgoing_mail_time?: string;
label_ids: number[];
rotten_time: string | null;
smart_bcc_email?: string;
acv?: number;
arr?: number;
mrr?: number;
custom_fields: Record<string, unknown>;
}
interface PipedriveStageV2 {
id: number;
order_nr: number;
name: string;
is_deleted: boolean;
deal_probability: number;
pipeline_id: number;
is_deal_rot_enabled: boolean;
days_to_rotten: number | null;
add_time: string;
update_time: string | null;
}
interface ListDealsResponseV2 {
data: PipedriveDealV2[];
additional_data?: {
pagination?: {
start: number;
limit: number;
more_items_in_collection: boolean;
next_cursor?: string;
};
};
}
interface GetDealResponseV2 {
data: PipedriveDealV2;
}
export const updatedDealStageTrigger = createTrigger({
auth: pipedriveAuth,
name: 'updated-deal-stage',
displayName: 'Updated Deal Stage',
description: "Triggers when a deal's stage is updated.",
type: TriggerStrategy.WEBHOOK,
props: {
stage_id: Property.Dropdown({
auth: pipedriveAuth,
displayName: 'Stage in Pipeline',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
placeholder: 'please connect your account.',
disabled: true,
options: [],
};
}
const authValue = auth as PiecePropValueSchema<typeof pipedriveAuth>;
const response = await pipedrivePaginatedV2ApiCall<PipedriveStageV2>({
accessToken: authValue.access_token,
apiDomain: authValue.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/stages',
});
const options: DropdownOption<number>[] = [];
for (const stage of response) {
options.push({
label: `${stage.name}`,
value: stage.id,
});
}
return {
disabled: false,
options,
};
},
}),
},
async onEnable(context) {
const response = await pipedriveApiCall<WebhookCreateResponse>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.POST,
resourceUri: '/v1/webhooks',
body: {
subscription_url: context.webhookUrl,
event_object: 'deal',
event_action: 'change',
version: '2.0',
},
});
await context.store.put<string>('updated-deal-stage-trigger', response.data.id);
},
async onDisable(context) {
const webhook = await context.store.get<string>('updated-deal-stage-trigger');
if (webhook) {
await pipedriveApiCall<WebhookCreateResponse>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.DELETE,
resourceUri: `/v1/webhooks/${webhook}`,
});
}
},
async test(context) {
const stageId = context.propsValue.stage_id;
const qs: RequestParams = {
limit: 10,
sort_by: 'update_time',
sort_direction: 'desc',
include_fields: DEAL_OPTIONAL_FIELDS.join(','),
};
if (stageId) {
qs['stage_id'] = stageId.toString();
}
const dealsResponse = await pipedriveApiCall<ListDealsResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/deals',
query: qs,
});
if (isNil(dealsResponse.data)) {
return [];
}
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/dealFields',
});
const result = [];
for (const deal of dealsResponse.data) {
const updatedDealProperties = pipedriveTransformCustomFields(customFieldsResponse, deal);
const stageResponse = await pipedriveApiCall<{ data: PipedriveStageV2 }>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: `/v2/stages/${updatedDealProperties.stage_id}`,
});
updatedDealProperties['stage'] = stageResponse.data;
result.push(updatedDealProperties);
}
return result;
},
async run(context) {
const stageId = context.propsValue.stage_id;
const payloadBody = context.payload.body as PayloadBody;
const currentDealData = payloadBody.data;
const previousDealData = payloadBody.previous;
// Check if previous data exists and has stage_id
if (!previousDealData || isNil(previousDealData.stage_id)) {
return [];
}
// Only trigger if stage_id actually changed
if (currentDealData.stage_id !== previousDealData.stage_id) {
// If stage filter is set, only trigger if new stage matches the filter
if (stageId && currentDealData.stage_id !== stageId) {
return [];
}
const dealResponse = await pipedriveApiCall<GetDealResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: `/v2/deals/${payloadBody.data.id}`,
query: {
include_fields: DEAL_OPTIONAL_FIELDS.join(','),
},
});
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/dealFields',
});
const updatedDealProperties = pipedriveTransformCustomFields(
customFieldsResponse,
dealResponse.data,
);
const stageResponse = await pipedriveApiCall<{ data: PipedriveStageV2 }>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: `/v2/stages/${currentDealData.stage_id}`,
});
updatedDealProperties['stage'] = stageResponse.data;
return [updatedDealProperties];
}
return [];
},
sampleData: {
id: 1,
creator_user_id: 8877,
owner_id: 8877,
person_id: 1101,
org_id: 5,
stage_id: 2,
title: 'Deal One',
value: 5000,
currency: 'EUR',
add_time: '2019-05-29T04:21:51Z',
update_time: '2019-11-28T16:19:50Z',
stage_change_time: '2019-11-28T15:41:22Z',
is_deleted: false,
status: 'open',
probability: null,
next_activity_id: 128,
last_activity_id: null,
lost_reason: null,
visible_to: 1,
close_time: null,
pipeline_id: 1,
won_time: '2019-11-27T11:40:36Z',
first_won_time: '2019-11-27T11:40:36Z',
lost_time: null,
products_count: 0,
files_count: 0,
notes_count: 2,
followers_count: 0,
email_messages_count: 4,
activities_count: 1,
done_activities_count: 0,
undone_activities_count: 1,
participants_count: 1,
expected_close_date: '2019-06-29',
last_incoming_mail_time: '2019-05-29T18:21:42Z',
last_outgoing_mail_time: '2019-05-30T03:45:35Z',
label_ids: [11],
rotten_time: null,
smart_bcc_email: 'company+deal1@pipedrivemail.com',
stage: {
id: 2,
order_nr: 1,
name: 'Qualification',
is_deleted: false,
deal_probability: false,
pipeline_id: 1,
is_deal_rot_enabled: false,
days_to_rotten: null,
add_time: '2018-09-04T06:24:59Z',
update_time: null,
},
},
});
type PayloadBody = {
data: PipedriveDealV2;
previous: PipedriveDealV2;
meta: {
action: string;
entity: string;
};
};

View File

@@ -0,0 +1,448 @@
import {
createTrigger,
DropdownOption,
DynamicPropsValue,
PiecePropValueSchema,
Property,
TriggerStrategy,
} from '@activepieces/pieces-framework';
import {
pipedriveApiCall,
pipedriveCommon,
pipedrivePaginatedV1ApiCall,
pipedriveTransformCustomFields,
} from '../common';
import { pipedriveAuth } from '../..';
import { AuthenticationType, httpClient, HttpMethod } from '@activepieces/pieces-common';
import { FieldsResponse, GetField, RequestParams } from '../common/types';
import { isNil } from '@activepieces/shared';
import { DEAL_OPTIONAL_FIELDS } from '../common/constants';
interface PipedriveDealV2 {
id: number;
title: string;
creator_user_id: number;
owner_id: number;
person_id: number | null;
org_id: number | null;
stage_id: number;
pipeline_id: number;
value: number;
currency: string;
add_time: string;
update_time: string;
stage_change_time: string;
is_deleted: boolean;
status: 'open' | 'won' | 'lost';
probability: number | null;
lost_reason: string | null;
visible_to: number;
close_time: string | null;
won_time: string | null;
first_won_time?: string;
lost_time: string | null;
products_count?: number;
files_count?: number;
notes_count?: number;
followers_count?: number;
email_messages_count?: number;
activities_count?: number;
done_activities_count?: number;
undone_activities_count?: number;
participants_count?: number;
expected_close_date: string | null;
last_incoming_mail_time?: string;
last_outgoing_mail_time?: string;
label_ids: number[];
rotten_time: string | null;
smart_bcc_email?: string;
acv?: number;
arr?: number;
mrr?: number;
custom_fields: Record<string, unknown>;
}
interface PipedriveStageV2 {
id: number;
order_nr: number;
name: string;
is_deleted: boolean;
deal_probability: number;
pipeline_id: number;
is_deal_rot_enabled: boolean;
days_to_rotten: number | null;
add_time: string;
update_time: string | null;
}
interface ListDealsResponseV2 {
data: PipedriveDealV2[];
additional_data?: {
pagination?: {
start: number;
limit: number;
more_items_in_collection: boolean;
next_cursor?: string;
};
};
}
interface GetDealResponseV2 {
data: PipedriveDealV2;
}
export const updatedDeal = createTrigger({
auth: pipedriveAuth,
name: 'updated_deal',
displayName: 'Updated Deal',
description: 'Triggers when a deal is updated.',
props: {
filter_by: Property.StaticDropdown({
displayName: 'Filter by',
required: false,
options: {
disabled: false,
options: [
{
label: 'Deal Status',
value: 'status',
},
{
label: 'Stage in Pipeline',
value: 'stage_id',
},
],
},
}),
filter_by_field_value: Property.DynamicProperties({
auth: pipedriveAuth,
displayName: 'Field Values',
required: false,
refreshers: ['filter_by'],
props: async ({ auth, filter_by }) => {
if (!auth || !filter_by) return {};
const props: DynamicPropsValue = {};
const authValue = auth as PiecePropValueSchema<typeof pipedriveAuth>;
const filterBy = filter_by as unknown as string;
if (filterBy === 'status') {
props['field_value'] = Property.StaticDropdown({
displayName: 'Deal Status',
required: true,
options: {
disabled: false,
options: [
{ label: 'Open', value: 'open' },
{ label: 'Won', value: 'won' },
{ label: 'Lost', value: 'lost' },
{ label: 'Deleted', value: 'deleted' },
],
},
});
}
if (filterBy === 'stage_id') {
const response = await httpClient.sendRequest<{
data: PipedriveStageV2[];
}>({
method: HttpMethod.GET,
url: `${authValue.data['api_domain']}/api/v2/stages`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authValue.access_token,
},
});
props['field_value'] = Property.StaticDropdown({
displayName: 'Stage in Pipeline',
required: true,
options: {
disabled: false,
options: response.body.data.map((stage) => {
return {
label: stage.name,
value: stage.id,
};
}),
},
});
}
return props;
},
}),
field_to_watch: Property.Dropdown({
auth: pipedriveAuth,
displayName: 'Field to watch for Changes On',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
placeholder: 'Connect your account',
disabled: true,
options: [],
};
}
const authValue = auth as PiecePropValueSchema<typeof pipedriveAuth>;
const response = await httpClient.sendRequest<FieldsResponse>({
method: HttpMethod.GET,
url: `${authValue.data['api_domain']}/api/v1/dealFields`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authValue.access_token,
},
});
const options: DropdownOption<string>[] = [];
for (const field of response.body.data) {
options.push({
label: field.name,
value: field.key,
});
}
return {
disabled: false,
options,
};
},
}),
},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const webhook = await pipedriveCommon.subscribeWebhook(
'deal',
'change',
context.webhookUrl!,
context.auth.data['api_domain'],
context.auth.access_token,
);
await context.store?.put<string>('_updated_deal_trigger', webhook.data.id);
},
async onDisable(context) {
const webhookId = await context.store.get<string>('_updated_deal_trigger');
if (webhookId) {
await pipedriveCommon.unsubscribeWebhook(
webhookId,
context.auth.data['api_domain'],
context.auth.access_token,
);
}
},
async test(context) {
const filterBy = context.propsValue.filter_by;
const filterByValue = context.propsValue.filter_by_field_value!['field_value'];
const qs: RequestParams = {
limit: 10,
sort_by: 'update_time',
sort_direction: 'desc',
include_fields: DEAL_OPTIONAL_FIELDS.join(','),
};
if (filterBy && filterByValue) {
qs[filterBy] = filterByValue;
}
const dealsResponse = await pipedriveApiCall<ListDealsResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/deals',
query: qs,
});
if (isNil(dealsResponse.data)) {
return [];
}
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/dealFields',
});
const result = [];
for (const deal of dealsResponse.data) {
const updatedDealProperties = pipedriveTransformCustomFields(customFieldsResponse, deal);
result.push(updatedDealProperties);
}
return result;
},
async run(context) {
const filterBy = context.propsValue.filter_by;
const filterByValue = context.propsValue.filter_by_field_value!['field_value'];
const fieldToWatch =
context.propsValue.field_to_watch === 'label'
? 'label_ids'
: context.propsValue.field_to_watch;
const payloadBody = context.payload.body as {
data: Record<string, any>;
previous: Record<string, any>;
meta: {
action: string;
entity: string;
};
};
const currentDealData = flattenCustomFields(payloadBody.data);
const previousDealData = flattenCustomFields(payloadBody.previous);
const noFilterAndNoField = !filterBy && !fieldToWatch;
const isFieldChanged =
fieldToWatch &&
fieldToWatch in previousDealData && // The previous object now only contains fields whose values have changed
currentDealData[fieldToWatch] !== previousDealData[fieldToWatch];
const isFilterMatched = filterBy && currentDealData[filterBy] === filterByValue;
if (
noFilterAndNoField ||
(!filterBy && isFieldChanged) ||
(isFilterMatched && (!fieldToWatch || isFieldChanged))
) {
const dealResponse = await pipedriveApiCall<GetDealResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: `/v2/deals/${payloadBody.data.id}`,
query: {
include_fields: DEAL_OPTIONAL_FIELDS.join(','),
},
});
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/dealFields',
});
const updatedDealProperties = pipedriveTransformCustomFields(
customFieldsResponse,
dealResponse.data,
);
return [updatedDealProperties];
}
return [];
},
sampleData: {
id: 1,
creator_user_id: 8877,
owner_id: 8877,
person_id: 1101,
org_id: 5,
stage_id: 2,
title: 'Deal One',
value: 5000,
currency: 'EUR',
add_time: '2019-05-29T04:21:51Z',
update_time: '2019-11-28T16:19:50Z',
stage_change_time: '2019-11-28T15:41:22Z',
is_deleted: false,
status: 'open',
probability: null,
next_activity_id: 128,
last_activity_id: null,
lost_reason: null,
visible_to: 1,
close_time: null,
pipeline_id: 1,
won_time: '2019-11-27T11:40:36Z',
first_won_time: '2019-11-27T11:40:36Z',
lost_time: null,
products_count: 0,
files_count: 0,
notes_count: 2,
followers_count: 0,
email_messages_count: 4,
activities_count: 1,
done_activities_count: 0,
undone_activities_count: 1,
participants_count: 1,
expected_close_date: '2019-06-29',
last_incoming_mail_time: '2019-05-29T18:21:42Z',
last_outgoing_mail_time: '2019-05-30T03:45:35Z',
label_ids: [11],
rotten_time: null,
smart_bcc_email: 'company+deal1@pipedrivemail.com',
},
});
function flattenCustomFields(deal: Record<string, any>): Record<string, any> {
const { custom_fields, ...rest } = deal;
if (!custom_fields) return rest;
const flatCustomFields: Record<string, any> = {};
for (const [key, value] of Object.entries(custom_fields as Record<string, any>)) {
if (isNil(value)) {
flatCustomFields[key] = value;
continue;
}
const type = value['type'] as string;
switch (type) {
case 'varchar':
case 'text':
case 'varchar_auto':
case 'double':
case 'phone':
case 'date':
flatCustomFields[key] = value?.value;
break;
case 'set':
flatCustomFields[key] = value?.values?.length
? value.values.map((v: any) => v.id).join(',')
: '';
break;
case 'enum':
case 'user':
case 'org':
case 'people':
flatCustomFields[key] = value?.id;
break;
case 'monetary':
flatCustomFields[key] = value?.value;
flatCustomFields[`${key}_currency`] = value?.currency;
break;
case 'time':
flatCustomFields[key] = value?.value;
flatCustomFields[`${key}_timezone_id`] = value?.timezone_id;
break;
case 'timerange':
flatCustomFields[key] = value?.from;
flatCustomFields[`${key}_timezone_id`] = value?.timezone_id;
flatCustomFields[`${key}_until`] = value?.until;
break;
case 'daterange':
flatCustomFields[key] = value?.from;
flatCustomFields[`${key}_until`] = value?.until;
break;
case 'address':
flatCustomFields[key] = value?.value;
flatCustomFields[`${key}_subpremise`] = value?.subpremise;
flatCustomFields[`${key}_street_number`] = value?.street_number;
flatCustomFields[`${key}_route`] = value?.route;
flatCustomFields[`${key}_sublocality`] = value?.sublocality;
flatCustomFields[`${key}_locality`] = value?.locality;
flatCustomFields[`${key}_admin_area_level_1`] = value?.admin_area_level_1;
flatCustomFields[`${key}_admin_area_level_2`] = value?.admin_area_level_2;
flatCustomFields[`${key}_country`] = value?.country;
flatCustomFields[`${key}_postal_code`] = value?.postal_code;
flatCustomFields[`${key}_formatted_address`] = value?.formatted_address;
break;
}
}
return {
...rest,
...flatCustomFields,
};
}

View File

@@ -0,0 +1,191 @@
import { pipedriveAuth } from '../../';
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import {
pipedriveApiCall,
pipedriveCommon,
pipedrivePaginatedV1ApiCall,
pipedriveTransformCustomFields,
} from '../common';
import { GetField } from '../common/types';
import { isNil } from '@activepieces/shared';
import { ORGANIZATION_OPTIONAL_FIELDS } from '../common/constants';
interface PipedriveOrganizationV2 {
id: number;
name: string;
owner_id: number;
add_time: string;
update_time: string;
is_deleted: boolean;
visible_to: number;
picture_id: number | null;
label_ids: number[];
address: {
value: string | null;
street_number: string | null;
route: string | null;
sublocality: string | null;
locality: string | null;
admin_area_level_1: string | null;
admin_area_level_2: string | null;
country: string | null;
postal_code: string | null;
formatted_address: string | null;
} | null;
custom_fields: Record<string, unknown>;
next_activity_id?: number | null;
last_activity_id?: number | null;
open_deals_count?: number;
related_open_deals_count?: number;
closed_deals_count?: number;
related_closed_deals_count?: number;
participant_open_deals_count?: number;
participant_closed_deals_count?: number;
email_messages_count?: number;
activities_count?: number;
done_activities_count?: number;
undone_activities_count?: number;
files_count?: number;
notes_count?: number;
followers_count?: number;
won_deals_count?: number;
related_won_deals_count?: number;
lost_deals_count?: number;
related_lost_deals_count?: number;
last_incoming_mail_time?: string | null;
last_outgoing_mail_time?: string | null;
marketing_status?: string;
doi_status?: string;
}
interface OrganizationListResponseV2 {
data: PipedriveOrganizationV2[];
additional_data?: {
pagination?: {
start: number;
limit: number;
more_items_in_collection: boolean;
next_cursor?: string;
};
};
}
interface GetOrganizationResponseV2 {
data: PipedriveOrganizationV2;
}
export const updatedOrganizationTrigger = createTrigger({
auth: pipedriveAuth,
name: 'updated-organization',
displayName: 'Updated Organization',
description: 'Triggers when an existing organization is updated.',
props: {},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const webhook = await pipedriveCommon.subscribeWebhook(
'organization',
'change',
context.webhookUrl!,
context.auth.data['api_domain'],
context.auth.access_token,
);
await context.store?.put<string>('_updated_organization_trigger', webhook.data.id);
},
async onDisable(context) {
const webhookId = await context.store.get<string>('_updated_organization_trigger');
if (webhookId !== null && webhookId !== undefined) {
await pipedriveCommon.unsubscribeWebhook(
webhookId,
context.auth.data['api_domain'],
context.auth.access_token,
);
}
},
async test(context) {
const response = await pipedriveApiCall<OrganizationListResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/organizations',
query: {
limit: 10,
sort_by: 'update_time',
sort_direction: 'desc',
include_fields: ORGANIZATION_OPTIONAL_FIELDS.join(','),
},
});
if (isNil(response.data)) {
return [];
}
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/organizationFields',
});
const result = [];
for (const org of response.data) {
const updatedOrgProperties = pipedriveTransformCustomFields(customFieldsResponse, org);
result.push(updatedOrgProperties);
}
return result;
},
async run(context) {
const payloadBody = context.payload.body as {
data: Record<string, any>;
previous: Record<string, any>;
};
const orgResponse = await pipedriveApiCall<GetOrganizationResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: `/v2/organizations/${payloadBody.data.id}`,
query: {
include_fields: ORGANIZATION_OPTIONAL_FIELDS.join(','),
},
});
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/organizationFields',
});
const updatedOrgProperties = pipedriveTransformCustomFields(
customFieldsResponse,
orgResponse.data,
);
return [updatedOrgProperties];
},
sampleData: {
id: 1,
owner_id: 22701301,
name: 'Pipedrive Sample Org',
add_time: '2024-12-04T03:49:06Z',
update_time: '2024-12-14T11:03:19Z',
is_deleted: false,
visible_to: 3,
picture_id: null,
label_ids: [],
address: {
value: 'Mustamäe tee 3, Tallinn, Estonia',
street_number: '3',
route: 'Mustamäe tee',
sublocality: 'Kristiine',
locality: 'Tallinn',
admin_area_level_1: 'Harju maakond',
admin_area_level_2: null,
country: 'Estonia',
postal_code: '10616',
formatted_address: 'Mustamäe tee 3, 10616 Tallinn, Estonia',
},
},
});

View File

@@ -0,0 +1,204 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { pipedriveAuth } from '../../';
import {
pipedriveApiCall,
pipedriveCommon,
pipedrivePaginatedV1ApiCall,
pipedriveTransformCustomFields,
} from '../common';
import { GetField } from '../common/types';
import { isNil } from '@activepieces/shared';
import { PERSON_OPTIONAL_FIELDS } from '../common/constants';
interface PipedrivePersonV2 {
id: number;
name: string;
first_name: string | null;
last_name: string | null;
owner_id: number;
org_id: number | null;
picture_id: number | null;
add_time: string;
update_time: string;
is_deleted: boolean;
visible_to: number;
phones: {
value: string;
primary: boolean;
label: string;
}[];
emails: {
value: string;
primary: boolean;
label: string;
}[];
label_ids: number[];
custom_fields: Record<string, unknown>;
next_activity_id?: number | null;
last_activity_id?: number | null;
open_deals_count?: number;
related_open_deals_count?: number;
closed_deals_count?: number;
related_closed_deals_count?: number;
participant_open_deals_count?: number;
participant_closed_deals_count?: number;
email_messages_count?: number;
activities_count?: number;
done_activities_count?: number;
undone_activities_count?: number;
files_count?: number;
notes_count?: number;
followers_count?: number;
won_deals_count?: number;
related_won_deals_count?: number;
lost_deals_count?: number;
related_lost_deals_count?: number;
last_incoming_mail_time?: string | null;
last_outgoing_mail_time?: string | null;
marketing_status?: string;
doi_status?: string;
}
interface PersonListResponseV2 {
data: PipedrivePersonV2[];
additional_data?: {
pagination?: {
start: number;
limit: number;
more_items_in_collection: boolean;
next_cursor?: string;
};
};
}
interface GetPersonResponseV2 {
data: PipedrivePersonV2;
}
export const updatedPerson = createTrigger({
auth: pipedriveAuth,
name: 'updated_person',
displayName: 'Updated Person',
description: 'Triggers when a person is updated.',
props: {},
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
const webhook = await pipedriveCommon.subscribeWebhook(
'person',
'change',
context.webhookUrl!,
context.auth.data['api_domain'],
context.auth.access_token,
);
await context.store?.put<string>('_updated_person_trigger', webhook.data.id);
},
async onDisable(context) {
const webhookId = await context.store.get<string>('_updated_person_trigger');
if (webhookId !== null && webhookId !== undefined) {
await pipedriveCommon.unsubscribeWebhook(
webhookId,
context.auth.data['api_domain'],
context.auth.access_token,
);
}
},
async test(context) {
const personsResponse = await pipedriveApiCall<PersonListResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v2/persons',
query: {
limit: 5,
sort_by: 'update_time',
sort_direction: 'desc',
include_fields: PERSON_OPTIONAL_FIELDS.join(','),
},
});
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/personFields',
});
if (isNil(personsResponse.data)) {
return [];
}
const result = [];
for (const person of personsResponse.data) {
const updatedPersonProperties = pipedriveTransformCustomFields(customFieldsResponse, person);
result.push(updatedPersonProperties);
}
return result;
},
async run(context) {
const payloadBody = context.payload.body as {
data: Record<string, any>;
previous: Record<string, any>;
};
const personResponse = await pipedriveApiCall<GetPersonResponseV2>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: `/v2/persons/${payloadBody.data.id}`,
query: {
include_fields: PERSON_OPTIONAL_FIELDS.join(','),
},
});
const customFieldsResponse = await pipedrivePaginatedV1ApiCall<GetField>({
accessToken: context.auth.access_token,
apiDomain: context.auth.data['api_domain'],
method: HttpMethod.GET,
resourceUri: '/v1/personFields',
});
const updatedPersonProperties = pipedriveTransformCustomFields(
customFieldsResponse,
personResponse.data,
);
return [updatedPersonProperties];
},
sampleData: {
id: 1,
owner_id: 123,
org_id: 1234,
name: 'Will Smith',
first_name: 'Will',
last_name: 'Smith',
is_deleted: false,
phones: [
{
value: '12345',
primary: true,
label: 'work',
},
],
emails: [
{
value: 'will.smith@example.com',
primary: true,
label: 'work',
},
],
add_time: '2017-10-18T13:23:07Z',
update_time: '2020-05-08T05:30:20Z',
visible_to: 3,
picture_id: 4,
next_activity_id: 128,
last_activity_id: 34,
last_incoming_mail_time: '2019-05-29T18:21:42Z',
last_outgoing_mail_time: '2019-05-30T03:45:35Z',
label_ids: [1],
marketing_status: 'no_consent',
},
});