Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cd17d84
Update delete method to a POST
askkaz Jun 30, 2023
63866be
commit test fix
askkaz Jun 30, 2023
dfd8614
Merge branch 'segmentio:main' into main
askkaz Sep 29, 2023
32974fb
Merge branch 'main' of https://github.com/Loops-so/action-destinations
danrowden Mar 27, 2024
7903e7d
Added support for event properties
danrowden Apr 8, 2024
be06dab
Updated tests
danrowden Apr 8, 2024
adf31f5
Merge branch 'segmentio:main' into main
danrowden Apr 9, 2024
792057e
Use properties
danrowden May 2, 2024
c19bc8e
Merge branch 'segmentio:main' into main
askkaz Jun 13, 2024
829cad0
Merge pull request #1 from Loops-so/event-properties
askkaz Jun 13, 2024
e75c63a
Update packages/destination-actions/src/destinations/loops/sendEvent/…
askkaz Jun 14, 2024
f34eccb
Update packages/destination-actions/src/destinations/loops/sendEvent/…
askkaz Jun 14, 2024
2609d56
Added mailing lists support
danrowden Jul 2, 2024
29a9189
Fix tests
danrowden Jul 2, 2024
b8fd48c
Merge pull request #2 from Loops-so/mailing-lists
askkaz Jul 9, 2024
effbb8e
Fix type
danrowden Jul 9, 2024
a893a44
Merge branch 'segmentio:main' into main
askkaz Jul 12, 2024
a80edef
New shape for mailing list data
danrowden Jul 25, 2024
b976431
Merge branch 'main' of https://github.com/Loops-so/action-destinations
danrowden Jul 25, 2024
6729bf1
Merge pull request #3 from segmentio/main
danrowden Jul 30, 2024
954b21d
Merge pull request #4 from segmentio/main
askkaz Aug 21, 2024
18a8bb0
update tests and snapshots
askkaz Aug 21, 2024
9d27c0c
Use different record IDs
danrowden Aug 21, 2024
a9b8c4f
Merge pull request #5 from Loops-so/fix_tests
askkaz Aug 22, 2024
8ca66d0
Re-formatted mailing lists
danrowden Aug 26, 2024
de67e21
Use `traits; test for `mailingLists`
danrowden Aug 28, 2024
b5ac1f8
More tests
danrowden Aug 28, 2024
f27e01f
Update generated-types.ts
danrowden Aug 28, 2024
6b16f4f
Added contact properties to Send event action
danrowden Mar 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Object {
"email": "bivjag@ennaato.st",
"firstName": "3dQ7ER]1HKW",
"lastName": "3dQ7ER]1HKW",
"mailingLists": Object {
"3dQ7ER]1HKW": true,
},
"source": "3dQ7ER]1HKW",
"subscribed": true,
"testType": "3dQ7ER]1HKW",
Expand All @@ -16,6 +19,7 @@ Object {

exports[`Testing snapshot for actions-loops destination: createOrUpdateContact action - required fields 1`] = `
Object {
"mailingLists": Object {},
"userId": "3dQ7ER]1HKW",
}
`;
Expand All @@ -27,6 +31,7 @@ Object {
"eventProperties": Object {
"testType": "aeKHha#$d",
},
"testType": "aeKHha#$d",
"userId": "aeKHha#$d",
}
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Object {
"email": "macepa@tariviz.sm",
"firstName": "IAIhDY9yOUxjQs(FSFfe",
"lastName": "IAIhDY9yOUxjQs(FSFfe",
"mailingLists": Object {
"IAIhDY9yOUxjQs(FSFfe": false,
},
"source": "IAIhDY9yOUxjQs(FSFfe",
"subscribed": false,
"testType": "IAIhDY9yOUxjQs(FSFfe",
Expand All @@ -16,6 +19,7 @@ Object {

exports[`Testing snapshot for Loops's createOrUpdateContact destination action: required fields 1`] = `
Object {
"mailingLists": Object {},
"userId": "IAIhDY9yOUxjQs(FSFfe",
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('Loops.createOrUpdateContact', () => {
})

it('should work', async () => {
/* Event data going into Segment */
const testPayloadIn = {
createdAt: '2025-05-05T14:17:38.089Z',
customAttributes: {
Expand All @@ -35,6 +36,7 @@ describe('Loops.createOrUpdateContact', () => {
userGroup: 'Alum',
userId: 'some-id-1'
}
/* Processed data coming out of Segment to the API */
const testPayloadOut = {
createdAt: '2025-05-05T14:17:38.089Z',
favoriteColor: 'blue',
Expand All @@ -44,7 +46,8 @@ describe('Loops.createOrUpdateContact', () => {
source: 'Segment',
subscribed: true,
userGroup: 'Alum',
userId: 'some-id-1'
userId: 'some-id-1',
mailingLists: {}
}
nock('https://app.loops.so/api/v1').put('/contacts/update', testPayloadOut).reply(200, {
success: true,
Expand All @@ -65,34 +68,136 @@ describe('Loops.createOrUpdateContact', () => {
})

it('should not work without email', async () => {
const testPayload = {
const testPayloadIn = {
firstName: 'Ellen',
userId: 'some-id-1'
}
nock('https://app.loops.so/api/v1').put('/contacts/update', testPayload).reply(400, {
const testPayloadOut = {
firstName: 'Ellen',
userId: 'some-id-1',
mailingLists: {}
}
nock('https://app.loops.so/api/v1').put('/contacts/update', testPayloadOut).reply(400, {
success: false,
message: 'userId not found and cannot create a new contact without an email.'
})
await expect(
testDestination.testAction('createOrUpdateContact', {
mapping: testPayload,
mapping: testPayloadIn,
settings: { apiKey: LOOPS_API_KEY }
})
).rejects.toThrow('Bad Request')
})

it('should work without optional fields', async () => {
const testPayload = {
const testPayloadIn = {
email: 'test@example.com',
userId: 'some-id-2'
}
const testPayloadOut = {
email: 'test@example.com',
userId: 'some-id-2',
mailingLists: {}
}
nock('https://app.loops.so/api/v1').put('/contacts/update', testPayloadOut).reply(200, {
success: true,
id: 'someId'
})

const responses = await testDestination.testAction('createOrUpdateContact', {
mapping: testPayloadIn,
settings: { apiKey: LOOPS_API_KEY }
})

expect(responses.length).toBe(1)
expect(responses[0].status).toBe(200)
expect(responses[0].data).toStrictEqual({
success: true,
id: 'someId'
})
})

it('should work with mailingLists array', async () => {
const testPayloadIn = {
email: 'test@example.com',
userId: 'some-id-2',
mailingLists: [
{ listId: '1234', subscribed: true },
{ listId: '74648', subscribed: false }
]
}
const testPayloadOut = {
email: 'test@example.com',
userId: 'some-id-2',
mailingLists: {
1234: true,
74648: false
}
}
nock('https://app.loops.so/api/v1').put('/contacts/update', testPayloadOut).reply(200, {
success: true,
id: 'someId'
})

const responses = await testDestination.testAction('createOrUpdateContact', {
mapping: testPayloadIn,
settings: { apiKey: LOOPS_API_KEY }
})

expect(responses.length).toBe(1)
expect(responses[0].status).toBe(200)
expect(responses[0].data).toStrictEqual({
success: true,
id: 'someId'
})
})

it('should work with empty mailingLists object', async () => {
const testPayloadIn = {
email: 'test@example.com',
userId: 'some-id-2',
mailingLists: []
}
const testPayloadOut = {
email: 'test@example.com',
userId: 'some-id-2',
mailingLists: {}
}
nock('https://app.loops.so/api/v1').put('/contacts/update', testPayloadOut).reply(200, {
success: true,
id: 'someId'
})

const responses = await testDestination.testAction('createOrUpdateContact', {
mapping: testPayloadIn,
settings: { apiKey: LOOPS_API_KEY }
})

expect(responses.length).toBe(1)
expect(responses[0].status).toBe(200)
expect(responses[0].data).toStrictEqual({
success: true,
id: 'someId'
})
})

it('should work with no mailingLists object', async () => {
const testPayloadIn = {
email: 'test@example.com',
userId: 'some-id-2'
}
nock('https://app.loops.so/api/v1').put('/contacts/update', testPayload).reply(200, {
const testPayloadOut = {
email: 'test@example.com',
userId: 'some-id-2',
mailingLists: {}
}
nock('https://app.loops.so/api/v1').put('/contacts/update', testPayloadOut).reply(200, {
success: true,
id: 'someId'
})

const responses = await testDestination.testAction('createOrUpdateContact', {
mapping: testPayload,
mapping: testPayloadIn,
settings: { apiKey: LOOPS_API_KEY }
})

Expand Down

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
Expand Up @@ -16,7 +16,7 @@ const action: ActionDefinition<Settings, Payload> = {
},
customAttributes: {
label: 'Custom Contact Attributes',
description: 'Attributes maintained by your team.',
description: 'Contact attributes maintained by your team.',
type: 'object',
required: false,
default: { '@path': '$.traits' }
Expand Down Expand Up @@ -49,6 +49,42 @@ const action: ActionDefinition<Settings, Payload> = {
required: false,
default: { '@path': '$.traits.lastName' }
},
mailingLists: {
label: 'Mailing Lists',
description:
'An array of objects containing key-value pairs of mailing list IDs as `listId` and a true/false `subscribed` value determining if the contact should be added to or removed from each list.',
type: 'object',
multiple: true,
required: false,
properties: {
listId: {
label: 'List ID',
description: 'The ID of the mailing list.',
type: 'string',
required: true
},
subscribed: {
label: 'subscribed',
description:
'true indicates that the user is to be added to the list, false will remove the user from the list.',
type: 'boolean',
required: true
}
},
default: {
'@arrayPath': [
'$.traits.mailingLists',
{
listId: {
'@path': '$.listId'
},
subscribed: {
'@path': '$.subscribed'
}
}
]
}
},
source: {
label: 'Source',
description: "The contact's source.",
Expand Down Expand Up @@ -88,11 +124,29 @@ const action: ActionDefinition<Settings, Payload> = {
},
perform: (request, { payload }) => {
const { customAttributes, ...rest } = payload

/* Re-shape mailing list data from a list of objects to a single object for the API */
const formattedMailingLists: Record<string, boolean> = {}
type listObj = { listId: string; subscribed: boolean }
if (payload.mailingLists) {
for (const list of Object.values(payload.mailingLists)) {
if (typeof list === 'object' && 'listId' in list && 'subscribed' in list) {
formattedMailingLists[(list as listObj).listId] = (list as listObj).subscribed
}
}
}

/* Now delete the mailingLists data from traits/customAttributes */
if (typeof customAttributes === 'object' && 'mailingLists' in customAttributes) {
delete customAttributes.mailingLists
}

return request('https://app.loops.so/api/v1/contacts/update', {
method: 'put',
json: {
...(typeof customAttributes === 'object' && customAttributes),
...rest
...rest,
mailingLists: formattedMailingLists
}
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Object {
"eventProperties": Object {
"testType": "gJCx1dPfi6rH2R",
},
"testType": "gJCx1dPfi6rH2R",
"userId": "gJCx1dPfi6rH2R",
}
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ describe('Loops.sendEvent', () => {
await testDestination.testAction('sendEvent', {
settings: { apiKey: LOOPS_API_KEY }
})
} catch (err) {
expect(err.message).toContain("missing the required field 'userId'.")
expect(err.message).toContain("missing the required field 'eventName'.")
} catch (err: unknown) {
if (err instanceof Error) {
expect(err.message).toContain("missing the required field 'userId'.")
expect(err.message).toContain("missing the required field 'eventName'.")
}
}
})

Expand Down Expand Up @@ -77,4 +79,42 @@ describe('Loops.sendEvent', () => {
expect(responses.length).toBe(1)
expect(responses[0].status).toBe(200)
})

it('should work with contact properties', async () => {
const testPayload = {
userId: 'some-id-1',
eventName: 'signup',
eventProperties: {
someField: true, // boolean
someField1: 'hello', // string
someField2: '2024-04-01T10:09:65Z' // date
},
contactProperties: {
firstName: 'Bob',
anIntegerProperty: 1
}
}
const testPayloadOut = {
userId: 'some-id-1',
eventName: 'signup',
eventProperties: {
someField: true,
someField1: 'hello',
someField2: '2024-04-01T10:09:65Z'
},
firstName: 'Bob',
anIntegerProperty: 1
}
nock('https://app.loops.so/api/v1').post('/events/send', testPayloadOut).reply(200, {
success: true
})

const responses = await testDestination.testAction('sendEvent', {
mapping: testPayload,
settings: { apiKey: LOOPS_API_KEY }
})

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