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,14 +1,34 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import { HttpModule } from '@nestjs/axios';
import { FetchMessagesJob } from 'src/workspace/messaging/jobs/fetch-messages.job'; 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({ @Module({
imports: [
WorkspaceDataSourceModule,
ObjectMetadataModule,
DataSourceModule,
HttpModule,
],
providers: [ providers: [
{ {
provide: FetchMessagesJob.name, provide: FetchMessagesJob.name,
useClass: FetchMessagesJob, useClass: FetchMessagesJob,
}, },
{
provide: CallWebhookJobsJob.name,
useClass: CallWebhookJobsJob,
},
{
provide: CallWebhookJob.name,
useClass: CallWebhookJob,
},
], ],
}) })
export class JobsModule { export class JobsModule {

View File

@ -3,4 +3,5 @@ export const QUEUE_DRIVER = Symbol('QUEUE_DRIVER');
export enum MessageQueue { export enum MessageQueue {
taskAssignedQueue = 'task-assigned-queue', taskAssignedQueue = 'task-assigned-queue',
messagingQueue = 'messaging-queue', messagingQueue = 'messaging-queue',
webhookQueue = 'webhook-queue',
} }

View File

@ -55,7 +55,11 @@ export class MessageQueueModule {
module: MessageQueueModule, module: MessageQueueModule,
imports: [JobsModule, ...(options.imports || [])], imports: [JobsModule, ...(options.imports || [])],
providers, providers,
exports: [MessageQueue.taskAssignedQueue, MessageQueue.messagingQueue], exports: [
MessageQueue.taskAssignedQueue,
MessageQueue.messagingQueue,
MessageQueue.webhookQueue,
],
}; };
} }
} }

View File

@ -326,6 +326,24 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
}); });
} }
public async findOneOrFailWithinWorkspace(
workspaceId: string,
options: FindOneOptions<ObjectMetadataEntity>,
): Promise<ObjectMetadataEntity> {
return this.objectMetadataRepository.findOneOrFail({
relations: [
'fields',
'fields.fromRelationMetadata',
'fields.toRelationMetadata',
],
...options,
where: {
...options.where,
workspaceId,
},
});
}
public async findManyWithinWorkspace( public async findManyWithinWorkspace(
workspaceId: string, workspaceId: string,
options?: FindManyOptions<ObjectMetadataEntity>, options?: FindManyOptions<ObjectMetadataEntity>,

View File

@ -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<CallWebhookJobsJobData>
{
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<void> {
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<CallWebhookJobData>(
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]`,
);
}
}

View File

@ -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<CallWebhookJobData> {
private readonly logger = new Logger(CallWebhookJob.name);
constructor(private readonly httpService: HttpService) {}
async handle(data: CallWebhookJobData): Promise<void> {
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}`,
);
}
}
}

View File

@ -1,5 +1,6 @@
import { import {
BadRequestException, BadRequestException,
Inject,
Injectable, Injectable,
InternalServerErrorException, InternalServerErrorException,
Logger, Logger,
@ -24,6 +25,13 @@ import {
import { WorkspaceQueryBuilderFactory } from 'src/workspace/workspace-query-builder/workspace-query-builder.factory'; import { WorkspaceQueryBuilderFactory } from 'src/workspace/workspace-query-builder/workspace-query-builder.factory';
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service'; 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 { parseResult } from 'src/workspace/workspace-query-runner/utils/parse-result.util';
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service'; import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
import { globalExceptionHandler } from 'src/filters/utils/global-exception-handler.util'; import { globalExceptionHandler } from 'src/filters/utils/global-exception-handler.util';
@ -41,6 +49,8 @@ export class WorkspaceQueryRunnerService {
constructor( constructor(
private readonly workspaceQueryBuilderFactory: WorkspaceQueryBuilderFactory, private readonly workspaceQueryBuilderFactory: WorkspaceQueryBuilderFactory,
private readonly workspaceDataSourceService: WorkspaceDataSourceService, private readonly workspaceDataSourceService: WorkspaceDataSourceService,
@Inject(MessageQueue.webhookQueue)
private readonly messageQueueService: MessageQueueService,
private readonly exceptionHandlerService: ExceptionHandlerService, private readonly exceptionHandlerService: ExceptionHandlerService,
) {} ) {}
@ -117,11 +127,19 @@ export class WorkspaceQueryRunnerService {
); );
const result = await this.execute(query, workspaceId); const result = await this.execute(query, workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>( const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result, result,
targetTableName, targetTableName,
'insertInto', 'insertInto',
)?.records; )?.records;
await this.triggerWebhooks<Record>(
parsedResults,
CallWebhookJobsJobOperation.create,
options,
);
return parsedResults;
} catch (exception) { } catch (exception) {
const error = globalExceptionHandler( const error = globalExceptionHandler(
exception, exception,
@ -136,9 +154,15 @@ export class WorkspaceQueryRunnerService {
args: CreateOneResolverArgs<Record>, args: CreateOneResolverArgs<Record>,
options: WorkspaceQueryRunnerOptions, options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> { ): Promise<Record | undefined> {
const records = await this.createMany({ data: [args.data] }, options); const results = await this.createMany({ data: [args.data] }, options);
return records?.[0]; await this.triggerWebhooks<Record>(
results,
CallWebhookJobsJobOperation.create,
options,
);
return results?.[0];
} }
async updateOne<Record extends IRecord = IRecord>( async updateOne<Record extends IRecord = IRecord>(
@ -153,11 +177,19 @@ export class WorkspaceQueryRunnerService {
); );
const result = await this.execute(query, workspaceId); const result = await this.execute(query, workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>( const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result, result,
targetTableName, targetTableName,
'update', 'update',
)?.records?.[0]; )?.records;
await this.triggerWebhooks<Record>(
parsedResults,
CallWebhookJobsJobOperation.update,
options,
);
return parsedResults?.[0];
} catch (exception) { } catch (exception) {
const error = globalExceptionHandler( const error = globalExceptionHandler(
exception, exception,
@ -180,11 +212,19 @@ export class WorkspaceQueryRunnerService {
); );
const result = await this.execute(query, workspaceId); const result = await this.execute(query, workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>( const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result, result,
targetTableName, targetTableName,
'deleteFrom', 'deleteFrom',
)?.records?.[0]; )?.records;
await this.triggerWebhooks<Record>(
parsedResults,
CallWebhookJobsJobOperation.delete,
options,
);
return parsedResults?.[0];
} catch (exception) { } catch (exception) {
const error = globalExceptionHandler( const error = globalExceptionHandler(
exception, exception,
@ -207,11 +247,19 @@ export class WorkspaceQueryRunnerService {
); );
const result = await this.execute(query, workspaceId); const result = await this.execute(query, workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>( const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result, result,
targetTableName, targetTableName,
'update', 'update',
)?.records; )?.records;
await this.triggerWebhooks<Record>(
parsedResults,
CallWebhookJobsJobOperation.update,
options,
);
return parsedResults;
} catch (exception) { } catch (exception) {
const error = globalExceptionHandler( const error = globalExceptionHandler(
exception, exception,
@ -237,11 +285,19 @@ export class WorkspaceQueryRunnerService {
); );
const result = await this.execute(query, workspaceId); const result = await this.execute(query, workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>( const parsedResults = this.parseResult<PGGraphQLMutation<Record>>(
result, result,
targetTableName, targetTableName,
'deleteFrom', 'deleteFrom',
)?.records; )?.records;
await this.triggerWebhooks<Record>(
parsedResults,
CallWebhookJobsJobOperation.delete,
options,
);
return parsedResults;
} catch (exception) { } catch (exception) {
const error = globalExceptionHandler( const error = globalExceptionHandler(
exception, exception,
@ -306,4 +362,26 @@ export class WorkspaceQueryRunnerService {
return this.parseResult(result, targetTableName, command); return this.parseResult(result, targetTableName, command);
} }
async triggerWebhooks<Record>(
jobsData: Record[] | undefined,
operation: CallWebhookJobsJobOperation,
options: WorkspaceQueryRunnerOptions,
) {
if (!Array.isArray(jobsData)) {
return;
}
jobsData.forEach((jobData) => {
this.messageQueueService.add<CallWebhookJobsJobData>(
CallWebhookJobsJob.name,
{
recordData: jobData,
workspaceId: options.workspaceId,
operation,
objectNameSingular: options.targetTableName,
},
{ retryLimit: 3 },
);
});
}
} }

View File

@ -1,20 +1,21 @@
import { Bundle, ZObject } from "zapier-platform-core"; import { Bundle, ZObject } from 'zapier-platform-core';
import requestDb, { requestSchema } from "../utils/requestDb"; import requestDb, { requestSchema } from '../utils/requestDb';
import handleQueryParams from "../utils/handleQueryParams"; import handleQueryParams from '../utils/handleQueryParams';
import { capitalize } from "../utils/capitalize"; import { capitalize } from '../utils/capitalize';
import { computeInputFields } from "../utils/computeInputFields"; import { computeInputFields } from '../utils/computeInputFields';
import { findObjectNamesSingularKey } from '../triggers/find_object_names_singular';
const recordInputFields = async (z: ZObject, bundle: Bundle) => { const recordInputFields = async (z: ZObject, bundle: Bundle) => {
const schema = await requestSchema(z, bundle) const schema = await requestSchema(z, bundle);
const infos = schema.components.schemas[bundle.inputData.nameSingular] const infos = schema.components.schemas[bundle.inputData.nameSingular];
return computeInputFields(infos); return computeInputFields(infos);
} };
const perform = async (z: ZObject, bundle: Bundle) => { const perform = async (z: ZObject, bundle: Bundle) => {
const data = bundle.inputData const data = bundle.inputData;
const nameSingular = data.nameSingular const nameSingular = data.nameSingular;
delete data.nameSingular delete data.nameSingular;
const query = ` const query = `
mutation create${capitalize(nameSingular)} { mutation create${capitalize(nameSingular)} {
create${capitalize(nameSingular)}( create${capitalize(nameSingular)}(
@ -25,28 +26,30 @@ const perform = async (z: ZObject, bundle: Bundle) => {
return await requestDb(z, bundle, query); return await requestDb(z, bundle, query);
}; };
export const createRecordKey = 'create_record';
export default { export default {
display: { display: {
description: 'Creates a new Record in Twenty', description: 'Creates a new Record in Twenty',
hidden: false, hidden: false,
label: 'Create New Record', label: 'Create New Record',
}, },
key: 'create_record', key: createRecordKey,
noun: 'Record', noun: 'Record',
operation: { operation: {
inputFields: [ inputFields: [
{ {
key: 'nameSingular', key: 'nameSingular',
required: true, required: true,
label: 'Name of the Record to create', label: 'Record Name',
dynamic: 'find_objects.nameSingular', dynamic: `${findObjectNamesSingularKey}.nameSingular`,
altersDynamicFields: true, altersDynamicFields: true,
}, },
recordInputFields recordInputFields,
], ],
sample: { sample: {
id: '179ed459-79cf-41d9-ab85-96397fa8e936', id: '179ed459-79cf-41d9-ab85-96397fa8e936',
}, },
perform perform,
}, },
} };

View File

@ -1,7 +1,22 @@
import findObjectNamesPlural, {
findObjectNamesPluralKey,
} from './triggers/find_object_names_plural';
const { version } = require('../package.json'); const { version } = require('../package.json');
import { version as platformVersion } from 'zapier-platform-core'; import { version as platformVersion } from 'zapier-platform-core';
import createRecord from './creates/create_record'; import createRecord, { createRecordKey } from './creates/create_record';
import findObjects from './triggers/find_objects' 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 authentication from './authentication';
import 'dotenv/config'; import 'dotenv/config';
@ -10,9 +25,13 @@ export default {
platformVersion, platformVersion,
authentication: authentication, authentication: authentication,
triggers: { triggers: {
[findObjects.key]: findObjects, [findObjectNamesSingularKey]: findObjectNamesSingular,
[findObjectNamesPluralKey]: findObjectNamesPlural,
[triggerRecordCreatedKey]: triggerRecordCreated,
[triggerRecordUpdatedKey]: triggerRecordUpdated,
[triggerRecordDeletedKey]: triggerRecordDeleted,
}, },
creates: { creates: {
[createRecord.key]: createRecord, [createRecordKey]: createRecord,
}, },
}; };

View File

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

View File

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

View File

@ -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,
},
};

View File

@ -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,
},
};

View File

@ -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,
},
}

View File

@ -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' },
],
},
};

View File

@ -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' }],
},
};

View File

@ -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' },
],
},
};

View File

@ -1,3 +1,3 @@
export const capitalize = (word: string): string => { export const capitalize = (word: string): string => {
return word.charAt(0).toUpperCase() + word.slice(1) return word.charAt(0).toUpperCase() + word.slice(1);
} };

View File

@ -1,19 +1,19 @@
import { labelling } from "../utils/labelling"; import { labelling } from '../utils/labelling';
type Infos = { type Infos = {
properties: { properties: {
[field: string]: { [field: string]: {
type: string; type: string;
properties?: { [field: string]: { type: string } } properties?: { [field: string]: { type: string } };
items?: { [$ref: string]: string } items?: { [$ref: string]: string };
} };
}, };
example: object, example: object;
required: string[] required: string[];
} };
export const computeInputFields = (infos: Infos): object[] => { export const computeInputFields = (infos: Infos): object[] => {
const result = [] const result = [];
for (const fieldName of Object.keys(infos.properties)) { for (const fieldName of Object.keys(infos.properties)) {
switch (infos.properties[fieldName].type) { switch (infos.properties[fieldName].type) {
@ -23,17 +23,19 @@ export const computeInputFields = (infos: Infos): object[] => {
if (!infos.properties[fieldName].properties) { if (!infos.properties[fieldName].properties) {
break; break;
} }
for (const subFieldName of Object.keys(infos.properties[fieldName].properties || {})) { for (const subFieldName of Object.keys(
infos.properties[fieldName].properties || {},
)) {
const field = { const field = {
key: `${fieldName}__${subFieldName}`, key: `${fieldName}__${subFieldName}`,
label: `${labelling(fieldName)}: ${labelling(subFieldName)}`, label: `${labelling(fieldName)}: ${labelling(subFieldName)}`,
type: infos.properties[fieldName].properties?.[subFieldName].type, type: infos.properties[fieldName].properties?.[subFieldName].type,
required: false, required: false,
} };
if (infos.required?.includes(fieldName)) { if (infos.required?.includes(fieldName)) {
field.required = true field.required = true;
} }
result.push(field) result.push(field);
} }
break; break;
default: default:
@ -42,13 +44,13 @@ export const computeInputFields = (infos: Infos): object[] => {
label: labelling(fieldName), label: labelling(fieldName),
type: infos.properties[fieldName].type, type: infos.properties[fieldName].type,
required: false, required: false,
} };
if (infos.required?.includes(fieldName)) { if (infos.required?.includes(fieldName)) {
field.required = true field.required = true;
} }
result.push(field) result.push(field);
} }
} }
return result return result;
} };

View File

@ -1,25 +1,29 @@
const handleQueryParams = (inputData: { [x: string]: any }): string => { const handleQueryParams = (inputData: { [x: string]: any }): string => {
const formattedInputData: {[x:string]: any} = {}; const formattedInputData: { [x: string]: any } = {};
Object.keys(inputData).forEach((key) => { Object.keys(inputData).forEach((key) => {
if(key.includes('__')) { if (key.includes('__')) {
const [objectKey, nestedObjectKey] = key.split('__') const [objectKey, nestedObjectKey] = key.split('__');
if (formattedInputData[objectKey]) { if (formattedInputData[objectKey]) {
formattedInputData[objectKey][nestedObjectKey] = inputData[key] formattedInputData[objectKey][nestedObjectKey] = inputData[key];
} else { } else {
formattedInputData[objectKey] = {[nestedObjectKey]: inputData[key]} formattedInputData[objectKey] = { [nestedObjectKey]: inputData[key] };
} }
} else { } else {
formattedInputData[key]=inputData[key] formattedInputData[key] = inputData[key];
} }
}) });
let result = ''; let result = '';
Object.keys(formattedInputData).forEach((key) => { Object.keys(formattedInputData).forEach((key) => {
let quote = ''; let quote = '';
if (typeof formattedInputData[key]==='object') { if (typeof formattedInputData[key] === 'object') {
result=result.concat(`${key}: {${handleQueryParams(formattedInputData[key])}}, `) result = result.concat(
`${key}: {${handleQueryParams(formattedInputData[key])}}, `,
);
} else { } else {
if (typeof formattedInputData[key] === 'string') quote = '"'; if (typeof formattedInputData[key] === 'string') quote = '"';
result = result.concat(`${key}: ${quote}${formattedInputData[key]}${quote}, `); result = result.concat(
`${key}: ${quote}${formattedInputData[key]}${quote}, `,
);
} }
}); });
if (result.length) result = result.slice(0, -2); // Remove the last ', ' if (result.length) result = result.slice(0, -2); // Remove the last ', '

View File

@ -1,9 +1,9 @@
import { capitalize } from "../utils/capitalize"; import { capitalize } from '../utils/capitalize';
export const labelling = (str: string): string => { export const labelling = (str: string): string => {
return str return str
.replace(/[A-Z]/g, letter => ` ${letter.toLowerCase()}`) .replace(/[A-Z]/g, (letter) => ` ${letter.toLowerCase()}`)
.split(' ') .split(' ')
.map((word)=> capitalize(word)) .map((word) => capitalize(word))
.join(' '); .join(' ');
} };

View File

@ -2,18 +2,17 @@ import { Bundle, HttpRequestOptions, ZObject } from 'zapier-platform-core';
export const requestSchema = async (z: ZObject, bundle: Bundle) => { export const requestSchema = async (z: ZObject, bundle: Bundle) => {
const options = { const options = {
url: `${process.env.SERVER_BASE_URL}/open-api`, url: `${process.env.SERVER_BASE_URL}/open-api`,
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Accept: 'application/json', Accept: 'application/json',
Authorization: `Bearer ${bundle.authData.apiKey}`, Authorization: `Bearer ${bundle.authData.apiKey}`,
}, },
} satisfies HttpRequestOptions; } satisfies HttpRequestOptions;
return z.request(options) return z.request(options).then((response) => response.json);
.then((response) => response.json) };
}
const requestDb = async (z: ZObject, bundle: Bundle, query: string) => { const requestDb = async (z: ZObject, bundle: Bundle, query: string) => {
const options = { const options = {
@ -37,7 +36,7 @@ const requestDb = async (z: ZObject, bundle: Bundle, query: string) => {
throw new z.errors.Error( throw new z.errors.Error(
`query: ${query}, error: ${JSON.stringify(results.errors)}`, `query: ${query}, error: ${JSON.stringify(results.errors)}`,
'ApiError', 'ApiError',
response.status response.status,
); );
} }
response.throwForStatus(); response.throwForStatus();
@ -47,9 +46,32 @@ const requestDb = async (z: ZObject, bundle: Bundle, query: string) => {
throw new z.errors.Error( throw new z.errors.Error(
`query: ${query}, error: ${err.message}`, `query: ${query}, error: ${err.message}`,
'Error', '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; export default requestDb;

View File

@ -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;
};