diff --git a/packages/nocodb/src/app.module.ts b/packages/nocodb/src/app.module.ts index f44bd3c6ee..9af6d4919e 100644 --- a/packages/nocodb/src/app.module.ts +++ b/packages/nocodb/src/app.module.ts @@ -24,6 +24,7 @@ import type { MiddlewareConsumer, OnApplicationBootstrap, } from '@nestjs/common'; +import { HookHandlerService } from './services/hook-handler.service'; @Module({ imports: [ @@ -43,6 +44,7 @@ import type { LocalStrategy, AuthTokenStrategy, BaseViewStrategy, + HookHandlerService, ], }) export class AppModule implements OnApplicationBootstrap { diff --git a/packages/nocodb/src/db/BaseModelSqlv2.ts b/packages/nocodb/src/db/BaseModelSqlv2.ts index 846a95f9c1..807b451ab8 100644 --- a/packages/nocodb/src/db/BaseModelSqlv2.ts +++ b/packages/nocodb/src/db/BaseModelSqlv2.ts @@ -11,24 +11,17 @@ import { UITypes, ViewTypes, } from 'nocodb-sdk'; -import ejs from 'ejs'; import Validator from 'validator'; import { customAlphabet } from 'nanoid'; import DOMPurify from 'isomorphic-dompurify'; import { v4 as uuidv4 } from 'uuid'; import { NcError } from '../helpers/catchError'; import getAst from '../helpers/getAst'; -import NcPluginMgrv2 from '../helpers/NcPluginMgrv2'; -import { - _transformSubmittedFormDataForEmail, - invokeWebhook, -} from '../helpers/webhookHelpers'; + import { Audit, Column, Filter, - FormView, - Hook, Model, Project, Sort, @@ -40,7 +33,8 @@ import { COMPARISON_SUB_OPS, IS_WITHIN_COMPARISON_SUB_OPS, } from '../models/Filter'; -import formSubmissionEmailTemplate from '../utils/common/formSubmissionEmailTemplate'; +import Noco from '../Noco' +import { HANDLE_WEBHOOK } from '../services/hook-handler.service' import formulaQueryBuilderv2 from './formulav2/formulaQueryBuilderv2'; import genRollupSelectv2 from './genRollupSelectv2'; import conditionV2 from './conditionV2'; @@ -2496,6 +2490,18 @@ class BaseModelSqlv2 { } private async handleHooks(hookName, prevData, newData, req): Promise { + + Noco.eventEmitter.emit(HANDLE_WEBHOOK, { + hookName, + prevData, + newData, + user: req?.user, + viewId: this.viewId, + modelId: this.model.id, + tnPath: this.tnPath, + }) +/* + const view = await View.get(this.viewId); // handle form view data submission @@ -2585,7 +2591,7 @@ class BaseModelSqlv2 { } } catch (e) { console.log('hooks :: error', hookName, e); - } + }*/ } // @ts-ignore diff --git a/packages/nocodb/src/services/hook-handler.service.spec.ts b/packages/nocodb/src/services/hook-handler.service.spec.ts new file mode 100644 index 0000000000..9b79fd5af6 --- /dev/null +++ b/packages/nocodb/src/services/hook-handler.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HookHandlerService } from './hook-handler.service'; + +describe('HookHandlerService', () => { + let service: HookHandlerService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [HookHandlerService], + }).compile(); + + service = module.get(HookHandlerService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/packages/nocodb/src/services/hook-handler.service.ts b/packages/nocodb/src/services/hook-handler.service.ts new file mode 100644 index 0000000000..1b50a6c39d --- /dev/null +++ b/packages/nocodb/src/services/hook-handler.service.ts @@ -0,0 +1,143 @@ +import { Injectable } from '@nestjs/common'; +import { UITypes, ViewTypes } from 'nocodb-sdk'; +import ejs from 'ejs'; +import NcPluginMgrv2 from '../helpers/NcPluginMgrv2'; +import { + _transformSubmittedFormDataForEmail, + invokeWebhook, +} from '../helpers/webhookHelpers'; +import { FormView, Hook, Model, View } from '../models'; +import { IEventEmitter } from '../modules/event-emitter/event-emitter.interface'; +import formSubmissionEmailTemplate from '../utils/common/formSubmissionEmailTemplate'; +import type { OnModuleDestroy, OnModuleInit } from '@nestjs/common'; +import type { UserType } from 'nocodb-sdk'; + +export const HANDLE_WEBHOOK = '__nc_handleHooks'; + +@Injectable() +export class HookHandlerService implements OnModuleInit, OnModuleDestroy { + private unsubscribe: () => void; + + constructor(private readonly eventEmitter: IEventEmitter) {} + + private async handleHooks({ + hookName, + prevData, + newData, + user, + viewId, + modelId, + tnPath, + }: { + hookName; + prevData; + newData; + user: UserType; + viewId: string; + modelId: string; + tnPath: string; + }): Promise { + const view = await View.get(viewId); + const model = await Model.get(modelId); + + // handle form view data submission + if ( + (hookName === 'after.insert' || hookName === 'after.bulkInsert') && + view.type === ViewTypes.FORM + ) { + try { + const formView = await view.getView(); + const { columns } = await FormView.getWithInfo(formView.fk_view_id); + const allColumns = await model.getColumns(); + const fieldById = columns.reduce( + (o: Record, f: Record) => ({ + ...o, + [f.fk_column_id]: f, + }), + {}, + ); + let order = 1; + const filteredColumns = allColumns + ?.map((c: Record) => ({ + ...c, + fk_column_id: c.id, + fk_view_id: formView.fk_view_id, + ...(fieldById[c.id] ? fieldById[c.id] : {}), + order: (fieldById[c.id] && fieldById[c.id].order) || order++, + id: fieldById[c.id] && fieldById[c.id].id, + })) + .sort( + (a: Record, b: Record) => + a.order - b.order, + ) + .filter( + (f: Record) => + f.show && + f.uidt !== UITypes.Rollup && + f.uidt !== UITypes.Lookup && + f.uidt !== UITypes.Formula && + f.uidt !== UITypes.QrCode && + f.uidt !== UITypes.Barcode && + f.uidt !== UITypes.SpecificDBType, + ) + .sort( + (a: Record, b: Record) => + a.order - b.order, + ) + .map((c: Record) => ({ + ...c, + required: !!(c.required || 0), + })); + + const emails = Object.entries(JSON.parse(formView?.email) || {}) + .filter((a) => a[1]) + .map((a) => a[0]); + if (emails?.length) { + const transformedData = _transformSubmittedFormDataForEmail( + newData, + formView, + filteredColumns, + ); + (await NcPluginMgrv2.emailAdapter(false))?.mailSend({ + to: emails.join(','), + subject: 'NocoDB Form', + html: ejs.render(formSubmissionEmailTemplate, { + data: transformedData, + tn: tnPath, + _tn: model.title, + }), + }); + } + } catch (e) { + console.log(e); + } + } + + try { + const [event, operation] = hookName.split('.'); + const hooks = await Hook.list({ + fk_model_id: modelId, + event, + operation, + }); + for (const hook of hooks) { + if (hook.active) { + invokeWebhook(hook, model, view, prevData, newData, user); + } + } + } catch (e) { + console.log('hooks :: error', hookName, e); + } + } + + onModuleInit(): any { + this.unsubscribe = this.eventEmitter.on( + HANDLE_WEBHOOK, + this.handleHooks.bind(this), + ); + } + + onModuleDestroy() { + this.unsubscribe?.(); + } +}