Skip to content

Commit

Permalink
[SendGrid Lists] - Sendgrid lists changes (#2643)
Browse files Browse the repository at this point in the history
* phone number validation

* bug fixes for sendgrid lists destination
  • Loading branch information
joe-ayoub-segment authored Dec 12, 2024
1 parent 53ed4c4 commit bf3f80b
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
export const MAX_BATCH_SIZE = 100

export const GET_LIST_URL = 'https://api.sendgrid.com/v3/marketing/lists'

export const CREATE_LIST_URL = 'https://api.sendgrid.com/v3/marketing/lists'

export const UPSERT_CONTACTS_URL = 'https://api.sendgrid.com/v3/marketing/contacts'

export const REMOVE_CONTACTS_FROM_LIST_URL = 'https://api.sendgrid.com/v3/marketing/lists/{list_id}/contacts?contact_ids={contact_ids}'
export const REMOVE_CONTACTS_FROM_LIST_URL =
'https://api.sendgrid.com/v3/marketing/lists/{list_id}/contacts?contact_ids={contact_ids}'

export const MAX_CHUNK_SIZE_REMOVE = 100

Expand All @@ -14,4 +17,6 @@ export const SEARCH_CONTACTS_URL = 'https://api.sendgrid.com/v3/marketing/contac

export const MAX_CHUNK_SIZE_SEARCH = 50

export const GET_CUSTOM_FIELDS_URL = 'https://api.sendgrid.com/v3/marketing/field_definitions'
export const GET_CUSTOM_FIELDS_URL = 'https://api.sendgrid.com/v3/marketing/field_definitions'

export const E164_REGEX = /^\+[1-9]\d{1,14}$/
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { AudienceDestinationDefinition } from '@segment/actions-core'
import { AudienceDestinationDefinition, RequestClient, IntegrationError } from '@segment/actions-core'
import type { Settings, AudienceSettings } from './generated-types'
import syncAudience from './syncAudience'
import { CREATE_LIST_URL } from './constants'
import { CreateAudienceReq, CreateAudienceResp } from './types'
import { GET_LIST_URL, CREATE_LIST_URL } from './constants'
import { GetListsResp, GetListByIDResp, CreateAudienceReq, CreateAudienceResp } from './types'

const destination: AudienceDestinationDefinition<Settings, AudienceSettings> = {
name: 'SendGrid Lists (Actions)',
Expand Down Expand Up @@ -48,19 +48,34 @@ const destination: AudienceDestinationDefinition<Settings, AudienceSettings> = {
full_audience_sync: false
},
async createAudience(request, createAudienceInput) {
const response = await request<CreateAudienceResp>(CREATE_LIST_URL, {
method: 'POST',
throwHttpErrors: false,
json: {
name: createAudienceInput?.audienceSettings?.listName ?? createAudienceInput.audienceName
} as CreateAudienceReq
})
const json = await response.json()
return { externalId: json.id }
const name = createAudienceInput?.audienceSettings?.listName ?? createAudienceInput.audienceName
const id = await getAudienceIdByName(request, name)
if (id) {
return { externalId: id }
} else {
const response = await request(CREATE_LIST_URL, {
method: 'POST',
throwHttpErrors: false,
json: {
name
} as CreateAudienceReq
})
const json: CreateAudienceResp = await response.json()
return { externalId: json.id }
}
},
async getAudience(_, getAudienceInput) {
return {
externalId: getAudienceInput.externalId
const id = await getAudienceIdById(_, getAudienceInput.externalId)
if (id) {
return {
externalId: getAudienceInput.externalId
}
} else {
throw new IntegrationError(
`Audience with externalId ${getAudienceInput.externalId} not found`,
'GET_AUDIENCE_ERROR',
404
)
}
}
},
Expand All @@ -69,4 +84,22 @@ const destination: AudienceDestinationDefinition<Settings, AudienceSettings> = {
}
}

export async function getAudienceIdByName(request: RequestClient, name: string): Promise<string | undefined> {
const response = await request(GET_LIST_URL, {
method: 'GET',
throwHttpErrors: false
})
const json: GetListsResp = await response.json()
return json.result.find((list) => list.name === name)?.id ?? undefined
}

export async function getAudienceIdById(request: RequestClient, externalId: string): Promise<string | undefined> {
const response = await request(`${GET_LIST_URL}/${externalId}`, {
method: 'GET',
throwHttpErrors: false
})
const json: GetListByIDResp = await response.json()
return json.id === externalId ? externalId : undefined
}

export default destination
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import nock from 'nock'
import { createTestEvent, createTestIntegration, SegmentEvent, PayloadValidationError } from '@segment/actions-core'
import Definition from '../../index'
import { Settings } from '../../generated-types'
import { validatePhone } from '../utils'

let testDestination = createTestIntegration(Definition)

Expand Down Expand Up @@ -545,4 +546,14 @@ describe('SendgridAudiences.syncAudience', () => {
expect(responses[3].status).toBe(200)
expect(responses[4].status).toBe(200)
})

it('phone number should be E.164', async () => {
const goodPhone = '+353123456789' // E.164
const badPhone = '123456789' // no +
const badPhone2 = '+3531234567890678' // too long

expect(validatePhone(goodPhone)).toBe(true)
expect(validatePhone(badPhone)).toBe(false)
expect(validatePhone(badPhone2)).toBe(false)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
SEARCH_CONTACTS_URL,
REMOVE_CONTACTS_FROM_LIST_URL,
MAX_CHUNK_SIZE_SEARCH,
MAX_CHUNK_SIZE_REMOVE
MAX_CHUNK_SIZE_REMOVE,
E164_REGEX
} from '../constants'
import { UpsertContactsReq, SearchContactsResp, AddRespError } from '../types'
import chunk from 'lodash/chunk'
Expand Down Expand Up @@ -102,6 +103,10 @@ function validate(payloads: Payload[], ignoreErrors: boolean, invalidEmails?: st
delete p.email
}

if (p.phone_number_id && !validatePhone(p.phone_number_id)) {
delete p.phone_number_id
}

if (p.custom_fields) {
p.custom_fields = Object.fromEntries(
Object.entries(p.custom_fields).filter(([_, value]) => typeof value === 'string' || typeof value === 'number')
Expand All @@ -116,7 +121,7 @@ function validate(payloads: Payload[], ignoreErrors: boolean, invalidEmails?: st
}
return [key, value]
})
);
)
}

const hasRequiredField = [p.email, p.anonymous_id, p.external_id, p.phone_number_id].some(Boolean)
Expand Down Expand Up @@ -191,3 +196,7 @@ function getQueryPart(identifier: keyof Payload, payloads: Payload[]): string {
const part = values.length > 0 ? `${identifier} IN (${values.map((value) => `'${value}'`).join(',')})` : ''
return part
}

export function validatePhone(phone: string): boolean {
return E164_REGEX.test(phone)
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
export interface GetListsResp {
result: Array<GetListByIDResp>
_metadata: {
count: number
}
}

export interface GetListByIDResp {
id: string
name: string
}

export interface CreateAudienceReq {
name: string
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const FIELD_REGEX = /\[(.*?)\]/

export const TOKEN_REGEX = /{{(.*?)}}/g

export const E164_REGEX = /^\+?[1-9]\d{1,14}$/
export const E164_REGEX = /^\+[1-9]\d{1,14}$/

export const TWILIO_SHORT_CODE_REGEX = /^[1-9]\d{4,5}$/

Expand Down

0 comments on commit bf3f80b

Please sign in to comment.