diff --git a/packages/twenty-server/src/integrations/message-queue/jobs.module.ts b/packages/twenty-server/src/integrations/message-queue/jobs.module.ts index b10504b88..2fed9eac0 100644 --- a/packages/twenty-server/src/integrations/message-queue/jobs.module.ts +++ b/packages/twenty-server/src/integrations/message-queue/jobs.module.ts @@ -1,14 +1,34 @@ import { Module } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; +import { HttpModule } from '@nestjs/axios'; import { FetchMessagesJob } from 'src/workspace/messaging/jobs/fetch-messages.job'; +import { CallWebhookJobsJob } from 'src/workspace/workspace-query-runner/jobs/call-webhook-jobs.job'; +import { CallWebhookJob } from 'src/workspace/workspace-query-runner/jobs/call-webhook.job'; +import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module'; +import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module'; +import { DataSourceModule } from 'src/metadata/data-source/data-source.module'; @Module({ + imports: [ + WorkspaceDataSourceModule, + ObjectMetadataModule, + DataSourceModule, + HttpModule, + ], providers: [ { provide: FetchMessagesJob.name, useClass: FetchMessagesJob, }, + { + provide: CallWebhookJobsJob.name, + useClass: CallWebhookJobsJob, + }, + { + provide: CallWebhookJob.name, + useClass: CallWebhookJob, + }, ], }) export class JobsModule { diff --git a/packages/twenty-server/src/integrations/message-queue/message-queue.constants.ts b/packages/twenty-server/src/integrations/message-queue/message-queue.constants.ts index 33d82c604..47d2bad68 100644 --- a/packages/twenty-server/src/integrations/message-queue/message-queue.constants.ts +++ b/packages/twenty-server/src/integrations/message-queue/message-queue.constants.ts @@ -3,4 +3,5 @@ export const QUEUE_DRIVER = Symbol('QUEUE_DRIVER'); export enum MessageQueue { taskAssignedQueue = 'task-assigned-queue', messagingQueue = 'messaging-queue', + webhookQueue = 'webhook-queue', } diff --git a/packages/twenty-server/src/integrations/message-queue/message-queue.module.ts b/packages/twenty-server/src/integrations/message-queue/message-queue.module.ts index a187d94cb..dfbdcb1ee 100644 --- a/packages/twenty-server/src/integrations/message-queue/message-queue.module.ts +++ b/packages/twenty-server/src/integrations/message-queue/message-queue.module.ts @@ -55,7 +55,11 @@ export class MessageQueueModule { module: MessageQueueModule, imports: [JobsModule, ...(options.imports || [])], providers, - exports: [MessageQueue.taskAssignedQueue, MessageQueue.messagingQueue], + exports: [ + MessageQueue.taskAssignedQueue, + MessageQueue.messagingQueue, + MessageQueue.webhookQueue, + ], }; } } diff --git a/packages/twenty-server/src/metadata/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/metadata/object-metadata/object-metadata.service.ts index 922fc72d9..3c63136d3 100644 --- a/packages/twenty-server/src/metadata/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/metadata/object-metadata/object-metadata.service.ts @@ -326,6 +326,24 @@ export class ObjectMetadataService extends TypeOrmQueryService, + ): Promise { + return this.objectMetadataRepository.findOneOrFail({ + relations: [ + 'fields', + 'fields.fromRelationMetadata', + 'fields.toRelationMetadata', + ], + ...options, + where: { + ...options.where, + workspaceId, + }, + }); + } + public async findManyWithinWorkspace( workspaceId: string, options?: FindManyOptions, diff --git a/packages/twenty-server/src/workspace/workspace-query-runner/jobs/call-webhook-jobs.job.ts b/packages/twenty-server/src/workspace/workspace-query-runner/jobs/call-webhook-jobs.job.ts new file mode 100644 index 000000000..707535b97 --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-query-runner/jobs/call-webhook-jobs.job.ts @@ -0,0 +1,79 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; + +import { MessageQueueJob } from 'src/integrations/message-queue/interfaces/message-queue-job.interface'; + +import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service'; +import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service'; +import { DataSourceService } from 'src/metadata/data-source/data-source.service'; +import { MessageQueueService } from 'src/integrations/message-queue/services/message-queue.service'; +import { MessageQueue } from 'src/integrations/message-queue/message-queue.constants'; +import { + CallWebhookJob, + CallWebhookJobData, +} from 'src/workspace/workspace-query-runner/jobs/call-webhook.job'; + +export enum CallWebhookJobsJobOperation { + create = 'create', + update = 'update', + delete = 'delete', +} + +export type CallWebhookJobsJobData = { + workspaceId: string; + objectNameSingular: string; + recordData: any; + operation: CallWebhookJobsJobOperation; +}; + +@Injectable() +export class CallWebhookJobsJob + implements MessageQueueJob +{ + private readonly logger = new Logger(CallWebhookJobsJob.name); + + constructor( + private readonly workspaceDataSourceService: WorkspaceDataSourceService, + private readonly objectMetadataService: ObjectMetadataService, + private readonly dataSourceService: DataSourceService, + @Inject(MessageQueue.webhookQueue) + private readonly messageQueueService: MessageQueueService, + ) {} + + async handle(data: CallWebhookJobsJobData): Promise { + const objectMetadataItem = + await this.objectMetadataService.findOneOrFailWithinWorkspace( + data.workspaceId, + { where: { nameSingular: data.objectNameSingular } }, + ); + const dataSourceMetadata = + await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail( + data.workspaceId, + ); + const workspaceDataSource = + await this.workspaceDataSourceService.connectToWorkspaceDataSource( + data.workspaceId, + ); + const operationName = `${data.operation}.${objectMetadataItem.namePlural}`; + const webhooks: { id: string; targetUrl: string }[] = + await workspaceDataSource?.query( + `SELECT * FROM ${dataSourceMetadata.schema}."webhook" WHERE operation='${operationName}'`, + ); + + webhooks.forEach((webhook) => { + this.messageQueueService.add( + CallWebhookJob.name, + { + recordData: data.recordData, + targetUrl: webhook.targetUrl, + }, + { retryLimit: 3 }, + ); + }); + + this.logger.log( + `CallWebhookJobsJob on operation '${operationName}' called on webhooks ids [\n"${webhooks + .map((webhook) => webhook.id) + .join('",\n"')}"\n]`, + ); + } +} diff --git a/packages/twenty-server/src/workspace/workspace-query-runner/jobs/call-webhook.job.ts b/packages/twenty-server/src/workspace/workspace-query-runner/jobs/call-webhook.job.ts new file mode 100644 index 000000000..a4a837ee0 --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-query-runner/jobs/call-webhook.job.ts @@ -0,0 +1,31 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; + +import { MessageQueueJob } from 'src/integrations/message-queue/interfaces/message-queue-job.interface'; + +export type CallWebhookJobData = { + targetUrl: string; + recordData: any; +}; + +@Injectable() +export class CallWebhookJob implements MessageQueueJob { + private readonly logger = new Logger(CallWebhookJob.name); + + constructor(private readonly httpService: HttpService) {} + + async handle(data: CallWebhookJobData): Promise { + try { + await this.httpService.axiosRef.post(data.targetUrl, data.recordData); + this.logger.log( + `CallWebhookJob successfully called on targetUrl '${ + data.targetUrl + }' with data: ${JSON.stringify(data.recordData)}`, + ); + } catch (err) { + throw new Error( + `Error calling webhook on targetUrl '${data.targetUrl}': ${err}`, + ); + } + } +} diff --git a/packages/twenty-server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts b/packages/twenty-server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts index a11075ba7..450cea667 100644 --- a/packages/twenty-server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts +++ b/packages/twenty-server/src/workspace/workspace-query-runner/workspace-query-runner.service.ts @@ -1,5 +1,6 @@ import { BadRequestException, + Inject, Injectable, InternalServerErrorException, Logger, @@ -24,6 +25,13 @@ import { import { WorkspaceQueryBuilderFactory } from 'src/workspace/workspace-query-builder/workspace-query-builder.factory'; import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service'; +import { MessageQueueService } from 'src/integrations/message-queue/services/message-queue.service'; +import { MessageQueue } from 'src/integrations/message-queue/message-queue.constants'; +import { + CallWebhookJobsJob, + CallWebhookJobsJobData, + CallWebhookJobsJobOperation, +} from 'src/workspace/workspace-query-runner/jobs/call-webhook-jobs.job'; import { parseResult } from 'src/workspace/workspace-query-runner/utils/parse-result.util'; import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service'; import { globalExceptionHandler } from 'src/filters/utils/global-exception-handler.util'; @@ -41,6 +49,8 @@ export class WorkspaceQueryRunnerService { constructor( private readonly workspaceQueryBuilderFactory: WorkspaceQueryBuilderFactory, private readonly workspaceDataSourceService: WorkspaceDataSourceService, + @Inject(MessageQueue.webhookQueue) + private readonly messageQueueService: MessageQueueService, private readonly exceptionHandlerService: ExceptionHandlerService, ) {} @@ -117,11 +127,19 @@ export class WorkspaceQueryRunnerService { ); const result = await this.execute(query, workspaceId); - return this.parseResult>( + const parsedResults = this.parseResult>( result, targetTableName, 'insertInto', )?.records; + + await this.triggerWebhooks( + parsedResults, + CallWebhookJobsJobOperation.create, + options, + ); + + return parsedResults; } catch (exception) { const error = globalExceptionHandler( exception, @@ -136,9 +154,15 @@ export class WorkspaceQueryRunnerService { args: CreateOneResolverArgs, options: WorkspaceQueryRunnerOptions, ): Promise { - const records = await this.createMany({ data: [args.data] }, options); + const results = await this.createMany({ data: [args.data] }, options); - return records?.[0]; + await this.triggerWebhooks( + results, + CallWebhookJobsJobOperation.create, + options, + ); + + return results?.[0]; } async updateOne( @@ -153,11 +177,19 @@ export class WorkspaceQueryRunnerService { ); const result = await this.execute(query, workspaceId); - return this.parseResult>( + const parsedResults = this.parseResult>( result, targetTableName, 'update', - )?.records?.[0]; + )?.records; + + await this.triggerWebhooks( + parsedResults, + CallWebhookJobsJobOperation.update, + options, + ); + + return parsedResults?.[0]; } catch (exception) { const error = globalExceptionHandler( exception, @@ -180,11 +212,19 @@ export class WorkspaceQueryRunnerService { ); const result = await this.execute(query, workspaceId); - return this.parseResult>( + const parsedResults = this.parseResult>( result, targetTableName, 'deleteFrom', - )?.records?.[0]; + )?.records; + + await this.triggerWebhooks( + parsedResults, + CallWebhookJobsJobOperation.delete, + options, + ); + + return parsedResults?.[0]; } catch (exception) { const error = globalExceptionHandler( exception, @@ -207,11 +247,19 @@ export class WorkspaceQueryRunnerService { ); const result = await this.execute(query, workspaceId); - return this.parseResult>( + const parsedResults = this.parseResult>( result, targetTableName, 'update', )?.records; + + await this.triggerWebhooks( + parsedResults, + CallWebhookJobsJobOperation.update, + options, + ); + + return parsedResults; } catch (exception) { const error = globalExceptionHandler( exception, @@ -237,11 +285,19 @@ export class WorkspaceQueryRunnerService { ); const result = await this.execute(query, workspaceId); - return this.parseResult>( + const parsedResults = this.parseResult>( result, targetTableName, 'deleteFrom', )?.records; + + await this.triggerWebhooks( + parsedResults, + CallWebhookJobsJobOperation.delete, + options, + ); + + return parsedResults; } catch (exception) { const error = globalExceptionHandler( exception, @@ -306,4 +362,26 @@ export class WorkspaceQueryRunnerService { return this.parseResult(result, targetTableName, command); } + + async triggerWebhooks( + jobsData: Record[] | undefined, + operation: CallWebhookJobsJobOperation, + options: WorkspaceQueryRunnerOptions, + ) { + if (!Array.isArray(jobsData)) { + return; + } + jobsData.forEach((jobData) => { + this.messageQueueService.add( + CallWebhookJobsJob.name, + { + recordData: jobData, + workspaceId: options.workspaceId, + operation, + objectNameSingular: options.targetTableName, + }, + { retryLimit: 3 }, + ); + }); + } } diff --git a/packages/twenty-zapier/src/creates/create_record.ts b/packages/twenty-zapier/src/creates/create_record.ts index e68fb5b6e..ee9ea7285 100644 --- a/packages/twenty-zapier/src/creates/create_record.ts +++ b/packages/twenty-zapier/src/creates/create_record.ts @@ -1,20 +1,21 @@ -import { Bundle, ZObject } from "zapier-platform-core"; -import requestDb, { requestSchema } from "../utils/requestDb"; -import handleQueryParams from "../utils/handleQueryParams"; -import { capitalize } from "../utils/capitalize"; -import { computeInputFields } from "../utils/computeInputFields"; +import { Bundle, ZObject } from 'zapier-platform-core'; +import requestDb, { requestSchema } from '../utils/requestDb'; +import handleQueryParams from '../utils/handleQueryParams'; +import { capitalize } from '../utils/capitalize'; +import { computeInputFields } from '../utils/computeInputFields'; +import { findObjectNamesSingularKey } from '../triggers/find_object_names_singular'; const recordInputFields = async (z: ZObject, bundle: Bundle) => { - const schema = await requestSchema(z, bundle) - const infos = schema.components.schemas[bundle.inputData.nameSingular] + const schema = await requestSchema(z, bundle); + const infos = schema.components.schemas[bundle.inputData.nameSingular]; return computeInputFields(infos); -} +}; const perform = async (z: ZObject, bundle: Bundle) => { - const data = bundle.inputData - const nameSingular = data.nameSingular - delete data.nameSingular + const data = bundle.inputData; + const nameSingular = data.nameSingular; + delete data.nameSingular; const query = ` mutation create${capitalize(nameSingular)} { create${capitalize(nameSingular)}( @@ -25,28 +26,30 @@ const perform = async (z: ZObject, bundle: Bundle) => { return await requestDb(z, bundle, query); }; +export const createRecordKey = 'create_record'; + export default { display: { description: 'Creates a new Record in Twenty', hidden: false, label: 'Create New Record', }, - key: 'create_record', + key: createRecordKey, noun: 'Record', operation: { inputFields: [ { key: 'nameSingular', required: true, - label: 'Name of the Record to create', - dynamic: 'find_objects.nameSingular', + label: 'Record Name', + dynamic: `${findObjectNamesSingularKey}.nameSingular`, altersDynamicFields: true, }, - recordInputFields + recordInputFields, ], sample: { id: '179ed459-79cf-41d9-ab85-96397fa8e936', }, - perform + perform, }, -} +}; diff --git a/packages/twenty-zapier/src/index.ts b/packages/twenty-zapier/src/index.ts index 698bef084..11d247590 100644 --- a/packages/twenty-zapier/src/index.ts +++ b/packages/twenty-zapier/src/index.ts @@ -1,7 +1,22 @@ +import findObjectNamesPlural, { + findObjectNamesPluralKey, +} from './triggers/find_object_names_plural'; + const { version } = require('../package.json'); import { version as platformVersion } from 'zapier-platform-core'; -import createRecord from './creates/create_record'; -import findObjects from './triggers/find_objects' +import createRecord, { createRecordKey } from './creates/create_record'; +import findObjectNamesSingular, { + findObjectNamesSingularKey, +} from './triggers/find_object_names_singular'; +import triggerRecordCreated, { + triggerRecordCreatedKey, +} from './triggers/trigger_record_created'; +import triggerRecordDeleted, { + triggerRecordDeletedKey, +} from './triggers/trigger_record_deleted'; +import triggerRecordUpdated, { + triggerRecordUpdatedKey, +} from './triggers/trigger_record_updated'; import authentication from './authentication'; import 'dotenv/config'; @@ -10,9 +25,13 @@ export default { platformVersion, authentication: authentication, triggers: { - [findObjects.key]: findObjects, + [findObjectNamesSingularKey]: findObjectNamesSingular, + [findObjectNamesPluralKey]: findObjectNamesPlural, + [triggerRecordCreatedKey]: triggerRecordCreated, + [triggerRecordUpdatedKey]: triggerRecordUpdated, + [triggerRecordDeletedKey]: triggerRecordDeleted, }, creates: { - [createRecord.key]: createRecord, + [createRecordKey]: createRecord, }, }; diff --git a/packages/twenty-zapier/src/test/authentication.test.ts b/packages/twenty-zapier/src/test/authentication.test.ts index 7750ce8ef..f0abbcf25 100644 --- a/packages/twenty-zapier/src/test/authentication.test.ts +++ b/packages/twenty-zapier/src/test/authentication.test.ts @@ -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'); diff --git a/packages/twenty-zapier/src/test/creates/create_record.test.ts b/packages/twenty-zapier/src/test/creates/create_record.test.ts index dd1b0026f..4ed0e214c 100644 --- a/packages/twenty-zapier/src/test/creates/create_record.test.ts +++ b/packages/twenty-zapier/src/test/creates/create_record.test.ts @@ -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'); - }) -}) + }); +}); diff --git a/packages/twenty-zapier/src/test/triggers/find_object_names_plural.test.ts b/packages/twenty-zapier/src/test/triggers/find_object_names_plural.test.ts new file mode 100644 index 000000000..f9837a50b --- /dev/null +++ b/packages/twenty-zapier/src/test/triggers/find_object_names_plural.test.ts @@ -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(); + }); +}); diff --git a/packages/twenty-zapier/src/test/triggers/find_object_names_singular.test.ts b/packages/twenty-zapier/src/test/triggers/find_object_names_singular.test.ts new file mode 100644 index 000000000..d775708ae --- /dev/null +++ b/packages/twenty-zapier/src/test/triggers/find_object_names_singular.test.ts @@ -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(); + }); +}); diff --git a/packages/twenty-zapier/src/test/triggers/find_objects.spec.ts b/packages/twenty-zapier/src/test/triggers/find_objects.spec.ts deleted file mode 100644 index a8fab9e24..000000000 --- a/packages/twenty-zapier/src/test/triggers/find_objects.spec.ts +++ /dev/null @@ -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() - }) -}) diff --git a/packages/twenty-zapier/src/test/triggers/trigger_record_created.test.ts b/packages/twenty-zapier/src/test/triggers/trigger_record_created.test.ts new file mode 100644 index 000000000..6400759ac --- /dev/null +++ b/packages/twenty-zapier/src/test/triggers/trigger_record_created.test.ts @@ -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(); + }); +}); diff --git a/packages/twenty-zapier/src/test/triggers/trigger_record_deleted.test.ts b/packages/twenty-zapier/src/test/triggers/trigger_record_deleted.test.ts new file mode 100644 index 000000000..1bb3e8fde --- /dev/null +++ b/packages/twenty-zapier/src/test/triggers/trigger_record_deleted.test.ts @@ -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); + }); +}); diff --git a/packages/twenty-zapier/src/test/triggers/trigger_record_updated.test.ts b/packages/twenty-zapier/src/test/triggers/trigger_record_updated.test.ts new file mode 100644 index 000000000..fe53153cb --- /dev/null +++ b/packages/twenty-zapier/src/test/triggers/trigger_record_updated.test.ts @@ -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(); + }); +}); diff --git a/packages/twenty-zapier/src/test/utils/capitalize.spec.ts b/packages/twenty-zapier/src/test/utils/capitalize.spec.ts deleted file mode 100644 index 4ca269990..000000000 --- a/packages/twenty-zapier/src/test/utils/capitalize.spec.ts +++ /dev/null @@ -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') - }) -}) diff --git a/packages/twenty-zapier/src/test/utils/capitalize.test.ts b/packages/twenty-zapier/src/test/utils/capitalize.test.ts new file mode 100644 index 000000000..96e7bdf34 --- /dev/null +++ b/packages/twenty-zapier/src/test/utils/capitalize.test.ts @@ -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'); + }); +}); diff --git a/packages/twenty-zapier/src/test/utils/computeInputFields.spec.ts b/packages/twenty-zapier/src/test/utils/computeInputFields.spec.ts deleted file mode 100644 index f7c2e34c6..000000000 --- a/packages/twenty-zapier/src/test/utils/computeInputFields.spec.ts +++ /dev/null @@ -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" }, - ]) - }) -}) diff --git a/packages/twenty-zapier/src/test/utils/computeInputFields.test.ts b/packages/twenty-zapier/src/test/utils/computeInputFields.test.ts new file mode 100644 index 000000000..ce46cd332 --- /dev/null +++ b/packages/twenty-zapier/src/test/utils/computeInputFields.test.ts @@ -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' }, + ]); + }); +}); diff --git a/packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts b/packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts index 174591f8c..c407c4a6c 100644 --- a/packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts +++ b/packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts @@ -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, diff --git a/packages/twenty-zapier/src/test/utils/labelize.spec.ts b/packages/twenty-zapier/src/test/utils/labelize.spec.ts deleted file mode 100644 index 002cc7dd3..000000000 --- a/packages/twenty-zapier/src/test/utils/labelize.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { labelling } from "../../utils/labelling"; - -describe('labelling', ()=> { - test('should label properly', ()=> { - expect(labelling('createdAt')).toEqual('Created At') - }) -}) diff --git a/packages/twenty-zapier/src/test/utils/labelize.test.ts b/packages/twenty-zapier/src/test/utils/labelize.test.ts new file mode 100644 index 000000000..529d047f9 --- /dev/null +++ b/packages/twenty-zapier/src/test/utils/labelize.test.ts @@ -0,0 +1,7 @@ +import { labelling } from '../../utils/labelling'; + +describe('labelling', () => { + test('should label properly', () => { + expect(labelling('createdAt')).toEqual('Created At'); + }); +}); diff --git a/packages/twenty-zapier/src/triggers/find_object_names_plural.ts b/packages/twenty-zapier/src/triggers/find_object_names_plural.ts new file mode 100644 index 000000000..5b9614a6f --- /dev/null +++ b/packages/twenty-zapier/src/triggers/find_object_names_plural.ts @@ -0,0 +1,27 @@ +import { Bundle, ZObject } from 'zapier-platform-core'; +import { requestSchema } from '../utils/requestDb'; + +const objectNamesPluralListRequest = async (z: ZObject, bundle: Bundle) => { + const schema = await requestSchema(z, bundle); + const tags: { name: string }[] = schema.tags; + return Object.values(tags) + .filter((tag) => tag.name !== 'General') + .map((tag) => { + return { id: tag.name, namePlural: tag.name }; + }); +}; + +export const findObjectNamesPluralKey = 'find_object_names_plural'; + +export default { + display: { + description: 'Find objects', + label: 'Find objects', + hidden: true, + }, + key: findObjectNamesPluralKey, + noun: 'Object', + operation: { + perform: objectNamesPluralListRequest, + }, +}; diff --git a/packages/twenty-zapier/src/triggers/find_object_names_singular.ts b/packages/twenty-zapier/src/triggers/find_object_names_singular.ts new file mode 100644 index 000000000..23a5617e2 --- /dev/null +++ b/packages/twenty-zapier/src/triggers/find_object_names_singular.ts @@ -0,0 +1,24 @@ +import { Bundle, ZObject } from 'zapier-platform-core'; +import { requestSchema } from '../utils/requestDb'; + +const objectListRequest = async (z: ZObject, bundle: Bundle) => { + const schema = await requestSchema(z, bundle); + return Object.keys(schema.components.schemas).map((schema) => { + return { id: schema, nameSingular: schema }; + }); +}; + +export const findObjectNamesSingularKey = 'find_object_names_singular'; + +export default { + display: { + description: 'Find objects', + label: 'Find objects', + hidden: true, + }, + key: findObjectNamesSingularKey, + noun: 'Object', + operation: { + perform: objectListRequest, + }, +}; diff --git a/packages/twenty-zapier/src/triggers/find_objects.ts b/packages/twenty-zapier/src/triggers/find_objects.ts deleted file mode 100644 index 62fff4aaf..000000000 --- a/packages/twenty-zapier/src/triggers/find_objects.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Bundle, ZObject } from "zapier-platform-core"; -import { requestSchema } from "../utils/requestDb"; - -const objectListRequest = async (z: ZObject, bundle: Bundle) => { - const schema = await requestSchema(z, bundle) - return Object.keys(schema.components.schemas).map((schema)=> { - return {id: schema, nameSingular: schema} - }) -} - -export default { - display: { - description: 'Find objects', - label: 'Find objects', - hidden: true, - }, - key: 'find_objects', - noun: 'Object', - operation: { - perform: objectListRequest, - }, -} diff --git a/packages/twenty-zapier/src/triggers/trigger_record_created.ts b/packages/twenty-zapier/src/triggers/trigger_record_created.ts new file mode 100644 index 000000000..99cdffa7f --- /dev/null +++ b/packages/twenty-zapier/src/triggers/trigger_record_created.ts @@ -0,0 +1,50 @@ +import { findObjectNamesPluralKey } from '../triggers/find_object_names_plural'; +import { + listSample, + Operation, + perform, + performUnsubscribe, + subscribe, +} from '../utils/triggers.utils'; +import { Bundle, ZObject } from 'zapier-platform-core'; + +export const triggerRecordCreatedKey = 'trigger_record_created'; + +const performSubscribe = (z: ZObject, bundle: Bundle) => + subscribe(z, bundle, Operation.create); +const performList = (z: ZObject, bundle: Bundle) => listSample(z, bundle); + +export default { + key: triggerRecordCreatedKey, + noun: 'Record', + display: { + label: 'Record Trigger Created', + description: 'Triggers when a Record is created.', + }, + operation: { + inputFields: [ + { + key: 'namePlural', + required: true, + label: 'Record Name', + dynamic: `${findObjectNamesPluralKey}.namePlural`, + altersDynamicFields: true, + }, + ], + type: 'hook', + performSubscribe, + performUnsubscribe, + perform, + performList, + sample: { + id: 'f75f6b2e-9442-4c72-aa95-47d8e5ec8cb3', + createdAt: '2023-10-19T07:37:25.306Z', + workspaceId: 'c8b070fc-c969-4ca5-837a-e7c3735734d2', + }, + outputFields: [ + { key: 'id', label: 'ID' }, + { key: 'createdAt', label: 'Created At' }, + { key: 'workspaceId', label: 'Workspace ID' }, + ], + }, +}; diff --git a/packages/twenty-zapier/src/triggers/trigger_record_deleted.ts b/packages/twenty-zapier/src/triggers/trigger_record_deleted.ts new file mode 100644 index 000000000..387ab3c5a --- /dev/null +++ b/packages/twenty-zapier/src/triggers/trigger_record_deleted.ts @@ -0,0 +1,44 @@ +import { findObjectNamesPluralKey } from '../triggers/find_object_names_plural'; +import { + perform, + listSample, + subscribe, + performUnsubscribe, + Operation, +} from '../utils/triggers.utils'; +import { Bundle, ZObject } from 'zapier-platform-core'; + +export const triggerRecordDeletedKey = 'trigger_record_deleted'; + +const performSubscribe = (z: ZObject, bundle: Bundle) => + subscribe(z, bundle, Operation.delete); +const performList = (z: ZObject, bundle: Bundle) => listSample(z, bundle, true); + +export default { + key: triggerRecordDeletedKey, + noun: 'Record', + display: { + label: 'Record Trigger Deleted', + description: 'Triggers when a Record is deleted.', + }, + operation: { + inputFields: [ + { + key: 'namePlural', + required: true, + label: 'Record Name', + dynamic: `${findObjectNamesPluralKey}.namePlural`, + altersDynamicFields: true, + }, + ], + type: 'hook', + performSubscribe, + performUnsubscribe, + perform, + performList, + sample: { + id: 'f75f6b2e-9442-4c72-aa95-47d8e5ec8cb3', + }, + outputFields: [{ key: 'id', label: 'ID' }], + }, +}; diff --git a/packages/twenty-zapier/src/triggers/trigger_record_updated.ts b/packages/twenty-zapier/src/triggers/trigger_record_updated.ts new file mode 100644 index 000000000..798d69cda --- /dev/null +++ b/packages/twenty-zapier/src/triggers/trigger_record_updated.ts @@ -0,0 +1,50 @@ +import { findObjectNamesPluralKey } from '../triggers/find_object_names_plural'; +import { + listSample, + Operation, + perform, + performUnsubscribe, + subscribe, +} from '../utils/triggers.utils'; +import { Bundle, ZObject } from 'zapier-platform-core'; + +export const triggerRecordUpdatedKey = 'trigger_record_updated'; + +const performSubscribe = (z: ZObject, bundle: Bundle) => + subscribe(z, bundle, Operation.update); +const performList = (z: ZObject, bundle: Bundle) => listSample(z, bundle); + +export default { + key: triggerRecordUpdatedKey, + noun: 'Record', + display: { + label: 'Record Trigger Updated', + description: 'Triggers when a Record is updated.', + }, + operation: { + inputFields: [ + { + key: 'namePlural', + required: true, + label: 'Record Name', + dynamic: `${findObjectNamesPluralKey}.namePlural`, + altersDynamicFields: true, + }, + ], + type: 'hook', + performSubscribe, + performUnsubscribe, + perform, + performList, + sample: { + id: 'f75f6b2e-9442-4c72-aa95-47d8e5ec8cb3', + createdAt: '2023-10-19T07:37:25.306Z', + workspaceId: 'c8b070fc-c969-4ca5-837a-e7c3735734d2', + }, + outputFields: [ + { key: 'id', label: 'ID' }, + { key: 'createdAt', label: 'Created At' }, + { key: 'workspaceId', label: 'Workspace ID' }, + ], + }, +}; diff --git a/packages/twenty-zapier/src/utils/capitalize.ts b/packages/twenty-zapier/src/utils/capitalize.ts index 9cd7cc893..046620ddf 100644 --- a/packages/twenty-zapier/src/utils/capitalize.ts +++ b/packages/twenty-zapier/src/utils/capitalize.ts @@ -1,3 +1,3 @@ export const capitalize = (word: string): string => { - return word.charAt(0).toUpperCase() + word.slice(1) -} + return word.charAt(0).toUpperCase() + word.slice(1); +}; diff --git a/packages/twenty-zapier/src/utils/computeInputFields.ts b/packages/twenty-zapier/src/utils/computeInputFields.ts index fa82329b3..381880c39 100644 --- a/packages/twenty-zapier/src/utils/computeInputFields.ts +++ b/packages/twenty-zapier/src/utils/computeInputFields.ts @@ -1,19 +1,19 @@ -import { labelling } from "../utils/labelling"; +import { labelling } from '../utils/labelling'; type Infos = { properties: { [field: string]: { type: string; - properties?: { [field: string]: { type: string } } - items?: { [$ref: string]: string } - } - }, - example: object, - required: string[] -} + properties?: { [field: string]: { type: string } }; + items?: { [$ref: string]: string }; + }; + }; + example: object; + required: string[]; +}; export const computeInputFields = (infos: Infos): object[] => { - const result = [] + const result = []; for (const fieldName of Object.keys(infos.properties)) { switch (infos.properties[fieldName].type) { @@ -23,17 +23,19 @@ export const computeInputFields = (infos: Infos): object[] => { if (!infos.properties[fieldName].properties) { break; } - for (const subFieldName of Object.keys(infos.properties[fieldName].properties || {})) { + for (const subFieldName of Object.keys( + infos.properties[fieldName].properties || {}, + )) { const field = { key: `${fieldName}__${subFieldName}`, label: `${labelling(fieldName)}: ${labelling(subFieldName)}`, type: infos.properties[fieldName].properties?.[subFieldName].type, required: false, - } + }; if (infos.required?.includes(fieldName)) { - field.required = true + field.required = true; } - result.push(field) + result.push(field); } break; default: @@ -42,13 +44,13 @@ export const computeInputFields = (infos: Infos): object[] => { label: labelling(fieldName), type: infos.properties[fieldName].type, required: false, - } + }; if (infos.required?.includes(fieldName)) { - field.required = true + field.required = true; } - result.push(field) + result.push(field); } } - return result -} + return result; +}; diff --git a/packages/twenty-zapier/src/utils/handleQueryParams.ts b/packages/twenty-zapier/src/utils/handleQueryParams.ts index 9b2eeda63..ce4971f48 100644 --- a/packages/twenty-zapier/src/utils/handleQueryParams.ts +++ b/packages/twenty-zapier/src/utils/handleQueryParams.ts @@ -1,25 +1,29 @@ const handleQueryParams = (inputData: { [x: string]: any }): string => { - const formattedInputData: {[x:string]: any} = {}; + const formattedInputData: { [x: string]: any } = {}; Object.keys(inputData).forEach((key) => { - if(key.includes('__')) { - const [objectKey, nestedObjectKey] = key.split('__') + if (key.includes('__')) { + const [objectKey, nestedObjectKey] = key.split('__'); if (formattedInputData[objectKey]) { - formattedInputData[objectKey][nestedObjectKey] = inputData[key] + formattedInputData[objectKey][nestedObjectKey] = inputData[key]; } else { - formattedInputData[objectKey] = {[nestedObjectKey]: inputData[key]} + formattedInputData[objectKey] = { [nestedObjectKey]: inputData[key] }; } } else { - formattedInputData[key]=inputData[key] + formattedInputData[key] = inputData[key]; } - }) + }); let result = ''; Object.keys(formattedInputData).forEach((key) => { let quote = ''; - if (typeof formattedInputData[key]==='object') { - result=result.concat(`${key}: {${handleQueryParams(formattedInputData[key])}}, `) + if (typeof formattedInputData[key] === 'object') { + result = result.concat( + `${key}: {${handleQueryParams(formattedInputData[key])}}, `, + ); } else { - if (typeof formattedInputData[key] === 'string') quote = '"'; - result = result.concat(`${key}: ${quote}${formattedInputData[key]}${quote}, `); + if (typeof formattedInputData[key] === 'string') quote = '"'; + result = result.concat( + `${key}: ${quote}${formattedInputData[key]}${quote}, `, + ); } }); if (result.length) result = result.slice(0, -2); // Remove the last ', ' diff --git a/packages/twenty-zapier/src/utils/labelling.ts b/packages/twenty-zapier/src/utils/labelling.ts index a9eb70918..5febd7cac 100644 --- a/packages/twenty-zapier/src/utils/labelling.ts +++ b/packages/twenty-zapier/src/utils/labelling.ts @@ -1,9 +1,9 @@ -import { capitalize } from "../utils/capitalize"; +import { capitalize } from '../utils/capitalize'; export const labelling = (str: string): string => { return str - .replace(/[A-Z]/g, letter => ` ${letter.toLowerCase()}`) + .replace(/[A-Z]/g, (letter) => ` ${letter.toLowerCase()}`) .split(' ') - .map((word)=> capitalize(word)) + .map((word) => capitalize(word)) .join(' '); -} +}; diff --git a/packages/twenty-zapier/src/utils/requestDb.ts b/packages/twenty-zapier/src/utils/requestDb.ts index d32c294f3..9e655b1d9 100644 --- a/packages/twenty-zapier/src/utils/requestDb.ts +++ b/packages/twenty-zapier/src/utils/requestDb.ts @@ -2,18 +2,17 @@ import { Bundle, HttpRequestOptions, ZObject } from 'zapier-platform-core'; export const requestSchema = async (z: ZObject, bundle: Bundle) => { const options = { - url: `${process.env.SERVER_BASE_URL}/open-api`, - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - Authorization: `Bearer ${bundle.authData.apiKey}`, - }, + url: `${process.env.SERVER_BASE_URL}/open-api`, + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: `Bearer ${bundle.authData.apiKey}`, + }, } satisfies HttpRequestOptions; - return z.request(options) - .then((response) => response.json) -} + return z.request(options).then((response) => response.json); +}; const requestDb = async (z: ZObject, bundle: Bundle, query: string) => { const options = { @@ -37,7 +36,7 @@ const requestDb = async (z: ZObject, bundle: Bundle, query: string) => { throw new z.errors.Error( `query: ${query}, error: ${JSON.stringify(results.errors)}`, 'ApiError', - response.status + response.status, ); } response.throwForStatus(); @@ -47,9 +46,32 @@ const requestDb = async (z: ZObject, bundle: Bundle, query: string) => { throw new z.errors.Error( `query: ${query}, error: ${err.message}`, 'Error', - err.status + err.status, ); }); }; +export const requestDbViaRestApi = ( + z: ZObject, + bundle: Bundle, + objectNamePlural: string, +) => { + const options = { + url: `${process.env.SERVER_BASE_URL}/rest/${objectNamePlural}?limit:3`, + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: `Bearer ${bundle.authData.apiKey}`, + }, + } satisfies HttpRequestOptions; + + return z + .request(options) + .then((response) => response.json.data[objectNamePlural]) + .catch((err) => { + throw new z.errors.Error(`Error: ${err.message}`, 'Error', err.status); + }); +}; + export default requestDb; diff --git a/packages/twenty-zapier/src/utils/triggers.utils.ts b/packages/twenty-zapier/src/utils/triggers.utils.ts new file mode 100644 index 000000000..19e7c2546 --- /dev/null +++ b/packages/twenty-zapier/src/utils/triggers.utils.ts @@ -0,0 +1,64 @@ +import { Bundle, ZObject } from 'zapier-platform-core'; +import requestDb, { requestDbViaRestApi } from '../utils/requestDb'; +import handleQueryParams from '../utils/handleQueryParams'; + +export enum Operation { + create = 'create', + update = 'update', + delete = 'delete', +} + +export const subscribe = async ( + z: ZObject, + bundle: Bundle, + operation: Operation, +) => { + const data = { + targetUrl: bundle.targetUrl, + operation: `${operation}.${bundle.inputData.namePlural}`, + }; + const result = await requestDb( + z, + bundle, + `mutation createWebhook {createWebhook(data:{${handleQueryParams( + data, + )}}) {id}}`, + ); + return result.data.createWebhook; +}; + +export const performUnsubscribe = async (z: ZObject, bundle: Bundle) => { + const data = { id: bundle.subscribeData?.id }; + const result = await requestDb( + z, + bundle, + `mutation deleteWebhook {deleteWebhook(${handleQueryParams(data)}) {id}}`, + ); + return result.data.deleteWebhook; +}; + +export const perform = (z: ZObject, bundle: Bundle) => { + return [bundle.cleanedRequest]; +}; + +export const listSample = async ( + z: ZObject, + bundle: Bundle, + onlyIds = false, +) => { + const result: { [key: string]: string }[] = await requestDbViaRestApi( + z, + bundle, + bundle.inputData.namePlural, + ); + + if (onlyIds) { + return result.map((res) => { + return { + id: res.id, + }; + }); + } + + return result; +};