2248 zapier integration implement typeorm eventsubscribers (#3122)

* Add new queue to twenty-server

* Add triggers to zapier

* Rename webhook operation

* Use find one or fail

* Use logger

* Fix typescript templating

* Add dedicated call webhook job

* Update logging

* Fix error handling
This commit is contained in:
martmull
2024-01-03 18:09:57 +01:00
committed by GitHub
parent 4ebaacc306
commit 65250839fb
36 changed files with 1040 additions and 209 deletions

View File

@ -1,11 +1,5 @@
import App from '../index';
import {
Bundle,
HttpRequestOptions,
createAppTester,
tools,
ZObject,
} from 'zapier-platform-core';
import { Bundle, createAppTester, tools, ZObject } from 'zapier-platform-core';
import getBundle from '../utils/getBundle';
import handleQueryParams from '../utils/handleQueryParams';
import requestDb from '../utils/requestDb';
@ -43,19 +37,20 @@ describe('custom auth', () => {
});
it('fails on bad auth token format', async () => {
const bundle = { authData: { apiKey: 'bad' } };
const bundle = getBundle();
bundle.authData.apiKey = 'bad';
try {
await appTester(App.authentication.test, bundle);
} catch (error: any) {
expect(error.message).toContain('UNAUTHENTICATED');
expect(error.message).toContain('Unauthorized');
return;
}
throw new Error('appTester should have thrown');
});
it('fails on invalid auth token', async () => {
const expiresAt = '2020-01-01 10:10:10.000'
const expiresAt = '2020-01-01 10:10:10.000';
const apiKeyBundle = getBundle({
name: 'Test',
expiresAt,
@ -65,15 +60,17 @@ describe('custom auth', () => {
apiKeyId: apiKeyId,
expiresAt,
});
const expiredToken = await appTester(generateApiKeyToken, generateTokenBundle);
const bundleWithExpiredApiKey = {
authData: { apiKey: expiredToken },
};
const expiredToken = await appTester(
generateApiKeyToken,
generateTokenBundle,
);
const bundleWithExpiredApiKey = getBundle({});
bundleWithExpiredApiKey.authData.apiKey = expiredToken;
try {
await appTester(App.authentication.test, bundleWithExpiredApiKey);
} catch (error: any) {
expect(error.message).toContain('UNAUTHENTICATED');
expect(error.message).toContain('Unauthorized');
return;
}
throw new Error('appTester should have thrown');

View File

@ -1,25 +1,29 @@
import App from '../../index';
import getBundle from "../../utils/getBundle";
import { Bundle, createAppTester, tools, ZObject } from "zapier-platform-core";
import requestDb from "../../utils/requestDb";
import getBundle from '../../utils/getBundle';
import { Bundle, createAppTester, tools, ZObject } from 'zapier-platform-core';
import requestDb from '../../utils/requestDb';
import { createRecordKey } from '../../creates/create_record';
const appTester = createAppTester(App);
tools.env.inject;
tools.env.inject();
describe('creates.create_record', () => {
describe('creates.[createRecordKey]', () => {
test('should run to create a Company Record', async () => {
const bundle = getBundle({
nameSingular: 'Company',
name: 'Company Name',
address: 'Company Address',
domainName: 'Company Domain Name',
linkedinLink: {url: '/linkedin_url', label: "Test linkedinUrl"},
xLink: {url: '/x_url', label: "Test xUrl"},
annualRecurringRevenue: {amountMicros:100000000000,currencyCode: 'USD'},
linkedinLink: { url: '/linkedin_url', label: 'Test linkedinUrl' },
xLink: { url: '/x_url', label: 'Test xUrl' },
annualRecurringRevenue: {
amountMicros: 100000000000,
currencyCode: 'USD',
},
idealCustomerProfile: true,
employees: 25,
});
const result = await appTester(
App.creates.create_record.operation.perform,
App.creates[createRecordKey].operation.perform,
bundle,
);
expect(result).toBeDefined();
@ -33,20 +37,20 @@ describe('creates.create_record', () => {
),
bundle,
);
expect(checkDbResult.data.company.annualRecurringRevenue.amountMicros).toEqual(
100000000000,
);
})
expect(
checkDbResult.data.company.annualRecurringRevenue.amountMicros,
).toEqual(100000000000);
});
test('should run to create a Person Record', async () => {
const bundle = getBundle({
nameSingular: 'Person',
name: {firstName: 'John', lastName: 'Doe'},
name: { firstName: 'John', lastName: 'Doe' },
email: 'johndoe@gmail.com',
phone: '+33610203040',
city: 'Paris',
});
const result = await appTester(
App.creates.create_record.operation.perform,
App.creates[createRecordKey].operation.perform,
bundle,
);
expect(result).toBeDefined();
@ -61,5 +65,5 @@ describe('creates.create_record', () => {
bundle,
);
expect(checkDbResult.data.person.phone).toEqual('+33610203040');
})
})
});
});

View File

@ -0,0 +1,19 @@
import { createAppTester, tools } from 'zapier-platform-core';
import getBundle from '../../utils/getBundle';
import App from '../../index';
import { findObjectNamesPluralKey } from '../../triggers/find_object_names_plural';
tools.env.inject();
const appTester = createAppTester(App);
describe('triggers.find_object_names_plural', () => {
test('should run', async () => {
const bundle = getBundle({});
const result = await appTester(
App.triggers[findObjectNamesPluralKey].operation.perform,
bundle,
);
expect(result).toBeDefined();
expect(result.length).toBeGreaterThan(1);
expect(result[0].namePlural).toBeDefined();
});
});

View File

@ -0,0 +1,19 @@
import { createAppTester, tools } from 'zapier-platform-core';
import getBundle from '../../utils/getBundle';
import App from '../../index';
import { findObjectNamesSingularKey } from '../../triggers/find_object_names_singular';
tools.env.inject();
const appTester = createAppTester(App);
describe('triggers.find_object_names_singular', () => {
test('should run', async () => {
const bundle = getBundle({});
const result = await appTester(
App.triggers[findObjectNamesSingularKey].operation.perform,
bundle,
);
expect(result).toBeDefined();
expect(result.length).toBeGreaterThan(1);
expect(result[0].nameSingular).toBeDefined();
});
});

View File

@ -1,17 +0,0 @@
import { createAppTester } from "zapier-platform-core";
import getBundle from '../../utils/getBundle';
import App from '../../index';
const appTester = createAppTester(App);
describe('triggers.find_objects', () => {
test('should run', async () => {
const bundle = getBundle({});
const result = await appTester(
App.triggers.find_objects.operation.perform,
bundle,
);
expect(result).toBeDefined();
expect(result.length).toBeGreaterThan(1)
expect(result[0].nameSingular).toBeDefined()
})
})

View File

@ -0,0 +1,93 @@
import { Bundle, createAppTester, ZObject } from 'zapier-platform-core';
import App from '../../index';
import getBundle from '../../utils/getBundle';
import requestDb from '../../utils/requestDb';
import { triggerRecordCreatedKey } from '../../triggers/trigger_record_created';
const appTester = createAppTester(App);
describe('triggers.trigger_record_created', () => {
test('should succeed to subscribe', async () => {
const bundle = getBundle({});
bundle.inputData.namePlural = 'companies';
bundle.targetUrl = 'https://test.com';
const result = await appTester(
App.triggers[triggerRecordCreatedKey].operation.performSubscribe,
bundle,
);
expect(result).toBeDefined();
expect(result.id).toBeDefined();
const checkDbResult = await appTester(
(z: ZObject, bundle: Bundle) =>
requestDb(
z,
bundle,
`query webhook {webhooks(filter: {id: {eq: "${result.id}"}}){edges {node {id operation}}}}`,
),
bundle,
);
expect(checkDbResult.data.webhooks.edges[0].node.operation).toEqual(
'create.companies',
);
});
test('should succeed to unsubscribe', async () => {
const bundle = getBundle({});
bundle.inputData.namePlural = 'companies';
bundle.targetUrl = 'https://test.com';
const result = await appTester(
App.triggers[triggerRecordCreatedKey].operation.performSubscribe,
bundle,
);
const unsubscribeBundle = getBundle({});
unsubscribeBundle.subscribeData = { id: result.id };
const unsubscribeResult = await appTester(
App.triggers[triggerRecordCreatedKey].operation.performUnsubscribe,
unsubscribeBundle,
);
expect(unsubscribeResult).toBeDefined();
expect(unsubscribeResult.id).toEqual(result.id);
const checkDbResult = await appTester(
(z: ZObject, bundle: Bundle) =>
requestDb(
z,
bundle,
`query webhook {webhooks(filter: {id: {eq: "${result.id}"}}){edges {node {id operation}}}}`,
),
bundle,
);
expect(checkDbResult.data.webhooks.edges.length).toEqual(0);
});
test('should load company from webhook', async () => {
const bundle = {
cleanedRequest: {
id: 'd6ccb1d1-a90b-4822-a992-a0dd946592c9',
name: '',
domainName: '',
createdAt: '2023-10-19 10:10:12.490',
address: '',
employees: null,
linkedinUrl: null,
xUrl: null,
annualRecurringRevenue: null,
idealCustomerProfile: false,
},
};
const results = await appTester(
App.triggers[triggerRecordCreatedKey].operation.perform,
bundle,
);
expect(results.length).toEqual(1);
const company = results[0];
expect(company.id).toEqual('d6ccb1d1-a90b-4822-a992-a0dd946592c9');
});
it('should load companies from list', async () => {
const bundle = getBundle({});
bundle.inputData.namePlural = 'companies';
const results = await appTester(
App.triggers[triggerRecordCreatedKey].operation.performList,
bundle,
);
expect(results.length).toBeGreaterThan(1);
const firstCompany = results[0];
expect(firstCompany).toBeDefined();
});
});

View File

@ -0,0 +1,95 @@
import { Bundle, createAppTester, ZObject } from 'zapier-platform-core';
import App from '../../index';
import getBundle from '../../utils/getBundle';
import requestDb from '../../utils/requestDb';
import { triggerRecordDeletedKey } from '../../triggers/trigger_record_deleted';
const appTester = createAppTester(App);
describe('triggers.trigger_record_deleted', () => {
test('should succeed to subscribe', async () => {
const bundle = getBundle({});
bundle.inputData.namePlural = 'companies';
bundle.targetUrl = 'https://test.com';
const result = await appTester(
App.triggers[triggerRecordDeletedKey].operation.performSubscribe,
bundle,
);
expect(result).toBeDefined();
expect(result.id).toBeDefined();
const checkDbResult = await appTester(
(z: ZObject, bundle: Bundle) =>
requestDb(
z,
bundle,
`query webhook {webhooks(filter: {id: {eq: "${result.id}"}}){edges {node {id operation}}}}`,
),
bundle,
);
expect(checkDbResult.data.webhooks.edges[0].node.operation).toEqual(
'delete.companies',
);
});
test('should succeed to unsubscribe', async () => {
const bundle = getBundle({});
bundle.inputData.namePlural = 'companies';
bundle.targetUrl = 'https://test.com';
const result = await appTester(
App.triggers[triggerRecordDeletedKey].operation.performSubscribe,
bundle,
);
const unsubscribeBundle = getBundle({});
unsubscribeBundle.subscribeData = { id: result.id };
const unsubscribeResult = await appTester(
App.triggers[triggerRecordDeletedKey].operation.performUnsubscribe,
unsubscribeBundle,
);
expect(unsubscribeResult).toBeDefined();
expect(unsubscribeResult.id).toEqual(result.id);
const checkDbResult = await appTester(
(z: ZObject, bundle: Bundle) =>
requestDb(
z,
bundle,
`query webhook {webhooks(filter: {id: {eq: "${result.id}"}}){edges {node {id operation}}}}`,
),
bundle,
);
expect(checkDbResult.data.webhooks.edges.length).toEqual(0);
});
test('should load company from webhook', async () => {
const bundle = {
cleanedRequest: {
id: 'd6ccb1d1-a90b-4822-a992-a0dd946592c9',
name: '',
domainName: '',
createdAt: '2023-10-19 10:10:12.490',
address: '',
employees: null,
linkedinUrl: null,
xUrl: null,
annualRecurringRevenue: null,
idealCustomerProfile: false,
},
};
const results = await appTester(
App.triggers[triggerRecordDeletedKey].operation.perform,
bundle,
);
expect(results.length).toEqual(1);
const company = results[0];
expect(company.id).toEqual('d6ccb1d1-a90b-4822-a992-a0dd946592c9');
});
it('should load companies from list', async () => {
const bundle = getBundle({});
bundle.inputData.namePlural = 'companies';
const results = await appTester(
App.triggers[triggerRecordDeletedKey].operation.performList,
bundle,
);
expect(results.length).toBeGreaterThan(1);
const firstCompany = results[0];
expect(firstCompany).toBeDefined();
expect(firstCompany.id).toBeDefined();
expect(Object.keys(firstCompany).length).toEqual(1);
});
});

View File

@ -0,0 +1,93 @@
import { Bundle, createAppTester, ZObject } from 'zapier-platform-core';
import App from '../../index';
import getBundle from '../../utils/getBundle';
import requestDb from '../../utils/requestDb';
import { triggerRecordUpdatedKey } from '../../triggers/trigger_record_updated';
const appTester = createAppTester(App);
describe('triggers.trigger_record_updated', () => {
test('should succeed to subscribe', async () => {
const bundle = getBundle({});
bundle.inputData.namePlural = 'companies';
bundle.targetUrl = 'https://test.com';
const result = await appTester(
App.triggers[triggerRecordUpdatedKey].operation.performSubscribe,
bundle,
);
expect(result).toBeDefined();
expect(result.id).toBeDefined();
const checkDbResult = await appTester(
(z: ZObject, bundle: Bundle) =>
requestDb(
z,
bundle,
`query webhook {webhooks(filter: {id: {eq: "${result.id}"}}){edges {node {id operation}}}}`,
),
bundle,
);
expect(checkDbResult.data.webhooks.edges[0].node.operation).toEqual(
'update.companies',
);
});
test('should succeed to unsubscribe', async () => {
const bundle = getBundle({});
bundle.inputData.namePlural = 'companies';
bundle.targetUrl = 'https://test.com';
const result = await appTester(
App.triggers[triggerRecordUpdatedKey].operation.performSubscribe,
bundle,
);
const unsubscribeBundle = getBundle({});
unsubscribeBundle.subscribeData = { id: result.id };
const unsubscribeResult = await appTester(
App.triggers[triggerRecordUpdatedKey].operation.performUnsubscribe,
unsubscribeBundle,
);
expect(unsubscribeResult).toBeDefined();
expect(unsubscribeResult.id).toEqual(result.id);
const checkDbResult = await appTester(
(z: ZObject, bundle: Bundle) =>
requestDb(
z,
bundle,
`query webhook {webhooks(filter: {id: {eq: "${result.id}"}}){edges {node {id operation}}}}`,
),
bundle,
);
expect(checkDbResult.data.webhooks.edges.length).toEqual(0);
});
test('should load company from webhook', async () => {
const bundle = {
cleanedRequest: {
id: 'd6ccb1d1-a90b-4822-a992-a0dd946592c9',
name: '',
domainName: '',
createdAt: '2023-10-19 10:10:12.490',
address: '',
employees: null,
linkedinUrl: null,
xUrl: null,
annualRecurringRevenue: null,
idealCustomerProfile: false,
},
};
const results = await appTester(
App.triggers[triggerRecordUpdatedKey].operation.perform,
bundle,
);
expect(results.length).toEqual(1);
const company = results[0];
expect(company.id).toEqual('d6ccb1d1-a90b-4822-a992-a0dd946592c9');
});
it('should load companies from list', async () => {
const bundle = getBundle({});
bundle.inputData.namePlural = 'companies';
const results = await appTester(
App.triggers[triggerRecordUpdatedKey].operation.performList,
bundle,
);
expect(results.length).toBeGreaterThan(1);
const firstCompany = results[0];
expect(firstCompany).toBeDefined();
});
});

View File

@ -1,8 +0,0 @@
import { capitalize } from "../../utils/capitalize";
describe('capitalize', ()=> {
test('should capitalize properly', ()=> {
expect(capitalize('word')).toEqual('Word')
expect(capitalize('word word')).toEqual('Word word')
})
})

View File

@ -0,0 +1,8 @@
import { capitalize } from '../../utils/capitalize';
describe('capitalize', () => {
test('should capitalize properly', () => {
expect(capitalize('word')).toEqual('Word');
expect(capitalize('word word')).toEqual('Word word');
});
});

View File

@ -1,42 +0,0 @@
import { computeInputFields } from "../../utils/computeInputFields";
describe('computeInputFields', ()=> {
test('should create Person input fields properly', ()=> {
const personInfos = {
type: "object",
properties: {
email: {
type: "string"
},
xLink: {
type: "object",
properties: {
url: {
type: "string"
},
label: {
type: "string"
}
}
},
avatarUrl: {
type: "string"
},
favorites: {
type: "array",
items: {
$ref: "#/components/schemas/Favorite"
}
},
},
example: {},
required: ['avatarUrl']
}
expect(computeInputFields(personInfos)).toEqual([
{ key: "email", label: "Email", required: false, type: "string" },
{ key: "xLink__url", label: "X Link: Url", required: false, type: "string" },
{ key: "xLink__label", label: "X Link: Label", required: false, type: "string" },
{ key: "avatarUrl", label: "Avatar Url", required: true, type: "string" },
])
})
})

View File

@ -0,0 +1,52 @@
import { computeInputFields } from '../../utils/computeInputFields';
describe('computeInputFields', () => {
test('should create Person input fields properly', () => {
const personInfos = {
type: 'object',
properties: {
email: {
type: 'string',
},
xLink: {
type: 'object',
properties: {
url: {
type: 'string',
},
label: {
type: 'string',
},
},
},
avatarUrl: {
type: 'string',
},
favorites: {
type: 'array',
items: {
$ref: '#/components/schemas/Favorite',
},
},
},
example: {},
required: ['avatarUrl'],
};
expect(computeInputFields(personInfos)).toEqual([
{ key: 'email', label: 'Email', required: false, type: 'string' },
{
key: 'xLink__url',
label: 'X Link: Url',
required: false,
type: 'string',
},
{
key: 'xLink__label',
label: 'X Link: Label',
required: false,
type: 'string',
},
{ key: 'avatarUrl', label: 'Avatar Url', required: true, type: 'string' },
]);
});
});

View File

@ -7,15 +7,15 @@ describe('utils.handleQueryParams', () => {
const expectedResult = '';
expect(result).toEqual(expectedResult);
});
test('should format', async () => {
test('should format', () => {
const inputData = {
name: 'Company Name',
address: 'Company Address',
domainName: 'Company Domain Name',
linkedinUrl__url: '/linkedin_url',
linkedinUrl__label: "Test linkedinUrl",
linkedinUrl__label: 'Test linkedinUrl',
xUrl__url: '/x_url',
xUrl__label: "Test xUrl",
xUrl__label: 'Test xUrl',
annualRecurringRevenue: 100000,
idealCustomerProfile: true,
employees: 25,

View File

@ -1,7 +0,0 @@
import { labelling } from "../../utils/labelling";
describe('labelling', ()=> {
test('should label properly', ()=> {
expect(labelling('createdAt')).toEqual('Created At')
})
})

View File

@ -0,0 +1,7 @@
import { labelling } from '../../utils/labelling';
describe('labelling', () => {
test('should label properly', () => {
expect(labelling('createdAt')).toEqual('Created At');
});
});