Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Salesforce custom object external #2485

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -19,4 +19,4 @@ any of the tasks you completed below during your testing._
- [ ] Tested end-to-end using the [local server](https://github.com/segmentio/action-destinations/blob/main/docs/testing.md#local-end-to-end-testing)
- [ ] [If destination is already live] Tested for backward compatibility of destination. **Note:** New required fields are a breaking change.
- [ ] [Segmenters] Tested in the staging environment
- [ ] [Segmenters] [If applicable for this change] Tested for regression with Hadron.
- [ ] [Segmenters] [If applicable for this change] Tested for regression with Hadron.
Original file line number Diff line number Diff line change
@@ -80,11 +80,7 @@ const destination: AudienceDestinationDefinition<Settings, AudienceSettings> = {
},

async createAudience(request, createAudienceInput) {
const {
settings,
audienceSettings: { audience_name } = {},
personas
} = createAudienceInput
const { settings, audienceSettings: { audience_name } = {}, personas } = createAudienceInput

if (!audience_name) {
throw new IntegrationError('Missing Audience Name', 'MISSING_REQUIRED_FIELD', 400)
Original file line number Diff line number Diff line change
@@ -133,17 +133,17 @@ export default action
// remove any key value pairs where the value is not a true or false string
function filterMailingLists(obj: { [key: string]: unknown }): { [key: string]: boolean } {
const result: { [key: string]: boolean } = {}
for (const key in obj) {
const value = obj[key];
for (const key in obj) {
const value = obj[key]

if (typeof value === 'string') {
if (value === 'true') {
result[key] = true;
result[key] = true
} else if (value === 'false') {
result[key] = false;
result[key] = false
}
} else if (typeof value === 'boolean') {
result[key] = value;
result[key] = value
}
}
return result
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
export const PAGE = 'page'

export const TRACK = 'track'

export const SUPPORTED_TYPES = [PAGE, TRACK]
export const MAX_CUSTOM_PROPS_PER_EVENT = 15
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export const EMAIL_SCHEMA_NAME = 'EMAIL_SHA256'
export const MAID_SCHEMA_NAME = 'MAID_SHA256'
export const MAID_SCHEMA_NAME = 'MAID_SHA256'
Original file line number Diff line number Diff line change
@@ -2,40 +2,40 @@ interface IDArray extends Array<string> {}

// POST https://ads-api.reddit.com/api/v3/custom_audiences/{audience_id}/users
export interface PopulateAudienceJSON {
data: {
action_type: 'ADD' | 'REMOVE'
column_order: string[] // EMAIL_SHA256 MAID_SHA256
user_data: Array<IDArray>
}
data: {
action_type: 'ADD' | 'REMOVE'
column_order: string[] // EMAIL_SHA256 MAID_SHA256
user_data: Array<IDArray>
}
}

// POST https://ads-api.reddit.com/api/v3/ad_accounts/{ad_account_id}/custom_audiences
export interface CreateAudienceJSON {
data: {
name: string
type: 'CUSTOMER_LIST'
}
data: {
name: string
type: 'CUSTOMER_LIST'
}
}

// 200 expected
export interface CreateAudienceResp {
data: {
name: string,
id: string,
status: "VALID"
}
data: {
name: string
id: string
status: 'VALID'
}
}

// 400 expected
export interface CreateAudienceRespError {
error: {
code: number,
message: string
}
error: {
code: number
message: string
}
}

// POST https://www.reddit.com/api/v1/access_token
export interface RedditAccessTokenRequest {
grant_type: string,
refresh_token: string
}
grant_type: string
refresh_token: string
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@

import { MAID_SCHEMA_NAME, EMAIL_SCHEMA_NAME } from './const'

export interface CreateAudienceResp {
id: string
id: string
}

export interface CreateAudienceReq {
data: {
name: string
type: string
}
data: {
name: string
type: string
}
}

export interface UpdateAudienceReq {
data: {
column_order: Columns
user_data: string[][],
action_type: 'ADD' | 'REMOVE'
}
data: {
column_order: Columns
user_data: string[][]
action_type: 'ADD' | 'REMOVE'
}
}

export type Columns = (typeof MAID_SCHEMA_NAME | typeof EMAIL_SCHEMA_NAME)[]
export type Columns = (typeof MAID_SCHEMA_NAME | typeof EMAIL_SCHEMA_NAME)[]
Original file line number Diff line number Diff line change
@@ -173,4 +173,3 @@ const hash = (value: string | undefined): string | undefined => {
hash.update(value)
return hash.digest('hex')
}

Original file line number Diff line number Diff line change
@@ -15,6 +15,6 @@ export interface RawMapping {
}

export interface ColumnHeader {
cleanName: string
cleanName: string
originalName: string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import nock from 'nock'
import { createTestEvent, createTestIntegration } from '@segment/actions-core'
import Destination from '../index'
import { API_VERSION } from '../sf-operations'

const testDestination = createTestIntegration(Destination)

const settings = {
instanceUrl: 'https://test.salesforce.com'
}

describe('Salesforce', () => {
describe('Custom Object by External Id', () => {
it('Should end events successfully', async () => {
const customObjectName = 'TestCustom__c'
const objectExternalId = 'Prospect__c'
const externalIdValue = '123456-7890-ABCD-0123-45678901234567'
const event = createTestEvent({
type: 'track',
event: 'Create Custom Object',
properties: {
email: 'sponge@seamail.com',
company: 'Krusty Krab',
last_name: 'Squarepants',
object_type: objectExternalId
},
userId: externalIdValue
})

nock(`${settings.instanceUrl}/services/data/${API_VERSION}/sobjects`)
.patch(`/${customObjectName}/${objectExternalId}/${externalIdValue}`)
.reply(200, {})

const responses = await testDestination.testAction('customObjectExternalId', {
event,
settings,
mapping: {
operation: 'create',
customObjectName: customObjectName,
externalIdField: {
'@path': '$.properties.object_type'
},
externalIdValue: {
'@path': '$.userId'
},
customFields: {
'@path': '$.properties'
}
}
})

expect(responses.length).toBe(1)
expect(responses[0].status).toBe(200)
})
})
})

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { ActionDefinition, ExecuteInput, RequestClient } from '@segment/actions-core'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
import { customFields, operation } from '../sf-properties'
import Salesforce, { generateSalesforceRequest } from '../sf-operations'

const action: ActionDefinition<Settings, Payload> = {
title: 'Custom Object by External Id',
description:
'Create, update, or upsert records in any custom or standard object in Salesforce, using its External ID.',
fields: {
operation: operation,
customObjectName: {
label: 'Salesforce Object',
description:
'The API name of the Salesforce object that records will be added or updated within. This can be a standard or custom object. Custom objects must be predefined in your Salesforce account and should end with "__c".',
type: 'string',
required: true,
dynamic: true
},
externalIdField: {
label: 'External ID Field',
description: 'The name of the field that will be used as the External ID.',
type: 'string',
required: true
},
externalIdValue: {
label: 'External Id Value',
description: 'The external id field value that will be used to update the record.',
type: 'string',
required: true
},
customFields: customFields
},
dynamicFields: {
customObjectName: async (request: RequestClient, data: ExecuteInput<Settings, Payload, any, any, any>) => {
const salesforceInstance: Salesforce = new Salesforce(
data.settings.instanceUrl,
await generateSalesforceRequest(data.settings, request)
)

return salesforceInstance.customObjectName()
}
},
perform: async (request, { settings, payload }) => {
const sf: Salesforce = new Salesforce(settings.instanceUrl, await generateSalesforceRequest(settings, request))

return sf.upsertCustomObject(payload, payload.customObjectName)
}
}

export default action
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import contact from './contact'
import account from './account'
import { authenticateWithPassword } from './sf-operations'

import customObjectExternalId from './customObjectExternalId'
import lead2 from './lead2'
import contact2 from './contact2'
import cases2 from './cases2'
@@ -126,6 +127,7 @@ const destination: DestinationDefinition<Settings> = {
contact,
opportunity,
account,
customObjectExternalId,
lead2,
customObject2,
account2,
Original file line number Diff line number Diff line change
@@ -6,13 +6,14 @@ import {
StatsContext
} from '@segment/actions-core'
import type { GenericPayload } from './sf-types'
import { Payload as CustomObjectExternalIdPayload } from './customObjectExternalId/generated-types'
import { mapObjectToShape } from './sf-object-to-shape'
import { buildCSVData, validateInstanceURL } from './sf-utils'
import { DynamicFieldResponse, createRequestClient } from '@segment/actions-core'
import { Settings } from './generated-types'
import { Logger } from '@segment/actions-core/destination-kit'

export const API_VERSION = 'v53.0'
export const API_VERSION = 'v62.0'

/**
* This error is triggered if the bulkHandler is ever triggered when the enable_batching setting is false.
@@ -272,9 +273,13 @@ export default class Salesforce {

if (syncMode === 'upsert') {
return await this.bulkUpsert(payloads, sobject, statsContext, logger)
} else if (syncMode === 'update') {
}

if (syncMode === 'update') {
return await this.bulkUpdate(payloads, sobject, statsContext, logger)
} else if (syncMode === 'add') {
}

if (syncMode === 'add') {
// Sync Mode does not have a "create" operation. We call it "add".
// "add" will be transformed into "create" in the bulkInsert function.
return await this.bulkInsert(payloads, sobject, statsContext, logger)
@@ -315,6 +320,18 @@ export default class Salesforce {
}
}

upsertCustomObject = async (payload: CustomObjectExternalIdPayload, customObjectName: string) => {
const result = await this.request(
`${this.instanceUrl}services/data/${API_VERSION}/sobjects/${customObjectName}/${payload.externalIdField}/${payload.externalIdValue}`,
{
method: 'patch',
json: payload.customFields
}
)

return result
}

private bulkInsert = async (
payloads: GenericPayload[],
sobject: string,
@@ -376,7 +393,7 @@ export default class Salesforce {
const jobId = await this.createBulkJob(sobject, idField, operation)
try {
await this.uploadBulkCSV(jobId, csv)
} catch (err) {
} catch (err: any) {
// always close the "bulk job" otherwise it will get
// stuck in "pending".
//
@@ -398,7 +415,7 @@ export default class Salesforce {

try {
return await this.closeBulkJob(jobId)
} catch (err) {
} catch (err: any) {
const message = err.response?.data[0]?.message || 'Failed to parse message'
const code = err.response?.data[0]?.errorCode || 'Failed to parse code'


Unchanged files with check annotations Beta

export class AggregateError extends IntegrationError {
static create(args: {
errors: any[]

Check warning on line 6 in packages/actions-shared/src/engage/utils/AggregateError.ts

GitHub Actions / Lint (18.x)

Unexpected any. Specify a different type
code?: string
status?: number
takeCodeAndStatusFromError?: any

Check warning on line 9 in packages/actions-shared/src/engage/utils/AggregateError.ts

GitHub Actions / Lint (18.x)

Unexpected any. Specify a different type
message?: (msg: string) => string
}) {
const firstErrorInfo = getErrorDetails(
return new AggregateError(args.errors, args.code, args.status, undefined, message)
}
constructor(public errors: any[], code?: string, status?: number, public data?: any, message?: string) {

Check warning on line 25 in packages/actions-shared/src/engage/utils/AggregateError.ts

GitHub Actions / Lint (18.x)

Unexpected any. Specify a different type

Check warning on line 25 in packages/actions-shared/src/engage/utils/AggregateError.ts

GitHub Actions / Lint (18.x)

Unexpected any. Specify a different type
super(message || 'Multiple errors', code!, status!)

Check warning on line 26 in packages/actions-shared/src/engage/utils/AggregateError.ts

GitHub Actions / Lint (18.x)

Forbidden non-null assertion

Check warning on line 26 in packages/actions-shared/src/engage/utils/AggregateError.ts

GitHub Actions / Lint (18.x)

Forbidden non-null assertion
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
export type GenericMethodDecorator<
TFunc extends (this: any, ...args: any[]) => any = (this: any, ...args: any) => any

Check warning on line 2 in packages/actions-shared/src/engage/utils/operationTracking/GenericMethodDecorator.ts

GitHub Actions / Lint (18.x)

Unexpected any. Specify a different type

Check warning on line 2 in packages/actions-shared/src/engage/utils/operationTracking/GenericMethodDecorator.ts

GitHub Actions / Lint (18.x)

Unexpected any. Specify a different type

Check warning on line 2 in packages/actions-shared/src/engage/utils/operationTracking/GenericMethodDecorator.ts

GitHub Actions / Lint (18.x)

Unexpected any. Specify a different type

Check warning on line 2 in packages/actions-shared/src/engage/utils/operationTracking/GenericMethodDecorator.ts

GitHub Actions / Lint (18.x)

Unexpected any. Specify a different type
> = (
target: ThisParameterType<TFunc>,
propertyKey: string,