From 0b9944517c2cce989b8be979ba6537e1c02c6e09 Mon Sep 17 00:00:00 2001 From: Dima Grossman <dima@grossman.io> Date: Wed, 12 Jun 2024 12:04:43 +0300 Subject: [PATCH] refactor(worker): Local instance selection handlebars (#5622) * feat: add i18n instance * feat: add workers * feat: instance selection * feat: add compilation * fix: update source file --- .idea/.name | 1 - .../runConfigurations/APPLICATION_GENERIC.xml | 2 +- .idea/runConfigurations/EE_AUTH.xml | 12 + .idea/runConfigurations/RUN_LOCAL_ENV.xml | 3 +- .idea/runConfigurations/SHARED_WEB.xml | 12 + .vscode/launch.json | 5 +- .../content-templates.controller.ts | 6 +- .../inbound-email-parse.usecase.ts | 10 +- .../send-message/send-message-chat.usecase.ts | 17 +- .../send-message-email.usecase.ts | 2 +- .../send-message/send-message-push.usecase.ts | 30 +- .../send-message/send-message-sms.usecase.ts | 17 +- .../send-message/send-message.base.ts | 5 +- .../compile-email-template.usecase.ts | 86 +++-- .../templates/basic.handlebars | 1 + .../compile-in-app-template.usecase.ts | 38 +- .../compile-step-template.usecase.ts | 35 +- .../compile-template.command.ts | 2 + .../compile-template/compile-template.spec.ts | 243 ++++++------ .../compile-template.usecase.ts | 359 +++++++++--------- .../conditions-filter.usecase.ts | 14 +- libs/application-generic/tsconfig.json | 1 + libs/application-generic/tsconfig.module.json | 1 + 23 files changed, 481 insertions(+), 421 deletions(-) delete mode 100644 .idea/.name create mode 100644 .idea/runConfigurations/EE_AUTH.xml create mode 100644 .idea/runConfigurations/SHARED_WEB.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 2ff8622f172..00000000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -package.json \ No newline at end of file diff --git a/.idea/runConfigurations/APPLICATION_GENERIC.xml b/.idea/runConfigurations/APPLICATION_GENERIC.xml index f2f2dc3a290..16afa4bc0f1 100644 --- a/.idea/runConfigurations/APPLICATION_GENERIC.xml +++ b/.idea/runConfigurations/APPLICATION_GENERIC.xml @@ -1,6 +1,6 @@ <component name="ProjectRunConfigurationManager"> <configuration default="false" name="APPLICATION GENERIC" type="js.build_tools.npm"> - <package-json value="$PROJECT_DIR$/packages/application-generic/package.json" /> + <package-json value="$PROJECT_DIR$/libs/application-generic/package.json" /> <command value="run" /> <scripts> <script value="watch:build" /> diff --git a/.idea/runConfigurations/EE_AUTH.xml b/.idea/runConfigurations/EE_AUTH.xml new file mode 100644 index 00000000000..cd00c3a5a85 --- /dev/null +++ b/.idea/runConfigurations/EE_AUTH.xml @@ -0,0 +1,12 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="EE-AUTH" type="js.build_tools.npm"> + <package-json value="$PROJECT_DIR$/enterprise/packages/auth/package.json" /> + <command value="run" /> + <scripts> + <script value="build:watch" /> + </scripts> + <node-interpreter value="project" /> + <envs /> + <method v="2" /> + </configuration> +</component> \ No newline at end of file diff --git a/.idea/runConfigurations/RUN_LOCAL_ENV.xml b/.idea/runConfigurations/RUN_LOCAL_ENV.xml index bfe0f90e5d4..f61c18fb646 100644 --- a/.idea/runConfigurations/RUN_LOCAL_ENV.xml +++ b/.idea/runConfigurations/RUN_LOCAL_ENV.xml @@ -3,12 +3,13 @@ <toRun name="API" type="js.build_tools.npm" /> <toRun name="APPLICATION GENERIC" type="js.build_tools.npm" /> <toRun name="DAL" type="js.build_tools.npm" /> + <toRun name="EE-AUTH" type="js.build_tools.npm" /> <toRun name="SHARED" type="js.build_tools.npm" /> + <toRun name="SHARED-WEB" type="js.build_tools.npm" /> <toRun name="TESTING" type="js.build_tools.npm" /> <toRun name="WEB" type="js.build_tools.npm" /> <toRun name="WORKER" type="js.build_tools.npm" /> <toRun name="WS" type="js.build_tools.npm" /> - <toRun name="auth > build:watch" type="js.build_tools.npm" /> <method v="2" /> </configuration> </component> \ No newline at end of file diff --git a/.idea/runConfigurations/SHARED_WEB.xml b/.idea/runConfigurations/SHARED_WEB.xml new file mode 100644 index 00000000000..21004381511 --- /dev/null +++ b/.idea/runConfigurations/SHARED_WEB.xml @@ -0,0 +1,12 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="SHARED-WEB" type="js.build_tools.npm"> + <package-json value="$PROJECT_DIR$/libs/shared-web/package.json" /> + <command value="run" /> + <scripts> + <script value="build:watch" /> + </scripts> + <node-interpreter value="project" /> + <envs /> + <method v="2" /> + </configuration> +</component> \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 62c8e896691..0df58e43d86 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -230,7 +230,7 @@ "compounds": [ { "name": "-- RUN FULL ENV - Local", - "configurations": ["API", "DAL", "EMBED", "SHARED", "TESTING LIB", "WS", "WEB", "WIDGET"] + "configurations": ["API", "DAL", "EMBED", "SHARED", "TESTING LIB", "WS", "WEB", "WIDGET", "worker"] }, { "name": "-- RUN FULL ENV - Test", @@ -242,7 +242,8 @@ "TESTING LIB", "WS - TEST ENV", "WEB", - "WIDGET - test" + "WIDGET - test", + "worker" ] } ] diff --git a/apps/api/src/app/content-templates/content-templates.controller.ts b/apps/api/src/app/content-templates/content-templates.controller.ts index 3d4c4918f79..b4ee4b2d5de 100644 --- a/apps/api/src/app/content-templates/content-templates.controller.ts +++ b/apps/api/src/app/content-templates/content-templates.controller.ts @@ -148,8 +148,8 @@ export class ContentTemplatesController { environmentId, organizationId ); - - await i18next.init({ + const instance = i18next.createInstance(); + await instance.init({ resources, ns: namespaces, defaultNS: false, @@ -168,6 +168,8 @@ export class ContentTemplatesController { }, }, }); + + return instance; } } catch (e) { Logger.error(e, `Unexpected error while importing enterprise modules`, 'TranslationsService'); diff --git a/apps/worker/src/app/workflow/usecases/inbound-email-parse/inbound-email-parse.usecase.ts b/apps/worker/src/app/workflow/usecases/inbound-email-parse/inbound-email-parse.usecase.ts index 438405bf737..7ed26951ff9 100644 --- a/apps/worker/src/app/workflow/usecases/inbound-email-parse/inbound-email-parse.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/inbound-email-parse/inbound-email-parse.usecase.ts @@ -44,12 +44,10 @@ export class InboundEmailParse { ); } - const compiledDomain = await this.compileTemplate.execute( - CompileTemplateCommand.create({ - template: currentParseWebhook as string, - data: job.payload, - }) - ); + const compiledDomain = await this.compileTemplate.execute({ + template: currentParseWebhook as string, + data: job.payload, + }); const userPayload: IUserWebhookPayload = { hmac: createHash(environment?.apiKeys[0]?.key, subscriber.subscriberId), diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message-chat.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message-chat.usecase.ts index 630de444054..8d84301440e 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/send-message-chat.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/send-message-chat.usecase.ts @@ -80,7 +80,11 @@ export class SendMessageChat extends SendMessageBase { if (!step?.template) throw new PlatformException('Chat channel template not found'); const { subscriber } = command.compileContext; - await this.initiateTranslations(command.environmentId, command.organizationId, subscriber.locale); + const i18nextInstance = await this.initiateTranslations( + command.environmentId, + command.organizationId, + subscriber.locale + ); const template = await this.processVariants(command); @@ -92,12 +96,11 @@ export class SendMessageChat extends SendMessageBase { try { if (!command.chimeraData) { - content = await this.compileTemplate.execute( - CompileTemplateCommand.create({ - template: step.template.content as string, - data: this.getCompilePayload(command.compileContext), - }) - ); + content = await this.compileTemplate.execute({ + template: step.template.content as string, + data: this.getCompilePayload(command.compileContext), + i18next: i18nextInstance, + }); } } catch (e) { await this.sendErrorHandlebars(command.job, e.message); diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message-email.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message-email.usecase.ts index a10b9972641..96b27a941ea 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/send-message-email.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/send-message-email.usecase.ts @@ -235,7 +235,7 @@ export class SendMessageEmail extends SendMessageBase { }); } } catch (e) { - Logger.error({ payload }, 'Compiling the email template or storing it or inlining it has failed', LOG_CONTEXT); + Logger.error({ payload, e }, 'Compiling the email template or storing it or inlining it has failed', LOG_CONTEXT); await this.sendErrorHandlebars(command.job, e.message); return; diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message-push.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message-push.usecase.ts index 136033dc094..978b1717a2f 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/send-message-push.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/send-message-push.usecase.ts @@ -80,7 +80,11 @@ export class SendMessagePush extends SendMessageBase { const { subscriber, step: stepData } = command.compileContext; const template = await this.processVariants(command); - await this.initiateTranslations(command.environmentId, command.organizationId, subscriber.locale); + const i18nInstance = await this.initiateTranslations( + command.environmentId, + command.organizationId, + subscriber.locale + ); if (template) { step.template = template; @@ -92,19 +96,17 @@ export class SendMessagePush extends SendMessageBase { try { if (!command.chimeraData) { - content = await this.compileTemplate.execute( - CompileTemplateCommand.create({ - template: step.template?.content as string, - data, - }) - ); - - title = await this.compileTemplate.execute( - CompileTemplateCommand.create({ - template: step.template?.title as string, - data, - }) - ); + content = await this.compileTemplate.execute({ + template: step.template?.content as string, + data, + i18next: i18nInstance, + }); + + title = await this.compileTemplate.execute({ + template: step.template?.title as string, + data, + i18next: i18nInstance, + }); } } catch (e) { await this.sendErrorHandlebars(command.job, e.message); diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts index 5569cd48455..c763f53d595 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts @@ -80,7 +80,11 @@ export class SendMessageSms extends SendMessageBase { const { subscriber } = command.compileContext; const template = await this.processVariants(command); - await this.initiateTranslations(command.environmentId, command.organizationId, subscriber.locale); + const i18nextInstance = await this.initiateTranslations( + command.environmentId, + command.organizationId, + subscriber.locale + ); if (template) { step.template = template; @@ -91,12 +95,11 @@ export class SendMessageSms extends SendMessageBase { try { if (!command.chimeraData) { - content = await this.compileTemplate.execute( - CompileTemplateCommand.create({ - template: step.template.content as string, - data: this.getCompilePayload(command.compileContext), - }) - ); + content = await this.compileTemplate.execute({ + template: step.template.content as string, + data: this.getCompilePayload(command.compileContext), + i18next: i18nextInstance, + }); if (!content) { throw new PlatformException(`Unexpected error: SMS content is missing`); diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message.base.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message.base.ts index 77771022702..0e1bdaa3d96 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/send-message.base.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/send-message.base.ts @@ -149,7 +149,8 @@ export abstract class SendMessageBase extends SendMessageType { organizationId ); - await i18next.init({ + const instance = i18next.createInstance(); + await instance.init({ resources, ns: namespaces, defaultNS: false, @@ -168,6 +169,8 @@ export abstract class SendMessageBase extends SendMessageType { }, }, }); + + return instance; } } catch (e) { Logger.error(e, `Unexpected error while importing enterprise modules`, 'TranslationsService'); diff --git a/libs/application-generic/src/usecases/compile-email-template/compile-email-template.usecase.ts b/libs/application-generic/src/usecases/compile-email-template/compile-email-template.usecase.ts index 080a21d0083..d764c815695 100644 --- a/libs/application-generic/src/usecases/compile-email-template/compile-email-template.usecase.ts +++ b/libs/application-generic/src/usecases/compile-email-template/compile-email-template.usecase.ts @@ -39,8 +39,9 @@ export class CompileEmailTemplate extends CompileTemplateBase { const verifyPayloadService = new VerifyPayloadService(); const organization = await this.getOrganization(command.organizationId); + let i18nInstance; if (initiateTranslations) { - await initiateTranslations( + i18nInstance = await initiateTranslations( command.environmentId, command.organizationId, command.locale || @@ -97,17 +98,26 @@ export class CompileEmailTemplate extends CompileTemplateBase { }; try { - subject = await this.renderContent(command.subject, payload); + subject = await this.renderContent( + command.subject, + payload, + i18nInstance + ); if (preheader) { - preheader = await this.renderContent(preheader, payload); + preheader = await this.renderContent(preheader, payload, i18nInstance); } + if (command.senderName) { - senderName = await this.renderContent(command.senderName, payload); + senderName = await this.renderContent( + command.senderName, + payload, + i18nInstance + ); } } catch (e: any) { throw new ApiException( - e?.message || `Message content could not be generated` + e?.message || `Email subject message content could not be generated` ); } @@ -115,6 +125,21 @@ export class CompileEmailTemplate extends CompileTemplateBase { layoutContent as string ); + if (isEditorMode) { + for (const block of content as IEmailBlock[]) { + block.content = await this.renderContent( + block.content, + payload, + i18nInstance + ); + block.url = await this.renderContent( + block.url || '', + payload, + i18nInstance + ); + } + } + const templateVariables = { ...payload, subject, @@ -123,31 +148,22 @@ export class CompileEmailTemplate extends CompileTemplateBase { blocks: isEditorMode ? content : [], }; - if (isEditorMode) { - for (const block of content as IEmailBlock[]) { - block.content = await this.renderContent(block.content, payload); - block.url = await this.renderContent(block.url || '', payload); - } - } - - const body = await this.compileTemplate.execute( - CompileTemplateCommand.create({ - template: !isEditorMode - ? (content as string) - : (helperBlocksContent as string), - data: templateVariables, - }) - ); + const body = await this.compileTemplate.execute({ + i18next: i18nInstance, + template: !isEditorMode + ? (content as string) + : (helperBlocksContent as string), + data: templateVariables, + }); templateVariables.body = body as string; const html = customLayout - ? await this.compileTemplate.execute( - CompileTemplateCommand.create({ - template: customLayout, - data: templateVariables, - }) - ) + ? await this.compileTemplate.execute({ + i18next: i18nInstance, + template: customLayout, + data: templateVariables, + }) : body; return { html, content, subject, senderName }; @@ -155,16 +171,16 @@ export class CompileEmailTemplate extends CompileTemplateBase { private async renderContent( content: string, - payload: Record<string, unknown> + payload: Record<string, unknown>, + i18nInstance: any ) { - const renderedContent = await this.compileTemplate.execute( - CompileTemplateCommand.create({ - template: content, - data: { - ...payload, - }, - }) - ); + const renderedContent = await this.compileTemplate.execute({ + i18next: i18nInstance, + template: content, + data: { + ...payload, + }, + }); return renderedContent?.trim() || ''; } diff --git a/libs/application-generic/src/usecases/compile-email-template/templates/basic.handlebars b/libs/application-generic/src/usecases/compile-email-template/templates/basic.handlebars index d58c4a9de14..5a01a5f7864 100644 --- a/libs/application-generic/src/usecases/compile-email-template/templates/basic.handlebars +++ b/libs/application-generic/src/usecases/compile-email-template/templates/basic.handlebars @@ -1,3 +1,4 @@ + {{#each blocks}} <div style="margin-bottom: 10px" data-test-id="block-item-wrapper"> {{#equals type 'text'}} diff --git a/libs/application-generic/src/usecases/compile-in-app-template/compile-in-app-template.usecase.ts b/libs/application-generic/src/usecases/compile-in-app-template/compile-in-app-template.usecase.ts index dd6a02b81bc..22b2584af95 100644 --- a/libs/application-generic/src/usecases/compile-in-app-template/compile-in-app-template.usecase.ts +++ b/libs/application-generic/src/usecases/compile-in-app-template/compile-in-app-template.usecase.ts @@ -31,8 +31,9 @@ export class CompileInAppTemplate extends CompileTemplateBase { ) { const organization = await this.getOrganization(command.organizationId); + let i18nInstance; if (initiateTranslations) { - await initiateTranslations( + i18nInstance = await initiateTranslations( command.environmentId, command.organizationId, command.locale || @@ -52,7 +53,8 @@ export class CompileInAppTemplate extends CompileTemplateBase { ? await this.compileInAppTemplate( command.content, payload, - organization + organization, + i18nInstance ) : ''; @@ -60,7 +62,8 @@ export class CompileInAppTemplate extends CompileTemplateBase { url = await this.compileInAppTemplate( command.cta?.data?.url, payload, - organization + organization, + i18nInstance ); } @@ -69,14 +72,15 @@ export class CompileInAppTemplate extends CompileTemplateBase { const buttonContent = await this.compileInAppTemplate( action.content, payload, - organization + organization, + i18nInstance ); ctaButtons.push({ type: action.type, content: buttonContent }); } } } catch (e: any) { throw new ApiException( - e?.message || `Message content could not be generated` + e?.message || `In-App Message content could not be generated` ); } @@ -86,19 +90,19 @@ export class CompileInAppTemplate extends CompileTemplateBase { private async compileInAppTemplate( content: string, payload: any, - organization: OrganizationEntity | null + organization: OrganizationEntity | null, + i18nInstance: any ): Promise<string> { - return await this.compileTemplate.execute( - CompileTemplateCommand.create({ - template: content as string, - data: { - ...payload, - branding: { - logo: organization?.branding?.logo, - color: organization?.branding?.color || '#f47373', - }, + return await this.compileTemplate.execute({ + i18next: i18nInstance, + template: content as string, + data: { + ...payload, + branding: { + logo: organization?.branding?.logo, + color: organization?.branding?.color || '#f47373', }, - }) - ); + }, + }); } } diff --git a/libs/application-generic/src/usecases/compile-step-template/compile-step-template.usecase.ts b/libs/application-generic/src/usecases/compile-step-template/compile-step-template.usecase.ts index c2a5bd3a253..e0b97177f19 100644 --- a/libs/application-generic/src/usecases/compile-step-template/compile-step-template.usecase.ts +++ b/libs/application-generic/src/usecases/compile-step-template/compile-step-template.usecase.ts @@ -30,8 +30,9 @@ export class CompileStepTemplate extends CompileTemplateBase { ) { const organization = await this.getOrganization(command.organizationId); + let i18nInstance; if (initiateTranslations) { - await initiateTranslations( + i18nInstance = await initiateTranslations( command.environmentId, command.organizationId, command.locale || @@ -47,14 +48,22 @@ export class CompileStepTemplate extends CompileTemplateBase { let title: string | undefined = undefined; try { - content = await this.compileStepTemplate(command.content, payload); + content = await this.compileStepTemplate( + command.content, + payload, + i18nInstance + ); if (command.title) { - title = await this.compileStepTemplate(command.title, payload); + title = await this.compileStepTemplate( + command.title, + payload, + i18nInstance + ); } } catch (e: any) { throw new ApiException( - e?.message || `Message content could not be generated` + e?.message || `Compile step content failed to generate` ); } @@ -63,15 +72,15 @@ export class CompileStepTemplate extends CompileTemplateBase { private async compileStepTemplate( content: string, - payload: any + payload: any, + i18nInstance?: any ): Promise<string> { - return await this.compileTemplate.execute( - CompileTemplateCommand.create({ - template: content as string, - data: { - ...payload, - }, - }) - ); + return await this.compileTemplate.execute({ + i18next: i18nInstance, + template: content as string, + data: { + ...payload, + }, + }); } } diff --git a/libs/application-generic/src/usecases/compile-template/compile-template.command.ts b/libs/application-generic/src/usecases/compile-template/compile-template.command.ts index 6fc7147263c..f84214dba4f 100644 --- a/libs/application-generic/src/usecases/compile-template/compile-template.command.ts +++ b/libs/application-generic/src/usecases/compile-template/compile-template.command.ts @@ -8,4 +8,6 @@ export class CompileTemplateCommand extends BaseCommand { @IsObject() data: any; // eslint-disable-line @typescript-eslint/no-explicit-any + + i18next?: any; // eslint-disable-line @typescript-eslint/no-explicit-any } diff --git a/libs/application-generic/src/usecases/compile-template/compile-template.spec.ts b/libs/application-generic/src/usecases/compile-template/compile-template.spec.ts index 9b2393168b2..9cca122c4a1 100644 --- a/libs/application-generic/src/usecases/compile-template/compile-template.spec.ts +++ b/libs/application-generic/src/usecases/compile-template/compile-template.spec.ts @@ -16,129 +16,116 @@ describe('Compile Template', function () { }); it('should render custom html', async function () { - const result = await useCase.execute( - CompileTemplateCommand.create({ - data: { - branding: { - color: '#e7e7e7e9', - }, - name: 'Test Name', + const result = await useCase.execute({ + data: { + branding: { + color: '#e7e7e7e9', }, - template: '<div>{{name}}</div>', - }) - ); + name: 'Test Name', + }, + template: '<div>{{name}}</div>', + }); expect(result).toEqual('<div>Test Name</div>'); }); it('should render pluralisation in html', async function () { - const result = await useCase.execute( - CompileTemplateCommand.create({ - data: { - branding: { - color: '#e7e7e7e9', - }, - dog_count: 1, - sausage_count: 2, + const result = await useCase.execute({ + data: { + branding: { + color: '#e7e7e7e9', }, - template: - '<div>{{dog_count}} {{pluralize dog_count "dog" "dogs"}} and {{sausage_count}} {{pluralize sausage_count "sausage" "sausages"}} for {{pluralize dog_count "him" "them"}}</div>', - }) - ); + dog_count: 1, + sausage_count: 2, + }, + template: + '<div>{{dog_count}} {{pluralize dog_count "dog" "dogs"}} and {{sausage_count}} {{pluralize sausage_count "sausage" "sausages"}} for {{pluralize dog_count "him" "them"}}</div>', + }); expect(result).toEqual('<div>1 dog and 2 sausages for him</div>'); }); it('should render unique values of array', async function () { - const result = await useCase.execute( - CompileTemplateCommand.create({ - data: { - names: [{ name: 'dog' }, { name: 'cat' }, { name: 'dog' }], - }, - template: - '<div>{{#each (unique names "name")}}{{this}}-{{/each}}</div>', - }) - ); + const result = await useCase.execute({ + data: { + names: [{ name: 'dog' }, { name: 'cat' }, { name: 'dog' }], + }, + template: '<div>{{#each (unique names "name")}}{{this}}-{{/each}}</div>', + }); expect(result).toEqual('<div>dog-cat-</div>'); }); it('should render groupBy values of array', async function () { - const result = await useCase.execute( - CompileTemplateCommand.create({ - data: { - names: [ - { - name: 'Name 1', - age: '30', - }, - { - name: 'Name 2', - age: '31', - }, - { - name: 'Name 1', - age: '32', - }, - ], - }, - template: - '{{#each (groupBy names "name")}}<h1>{{key}}</h1>{{#each items}}{{age}}-{{/each}}{{/each}}', - }) - ); + const result = await useCase.execute({ + data: { + names: [ + { + name: 'Name 1', + age: '30', + }, + { + name: 'Name 2', + age: '31', + }, + { + name: 'Name 1', + age: '32', + }, + ], + }, + template: + '{{#each (groupBy names "name")}}<h1>{{key}}</h1>{{#each items}}{{age}}-{{/each}}{{/each}}', + }); expect(result).toEqual('<h1>Name 1</h1>30-32-<h1>Name 2</h1>31-'); }); it('should render sortBy values of array', async function () { - const result = await useCase.execute( - CompileTemplateCommand.create({ - data: { - people: [ - { - name: 'a75', - item1: false, - item2: false, - id: 1, - updated_at: '2023-01-01T06:25:24Z', - }, - { - name: 'z32', - item1: true, - item2: false, - id: 3, - updated_at: '2023-01-09T11:25:13Z', - }, - { - name: 'e77', - item1: false, - item2: false, - id: 2, - updated_at: '2023-01-05T04:13:24Z', - }, - ], - }, - template: `{{#each (sortBy people 'updated_at')}}{{name}} - {{id}}{{/each}}`, - }) - ); + const result = await useCase.execute({ + data: { + people: [ + { + name: 'a75', + item1: false, + item2: false, + id: 1, + updated_at: '2023-01-01T06:25:24Z', + }, + { + name: 'z32', + item1: true, + item2: false, + id: 3, + updated_at: '2023-01-09T11:25:13Z', + }, + { + name: 'e77', + item1: false, + item2: false, + id: 2, + updated_at: '2023-01-05T04:13:24Z', + }, + ], + }, + template: `{{#each (sortBy people 'updated_at')}}{{name}} - {{id}}{{/each}}`, + }); expect(result).toEqual('a75 - 1e77 - 2z32 - 3'); }); it('should allow the user to specify handlebars helpers', async function () { - const result = await useCase.execute( - CompileTemplateCommand.create({ - data: { - branding: { - color: '#e7e7e7e9', - }, - message: 'hello world', - messageTwo: 'hEllo world', + const result = await useCase.execute({ + data: { + branding: { + color: '#e7e7e7e9', }, - template: - '<div>{{titlecase message}} and {{lowercase messageTwo}} and {{uppercase message}}</div>', - }) - ); + message: 'hello world', + messageTwo: 'hEllo world', + }, + template: + '<div>{{titlecase message}} and {{lowercase messageTwo}} and {{uppercase message}}</div>', + }); expect(result).toEqual( '<div>Hello World and hello world and HELLO WORLD</div>' @@ -146,65 +133,55 @@ describe('Compile Template', function () { }); it('should allow apostrophes to be in data', async function () { - const result = await useCase.execute( - CompileTemplateCommand.create({ - data: { - message: "hello' world", - }, - template: '<div>{{message}}</div>', - }) - ); + const result = await useCase.execute({ + data: { + message: "hello' world", + }, + template: '<div>{{message}}</div>', + }); expect(result).toEqual("<div>hello' world</div>"); }); describe('Date Formation', function () { it('should allow user to format the date', async function () { - const result = await useCase.execute( - CompileTemplateCommand.create({ - data: { - date: '2020-01-01', - }, - template: "<div>{{dateFormat date 'EEEE, MMMM Do yyyy'}}</div>", - }) - ); + const result = await useCase.execute({ + data: { + date: '2020-01-01', + }, + template: "<div>{{dateFormat date 'EEEE, MMMM Do yyyy'}}</div>", + }); expect(result).toEqual('<div>Wednesday, January 1st 2020</div>'); }); it('should not fail and return same date for invalid date', async function () { - const result = await useCase.execute( - CompileTemplateCommand.create({ - data: { - date: 'ABCD', - }, - template: "<div>{{dateFormat date 'EEEE, MMMM Do yyyy'}}</div>", - }) - ); + const result = await useCase.execute({ + data: { + date: 'ABCD', + }, + template: "<div>{{dateFormat date 'EEEE, MMMM Do yyyy'}}</div>", + }); expect(result).toEqual('<div>ABCD</div>'); }); }); describe('Number formating', () => { it('should format number', async () => { - const result = await useCase.execute( - CompileTemplateCommand.create({ - data: { number: 1000000000 }, - template: - '<div>{{numberFormat number decimalSep="," decimalLength="2" thousandsSep="|"}}</div>', - }) - ); + const result = await useCase.execute({ + data: { number: 1000000000 }, + template: + '<div>{{numberFormat number decimalSep="," decimalLength="2" thousandsSep="|"}}</div>', + }); expect(result).toEqual('<div>1|000|000|000,00</div>'); }); it('should not fail and return passed value', async () => { - const result = await useCase.execute( - CompileTemplateCommand.create({ - data: { number: 'Not a number' }, - template: - '<div>{{numberFormat number decimalSep="," decimalLength="2" thousandsSep="|"}}</div>', - }) - ); + const result = await useCase.execute({ + data: { number: 'Not a number' }, + template: + '<div>{{numberFormat number decimalSep="," decimalLength="2" thousandsSep="|"}}</div>', + }); expect(result).toEqual('<div>Not a number</div>'); }); diff --git a/libs/application-generic/src/usecases/compile-template/compile-template.usecase.ts b/libs/application-generic/src/usecases/compile-template/compile-template.usecase.ts index 61c452b5e13..7253a4a8184 100644 --- a/libs/application-generic/src/usecases/compile-template/compile-template.usecase.ts +++ b/libs/application-generic/src/usecases/compile-template/compile-template.usecase.ts @@ -4,7 +4,6 @@ import { format } from 'date-fns'; import { HandlebarHelpersEnum } from '@novu/shared'; import { CompileTemplateCommand } from './compile-template.command'; -import * as i18next from 'i18next'; import { ApiException } from '../../utils/exceptions'; const assertResult = (condition: boolean, options) => { @@ -13,212 +12,228 @@ const assertResult = (condition: boolean, options) => { return typeof fn === 'function' ? fn(this) : condition; }; -Handlebars.registerHelper( - HandlebarHelpersEnum.I18N, - function (key, { hash, data, fn }) { - const options = { - ...data.root.i18next, - ...hash, - returnObjects: false, - }; +function createHandlebarsInstance(i18next: any) { + const handlebars = Handlebars.create(); + + handlebars.registerHelper('json', function (context) { + return JSON.stringify(context); + }); + + if (i18next) { + handlebars.registerHelper( + HandlebarHelpersEnum.I18N, + function (key, { hash, data, fn }) { + const options = { + ...data.root.i18next, + ...hash, + returnObjects: false, + }; + + const replace = (options.replace = { + // eslint-disable-next-line + // @ts-ignore + ...this, + ...options.replace, + ...hash, + }); + delete replace.i18next; // may creep in if this === data.root + + if (fn) { + options.defaultValue = fn(replace); + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return new handlebars.SafeString(i18next.t(key, options)); + } + ); + } - const replace = (options.replace = { + handlebars.registerHelper( + HandlebarHelpersEnum.EQUALS, + function (arg1, arg2, options) { // eslint-disable-next-line - // @ts-ignore - ...this, - ...options.replace, - ...hash, - }); - delete replace.i18next; // may creep in if this === data.root - - if (fn) { - options.defaultValue = fn(replace); + // @ts-expect-error + return arg1 == arg2 ? options.fn(this) : options.inverse(this); } - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return new Handlebars.SafeString(i18next.t(key, options)); - } -); -Handlebars.registerHelper( - HandlebarHelpersEnum.EQUALS, - function (arg1, arg2, options) { - // eslint-disable-next-line - // @ts-expect-error - return arg1 == arg2 ? options.fn(this) : options.inverse(this); - } -); - -Handlebars.registerHelper(HandlebarHelpersEnum.TITLECASE, function (value) { - return value - ?.split(' ') - .map( - (letter) => letter.charAt(0).toUpperCase() + letter.slice(1).toLowerCase() - ) - .join(' '); -}); - -Handlebars.registerHelper(HandlebarHelpersEnum.UPPERCASE, function (value) { - return value?.toUpperCase(); -}); - -Handlebars.registerHelper(HandlebarHelpersEnum.LOWERCASE, function (value) { - return value?.toLowerCase(); -}); - -Handlebars.registerHelper( - HandlebarHelpersEnum.PLURALIZE, - function (number, single, plural) { - return number === 1 ? single : plural; - } -); - -Handlebars.registerHelper( - HandlebarHelpersEnum.DATEFORMAT, - function (date, dateFormat) { - // Format date if parameters are valid - if (date && dateFormat && !isNaN(Date.parse(date))) { - return format(new Date(date), dateFormat); + ); + + handlebars.registerHelper(HandlebarHelpersEnum.TITLECASE, function (value) { + return value + ?.split(' ') + .map( + (letter) => + letter.charAt(0).toUpperCase() + letter.slice(1).toLowerCase() + ) + .join(' '); + }); + + handlebars.registerHelper(HandlebarHelpersEnum.UPPERCASE, function (value) { + return value?.toUpperCase(); + }); + + handlebars.registerHelper(HandlebarHelpersEnum.LOWERCASE, function (value) { + return value?.toLowerCase(); + }); + + handlebars.registerHelper( + HandlebarHelpersEnum.PLURALIZE, + function (number, single, plural) { + return number === 1 ? single : plural; } - - return date; - } -); - -Handlebars.registerHelper( - HandlebarHelpersEnum.GROUP_BY, - function (array, property) { - if (!Array.isArray(array)) return []; - const map = {}; - array.forEach((item) => { - if (item[property]) { - const key = item[property]; - if (!map[key]) { - map[key] = [item]; - } else { - map[key].push(item); - } + ); + + handlebars.registerHelper( + HandlebarHelpersEnum.DATEFORMAT, + function (date, dateFormat) { + // Format date if parameters are valid + if (date && dateFormat && !isNaN(Date.parse(date))) { + return format(new Date(date), dateFormat); } - }); - const result = []; - for (const [key, value] of Object.entries(map)) { - result.push({ key: key, items: value }); + return date; } - - return result; - } -); - -Handlebars.registerHelper( - HandlebarHelpersEnum.UNIQUE, - function (array, property) { - if (!Array.isArray(array)) return ''; - - return array - .map((item) => { + ); + + handlebars.registerHelper( + HandlebarHelpersEnum.GROUP_BY, + function (array, property) { + if (!Array.isArray(array)) return []; + const map = {}; + array.forEach((item) => { if (item[property]) { - return item[property]; + const key = item[property]; + if (!map[key]) { + map[key] = [item]; + } else { + map[key].push(item); + } } - }) - .filter((value, index, self) => self.indexOf(value) === index); - } -); + }); -Handlebars.registerHelper( - HandlebarHelpersEnum.SORT_BY, - function (array, property) { - if (!Array.isArray(array)) return ''; - if (!property) return array.sort(); + const result = []; + for (const [key, value] of Object.entries(map)) { + result.push({ key: key, items: value }); + } - return array.sort(function (a, b) { - const _x = a[property]; - const _y = b[property]; + return result; + } + ); + + handlebars.registerHelper( + HandlebarHelpersEnum.UNIQUE, + function (array, property) { + if (!Array.isArray(array)) return ''; + + return array + .map((item) => { + if (item[property]) { + return item[property]; + } + }) + .filter((value, index, self) => self.indexOf(value) === index); + } + ); - return _x < _y ? -1 : _x > _y ? 1 : 0; - }); - } -); - -// based on: https://gist.github.com/DennyLoko/61882bc72176ca74a0f2 -Handlebars.registerHelper( - HandlebarHelpersEnum.NUMBERFORMAT, - function (number, options) { - if (isNaN(number)) { - return number; + handlebars.registerHelper( + HandlebarHelpersEnum.SORT_BY, + function (array, property) { + if (!Array.isArray(array)) return ''; + if (!property) return array.sort(); + + return array.sort(function (a, b) { + const _x = a[property]; + const _y = b[property]; + + return _x < _y ? -1 : _x > _y ? 1 : 0; + }); } + ); + + // based on: https://gist.github.com/DennyLoko/61882bc72176ca74a0f2 + handlebars.registerHelper( + HandlebarHelpersEnum.NUMBERFORMAT, + function (number, options) { + if (isNaN(number)) { + return number; + } - const decimalLength = options.hash.decimalLength || 2; - const thousandsSep = options.hash.thousandsSep || ','; - const decimalSep = options.hash.decimalSep || '.'; + const decimalLength = options.hash.decimalLength || 2; + const thousandsSep = options.hash.thousandsSep || ','; + const decimalSep = options.hash.decimalSep || '.'; - const value = parseFloat(number); + const value = parseFloat(number); - const re = '\\d(?=(\\d{3})+' + (decimalLength > 0 ? '\\D' : '$') + ')'; + const re = '\\d(?=(\\d{3})+' + (decimalLength > 0 ? '\\D' : '$') + ')'; - const num = value.toFixed(Math.max(0, ~~decimalLength)); + const num = value.toFixed(Math.max(0, ~~decimalLength)); - return (decimalSep ? num.replace('.', decimalSep) : num).replace( - new RegExp(re, 'g'), - '$&' + thousandsSep - ); - } -); + return (decimalSep ? num.replace('.', decimalSep) : num).replace( + new RegExp(re, 'g'), + '$&' + thousandsSep + ); + } + ); -Handlebars.registerHelper( - HandlebarHelpersEnum.GT, - function (arg1, arg2, options) { - return assertResult(arg1 > arg2, options); - } -); + handlebars.registerHelper( + HandlebarHelpersEnum.GT, + function (arg1, arg2, options) { + return assertResult(arg1 > arg2, options); + } + ); -Handlebars.registerHelper( - HandlebarHelpersEnum.GTE, - function (arg1, arg2, options) { - return assertResult(arg1 >= arg2, options); - } -); + handlebars.registerHelper( + HandlebarHelpersEnum.GTE, + function (arg1, arg2, options) { + return assertResult(arg1 >= arg2, options); + } + ); -Handlebars.registerHelper( - HandlebarHelpersEnum.LT, - function (arg1, arg2, options) { - return assertResult(arg1 < arg2, options); - } -); + handlebars.registerHelper( + HandlebarHelpersEnum.LT, + function (arg1, arg2, options) { + return assertResult(arg1 < arg2, options); + } + ); -Handlebars.registerHelper( - HandlebarHelpersEnum.LTE, - function (arg1, arg2, options) { - return assertResult(arg1 <= arg2, options); - } -); + handlebars.registerHelper( + HandlebarHelpersEnum.LTE, + function (arg1, arg2, options) { + return assertResult(arg1 <= arg2, options); + } + ); -Handlebars.registerHelper( - HandlebarHelpersEnum.EQ, - function (arg1, arg2, options) { - return assertResult(arg1 === arg2, options); - } -); + handlebars.registerHelper( + HandlebarHelpersEnum.EQ, + function (arg1, arg2, options) { + return assertResult(arg1 === arg2, options); + } + ); -Handlebars.registerHelper( - HandlebarHelpersEnum.NE, - function (arg1, arg2, options) { - return assertResult(arg1 !== arg2, options); - } -); + handlebars.registerHelper( + HandlebarHelpersEnum.NE, + function (arg1, arg2, options) { + return assertResult(arg1 !== arg2, options); + } + ); + + return handlebars; +} @Injectable() export class CompileTemplate { async execute(command: CompileTemplateCommand): Promise<string> { const templateContent = command.template || ''; + let result = ''; try { - const template = Handlebars.compile(templateContent); + const handlebars = createHandlebarsInstance(command.i18next); + const template = handlebars.compile(templateContent); result = template(command.data, {}); } catch (e: any) { throw new ApiException( - e?.message || `Message content could not be generated` + e?.message || `Handlebars message content could not be generated ${e}` ); } diff --git a/libs/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts b/libs/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts index 703eeadfbf8..8e0c0a4647e 100644 --- a/libs/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts +++ b/libs/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts @@ -645,14 +645,12 @@ export class ConditionsFilter extends Filter { job: IJob ): Promise<string | undefined> { try { - return await this.compileTemplate.execute( - CompileTemplateCommand.create({ - template: value, - data: { - ...variables, - }, - }) - ); + return await this.compileTemplate.execute({ + template: value, + data: { + ...variables, + }, + }); } catch (e: any) { await this.executionLogRoute.execute( ExecutionLogRouteCommand.create({ diff --git a/libs/application-generic/tsconfig.json b/libs/application-generic/tsconfig.json index 3f478640993..e417b7aaf4c 100644 --- a/libs/application-generic/tsconfig.json +++ b/libs/application-generic/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "sourceMap": true, "strictNullChecks": false, "allowSyntheticDefaultImports": true, "outDir": "build/main", diff --git a/libs/application-generic/tsconfig.module.json b/libs/application-generic/tsconfig.module.json index 9df84697738..82d60a2e6d7 100644 --- a/libs/application-generic/tsconfig.module.json +++ b/libs/application-generic/tsconfig.module.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig", "compilerOptions": { + "sourceMap": true, "target": "esnext", "outDir": "build/module", "module": "esnext",