From e31f6f91f176513df62594c0c45c9603e452a7c5 Mon Sep 17 00:00:00 2001 From: Kishan Parmar <135701940+kishanprmr@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:52:21 +0530 Subject: [PATCH 1/2] feat(salesforce): add queue option for new case trigger (#11452) --- bun.lock | 2 +- .../pieces/community/salesforce/package.json | 2 +- .../pieces/community/salesforce/src/index.ts | 184 +++++++++--------- .../salesforce/src/lib/common/index.ts | 39 ++++ .../src/lib/trigger/new-case-in-queue.ts | 130 +++++++++++++ .../salesforce/src/lib/trigger/new-case.ts | 68 ------- 6 files changed, 263 insertions(+), 162 deletions(-) create mode 100644 packages/pieces/community/salesforce/src/lib/trigger/new-case-in-queue.ts delete mode 100644 packages/pieces/community/salesforce/src/lib/trigger/new-case.ts diff --git a/bun.lock b/bun.lock index 022ff676120..01f2132171a 100644 --- a/bun.lock +++ b/bun.lock @@ -5404,7 +5404,7 @@ }, "packages/pieces/community/salesforce": { "name": "@activepieces/piece-salesforce", - "version": "0.5.3", + "version": "0.6.0", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", diff --git a/packages/pieces/community/salesforce/package.json b/packages/pieces/community/salesforce/package.json index 479c1cd3c0c..62ce68f20c2 100644 --- a/packages/pieces/community/salesforce/package.json +++ b/packages/pieces/community/salesforce/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-salesforce", - "version": "0.5.3", + "version": "0.6.0", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "scripts": { diff --git a/packages/pieces/community/salesforce/src/index.ts b/packages/pieces/community/salesforce/src/index.ts index d250d774009..0cf2d85de71 100644 --- a/packages/pieces/community/salesforce/src/index.ts +++ b/packages/pieces/community/salesforce/src/index.ts @@ -1,8 +1,8 @@ import { - PieceAuth, - Property, - createPiece, - OAuth2PropertyValue, + PieceAuth, + Property, + createPiece, + OAuth2PropertyValue, } from '@activepieces/pieces-framework'; import { PieceCategory } from '@activepieces/shared'; import { createCustomApiCallAction } from '@activepieces/pieces-common'; @@ -42,97 +42,97 @@ import { newOutboundMessage } from './lib/trigger/new-outbound-message'; import { newRecord } from './lib/trigger/new-record'; import { newUpdatedFile } from './lib/trigger/new-updated-file'; import { exportReport } from './lib/action/export-report'; -import { newCaseCreatedTrigger } from './lib/trigger/new-case'; +import { newCaseCreatedTrigger } from './lib/trigger/new-case-in-queue'; export const salesforceAuth = PieceAuth.OAuth2({ - props: { - environment: Property.StaticDropdown({ - displayName: 'Environment', - description: 'Choose environment', - required: true, - options: { - options: [ - { - label: 'Production', - value: 'login', - }, - { - label: 'Development', - value: 'test', - }, - ], - }, - defaultValue: 'login', - }), - }, - required: true, - description: 'Authenticate with Salesforce Production', - authUrl: 'https://{environment}.salesforce.com/services/oauth2/authorize', - tokenUrl: 'https://{environment}.salesforce.com/services/oauth2/token', - scope: ['refresh_token', 'full', 'api'], + props: { + environment: Property.StaticDropdown({ + displayName: 'Environment', + description: 'Choose environment', + required: true, + options: { + options: [ + { + label: 'Production', + value: 'login', + }, + { + label: 'Development', + value: 'test', + }, + ], + }, + defaultValue: 'login', + }), + }, + required: true, + description: 'Authenticate with Salesforce Production', + authUrl: 'https://{environment}.salesforce.com/services/oauth2/authorize', + tokenUrl: 'https://{environment}.salesforce.com/services/oauth2/token', + scope: ['refresh_token', 'full', 'api'], }); export const salesforce = createPiece({ - displayName: 'Salesforce', - description: 'CRM software solutions and enterprise cloud computing', - minimumSupportedRelease: '0.30.0', - logoUrl: 'https://cdn.activepieces.com/pieces/salesforce.png', - authors: [ - 'HKudria', - 'tanoggy', - 'landonmoir', - 'kishanprmr', - 'khaledmashaly', - 'abuaboud', - 'Pranith124', - 'sanket-a11y', - ], - categories: [PieceCategory.SALES_AND_CRM], - auth: salesforceAuth, - actions: [ - addContactToCampaign, - addFileToRecord, - addLeadToCampaign, - createAttachment, - createCase, - createContact, - createLead, - createNote, - createOpportunity, - createRecord, - createTask, - deleteOpportunity, - deleteRecord, - exportReport, - findChildRecords, - findRecord, - findRecordsByQuery, - getRecordAttachments, - runQuery, - runReport, - sendEmail, - updateContact, - updateLead, - updateRecord, - upsertByExternalId, - upsertByExternalIdBulk, - createCustomApiCallAction({ - baseUrl: (auth) => (auth as OAuth2PropertyValue).data['instance_url'], - auth: salesforceAuth, - authMapping: async (auth) => ({ - Authorization: `Bearer ${(auth as OAuth2PropertyValue).access_token}`, - }), - }), - ], - triggers: [ - newCaseAttachment, - newContact, - newFieldHistoryEvent, - newLead, - newOrUpdatedRecord, - newOutboundMessage, - newRecord, - newUpdatedFile, - newCaseCreatedTrigger, - ], + displayName: 'Salesforce', + description: 'CRM software solutions and enterprise cloud computing', + minimumSupportedRelease: '0.30.0', + logoUrl: 'https://cdn.activepieces.com/pieces/salesforce.png', + authors: [ + 'HKudria', + 'tanoggy', + 'landonmoir', + 'kishanprmr', + 'khaledmashaly', + 'abuaboud', + 'Pranith124', + 'sanket-a11y', + ], + categories: [PieceCategory.SALES_AND_CRM], + auth: salesforceAuth, + actions: [ + addContactToCampaign, + addFileToRecord, + addLeadToCampaign, + createAttachment, + createCase, + createContact, + createLead, + createNote, + createOpportunity, + createRecord, + createTask, + deleteOpportunity, + deleteRecord, + exportReport, + findChildRecords, + findRecord, + findRecordsByQuery, + getRecordAttachments, + runQuery, + runReport, + sendEmail, + updateContact, + updateLead, + updateRecord, + upsertByExternalId, + upsertByExternalIdBulk, + createCustomApiCallAction({ + baseUrl: (auth) => (auth as OAuth2PropertyValue).data['instance_url'], + auth: salesforceAuth, + authMapping: async (auth) => ({ + Authorization: `Bearer ${(auth as OAuth2PropertyValue).access_token}`, + }), + }), + ], + triggers: [ + newCaseAttachment, + newContact, + newFieldHistoryEvent, + newLead, + newOrUpdatedRecord, + newOutboundMessage, + newRecord, + newUpdatedFile, + newCaseCreatedTrigger, + ], }); diff --git a/packages/pieces/community/salesforce/src/lib/common/index.ts b/packages/pieces/community/salesforce/src/lib/common/index.ts index 8c67096edea..f9f9cf8cc17 100644 --- a/packages/pieces/community/salesforce/src/lib/common/index.ts +++ b/packages/pieces/community/salesforce/src/lib/common/index.ts @@ -557,6 +557,45 @@ export const salesforcesCommon = { } }, }), + caseQueueId: Property.Dropdown({ + auth: salesforceAuth, + displayName: 'Case Queue', + description: 'The queue to monitor for new cases.', + required: true, + refreshers: [], + refreshOnSearch: true, + options: async ({ auth }, { searchValue }) => { + if (!auth) { + return { + disabled: true, + placeholder: 'Connect your account first', + options: [], + }; + } + + let query = ` + SELECT QueueId, Queue.Name + FROM QueueSobject + WHERE SobjectType = 'Case' + `; + + if (searchValue) { + const sanitizedSearch = searchValue.replace(/'/g, "\\'"); + query += ` AND Queue.Name LIKE '${sanitizedSearch}%'`; + } + + const response = await querySalesforceApi<{ + records: { QueueId: string; Queue: { Name: string } }[]; + }>(HttpMethod.GET, auth as OAuth2PropertyValue, query); + return { + disabled: false, + options: response.body.records.map((record) => ({ + label: record.Queue.Name, + value: record.QueueId, + })), + }; + }, + }), optionalContact: Property.Dropdown({ auth: salesforceAuth, displayName: 'Contact', diff --git a/packages/pieces/community/salesforce/src/lib/trigger/new-case-in-queue.ts b/packages/pieces/community/salesforce/src/lib/trigger/new-case-in-queue.ts new file mode 100644 index 00000000000..cd58400e4ab --- /dev/null +++ b/packages/pieces/community/salesforce/src/lib/trigger/new-case-in-queue.ts @@ -0,0 +1,130 @@ +import { DedupeStrategy, HttpMethod, Polling, pollingHelper } from '@activepieces/pieces-common'; +import { + AppConnectionValueForAuthProperty, + TriggerStrategy, + createTrigger, +} from '@activepieces/pieces-framework'; +import { querySalesforceApi, salesforcesCommon } from '../common'; + +import dayjs from 'dayjs'; +import { salesforceAuth } from '../..'; + +// https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_fields.htm +// Hard Salesforce limit: FIELDS(ALL) returns at most 200 records per query +const FIELDS_ALL_LIMIT = 200; + +export const newCaseCreatedTrigger = createTrigger({ + auth: salesforceAuth, + name: 'new_case', + displayName: 'New Case in Queue', + description: 'Triggers when a new Case record is assigned to a specified queue.', + props: { + caseQueueId: salesforcesCommon.caseQueueId, + }, + sampleData: undefined, + type: TriggerStrategy.POLLING, + async test(ctx) { + return pollingHelper.test(polling, ctx); + }, + async onEnable(ctx) { + return pollingHelper.onEnable(polling, ctx); + }, + async onDisable(ctx) { + return pollingHelper.onDisable(polling, ctx); + }, + async run(ctx) { + return pollingHelper.poll(polling, ctx); + }, +}); + +const polling: Polling< + AppConnectionValueForAuthProperty, + { caseQueueId: string } +> = { + strategy: DedupeStrategy.TIMEBASED, + items: async ({ auth, lastFetchEpochMS, propsValue }) => { + const { caseQueueId } = propsValue; + + if (lastFetchEpochMS === 0) { + // Test mode: return the 10 most recent cases currently in the queue + const response = await querySalesforceApi<{ records: { CreatedDate: string }[] }>( + HttpMethod.GET, + auth, + `SELECT FIELDS(ALL) FROM Case WHERE OwnerId = '${caseQueueId}' ORDER BY CreatedDate DESC LIMIT 10`, + ); + + return (response.body?.['records'] ?? []).map((record: { CreatedDate: string }) => ({ + epochMilliSeconds: dayjs(record.CreatedDate).valueOf(), + data: record, + })); + } + + const isoDate = dayjs(lastFetchEpochMS).toISOString(); + + // Step 1a: Cases moved to queue after creation via owner change (CaseHistory). + // Assignment Rules that fire during case creation do NOT write a CaseHistory entry, + // so we also query Case directly (Step 1b) to cover that scenario. + const historyResponse = await querySalesforceApi<{ + records: { CaseId: string; CreatedDate: string; NewValue: string }[]; + }>( + HttpMethod.GET, + auth, + `SELECT CaseId, CreatedDate, NewValue FROM CaseHistory WHERE Field = 'Owner' AND CreatedDate > ${isoDate}`, + ); + + // NewValue is not filterable in SOQL — filter in code. + const historyRecords = (historyResponse.body?.['records'] ?? []).filter( + (r) => r.NewValue === caseQueueId, + ); + + // Step 1b: Cases assigned to queue on creation via Assignment Rules. + // These never produce a CaseHistory entry — detected via Case.CreatedDate. + // Cases created before isoDate are excluded, so no overlap with Step 1a. + const directResponse = await querySalesforceApi<{ + records: { Id: string; CreatedDate: string }[]; + }>( + HttpMethod.GET, + auth, + `SELECT Id, CreatedDate FROM Case WHERE OwnerId = '${caseQueueId}' AND CreatedDate > ${isoDate}`, + ); + + const directRecords = directResponse.body?.['records'] ?? []; + + // Merge both sources — CaseHistory overwrites on duplicate since its CreatedDate + // reflects the actual assignment time, which is more accurate than Case.CreatedDate. + const caseIdToQueueDate: Record = {}; + for (const r of directRecords) { + caseIdToQueueDate[r.Id] = r.CreatedDate; + } + for (const r of historyRecords) { + caseIdToQueueDate[r.CaseId] = r.CreatedDate; + } + + if (Object.keys(caseIdToQueueDate).length === 0) return []; + + const allIds = Object.keys(caseIdToQueueDate); + + // Step 2: Fetch full Case records in sequential batches of 200 (FIELDS(ALL) limit). + const allCaseRecords: Record[] = []; + + for (let i = 0; i < allIds.length; i += FIELDS_ALL_LIMIT) { + const chunk = allIds.slice(i, i + FIELDS_ALL_LIMIT); + const ids = chunk.map((id) => `'${id}'`).join(','); + + const caseResponse = await querySalesforceApi<{ records: Record[] }>( + HttpMethod.GET, + auth, + `SELECT FIELDS(ALL) FROM Case WHERE Id IN (${ids}) LIMIT ${FIELDS_ALL_LIMIT}`, + ); + + allCaseRecords.push(...(caseResponse.body?.['records'] ?? [])); + } + + return allCaseRecords.map((record) => ({ + epochMilliSeconds: dayjs( + (caseIdToQueueDate[record['Id'] as string] ?? record['CreatedDate']) as string, + ).valueOf(), + data: record, + })); + }, +}; diff --git a/packages/pieces/community/salesforce/src/lib/trigger/new-case.ts b/packages/pieces/community/salesforce/src/lib/trigger/new-case.ts deleted file mode 100644 index 78c3498c35f..00000000000 --- a/packages/pieces/community/salesforce/src/lib/trigger/new-case.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { - DedupeStrategy, - HttpMethod, - Polling, - pollingHelper, -} from '@activepieces/pieces-common'; -import { - AppConnectionValueForAuthProperty, - TriggerStrategy, - createTrigger, -} from '@activepieces/pieces-framework'; -import { querySalesforceApi } from '../common'; - -import dayjs from 'dayjs'; -import { salesforceAuth } from '../..'; - -export const newCaseCreatedTrigger = createTrigger({ - auth: salesforceAuth, - name: 'new_case', - displayName: 'New Case', - description: 'Triggers when a new Case record is created.', - props: {}, - sampleData: undefined, - type: TriggerStrategy.POLLING, - async test(ctx) { - return await pollingHelper.test(polling, ctx); - }, - async onEnable(ctx) { - await pollingHelper.onEnable(polling, ctx); - }, - async onDisable(ctx) { - await pollingHelper.onDisable(polling, ctx); - }, - async run(ctx) { - return await pollingHelper.poll(polling, ctx); - }, -}); - -const polling: Polling< - AppConnectionValueForAuthProperty, - Record -> = { - strategy: DedupeStrategy.TIMEBASED, - items: async ({ auth, lastFetchEpochMS }) => { - const isoDate = dayjs(lastFetchEpochMS).toISOString(); - - const isTest = lastFetchEpochMS === 0; - - const query = ` - SELECT FIELDS(ALL) - FROM Case - ${isTest ? '' : `WHERE CreatedDate > ${isoDate}`} - ORDER BY CreatedDate DESC - LIMIT ${isTest ? 10 : 200} - `; - - const response = await querySalesforceApi<{ - records: { CreatedDate: string }[]; - }>(HttpMethod.GET, auth, query); - - const records = response.body?.['records'] || []; - - return records.map((record: { CreatedDate: string }) => ({ - epochMilliSeconds: dayjs(record.CreatedDate).valueOf(), - data: record, - })); - }, -}; From 5cf208f45368023b33851c0beb96f8276174c8f9 Mon Sep 17 00:00:00 2001 From: Kishan Parmar <135701940+kishanprmr@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:58:44 +0530 Subject: [PATCH 2/2] fix: include custom pieces directory in workspaces (#11455) --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index e0eea702ec0..1ced7058288 100644 --- a/package.json +++ b/package.json @@ -383,7 +383,8 @@ "packages/pieces/framework", "packages/pieces/common", "packages/pieces/core/*", - "packages/pieces/community/*" + "packages/pieces/community/*", + "packages/pieces/custom/*" ], "resolutions": { "rollup": "npm:@rollup/wasm-node"