diff --git a/apps/api/src/hook/hook.factory.spec.ts b/apps/api/src/hook/hook.factory.spec.ts index dc1b822..fde1942 100644 --- a/apps/api/src/hook/hook.factory.spec.ts +++ b/apps/api/src/hook/hook.factory.spec.ts @@ -1,7 +1,7 @@ import { ConfigService } from '@nestjs/config'; import { DataCordProcessor } from './processors/datacord.processor'; import { EmailProcessor } from './processors/email.processor'; -import { HookProcessorFactory } from './hook.factory'; +import { HookFactory } from './hook.factory'; import { HookType } from '../models'; import { HttpService } from '@nestjs/axios'; import { ModuleRef } from '@nestjs/core'; @@ -13,7 +13,7 @@ import { faker } from '@faker-js/faker'; describe('HookProcessorFactory', () => { let moduleRef: ModuleRef; - let factory: HookProcessorFactory; + let factory: HookFactory; let emailProcessor: EmailProcessor; let dataCordProcessor: DataCordProcessor; let webhookProcessor: WebhookProcessor; @@ -48,7 +48,7 @@ describe('HookProcessorFactory', () => { useValue: {}, }, { - provide: HookProcessorFactory, + provide: HookFactory, useValue: { getProcessor: jest.fn(), }, @@ -84,7 +84,7 @@ describe('HookProcessorFactory', () => { emailProcessor = module.get(EmailProcessor); webhookProcessor = module.get(WebhookProcessor); dataCordProcessor = module.get(DataCordProcessor); - factory = new HookProcessorFactory(moduleRef); + factory = new HookFactory(moduleRef); }); it('should return an instance of EmailProcessor for HookType.email', () => { diff --git a/apps/api/src/hook/hook.factory.ts b/apps/api/src/hook/hook.factory.ts index 7859820..db5a859 100644 --- a/apps/api/src/hook/hook.factory.ts +++ b/apps/api/src/hook/hook.factory.ts @@ -1,15 +1,21 @@ import { Injectable, Type } from '@nestjs/common'; import { DataCordProcessor } from './processors/datacord.processor'; +import { DataCordTemplating } from './templater/datacord.hook.templating'; import { EmailProcessor } from './processors/email.processor'; import { HookInterface } from './models/hook.interface'; +import { HookTemplatingInterface } from './templater/hook.templating'; import { HookType } from '../models'; import { ModuleRef } from '@nestjs/core'; import { WebhookProcessor } from './processors/webhook.processor'; +import { WebhookTemplating } from './templater/webhook.hook.templating'; @Injectable() -export class HookProcessorFactory { +export class HookFactory { private readonly processorMap: { [key in HookType]: Type }; + private readonly hookTemplatingMap: { + [key in HookType]: Type; + }; constructor(private moduleRef: ModuleRef) { this.processorMap = { @@ -17,6 +23,12 @@ export class HookProcessorFactory { [HookType.webhook]: WebhookProcessor, [HookType.datacord]: DataCordProcessor, }; + + this.hookTemplatingMap = { + [HookType.email]: WebhookTemplating, + [HookType.webhook]: WebhookTemplating, + [HookType.datacord]: DataCordTemplating, + }; } getProcessor(hookType: HookType): HookInterface { @@ -26,4 +38,12 @@ export class HookProcessorFactory { } return this.moduleRef.get(processorClass, { strict: false }); } + + getTemplating(hookType: HookType): HookTemplatingInterface { + const templatingClass = this.hookTemplatingMap[hookType]; + if (!templatingClass) { + throw new Error(`Unsupported hook type: ${hookType}`); + } + return this.moduleRef.get(templatingClass, { strict: false }); + } } diff --git a/apps/api/src/hook/hook.module.ts b/apps/api/src/hook/hook.module.ts index 4b28575..d7764d3 100644 --- a/apps/api/src/hook/hook.module.ts +++ b/apps/api/src/hook/hook.module.ts @@ -1,10 +1,13 @@ import { ConfigModule } from '@nestjs/config'; import { DataCordProcessor } from './processors/datacord.processor'; +import { DataCordTemplating } from './templater/datacord.hook.templating'; import { EmailModule } from '../email/email.module'; import { EmailProcessor } from './processors/email.processor'; -import { HookProcessorFactory } from './hook.factory'; +import { EmailTemplating } from './templater/email.hook.templating'; +import { HookFactory } from './hook.factory'; import { Module } from '@nestjs/common'; import { WebhookProcessor } from './processors/webhook.processor'; +import { WebhookTemplating } from './templater/webhook.hook.templating'; @Module({ imports: [EmailModule, ConfigModule], @@ -12,8 +15,11 @@ import { WebhookProcessor } from './processors/webhook.processor'; EmailProcessor, WebhookProcessor, DataCordProcessor, - HookProcessorFactory, + WebhookTemplating, + DataCordTemplating, + EmailTemplating, + HookFactory, ], - exports: [HookProcessorFactory], + exports: [HookFactory], }) export class HookModule {} diff --git a/apps/api/src/hook/processors/webhook.processor.spec.ts b/apps/api/src/hook/processors/webhook.processor.spec.ts index c9bb9b5..3286346 100644 --- a/apps/api/src/hook/processors/webhook.processor.spec.ts +++ b/apps/api/src/hook/processors/webhook.processor.spec.ts @@ -25,7 +25,9 @@ describe('WebHookProcessor', () => { const mockResponse = { data: faker.string.alpha(), status: 200, - headers: ['content-type: application/json'], + headers: { + 'content-type': 'application/json', + }, }; mockAxios.mockResolvedValueOnce(mockResponse); const webHookRequest = MockFactory(WebHookRequestFixture).one(); @@ -93,7 +95,9 @@ describe('WebHookProcessor', () => { }, }, status: 200, - headers: ['content-type: application/json'], + headers: { + 'content-type': 'application/json', + }, }; mockAxios.mockResolvedValueOnce(mockResponse); @@ -126,7 +130,9 @@ describe('WebHookProcessor', () => { const webHookRequest = { url: 'http://example.com/api/data', method: WebhookHttpMethod.GET, - headers: ['Accept: application/json'], + headers: { + 'content-type': 'application/json', + }, values: {}, success_path: Constants.prefixPattern + 'someProperty.anotherProperty.targetProperty', diff --git a/apps/api/src/hook/processors/webhook.processor.ts b/apps/api/src/hook/processors/webhook.processor.ts index 5c2bc3a..ae38e7e 100644 --- a/apps/api/src/hook/processors/webhook.processor.ts +++ b/apps/api/src/hook/processors/webhook.processor.ts @@ -16,21 +16,7 @@ export class WebhookProcessor implements HookInterface { async sendWebhookAsync(params: WebhookParams): Promise { try { - const { - url, - headers: headerStrings, - method, - values, - success_path, - } = params; - const headers = {}; - - for (const header of headerStrings) { - const index = header.indexOf(':'); - const key = header.substring(0, index).trim(); - const value = header.substring(index + 1).trim(); - headers[key] = value; - } + const { url, headers, method, values, success_path } = params; const requestData = {}; for (const key in values) { diff --git a/apps/api/src/hook/templater/datacord.hook.templating.ts b/apps/api/src/hook/templater/datacord.hook.templating.ts new file mode 100644 index 0000000..a27c776 --- /dev/null +++ b/apps/api/src/hook/templater/datacord.hook.templating.ts @@ -0,0 +1,15 @@ +import { DataCordParams } from '../models'; +import { HookTemplating } from './hook.templating'; + +export class DataCordTemplating extends HookTemplating { + //override applyToTemplate in abstract base class + applyTemplate( + params: DataCordParams, + templateKeyPairs: { [key: string]: string }, + ): void { + params.values = this.transformTemplateValues( + params.values, + templateKeyPairs, + ); + } +} diff --git a/apps/api/src/hook/templater/email.hook.templating.ts b/apps/api/src/hook/templater/email.hook.templating.ts new file mode 100644 index 0000000..312ece7 --- /dev/null +++ b/apps/api/src/hook/templater/email.hook.templating.ts @@ -0,0 +1,11 @@ +import { HookTemplating } from './hook.templating'; +import { WebhookParams } from '../../models'; + +export class EmailTemplating extends HookTemplating { + applyTemplate( + params: WebhookParams, + templateKeyPairs: { [key: string]: string }, + ): void { + console.log('EmailTemplating', params, templateKeyPairs); + } +} diff --git a/apps/api/src/hook/templater/hook.templating.ts b/apps/api/src/hook/templater/hook.templating.ts new file mode 100644 index 0000000..6dae94b --- /dev/null +++ b/apps/api/src/hook/templater/hook.templating.ts @@ -0,0 +1,52 @@ +import { Constants, HookParams } from '../../models'; + +export interface HookTemplatingInterface { + applyTemplate( + params: HookParams, + templateKeyPairs: { [key: string]: string }, + ): void; +} + +export abstract class HookTemplating implements HookTemplatingInterface { + abstract applyTemplate( + params: any, + templateKeyPairs: { [key: string]: string }, + ): void; + + transformTemplateValues = ( + values: { [key: string]: string }, + output: { [key: string]: string }, + ): { [key: string]: string } => { + const transformedValues: { [key: string]: string } = {}; + const valuesCopy = JSON.parse(JSON.stringify(values)) as { + [key: string]: string; + }; + const regexPattern = `\\{(?:\\${Constants.prefixPattern})?fields\\.([^}]+)\\}`; + const fieldPlaceholderPattern = new RegExp(regexPattern); + const globalFieldPlaceholderPattern = new RegExp(regexPattern, 'g'); + + Object.entries(valuesCopy).forEach(([key, value]) => { + const originalValue = value; + const matches = originalValue.match(globalFieldPlaceholderPattern); + + if (matches) { + let transformedValue = originalValue; + matches.forEach((match) => { + const fieldPattern = fieldPlaceholderPattern; + const field = match + .replace(fieldPattern, '$1') + .replace('.value', '') + .replace(Constants.encryptSuffix, ''); + transformedValue = transformedValue.replace( + match, + output[field] || output[field + Constants.encryptSuffix] || '', + ); + }); + transformedValues[key] = transformedValue; + } else { + transformedValues[key] = value; + } + }); + return transformedValues; + }; +} diff --git a/apps/api/src/hook/templater/webhook.hook.templating.ts b/apps/api/src/hook/templater/webhook.hook.templating.ts new file mode 100644 index 0000000..64a1290 --- /dev/null +++ b/apps/api/src/hook/templater/webhook.hook.templating.ts @@ -0,0 +1,18 @@ +import { HookTemplating } from './hook.templating'; +import { WebhookParams } from '../../models'; + +export class WebhookTemplating extends HookTemplating { + applyTemplate( + params: WebhookParams, + templateKeyPairs: { [key: string]: string }, + ): void { + params.values = this.transformTemplateValues( + params.values, + templateKeyPairs, + ); + params.headers = this.transformTemplateValues( + params.headers, + templateKeyPairs, + ); + } +} diff --git a/apps/api/src/manifest/manifest.controller.spec.ts b/apps/api/src/manifest/manifest.controller.spec.ts index cd91f36..524d4f4 100644 --- a/apps/api/src/manifest/manifest.controller.spec.ts +++ b/apps/api/src/manifest/manifest.controller.spec.ts @@ -7,7 +7,7 @@ import Ajv from 'ajv'; import { BullModule } from '@nestjs/bull'; import { ConfigService } from '@nestjs/config'; import { EmailProcessor } from '../hook/processors/email.processor'; -import { HookProcessorFactory } from '../hook/hook.factory'; +import { HookFactory } from '../hook/hook.factory'; import { HttpService } from '@nestjs/axios'; import { HttpStatus } from '@nestjs/common'; import { ManifestController } from './manifest.controller'; @@ -94,7 +94,7 @@ describe('ManifestController', () => { ManifestService, { provide: 'Ajv', useValue: new Ajv({ allErrors: true }) }, { - provide: HookProcessorFactory, + provide: HookFactory, useValue: { getProcessor: jest.fn(), }, diff --git a/apps/api/src/manifest/manifest.service.spec.ts b/apps/api/src/manifest/manifest.service.spec.ts index 8156aa3..a7712fa 100644 --- a/apps/api/src/manifest/manifest.service.spec.ts +++ b/apps/api/src/manifest/manifest.service.spec.ts @@ -15,7 +15,7 @@ import { decrypt, generateHmac, verifyHmac } from '../utils/crypto-helpers'; import Ajv from 'ajv'; import { AxiosResponse } from 'axios'; import { EmailProcessor } from '../hook/processors/email.processor'; -import { HookProcessorFactory } from './../hook/hook.factory'; +import { HookFactory } from './../hook/hook.factory'; import { HttpService } from '@nestjs/axios'; import { ManifestService } from './manifest.service'; import { MockFactory } from 'mockingbird'; @@ -36,7 +36,7 @@ describe('ManifestService', () => { 'd01858dd2f86ab1d3a7c4a152e6b3755a9eff744999b3a07c17fb9cbb363154e'; let manifestService: ManifestService; let httpService: HttpService; - let hookProcessorFactory: HookProcessorFactory; + let hookProcessorFactory: HookFactory; let webHookProcessor: WebhookProcessor; const queueMock = { add: jest.fn() }; @@ -56,7 +56,7 @@ describe('ManifestService', () => { }, }, { - provide: HookProcessorFactory, + provide: HookFactory, useValue: { getProcessor: jest.fn(), }, @@ -83,7 +83,7 @@ describe('ManifestService', () => { manifestService = module.get(ManifestService); httpService = module.get(HttpService); hookProcessorFactory = - module.get(HookProcessorFactory); + module.get(HookFactory); webHookProcessor = module.get(WebhookProcessor); }); diff --git a/apps/api/src/manifest/manifest.service.ts b/apps/api/src/manifest/manifest.service.ts index 3dd534e..3a5fe21 100644 --- a/apps/api/src/manifest/manifest.service.ts +++ b/apps/api/src/manifest/manifest.service.ts @@ -10,7 +10,6 @@ import { Hook, HookType, TildaManifest, - WebhookParams, } from '../models'; import { GetManifestError } from './errors/manifest.error'; import { @@ -23,9 +22,8 @@ import TildaManifestSchema from './manifest.schema'; import Ajv from 'ajv'; import { InjectQueue } from '@nestjs/bull'; import { Queue } from 'bull'; -import { HookProcessorFactory } from '../hook/hook.factory'; +import { HookFactory } from '../hook/hook.factory'; import { PreHookResponse, ManifestRequest } from './models'; -import { DataCordParams } from '../hook/models'; @Injectable() export class ManifestService { @@ -34,7 +32,7 @@ export class ManifestService { @Inject('Ajv') private readonly ajv: Ajv, @InjectQueue('hook-queue') private readonly hookQueue: Queue, - private readonly hookFactory: HookProcessorFactory, + private readonly hookFactory: HookFactory, ) {} async handlePostHooks(hooks: Hook[]): Promise { @@ -259,43 +257,6 @@ export class ManifestService { return combinedPayload; }; - transformTemplateValues = ( - values: { [key: string]: string }, - output: { [key: string]: string }, - ): { [key: string]: string } => { - const transformedValues: { [key: string]: string } = {}; - const valuesCopy = JSON.parse(JSON.stringify(values)) as { - [key: string]: string; - }; - const regexPattern = `\\{(?:\\${Constants.prefixPattern})?fields\\.([^}]+)\\}`; - const fieldPlaceholderPattern = new RegExp(regexPattern); - const globalFieldPlaceholderPattern = new RegExp(regexPattern, 'g'); - - Object.entries(valuesCopy).forEach(([key, value]) => { - const originalValue = value; - const matches = originalValue.match(globalFieldPlaceholderPattern); - - if (matches) { - let transformedValue = originalValue; - matches.forEach((match) => { - const fieldPattern = fieldPlaceholderPattern; - const field = match - .replace(fieldPattern, '$1') - .replace('.value', '') - .replace(Constants.encryptSuffix, ''); - transformedValue = transformedValue.replace( - match, - output[field] || output[field + Constants.encryptSuffix] || '', - ); - }); - transformedValues[key] = transformedValue; - } else { - transformedValues[key] = value; - } - }); - return transformedValues; - }; - applyTemplateToHooks = ( manifest: TildaManifest, payload: { [key: string]: string }, @@ -304,24 +265,9 @@ export class ManifestService { const updateHookParameters = (hooks: Hook[]): void => { hooks.forEach((hook) => { - if (hook.factory === HookType.datacord) { - const params = hook.params as DataCordParams; - params.values = this.transformTemplateValues( - params.values, - templateValues, - ); - } - if (hook.factory === HookType.webhook) { - const params = hook.params as WebhookParams; - params.values = this.transformTemplateValues( - params.values, - templateValues, - ); - params.headers = this.transformTemplateValues( - params.headers, - templateValues, - ); - } + this.hookFactory + .getTemplating(hook.factory) + .applyTemplate(hook.params, templateValues); }); }; diff --git a/apps/api/src/queue/hook.queue.spec.ts b/apps/api/src/queue/hook.queue.spec.ts index 90d1d60..56a6418 100644 --- a/apps/api/src/queue/hook.queue.spec.ts +++ b/apps/api/src/queue/hook.queue.spec.ts @@ -3,7 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { EmailProcessor } from '../hook/processors/email.processor'; import { EmailRequest } from '../hook/models'; -import { HookProcessorFactory } from '../hook/hook.factory'; +import { HookFactory } from '../hook/hook.factory'; import { HookQueue } from './hook.queue'; import { MockFactory } from 'mockingbird'; import { WebHookRequestFixture } from '../../test/fixtures'; @@ -12,7 +12,7 @@ import { faker } from '@faker-js/faker'; describe('HookQueue', () => { let hookQueue: HookQueue; - let hookProcessorFactoryMock: HookProcessorFactory; + let hookProcessorFactoryMock: HookFactory; let emailProcessorMock: EmailProcessor; let webHookProcessorMock: WebhookProcessor; @@ -21,7 +21,7 @@ describe('HookQueue', () => { providers: [ HookQueue, { - provide: HookProcessorFactory, + provide: HookFactory, useValue: { getProcessor: jest.fn(), }, @@ -43,7 +43,7 @@ describe('HookQueue', () => { hookQueue = module.get(HookQueue); hookProcessorFactoryMock = - module.get(HookProcessorFactory); + module.get(HookFactory); emailProcessorMock = module.get(EmailProcessor); webHookProcessorMock = module.get(WebhookProcessor); }); diff --git a/apps/api/src/queue/hook.queue.ts b/apps/api/src/queue/hook.queue.ts index c47359d..53cf65d 100644 --- a/apps/api/src/queue/hook.queue.ts +++ b/apps/api/src/queue/hook.queue.ts @@ -6,11 +6,11 @@ import { } from '@nestjs/bull'; import { Hook } from '../models'; -import { HookProcessorFactory } from '../hook/hook.factory'; +import { HookFactory } from '../hook/hook.factory'; @Processor('hook-queue') export class HookQueue { - constructor(private readonly hookFactory: HookProcessorFactory) {} + constructor(private readonly hookFactory: HookFactory) {} @OnQueueError() OnQueueError(err: Error) {