From 51a06b3ebd797bc98188ca64a5efdd7ae680209d Mon Sep 17 00:00:00 2001 From: martmull Date: Wed, 18 Oct 2023 17:56:40 +0200 Subject: [PATCH] 2052 zapier integration 5 deploy twenty zapier app into the public repository (#2101) * Add create_company Zap action * Add testing for that action * Core review returns --- packages/twenty-zapier/src/authentication.ts | 40 ++----- .../src/creates/create_company.ts | 106 ++++++++++++++++++ .../src/creates/create_person.ts | 17 ++- packages/twenty-zapier/src/index.ts | 6 +- .../src/test/authentication.test.ts | 12 +- .../src/test/creates/create_company.test.ts | 68 +++++++++++ .../src/test/creates/create_person.test.ts | 56 +++++++-- .../src/test/utils/handleQueryParams.test.ts | 33 ++++++ packages/twenty-zapier/src/utils/getBundle.ts | 7 ++ .../src/utils/handleQueryParams.ts | 11 ++ packages/twenty-zapier/src/utils/requestDb.ts | 38 +++++++ 11 files changed, 334 insertions(+), 60 deletions(-) create mode 100644 packages/twenty-zapier/src/creates/create_company.ts create mode 100644 packages/twenty-zapier/src/test/creates/create_company.test.ts create mode 100644 packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts create mode 100644 packages/twenty-zapier/src/utils/getBundle.ts create mode 100644 packages/twenty-zapier/src/utils/handleQueryParams.ts create mode 100644 packages/twenty-zapier/src/utils/requestDb.ts diff --git a/packages/twenty-zapier/src/authentication.ts b/packages/twenty-zapier/src/authentication.ts index bb3843189..413010502 100644 --- a/packages/twenty-zapier/src/authentication.ts +++ b/packages/twenty-zapier/src/authentication.ts @@ -1,38 +1,12 @@ -import { Bundle, HttpRequestOptions, ZObject } from 'zapier-platform-core'; +import { Bundle, ZObject } from 'zapier-platform-core'; +import requestDb from './utils/requestDb'; const testAuthentication = async (z: ZObject, bundle: Bundle) => { - const options = { - url: `${process.env.SERVER_BASE_URL}/graphql`, - method: 'POST', - headers: { - Authorization: `Bearer ${bundle.authData.apiKey}`, - }, - body: { - query: 'query currentWorkspace {currentWorkspace {id displayName}}', - }, - } satisfies HttpRequestOptions; - - return z - .request(options) - .then((response) => { - const results = response.json; - if (results.errors) { - throw new z.errors.Error( - 'The API Key you supplied is incorrect', - 'AuthenticationError', - results.errors, - ); - } - response.throwForStatus(); - return results; - }) - .catch((err) => { - throw new z.errors.Error( - 'The API Key you supplied is incorrect', - 'AuthenticationError', - err.message, - ); - }); + return await requestDb( + z, + bundle, + 'query currentWorkspace {currentWorkspace {id displayName}}', + ); }; export default { diff --git a/packages/twenty-zapier/src/creates/create_company.ts b/packages/twenty-zapier/src/creates/create_company.ts new file mode 100644 index 000000000..e18df1f8d --- /dev/null +++ b/packages/twenty-zapier/src/creates/create_company.ts @@ -0,0 +1,106 @@ +import { Bundle, ZObject } from 'zapier-platform-core'; +import handleQueryParams from '../utils/handleQueryParams'; + +const perform = async (z: ZObject, bundle: Bundle) => { + const response = await z.request({ + body: { + query: ` + mutation CreateCompany { + createOneCompany( + data:{${handleQueryParams(bundle.inputData)}} + ) + {id} + }`, + }, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: `Bearer ${bundle.authData.apiKey}`, + }, + method: 'POST', + url: `${process.env.SERVER_BASE_URL}/graphql`, + }); + return response.json; +}; +export default { + display: { + description: 'Creates a new Company in Twenty', + hidden: false, + label: 'Create New Company', + }, + key: 'create_company', + noun: 'Company', + operation: { + inputFields: [ + { + key: 'name', + label: 'Company Name', + type: 'string', + required: true, + list: false, + altersDynamicFields: false, + }, + { + key: 'address', + label: 'Address', + type: 'string', + required: true, + list: false, + altersDynamicFields: false, + }, + { + key: 'domainName', + label: 'Url', + type: 'string', + required: true, + list: false, + altersDynamicFields: false, + }, + { + key: 'linkedinUrl', + label: 'Linkedin', + type: 'string', + required: false, + list: false, + altersDynamicFields: false, + }, + { + key: 'xUrl', + label: 'Twitter', + type: 'string', + required: false, + list: false, + altersDynamicFields: false, + }, + { + key: 'annualRecurringRevenue', + label: 'ARR (Annual Recurring Revenue)', + type: 'number', + required: false, + list: false, + altersDynamicFields: false, + }, + { + key: 'idealCustomerProfile', + label: 'ICP (Ideal Customer Profile)', + type: 'boolean', + required: false, + list: false, + altersDynamicFields: false, + }, + { + key: 'employees', + label: 'Employees (number of)', + type: 'number', + required: false, + list: false, + altersDynamicFields: false, + }, + ], + sample: { + name: 'Apple', + address: 'Cupertino', + }, + perform, + }, +}; diff --git a/packages/twenty-zapier/src/creates/create_person.ts b/packages/twenty-zapier/src/creates/create_person.ts index 0c55cb58e..1fe799a5f 100644 --- a/packages/twenty-zapier/src/creates/create_person.ts +++ b/packages/twenty-zapier/src/creates/create_person.ts @@ -1,17 +1,16 @@ import { Bundle, ZObject } from 'zapier-platform-core'; +import handleQueryParams from '../utils/handleQueryParams'; const perform = async (z: ZObject, bundle: Bundle) => { const response = await z.request({ body: { - query: `mutation - CreatePerson { - createOnePerson(data:{ - firstName: "${bundle.inputData.firstName}", - lastName: "${bundle.inputData.lastName}", - email: "${bundle.inputData.email}", - phone: "${bundle.inputData.phone}", - city: "${bundle.inputData.city}" - }){id}}`, + query: ` + mutation CreatePerson { + createOnePerson( + data:{${handleQueryParams(bundle.inputData)}} + ) + {id} + }`, }, headers: { 'Content-Type': 'application/json', diff --git a/packages/twenty-zapier/src/index.ts b/packages/twenty-zapier/src/index.ts index 8bc5dea95..581691ea4 100644 --- a/packages/twenty-zapier/src/index.ts +++ b/packages/twenty-zapier/src/index.ts @@ -1,11 +1,15 @@ const { version } = require('../package.json'); import { version as platformVersion } from 'zapier-platform-core'; import createPerson from './creates/create_person'; +import createCompany from './creates/create_company'; import authentication from './authentication'; export default { version, platformVersion, authentication: authentication, - creates: { [createPerson.key]: createPerson }, + creates: { + [createPerson.key]: createPerson, + [createCompany.key]: createCompany, + }, }; diff --git a/packages/twenty-zapier/src/test/authentication.test.ts b/packages/twenty-zapier/src/test/authentication.test.ts index d301e8a40..89a680cb2 100644 --- a/packages/twenty-zapier/src/test/authentication.test.ts +++ b/packages/twenty-zapier/src/test/authentication.test.ts @@ -5,8 +5,8 @@ import { createAppTester, tools, ZObject, - AppError, } from 'zapier-platform-core'; +import getBundle from '../utils/getBundle'; const appTester = createAppTester(App); tools.env.inject(); @@ -36,7 +36,7 @@ const apiKey = String(process.env.API_KEY); describe('custom auth', () => { it('passes authentication and returns json', async () => { - const bundle = { authData: { apiKey } }; + const bundle = getBundle(); const response = await appTester(App.authentication.test, bundle); expect(response.data).toHaveProperty('currentWorkspace'); expect(response.data.currentWorkspace).toHaveProperty('displayName'); @@ -55,10 +55,10 @@ describe('custom auth', () => { }); it('fails on invalid auth token', async () => { - const bundle = { - authData: { apiKey }, - inputData: { name: 'Test', expiresAt: '2020-01-01 10:10:10.000' }, - }; + const bundle = getBundle({ + name: 'Test', + expiresAt: '2020-01-01 10:10:10.000', + }); const expiredToken = await appTester(generateKey, bundle); const bundleWithExpiredApiKey = { authData: { apiKey: expiredToken }, diff --git a/packages/twenty-zapier/src/test/creates/create_company.test.ts b/packages/twenty-zapier/src/test/creates/create_company.test.ts new file mode 100644 index 000000000..624948277 --- /dev/null +++ b/packages/twenty-zapier/src/test/creates/create_company.test.ts @@ -0,0 +1,68 @@ +import App from '../../index'; +import { Bundle, createAppTester, tools, ZObject } from 'zapier-platform-core'; +import getBundle from '../../utils/getBundle'; +import requestDb from '../../utils/requestDb'; +const appTester = createAppTester(App); +tools.env.inject; + +describe('creates.create_company', () => { + test('should run', async () => { + const bundle = getBundle({ + name: 'Company Name', + address: 'Company Address', + domainName: 'Company Domain Name', + linkedinUrl: 'Test linkedinUrl', + xUrl: 'Test xUrl', + annualRecurringRevenue: 100000, + idealCustomerProfile: true, + employees: 25, + }); + const result = await appTester( + App.creates.create_company.operation.perform, + bundle, + ); + expect(result).toBeDefined(); + expect(result.data?.createOneCompany?.id).toBeDefined(); + const checkDbResult = await appTester( + (z: ZObject, bundle: Bundle) => + requestDb( + z, + bundle, + `query findCompany {findUniqueCompany(where: {id: "${result.data.createOneCompany.id}"}){id, annualRecurringRevenue}}`, + ), + bundle, + ); + expect(checkDbResult.data.findUniqueCompany.annualRecurringRevenue).toEqual( + 100000, + ); + }); + test('should run with not required missing params', async () => { + const bundle = getBundle({ + name: 'Company Name', + address: 'Company Address', + domainName: 'Company Domain Name', + linkedinUrl: 'Test linkedinUrl', + xUrl: 'Test xUrl', + idealCustomerProfile: true, + employees: 25, + }); + const result = await appTester( + App.creates.create_company.operation.perform, + bundle, + ); + expect(result).toBeDefined(); + expect(result.data?.createOneCompany?.id).toBeDefined(); + const checkDbResult = await appTester( + (z: ZObject, bundle: Bundle) => + requestDb( + z, + bundle, + `query findCompany {findUniqueCompany(where: {id: "${result.data.createOneCompany.id}"}){id, annualRecurringRevenue}}`, + ), + bundle, + ); + expect(checkDbResult.data.findUniqueCompany.annualRecurringRevenue).toEqual( + null, + ); + }); +}); diff --git a/packages/twenty-zapier/src/test/creates/create_person.test.ts b/packages/twenty-zapier/src/test/creates/create_person.test.ts index a53eef833..7003cd055 100644 --- a/packages/twenty-zapier/src/test/creates/create_person.test.ts +++ b/packages/twenty-zapier/src/test/creates/create_person.test.ts @@ -1,25 +1,59 @@ import App from '../../index'; -import { createAppTester, tools } from 'zapier-platform-core'; +import { Bundle, createAppTester, tools, ZObject } from 'zapier-platform-core'; +import getBundle from '../../utils/getBundle'; +import requestDb from '../../utils/requestDb'; const appTester = createAppTester(App); tools.env.inject(); describe('creates.create_person', () => { test('should run', async () => { - const bundle = { - authData: { apiKey: String(process.env.API_KEY) }, - inputData: { - firstName: 'John', - lastName: 'Doe', - email: 'johndoe@gmail.com', - phone: '+33610203040', - city: 'Paris', - }, - }; + const bundle = getBundle({ + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@gmail.com', + phone: '+33610203040', + city: 'Paris', + }); const results = await appTester( App.creates.create_person.operation.perform, bundle, ); expect(results).toBeDefined(); expect(results.data?.createOnePerson?.id).toBeDefined(); + const checkDbResult = await appTester( + (z: ZObject, bundle: Bundle) => + requestDb( + z, + bundle, + `query findPerson {findUniquePerson(id: "${results.data.createOnePerson.id}"){id, phone}}`, + ), + bundle, + ); + expect(checkDbResult.data.findUniquePerson.phone).toEqual('+33610203040'); + }); + + test('should run with not required missing params', async () => { + const bundle = getBundle({ + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@gmail.com', + city: 'Paris', + }); + const results = await appTester( + App.creates.create_person.operation.perform, + bundle, + ); + expect(results).toBeDefined(); + expect(results.data?.createOnePerson?.id).toBeDefined(); + const checkDbResult = await appTester( + (z: ZObject, bundle: Bundle) => + requestDb( + z, + bundle, + `query findPerson {findUniquePerson(id: "${results.data.createOnePerson.id}"){id, phone}}`, + ), + bundle, + ); + expect(checkDbResult.data.findUniquePerson.phone).toEqual(null); }); }); diff --git a/packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts b/packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts new file mode 100644 index 000000000..bdf4cae2d --- /dev/null +++ b/packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts @@ -0,0 +1,33 @@ +import handleQueryParams from '../../utils/handleQueryParams'; + +describe('utils.handleQueryParams', () => { + test('should handle empty values', () => { + const inputData = {}; + const result = handleQueryParams(inputData); + const expectedResult = ''; + expect(result).toEqual(expectedResult); + }); + test('should format', async () => { + const inputData = { + name: 'Company Name', + address: 'Company Address', + domainName: 'Company Domain Name', + linkedinUrl: 'Test linkedinUrl', + xUrl: 'Test xUrl', + annualRecurringRevenue: 100000, + idealCustomerProfile: true, + employees: 25, + }; + const result = handleQueryParams(inputData); + const expectedResult = + 'name: "Company Name", ' + + 'address: "Company Address", ' + + 'domainName: "Company Domain Name", ' + + 'linkedinUrl: "Test linkedinUrl", ' + + 'xUrl: "Test xUrl", ' + + 'annualRecurringRevenue: 100000, ' + + 'idealCustomerProfile: true, ' + + 'employees: 25'; + expect(result).toEqual(expectedResult); + }); +}); diff --git a/packages/twenty-zapier/src/utils/getBundle.ts b/packages/twenty-zapier/src/utils/getBundle.ts new file mode 100644 index 000000000..318b5bf7a --- /dev/null +++ b/packages/twenty-zapier/src/utils/getBundle.ts @@ -0,0 +1,7 @@ +const getBundle = (inputData?: object) => { + return { + authData: { apiKey: String(process.env.API_KEY) }, + inputData, + }; +}; +export default getBundle; diff --git a/packages/twenty-zapier/src/utils/handleQueryParams.ts b/packages/twenty-zapier/src/utils/handleQueryParams.ts new file mode 100644 index 000000000..ff751342e --- /dev/null +++ b/packages/twenty-zapier/src/utils/handleQueryParams.ts @@ -0,0 +1,11 @@ +const handleQueryParams = (inputData: { [x: string]: any }): string => { + let result = ''; + Object.keys(inputData).forEach((key) => { + let quote = ''; + if (typeof inputData[key] === 'string') quote = '"'; + result = result.concat(`${key}: ${quote}${inputData[key]}${quote}, `); + }); + if (result.length) result = result.slice(0, -2); // Remove the last ', ' + return result; +}; +export default handleQueryParams; diff --git a/packages/twenty-zapier/src/utils/requestDb.ts b/packages/twenty-zapier/src/utils/requestDb.ts new file mode 100644 index 000000000..9d81a07ee --- /dev/null +++ b/packages/twenty-zapier/src/utils/requestDb.ts @@ -0,0 +1,38 @@ +import { Bundle, HttpRequestOptions, ZObject } from 'zapier-platform-core'; + +const requestDb = async (z: ZObject, bundle: Bundle, query: string) => { + const options = { + url: `${process.env.SERVER_BASE_URL}/graphql`, + method: 'POST', + headers: { + Authorization: `Bearer ${bundle.authData.apiKey}`, + }, + body: { + query, + }, + } satisfies HttpRequestOptions; + + return z + .request(options) + .then((response) => { + const results = response.json; + if (results.errors) { + throw new z.errors.Error( + 'The API Key you supplied is incorrect', + 'AuthenticationError', + results.errors, + ); + } + response.throwForStatus(); + return results; + }) + .catch((err) => { + throw new z.errors.Error( + 'The API Key you supplied is incorrect', + 'AuthenticationError', + err.message, + ); + }); +}; + +export default requestDb;