diff --git a/components/pipedrive/actions/add-activity/add-activity.mjs b/components/pipedrive/actions/add-activity/add-activity.mjs index d38b160d1c405..2c96278c47155 100644 --- a/components/pipedrive/actions/add-activity/add-activity.mjs +++ b/components/pipedrive/actions/add-activity/add-activity.mjs @@ -7,7 +7,7 @@ export default { key: "pipedrive-add-activity", name: "Add Activity", description: "Adds a new activity. Includes `more_activities_scheduled_in_context` property in response's `additional_data` which indicates whether there are more undone activities scheduled with the same deal, person or organization (depending on the supplied data). See the Pipedrive API docs for Activities [here](https://developers.pipedrive.com/docs/api/v1/#!/Activities). For info on [adding an activity in Pipedrive](https://developers.pipedrive.com/docs/api/v1/Activities#addActivity)", - version: "0.1.9", + version: "0.1.10", type: "action", props: { pipedriveApp, diff --git a/components/pipedrive/actions/add-deal/add-deal.mjs b/components/pipedrive/actions/add-deal/add-deal.mjs index f7bee4947d332..fe13f92baa689 100644 --- a/components/pipedrive/actions/add-deal/add-deal.mjs +++ b/components/pipedrive/actions/add-deal/add-deal.mjs @@ -5,7 +5,7 @@ export default { key: "pipedrive-add-deal", name: "Add Deal", description: "Adds a new deal. See the Pipedrive API docs for Deals [here](https://developers.pipedrive.com/docs/api/v1/Deals#addDeal)", - version: "0.1.9", + version: "0.1.10", type: "action", props: { pipedriveApp, diff --git a/components/pipedrive/actions/add-lead/add-lead.mjs b/components/pipedrive/actions/add-lead/add-lead.mjs index fc2c15086c0b1..850ed90947eee 100644 --- a/components/pipedrive/actions/add-lead/add-lead.mjs +++ b/components/pipedrive/actions/add-lead/add-lead.mjs @@ -6,7 +6,7 @@ export default { key: "pipedrive-add-lead", name: "Add Lead", description: "Create a new lead in Pipedrive. [See the documentation](https://developers.pipedrive.com/docs/api/v1/Leads#addLead)", - version: "0.0.3", + version: "0.0.4", type: "action", props: { pipedrive, diff --git a/components/pipedrive/actions/add-note/add-note.mjs b/components/pipedrive/actions/add-note/add-note.mjs index bf14955954144..2f33e6e7384d3 100644 --- a/components/pipedrive/actions/add-note/add-note.mjs +++ b/components/pipedrive/actions/add-note/add-note.mjs @@ -5,7 +5,7 @@ export default { key: "pipedrive-add-note", name: "Add Note", description: "Adds a new note. For info on [adding an note in Pipedrive](https://developers.pipedrive.com/docs/api/v1/Notes#addNote)", - version: "0.0.7", + version: "0.0.8", type: "action", props: { pipedriveApp, diff --git a/components/pipedrive/actions/add-organization/add-organization.mjs b/components/pipedrive/actions/add-organization/add-organization.mjs index 8db9b9f9f9e1e..55d5f0ba5e9f1 100644 --- a/components/pipedrive/actions/add-organization/add-organization.mjs +++ b/components/pipedrive/actions/add-organization/add-organization.mjs @@ -5,7 +5,7 @@ export default { key: "pipedrive-add-organization", name: "Add Organization", description: "Adds a new organization. See the Pipedrive API docs for Organizations [here](https://developers.pipedrive.com/docs/api/v1/Organizations#addOrganization)", - version: "0.1.9", + version: "0.1.10", type: "action", props: { pipedriveApp, diff --git a/components/pipedrive/actions/add-person/add-person.mjs b/components/pipedrive/actions/add-person/add-person.mjs index 2c1b102620aeb..1d3bbc901ee71 100644 --- a/components/pipedrive/actions/add-person/add-person.mjs +++ b/components/pipedrive/actions/add-person/add-person.mjs @@ -6,7 +6,7 @@ export default { key: "pipedrive-add-person", name: "Add Person", description: "Adds a new person. See the Pipedrive API docs for People [here](https://developers.pipedrive.com/docs/api/v1/Persons#addPerson)", - version: "0.1.9", + version: "0.1.10", type: "action", props: { pipedriveApp, diff --git a/components/pipedrive/actions/remove-duplicate-notes/remove-duplicate-notes.mjs b/components/pipedrive/actions/remove-duplicate-notes/remove-duplicate-notes.mjs new file mode 100644 index 0000000000000..c8114ffe86ace --- /dev/null +++ b/components/pipedrive/actions/remove-duplicate-notes/remove-duplicate-notes.mjs @@ -0,0 +1,148 @@ +import pipedriveApp from "../../pipedrive.app.mjs"; +import { decode } from "html-entities"; + +export default { + key: "pipedrive-remove-duplicate-notes", + name: "Remove Duplicate Notes", + description: "Remove duplicate notes from an object in Pipedrive. See the documentation for [getting notes](https://developers.pipedrive.com/docs/api/v1/Notes#getNotes) and [deleting notes](https://developers.pipedrive.com/docs/api/v1/Notes#deleteNote)", + version: "0.0.1", + type: "action", + props: { + pipedriveApp, + leadId: { + propDefinition: [ + pipedriveApp, + "leadId", + ], + description: "The ID of the lead that the notes are attached to", + }, + dealId: { + propDefinition: [ + pipedriveApp, + "dealId", + ], + description: "The ID of the deal that the notes are attached to", + }, + personId: { + propDefinition: [ + pipedriveApp, + "personId", + ], + description: "The ID of the person that the notes are attached to", + }, + organizationId: { + propDefinition: [ + pipedriveApp, + "organizationId", + ], + description: "The ID of the organization that the notes are attached to", + }, + userId: { + propDefinition: [ + pipedriveApp, + "userId", + ], + description: "The ID of the user that the notes are attached to", + }, + projectId: { + propDefinition: [ + pipedriveApp, + "projectId", + ], + description: "The ID of the project that the notes are attached to", + }, + keyword: { + type: "string", + label: "Keyword", + description: "Only remove duplicate notes that contain the specified keyword(s)", + optional: true, + }, + }, + methods: { + getDuplicateNotes(notes) { + const seenContent = new Map(); + const uniqueNotes = []; + const duplicates = []; + + // Sort notes by add_time (ascending) to keep the oldest duplicate + const sortedNotes = notes.sort((a, b) => { + const dateA = new Date(a.add_time); + const dateB = new Date(b.add_time); + return dateA - dateB; + }); + + for (const note of sortedNotes) { + // Normalize content by removing extra whitespace and converting to lowercase + const decodedContent = decode(note.content || ""); + const normalizedContent = decodedContent?.replace(/^\s*|\s*$/gi, "").trim() + .toLowerCase(); + + if (!normalizedContent) { + // Skip notes with empty content + continue; + } + + if (seenContent.has(normalizedContent)) { + // This is a duplicate + duplicates.push({ + duplicate: note, + original: seenContent.get(normalizedContent), + }); + } else { + // This is the first occurrence + seenContent.set(normalizedContent, note); + uniqueNotes.push(note); + } + } + + return { + uniqueNotes, + duplicates, + duplicateCount: duplicates.length, + }; + }, + }, + async run({ $ }) { + let notes = await this.pipedriveApp.getPaginatedResources({ + fn: this.pipedriveApp.getNotes, + params: { + user_id: this.userId, + lead_id: this.leadId, + deal_id: this.dealId, + person_id: this.personId, + org_id: this.organizationId, + project_id: this.projectId, + }, + }); + + if (this.keyword) { + notes = notes.filter((note) => + note.content?.toLowerCase().includes(this.keyword.toLowerCase())); + } + + let result = { + notes, + totalNotes: notes.length, + }; + + const { + uniqueNotes, duplicates, duplicateCount, + } = this.getDuplicateNotes(notes); + + for (const note of duplicates) { + await this.pipedriveApp.deleteNote(note.duplicate.id); + } + + result = { + notes: uniqueNotes, + totalNotes: uniqueNotes.length, + duplicatesFound: duplicateCount, + duplicates: duplicates, + originalCount: notes.length, + }; + + $.export("$summary", `Found ${notes.length} total note(s), removed ${duplicateCount} duplicate(s), returning ${uniqueNotes.length} unique note(s)`); + + return result; + }, +}; diff --git a/components/pipedrive/actions/search-notes/search-notes.mjs b/components/pipedrive/actions/search-notes/search-notes.mjs new file mode 100644 index 0000000000000..9f76f363c3773 --- /dev/null +++ b/components/pipedrive/actions/search-notes/search-notes.mjs @@ -0,0 +1,179 @@ +import pipedriveApp from "../../pipedrive.app.mjs"; + +export default { + key: "pipedrive-search-notes", + name: "Search Notes", + description: "Search for notes in Pipedrive. [See the documentation](https://developers.pipedrive.com/docs/api/v1/Notes#getNotes)", + version: "0.0.1", + type: "action", + props: { + pipedriveApp, + searchTerm: { + type: "string", + label: "Search Term", + description: "The term to search for in the note content", + optional: true, + }, + leadId: { + propDefinition: [ + pipedriveApp, + "leadId", + ], + description: "The ID of the lead that the note is attached to", + }, + dealId: { + propDefinition: [ + pipedriveApp, + "dealId", + ], + description: "The ID of the deal that the note is attached to", + }, + personId: { + propDefinition: [ + pipedriveApp, + "personId", + ], + description: "The ID of the person that the note is attached to", + }, + organizationId: { + propDefinition: [ + pipedriveApp, + "organizationId", + ], + description: "The ID of the organization that the note is attached to", + }, + userId: { + propDefinition: [ + pipedriveApp, + "userId", + ], + description: "The ID of the user that the note is attached to", + }, + projectId: { + propDefinition: [ + pipedriveApp, + "projectId", + ], + description: "The ID of the project that the note is attached to", + }, + sortField: { + type: "string", + label: "Sort Field", + description: "The field name to sort by", + options: [ + "id", + "user_id", + "deal_id", + "org_id", + "person_id", + "content", + "add_time", + "update_time", + ], + optional: true, + }, + sortDirection: { + type: "string", + label: "Sort Direction", + description: "The direction to sort the results in", + options: [ + "ASC", + "DESC", + ], + default: "DESC", + optional: true, + }, + startDate: { + type: "string", + label: "Start Date", + description: "The date in format of YYYY-MM-DD from which notes to fetch", + optional: true, + }, + endDate: { + type: "string", + label: "End Date", + description: "The date in format of YYYY-MM-DD until which notes to fetch to", + optional: true, + }, + pinnedToLeadFlag: { + type: "boolean", + label: "Pinned to Lead Flag", + description: "If `true`, the results are filtered by note to lead pinning state", + optional: true, + }, + pinnedToDealFlag: { + type: "boolean", + label: "Pinned to Deal Flag", + description: "If `true`, the results are filtered by note to deal pinning state", + optional: true, + }, + pinnedToOrganizationFlag: { + type: "boolean", + label: "Pinned to Organization Flag", + description: "If `true`, the results are filtered by note to organization pinning state", + optional: true, + }, + pinnedToPersonFlag: { + type: "boolean", + label: "Pinned to Person Flag", + description: "If `true`, the results are filtered by note to person pinning state", + optional: true, + }, + pinnedToProjectFlag: { + type: "boolean", + label: "Pinned to Project Flag", + description: "If `true`, the results are filtered by note to project pinning state", + optional: true, + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "The maximum number of results to return", + optional: true, + }, + }, + async run({ $ }) { + let notes = await this.pipedriveApp.getPaginatedResources({ + fn: this.pipedriveApp.getNotes, + params: { + user_id: this.userId, + lead_id: this.leadId, + deal_id: this.dealId, + person_id: this.personId, + org_id: this.organizationId, + project_id: this.projectId, + sort: this.sortField + ? `${this.sortField} ${this.sortDirection}` + : undefined, + pinned_to_lead_flag: this.pinnedToLeadFlag === true + ? 1 + : undefined, + pinned_to_deal_flag: this.pinnedToDealFlag === true + ? 1 + : undefined, + pinned_to_organization_flag: this.pinnedToOrganizationFlag === true + ? 1 + : undefined, + pinned_to_person_flag: this.pinnedToPersonFlag === true + ? 1 + : undefined, + pinned_to_project_flag: this.pinnedToProjectFlag === true + ? 1 + : undefined, + start_date: this.startDate, + end_date: this.endDate, + }, + max: this.maxResults, + }); + + if (this.searchTerm) { + notes = notes.filter((note) => + note.content?.toLowerCase().includes(this.searchTerm.toLowerCase())); + } + + $.export("$summary", `Successfully found ${notes.length} note${notes.length === 1 + ? "" + : "s"}`); + return notes; + }, +}; diff --git a/components/pipedrive/actions/search-persons/search-persons.mjs b/components/pipedrive/actions/search-persons/search-persons.mjs index a47ddb53a0108..c1b47f7700785 100644 --- a/components/pipedrive/actions/search-persons/search-persons.mjs +++ b/components/pipedrive/actions/search-persons/search-persons.mjs @@ -7,7 +7,7 @@ export default { key: "pipedrive-search-persons", name: "Search persons", description: "Searches all Persons by `name`, `email`, `phone`, `notes` and/or custom fields. This endpoint is a wrapper of `/v1/itemSearch` with a narrower OAuth scope. Found Persons can be filtered by Organization ID. See the Pipedrive API docs [here](https://developers.pipedrive.com/docs/api/v1/Persons#searchPersons)", - version: "0.1.9", + version: "0.1.10", type: "action", props: { pipedriveApp, diff --git a/components/pipedrive/actions/update-deal/update-deal.mjs b/components/pipedrive/actions/update-deal/update-deal.mjs index cbebcbde1b339..e1ef1d03977af 100644 --- a/components/pipedrive/actions/update-deal/update-deal.mjs +++ b/components/pipedrive/actions/update-deal/update-deal.mjs @@ -5,7 +5,7 @@ export default { key: "pipedrive-update-deal", name: "Update Deal", description: "Updates the properties of a deal. See the Pipedrive API docs for Deals [here](https://developers.pipedrive.com/docs/api/v1/Deals#updateDeal)", - version: "0.1.11", + version: "0.1.12", type: "action", props: { pipedriveApp, diff --git a/components/pipedrive/actions/update-person/update-person.mjs b/components/pipedrive/actions/update-person/update-person.mjs index 39608512fb703..13f447d3030ce 100644 --- a/components/pipedrive/actions/update-person/update-person.mjs +++ b/components/pipedrive/actions/update-person/update-person.mjs @@ -6,7 +6,7 @@ export default { key: "pipedrive-update-person", name: "Update Person", description: "Updates an existing person in Pipedrive. [See the documentation](https://developers.pipedrive.com/docs/api/v1/Persons#updatePerson)", - version: "0.0.1", + version: "0.0.2", type: "action", props: { pipedriveApp, diff --git a/components/pipedrive/package.json b/components/pipedrive/package.json index 193b0be48bae4..79cbd3091f598 100644 --- a/components/pipedrive/package.json +++ b/components/pipedrive/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/pipedrive", - "version": "0.5.1", + "version": "0.6.0", "description": "Pipedream Pipedrive Components", "main": "pipedrive.app.mjs", "keywords": [ @@ -15,6 +15,7 @@ }, "dependencies": { "@pipedream/platform": "^3.0.3", + "html-entities": "^2.6.0", "pipedrive": "^24.1.1" } } diff --git a/components/pipedrive/pipedrive.app.mjs b/components/pipedrive/pipedrive.app.mjs index d90fafbe6f780..8c985902defd0 100644 --- a/components/pipedrive/pipedrive.app.mjs +++ b/components/pipedrive/pipedrive.app.mjs @@ -355,6 +355,10 @@ export default { const leadLabelsApi = this.api("LeadLabelsApi"); return leadLabelsApi.getLeadLabels(opts); }, + getNotes(opts = {}) { + const notesApi = this.api("NotesApi"); + return notesApi.getNotes(opts); + }, addActivity(opts = {}) { const activityApi = this.api("ActivitiesApi", "v2"); return activityApi.addActivity({ @@ -425,5 +429,45 @@ export default { UpdatePersonRequest: opts, }); }, + deleteNote(noteId) { + const notesApi = this.api("NotesApi"); + return notesApi.deleteNote({ + id: noteId, + }); + }, + async *paginate({ + fn, params, max, + }) { + params = { + ...params, + start: 0, + limit: 100, + }; + let hasMore, count = 0; + do { + const { + data, additional_data: additionalData, + } = await fn(params); + if (!data?.length) { + return; + } + for (const item of data) { + yield item; + if (max && ++count >= max) { + return; + } + } + params.start += params.limit; + hasMore = additionalData.pagination.more_items_in_collection; + } while (hasMore); + }, + async getPaginatedResources(opts) { + const results = []; + const resources = this.paginate(opts); + for await (const resource of resources) { + results.push(resource); + } + return results; + }, }, }; diff --git a/components/pipedrive/sources/new-deal-instant/new-deal-instant.mjs b/components/pipedrive/sources/new-deal-instant/new-deal-instant.mjs index 437f92de339e1..881bda6ffdd18 100644 --- a/components/pipedrive/sources/new-deal-instant/new-deal-instant.mjs +++ b/components/pipedrive/sources/new-deal-instant/new-deal-instant.mjs @@ -6,7 +6,7 @@ export default { key: "pipedrive-new-deal-instant", name: "New Deal (Instant)", description: "Emit new event when a new deal is created.", - version: "0.0.5", + version: "0.0.6", type: "source", dedupe: "unique", methods: { diff --git a/components/pipedrive/sources/new-person-instant/new-person-instant.mjs b/components/pipedrive/sources/new-person-instant/new-person-instant.mjs index ddbb174757caa..210aac876d859 100644 --- a/components/pipedrive/sources/new-person-instant/new-person-instant.mjs +++ b/components/pipedrive/sources/new-person-instant/new-person-instant.mjs @@ -6,7 +6,7 @@ export default { key: "pipedrive-new-person-instant", name: "New Person (Instant)", description: "Emit new event when a new person is created.", - version: "0.0.5", + version: "0.0.6", type: "source", dedupe: "unique", methods: { diff --git a/components/pipedrive/sources/updated-deal-instant/updated-deal-instant.mjs b/components/pipedrive/sources/updated-deal-instant/updated-deal-instant.mjs index 89aa2b8eb8bf7..69b221e2e9168 100644 --- a/components/pipedrive/sources/updated-deal-instant/updated-deal-instant.mjs +++ b/components/pipedrive/sources/updated-deal-instant/updated-deal-instant.mjs @@ -6,7 +6,7 @@ export default { key: "pipedrive-updated-deal-instant", name: "New Deal Update (Instant)", description: "Emit new event when a deal is updated.", - version: "0.0.5", + version: "0.0.6", type: "source", dedupe: "unique", methods: { diff --git a/components/pipedrive/sources/updated-person-instant/updated-person-instant.mjs b/components/pipedrive/sources/updated-person-instant/updated-person-instant.mjs index 6b808b8d12f63..cb09964ece5a7 100644 --- a/components/pipedrive/sources/updated-person-instant/updated-person-instant.mjs +++ b/components/pipedrive/sources/updated-person-instant/updated-person-instant.mjs @@ -6,7 +6,7 @@ export default { key: "pipedrive-updated-person-instant", name: "Updated Person (Instant)", description: "Emit new event when a person is updated.", - version: "0.0.5", + version: "0.0.6", type: "source", dedupe: "unique", methods: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 57e31305818d9..523e19a1d572d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10030,6 +10030,9 @@ importers: '@pipedream/platform': specifier: ^3.0.3 version: 3.0.3 + html-entities: + specifier: ^2.6.0 + version: 2.6.0 pipedrive: specifier: ^24.1.1 version: 24.1.1 @@ -15639,14 +15642,6 @@ importers: specifier: ^6.0.0 version: 6.2.0 - modelcontextprotocol/node_modules2/@modelcontextprotocol/sdk/dist/cjs: {} - - modelcontextprotocol/node_modules2/@modelcontextprotocol/sdk/dist/esm: {} - - modelcontextprotocol/node_modules2/zod-to-json-schema/dist/cjs: {} - - modelcontextprotocol/node_modules2/zod-to-json-schema/dist/esm: {} - packages/ai: dependencies: '@pipedream/sdk': @@ -24609,8 +24604,8 @@ packages: html-entities@1.4.0: resolution: {integrity: sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==} - html-entities@2.5.2: - resolution: {integrity: sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==} + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -34332,7 +34327,7 @@ snapshots: fast-xml-parser: 4.5.0 gaxios: 6.7.1 google-auth-library: 9.15.0 - html-entities: 2.5.2 + html-entities: 2.6.0 mime: 3.0.0 p-limit: 3.1.0 retry-request: 7.0.2 @@ -43012,8 +43007,7 @@ snapshots: html-entities@1.4.0: {} - html-entities@2.5.2: - optional: true + html-entities@2.6.0: {} html-escaper@2.0.2: {}