13233 zapier update route to create workflow apikey etc (#13239)

Fix webhook creation utils and some tests
This commit is contained in:
martmull
2025-07-16 16:23:35 +02:00
committed by GitHub
parent 5ba98475d4
commit 1fc087aeac
7 changed files with 130 additions and 66 deletions

View File

@ -1,16 +1,8 @@
{ {
"name": "twenty-zapier", "name": "twenty-zapier",
"version": "2.0.1", "version": "2.0.2",
"description": "Effortlessly sync Twenty with 3000+ apps. Automate tasks, boost productivity, and supercharge your customer relationships!", "description": "Effortlessly sync Twenty with 3000+ apps. Automate tasks, boost productivity, and supercharge your customer relationships!",
"main": "src/index.ts", "main": "src/index.ts",
"scripts": {
"nx": "NX_DEFAULT_PROJECT=twenty-zapier node ../../node_modules/nx/bin/nx.js",
"format": "prettier . --write \"!build\"",
"test": "yarn build && jest --testTimeout 10000 --rootDir ./lib/test",
"validate": "yarn build && zapier validate",
"versions": "yarn build && zapier versions",
"watch": "yarn clean && npx tsc --watch"
},
"engines": { "engines": {
"node": "^22.12.0", "node": "^22.12.0",
"npm": "please-use-yarn", "npm": "please-use-yarn",
@ -21,8 +13,11 @@
"convertedByCLIVersion": "15.4.1" "convertedByCLIVersion": "15.4.1"
}, },
"dependencies": { "dependencies": {
"@sniptt/guards": "^0.2.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"zapier-platform-core": "15.5.1" "libphonenumber-js": "^1.10.26",
"zapier-platform-core": "15.5.1",
"zod": "3.23.8"
}, },
"devDependencies": { "devDependencies": {
"jest": "29.7.0", "jest": "29.7.0",

View File

@ -12,6 +12,41 @@
}, },
"dependsOn": ["^build"] "dependsOn": ["^build"]
}, },
"format": {
"executor": "nx:run-commands",
"options": {
"cwd": "{projectRoot}",
"commands": ["prettier . --write \"!build\""]
}
},
"test": {
"executor": "nx:run-commands",
"options": {
"cwd": "{projectRoot}",
"commands": ["NODE_ENV=test && nx run twenty-zapier:build && jest --testTimeout 10000 --rootDir ./lib/test"]
}
},
"validate": {
"executor": "nx:run-commands",
"options": {
"cwd": "{projectRoot}",
"commands": ["nx run twenty-zapier:build && zapier validate"]
}
},
"versions": {
"executor": "nx:run-commands",
"options": {
"cwd": "{projectRoot}",
"commands": ["nx run twenty-zapier:build && zapier versions"]
}
},
"watch":{
"executor": "nx:run-commands",
"options": {
"cwd": "{projectRoot}",
"commands": ["nx run twenty-zapier:clean && npx tsc --watch"]
}
},
"clean": { "clean": {
"executor": "nx:run-commands", "executor": "nx:run-commands",
"options": { "options": {

View File

@ -10,56 +10,68 @@ const appTester = createAppTester(App);
describe('triggers.trigger_record.created', () => { describe('triggers.trigger_record.created', () => {
test('should succeed to subscribe', async () => { test('should succeed to subscribe', async () => {
const bundle = getBundle({}); const bundle = getBundle({});
bundle.inputData.nameSingular = 'company'; bundle.inputData.nameSingular = 'company';
bundle.inputData.operation = DatabaseEventAction.CREATED; bundle.inputData.operation = DatabaseEventAction.CREATED;
bundle.targetUrl = 'https://test.com'; bundle.targetUrl = 'https://test.com';
const result = await appTester( const result = await appTester(
App.triggers[triggerRecordKey].operation.performSubscribe, App.triggers[triggerRecordKey].operation.performSubscribe,
bundle, bundle,
); );
expect(result).toBeDefined(); expect(result).toBeDefined();
expect(result.id).toBeDefined(); expect(result.id).toBeDefined();
const checkDbResult = await appTester( const checkDbResult = await appTester(
(z: ZObject, bundle: Bundle) => (z: ZObject, bundle: Bundle) =>
requestDb( requestDb(
z, z,
bundle, bundle,
`query webhook {webhooks(filter: {id: {eq: "${result.id}"}}){edges {node {id operations}}}}`, `query webhook {webhook(input: {id: "${result.id}"}){id operations}}`,
), ),
bundle, bundle,
); );
expect(checkDbResult.data.webhooks.edges[0].node.operations[0]).toEqual(
'company.created', expect(checkDbResult.data.webhook.operations[0]).toEqual('company.created');
);
}); });
test('should succeed to unsubscribe', async () => { test('should succeed to unsubscribe', async () => {
const bundle = getBundle({}); const bundle = getBundle({});
bundle.inputData.nameSingular = 'company'; bundle.inputData.nameSingular = 'company';
bundle.inputData.operation = DatabaseEventAction.CREATED; bundle.inputData.operation = DatabaseEventAction.CREATED;
bundle.targetUrl = 'https://test.com'; bundle.targetUrl = 'https://test.com';
const result = await appTester( const result = await appTester(
App.triggers[triggerRecordKey].operation.performSubscribe, App.triggers[triggerRecordKey].operation.performSubscribe,
bundle, bundle,
); );
const unsubscribeBundle = getBundle({}); const unsubscribeBundle = getBundle({});
unsubscribeBundle.subscribeData = { id: result.id }; unsubscribeBundle.subscribeData = { id: result.id };
const unsubscribeResult = await appTester( const unsubscribeResult = await appTester(
App.triggers[triggerRecordKey].operation.performUnsubscribe, App.triggers[triggerRecordKey].operation.performUnsubscribe,
unsubscribeBundle, unsubscribeBundle,
); );
expect(unsubscribeResult).toBeDefined(); expect(unsubscribeResult).toBeDefined();
expect(unsubscribeResult.id).toEqual(result.id); expect(unsubscribeResult.id).toEqual(result.id);
const checkDbResult = await appTester( const checkDbResult = await appTester(
(z: ZObject, bundle: Bundle) => (z: ZObject, bundle: Bundle) =>
requestDb( requestDb(z, bundle, `query webhook {webhooks {id}}`),
z,
bundle,
`query webhook {webhooks(filter: {id: {eq: "${result.id}"}}){edges {node {id operations}}}}`,
),
bundle, bundle,
); );
expect(checkDbResult.data.webhooks.edges.length).toEqual(0); expect(
// @ts-expect-error legacy noImplicitAny
checkDbResult.data.webhooks.filter((webhook) => webhook.id === result.id)
.length,
).toEqual(0);
}); });
test('should load company from webhook', async () => { test('should load company from webhook', async () => {
const bundle = { const bundle = {
cleanedRequest: { cleanedRequest: {
@ -77,24 +89,33 @@ describe('triggers.trigger_record.created', () => {
}, },
}, },
}; };
const results = await appTester( const results = await appTester(
App.triggers[triggerRecordKey].operation.perform, App.triggers[triggerRecordKey].operation.perform,
bundle, bundle,
); );
expect(results.length).toEqual(1); expect(results.length).toEqual(1);
const company = results[0]; const company = results[0];
expect(company.record.id).toEqual('d6ccb1d1-a90b-4822-a992-a0dd946592c9'); expect(company.record.id).toEqual('d6ccb1d1-a90b-4822-a992-a0dd946592c9');
}); });
it('should load companies from list', async () => { it('should load companies from list', async () => {
const bundle = getBundle({}); const bundle = getBundle({});
bundle.inputData.nameSingular = 'company'; bundle.inputData.nameSingular = 'company';
bundle.inputData.operation = DatabaseEventAction.CREATED; bundle.inputData.operation = DatabaseEventAction.CREATED;
const results = await appTester( const results = await appTester(
App.triggers[triggerRecordKey].operation.performList, App.triggers[triggerRecordKey].operation.performList,
bundle, bundle,
); );
expect(results.length).toBeGreaterThan(1); expect(results.length).toBeGreaterThan(1);
const firstCompany = results[0]; const firstCompany = results[0];
expect(firstCompany.record).toBeDefined(); expect(firstCompany.record).toBeDefined();
}); });
}); });
@ -102,66 +123,83 @@ describe('triggers.trigger_record.created', () => {
describe('triggers.trigger_record.update', () => { describe('triggers.trigger_record.update', () => {
test('should succeed to subscribe', async () => { test('should succeed to subscribe', async () => {
const bundle = getBundle({}); const bundle = getBundle({});
bundle.inputData.nameSingular = 'company'; bundle.inputData.nameSingular = 'company';
bundle.inputData.operation = DatabaseEventAction.UPDATED; bundle.inputData.operation = DatabaseEventAction.UPDATED;
bundle.targetUrl = 'https://test.com'; bundle.targetUrl = 'https://test.com';
const result = await appTester( const result = await appTester(
App.triggers[triggerRecordKey].operation.performSubscribe, App.triggers[triggerRecordKey].operation.performSubscribe,
bundle, bundle,
); );
expect(result).toBeDefined(); expect(result).toBeDefined();
expect(result.id).toBeDefined(); expect(result.id).toBeDefined();
const checkDbResult = await appTester( const checkDbResult = await appTester(
(z: ZObject, bundle: Bundle) => (z: ZObject, bundle: Bundle) =>
requestDb( requestDb(
z, z,
bundle, bundle,
`query webhook {webhooks(filter: {id: {eq: "${result.id}"}}){edges {node {id operations}}}}`, `query webhook {webhook(input: {id: "${result.id}"}){id operations}}`,
), ),
bundle, bundle,
); );
expect(checkDbResult.data.webhooks.edges[0].node.operations[0]).toEqual(
expect(checkDbResult.data.webhooks.operations[0]).toEqual(
'company.updated', 'company.updated',
); );
}); });
test('should succeed to unsubscribe', async () => { test('should succeed to unsubscribe', async () => {
const bundle = getBundle({}); const bundle = getBundle({});
bundle.inputData.nameSingular = 'company'; bundle.inputData.nameSingular = 'company';
bundle.inputData.operation = DatabaseEventAction.UPDATED; bundle.inputData.operation = DatabaseEventAction.UPDATED;
bundle.targetUrl = 'https://test.com'; bundle.targetUrl = 'https://test.com';
const result = await appTester( const result = await appTester(
App.triggers[triggerRecordKey].operation.performSubscribe, App.triggers[triggerRecordKey].operation.performSubscribe,
bundle, bundle,
); );
const unsubscribeBundle = getBundle({}); const unsubscribeBundle = getBundle({});
unsubscribeBundle.subscribeData = { id: result.id }; unsubscribeBundle.subscribeData = { id: result.id };
const unsubscribeResult = await appTester( const unsubscribeResult = await appTester(
App.triggers[triggerRecordKey].operation.performUnsubscribe, App.triggers[triggerRecordKey].operation.performUnsubscribe,
unsubscribeBundle, unsubscribeBundle,
); );
expect(unsubscribeResult).toBeDefined(); expect(unsubscribeResult).toBeDefined();
expect(unsubscribeResult.id).toEqual(result.id); expect(unsubscribeResult.id).toEqual(result.id);
const checkDbResult = await appTester( const checkDbResult = await appTester(
(z: ZObject, bundle: Bundle) => (z: ZObject, bundle: Bundle) =>
requestDb( requestDb(z, bundle, `query webhook {webhooks {id}}`),
z,
bundle,
`query webhook {webhooks(filter: {id: {eq: "${result.id}"}}){edges {node {id operations}}}}`,
),
bundle, bundle,
); );
expect(checkDbResult.data.webhooks.edges.length).toEqual(0); expect(
// @ts-expect-error legacy noImplicitAny
checkDbResult.data.webhooks.filter((webhook) => webhook.id === result.id)
.length,
).toEqual(0);
}); });
it('should load companies from list', async () => { it('should load companies from list', async () => {
const bundle = getBundle({}); const bundle = getBundle({});
bundle.inputData.nameSingular = 'company'; bundle.inputData.nameSingular = 'company';
bundle.inputData.operation = DatabaseEventAction.UPDATED; bundle.inputData.operation = DatabaseEventAction.UPDATED;
const results = await appTester( const results = await appTester(
App.triggers[triggerRecordKey].operation.performList, App.triggers[triggerRecordKey].operation.performList,
bundle, bundle,
); );
expect(results.length).toBeGreaterThan(1); expect(results.length).toBeGreaterThan(1);
const firstCompany = results[0]; const firstCompany = results[0];
expect(firstCompany.record).toBeDefined(); expect(firstCompany.record).toBeDefined();
expect(firstCompany.updatedFields).toBeDefined(); expect(firstCompany.updatedFields).toBeDefined();
}); });
@ -170,66 +208,83 @@ describe('triggers.trigger_record.update', () => {
describe('triggers.trigger_record.delete', () => { describe('triggers.trigger_record.delete', () => {
test('should succeed to subscribe', async () => { test('should succeed to subscribe', async () => {
const bundle = getBundle({}); const bundle = getBundle({});
bundle.inputData.nameSingular = 'company'; bundle.inputData.nameSingular = 'company';
bundle.inputData.operation = DatabaseEventAction.DELETED; bundle.inputData.operation = DatabaseEventAction.DELETED;
bundle.targetUrl = 'https://test.com'; bundle.targetUrl = 'https://test.com';
const result = await appTester( const result = await appTester(
App.triggers[triggerRecordKey].operation.performSubscribe, App.triggers[triggerRecordKey].operation.performSubscribe,
bundle, bundle,
); );
expect(result).toBeDefined(); expect(result).toBeDefined();
expect(result.id).toBeDefined(); expect(result.id).toBeDefined();
const checkDbResult = await appTester( const checkDbResult = await appTester(
(z: ZObject, bundle: Bundle) => (z: ZObject, bundle: Bundle) =>
requestDb( requestDb(
z, z,
bundle, bundle,
`query webhook {webhooks(filter: {id: {eq: "${result.id}"}}){edges {node {id operations}}}}`, `query webhook {webhook(input: {id: "${result.id}"}){id operations}}`,
), ),
bundle, bundle,
); );
expect(checkDbResult.data.webhooks.edges[0].node.operations[0]).toEqual(
expect(checkDbResult.data.webhooks.operations[0]).toEqual(
'company.deleted', 'company.deleted',
); );
}); });
test('should succeed to unsubscribe', async () => { test('should succeed to unsubscribe', async () => {
const bundle = getBundle({}); const bundle = getBundle({});
bundle.inputData.nameSingular = 'company'; bundle.inputData.nameSingular = 'company';
bundle.inputData.operation = DatabaseEventAction.DELETED; bundle.inputData.operation = DatabaseEventAction.DELETED;
bundle.targetUrl = 'https://test.com'; bundle.targetUrl = 'https://test.com';
const result = await appTester( const result = await appTester(
App.triggers[triggerRecordKey].operation.performSubscribe, App.triggers[triggerRecordKey].operation.performSubscribe,
bundle, bundle,
); );
const unsubscribeBundle = getBundle({}); const unsubscribeBundle = getBundle({});
unsubscribeBundle.subscribeData = { id: result.id }; unsubscribeBundle.subscribeData = { id: result.id };
const unsubscribeResult = await appTester( const unsubscribeResult = await appTester(
App.triggers[triggerRecordKey].operation.performUnsubscribe, App.triggers[triggerRecordKey].operation.performUnsubscribe,
unsubscribeBundle, unsubscribeBundle,
); );
expect(unsubscribeResult).toBeDefined(); expect(unsubscribeResult).toBeDefined();
expect(unsubscribeResult.id).toEqual(result.id); expect(unsubscribeResult.id).toEqual(result.id);
const checkDbResult = await appTester( const checkDbResult = await appTester(
(z: ZObject, bundle: Bundle) => (z: ZObject, bundle: Bundle) =>
requestDb( requestDb(z, bundle, `query webhook {webhooks {id}}`),
z,
bundle,
`query webhook {webhooks(filter: {id: {eq: "${result.id}"}}){edges {node {id operations}}}}`,
),
bundle, bundle,
); );
expect(checkDbResult.data.webhooks.edges.length).toEqual(0); expect(
// @ts-expect-error legacy noImplicitAny
checkDbResult.data.webhooks.filter((webhook) => webhook.id === result.id)
.length,
).toEqual(0);
}); });
it('should load companies from list', async () => { it('should load companies from list', async () => {
const bundle = getBundle({}); const bundle = getBundle({});
bundle.inputData.nameSingular = 'company'; bundle.inputData.nameSingular = 'company';
bundle.inputData.operation = DatabaseEventAction.DELETED; bundle.inputData.operation = DatabaseEventAction.DELETED;
const results = await appTester( const results = await appTester(
App.triggers[triggerRecordKey].operation.performList, App.triggers[triggerRecordKey].operation.performList,
bundle, bundle,
); );
expect(results.length).toBeGreaterThan(1); expect(results.length).toBeGreaterThan(1);
const firstCompany = results[0]; const firstCompany = results[0];
expect(firstCompany).toBeDefined(); expect(firstCompany).toBeDefined();
expect(firstCompany.record.id).toBeDefined(); expect(firstCompany.record.id).toBeDefined();
expect(Object.keys(firstCompany).length).toEqual(1); expect(Object.keys(firstCompany).length).toEqual(1);

View File

@ -179,24 +179,6 @@ describe('computeInputFields', () => {
list: false, list: false,
placeholder: undefined, placeholder: undefined,
}, },
{
key: 'xLink__url',
label: 'X: Url',
type: 'string',
helpText: 'Contacts X/Twitter account: Link Url',
required: false,
list: false,
placeholder: undefined,
},
{
key: 'xLink__label',
label: 'X: Label',
type: 'string',
helpText: 'Contacts X/Twitter account: Link Label',
required: false,
list: false,
placeholder: undefined,
},
{ {
key: 'whatsapp__primaryLinkLabel', key: 'whatsapp__primaryLinkLabel',
label: 'Whatsapp: Primary Link Label', label: 'Whatsapp: Primary Link Label',
@ -224,15 +206,6 @@ describe('computeInputFields', () => {
list: true, list: true,
placeholder: '{ url: "", label: "" }', placeholder: '{ url: "", label: "" }',
}, },
{
key: 'email',
label: 'Email',
type: 'string',
helpText: 'Contacts Email',
required: false,
list: false,
placeholder: undefined,
},
{ {
key: 'companyId', key: 'companyId',
label: 'Company id (foreign key)', label: 'Company id (foreign key)',

View File

@ -45,7 +45,7 @@ describe('utils.handleQueryParams', () => {
'linkedinUrl: {url: "/linkedin_url", label: "Test linkedinUrl"}, ' + 'linkedinUrl: {url: "/linkedin_url", label: "Test linkedinUrl"}, ' +
'whatsapp: {primaryLinkUrl: "/whatsapp_url", primaryLinkLabel: "Whatsapp Link", secondaryLinks: [{url: \'/secondary_whatsapp_url\',label: \'Secondary Whatsapp Link\'}]}, ' + 'whatsapp: {primaryLinkUrl: "/whatsapp_url", primaryLinkLabel: "Whatsapp Link", secondaryLinks: [{url: \'/secondary_whatsapp_url\',label: \'Secondary Whatsapp Link\'}]}, ' +
'emails: {primaryEmail: "primary@email.com", additionalEmails: ["secondary@email.com"]}, ' + 'emails: {primaryEmail: "primary@email.com", additionalEmails: ["secondary@email.com"]}, ' +
'phones: {primaryPhoneNumber: "322110011", primaryPhoneCountryCode: "FR", primaryPhoneCallingCode: "+33", additionalPhones: [{ phoneNumber: \'322110012\', countryCode: \'+33\' }]}, ' + 'phones: {primaryPhoneNumber: "322110011", primaryPhoneCountryCode: "FR", primaryPhoneCallingCode: "+33", additionalPhones: [{ phoneNumber: \'322110012\', countryCode: \'FR\', callingCode: \'+33\' }]}, ' +
'xUrl: {url: "/x_url", label: "Test xUrl"}, ' + 'xUrl: {url: "/x_url", label: "Test xUrl"}, ' +
'annualRecurringRevenue: 100000, ' + 'annualRecurringRevenue: 100000, ' +
'idealCustomerProfile: true, ' + 'idealCustomerProfile: true, ' +

View File

@ -18,13 +18,15 @@ export const performSubscribe = async (z: ZObject, bundle: Bundle) => {
operations: [ operations: [
`${bundle.inputData.nameSingular}.${bundle.inputData.operation}`, `${bundle.inputData.nameSingular}.${bundle.inputData.operation}`,
], ],
secret: '',
}; };
const result = await requestDb( const result = await requestDb(
z, z,
bundle, bundle,
`mutation createWebhook {createWebhook(data:{${handleQueryParams( `mutation createWebhook {createWebhook(input:{${handleQueryParams(
data, data,
)}}) {id}}`, )}}) {id}}`,
'metadata',
); );
return result.data.createWebhook; return result.data.createWebhook;
}; };
@ -34,7 +36,8 @@ export const performUnsubscribe = async (z: ZObject, bundle: Bundle) => {
const result = await requestDb( const result = await requestDb(
z, z,
bundle, bundle,
`mutation deleteWebhook {deleteWebhook(${handleQueryParams(data)}) {id}}`, `mutation deleteWebhook {deleteWebhook(${handleQueryParams(data)})}`,
'metadata',
); );
return result.data.deleteWebhook; return result.data.deleteWebhook;
}; };

View File

@ -57174,11 +57174,14 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "twenty-zapier@workspace:packages/twenty-zapier" resolution: "twenty-zapier@workspace:packages/twenty-zapier"
dependencies: dependencies:
"@sniptt/guards": "npm:^0.2.0"
dotenv: "npm:^16.4.5" dotenv: "npm:^16.4.5"
jest: "npm:29.7.0" jest: "npm:29.7.0"
libphonenumber-js: "npm:^1.10.26"
rimraf: "npm:^3.0.2" rimraf: "npm:^3.0.2"
zapier-platform-cli: "npm:^15.4.1" zapier-platform-cli: "npm:^15.4.1"
zapier-platform-core: "npm:15.5.1" zapier-platform-core: "npm:15.5.1"
zod: "npm:3.23.8"
languageName: unknown languageName: unknown
linkType: soft linkType: soft