Kaynağa Gözat

feat(email-plugin): Generate test emails from dev mailbox

Michael Bromley 6 yıl önce
ebeveyn
işleme
35105ec7d0

+ 0 - 1
packages/core/package.json

@@ -90,7 +90,6 @@
     "gulp": "^4.0.0",
     "mysql": "^2.16.0",
     "node-libcurl": "^1.3.3",
-    "opn": "^5.4.0",
     "pg": "^7.8.0",
     "rimraf": "^2.6.3",
     "sql.js": "^0.5.0",

+ 39 - 1
packages/email-plugin/dev-mailbox.html

@@ -23,7 +23,6 @@
         }
         button#refresh {
             margin-left: 12px;
-            border: 1px solid #15a9df;
             border-radius: 3px;
             padding: 3px 6px;
             display: flex;
@@ -33,6 +32,20 @@
             margin-left: 6px;
             font-size: 16px;
         }
+        .generate-controls {
+            flex: 1;
+            display: flex;
+            justify-content: flex-end;
+        }
+        input, select, button {
+            padding: 6px;
+            border-radius: 3px;
+            border: 1px solid #15a9df;
+            margin-left: 3px;
+        }
+        #language-code {
+            width: 32px;
+        }
         .content {
             display: flex;
             flex: 1;
@@ -83,6 +96,11 @@
             <span class="label">Refresh</span>
         </button>
     </div>
+    <div class="generate-controls">
+        <select id="type-selector"></select>
+        <input id="language-code" value="en" type="text">
+        <button id="generate-test">Generate test</button>
+    </div>
 </div>
 <div class="content">
     <div class="list">
@@ -95,6 +113,26 @@
     const refreshButton = document.querySelector('button#refresh');
     refreshButton.addEventListener('click', refreshInbox);
 
+    const typeSelect = document.querySelector('#type-selector');
+    fetch('./types')
+        .then(res => res.json())
+        .then(res => {
+            res.forEach(type => {
+                const option = document.createElement('option');
+                option.value = type;
+                option.text = type;
+                typeSelect.appendChild(option);
+            });
+        });
+
+    const languageCodeInput = document.querySelector('#language-code');
+    const generateTestButton = document.querySelector('#generate-test');
+    generateTestButton.addEventListener('click', e => {
+        fetch(`./generate/${typeSelect.value}/${languageCodeInput.value}`)
+            .then(() => new Promise(resolve => setTimeout(resolve, 500)))
+            .then(() => refreshInbox());
+    });
+
     const list = document.querySelector('.list');
     refreshInbox();
 

+ 0 - 143
packages/email-plugin/preview/email-contexts.ts

@@ -1,143 +0,0 @@
-import { LanguageCode } from '@vendure/common/lib/generated-types';
-
-import { RequestContext } from '../../api/common/request-context';
-import { Channel } from '../../entity/channel/channel.entity';
-import { Customer } from '../../entity/customer/customer.entity';
-import { OrderItem } from '../../entity/order-item/order-item.entity';
-import { OrderLine } from '../../entity/order-line/order-line.entity';
-import { Order } from '../../entity/order/order.entity';
-import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
-import { User } from '../../entity/user/user.entity';
-import { AccountRegistrationEvent } from '../../event-bus/events/account-registration-event';
-import { OrderStateTransitionEvent } from '../../event-bus/events/order-state-transition-event';
-import { defaultEmailTypes } from '../default-email-types';
-import { EmailContext } from '../email-context';
-
-export function getOrderReceiptContext():
-    | EmailContext<'order-confirmation', OrderStateTransitionEvent>
-    | undefined {
-    const event = new OrderStateTransitionEvent(
-        'ArrangingPayment',
-        'PaymentSettled',
-        createRequestContext(),
-        new Order({
-            id: '6',
-            createdAt: '2018-10-31T15:18:29.261Z',
-            updatedAt: '2018-10-31T15:24:17.000Z',
-            code: 'T3EPGJKTVZPBD6Z9',
-            state: 'ArrangingPayment',
-            active: true,
-            customer: new Customer({
-                id: '3',
-                firstName: 'Horacio',
-                lastName: 'Franecki',
-                emailAddress: 'Horacio.Franecki23@hotmail.com',
-            }),
-            lines: [
-                new OrderLine({
-                    id: '5',
-                    featuredAsset: {
-                        preview: 'http://localhost:3000/assets/mikkel-bech-748940-unsplash__49__preview.jpg',
-                    },
-                    productVariant: new ProductVariant({
-                        id: '3',
-                        name: 'en Intelligent Cotton Salad Small',
-                        sku: '5x7ss',
-                    }),
-                    items: [
-                        new OrderItem({
-                            id: '6',
-                            unitPrice: 745,
-                            unitPriceIncludesTax: false,
-                            taxRate: 20,
-                            pendingAdjustments: [],
-                        }),
-                    ],
-                }),
-                new OrderLine({
-                    id: '6',
-                    featuredAsset: {
-                        preview: 'http://localhost:3000/assets/mikkel-bech-748940-unsplash__49__preview.jpg',
-                    },
-                    productVariant: new ProductVariant({
-                        id: '4',
-                        name: 'en Intelligent Cotton Salad Large',
-                        sku: '5x7ss',
-                    }),
-                    items: [
-                        new OrderItem({
-                            id: '7',
-                            unitPrice: 745,
-                            unitPriceIncludesTax: false,
-                            taxRate: 20,
-                            pendingAdjustments: [],
-                        }),
-                    ],
-                }),
-            ],
-            subTotal: 1788,
-            subTotalBeforeTax: 1490,
-            shipping: 1000,
-            shippingMethod: {
-                code: 'express-flat-rate',
-                description: 'Express Shipping',
-                id: '2',
-            },
-            shippingAddress: {
-                fullName: 'Horacio Franecki',
-                company: '',
-                streetLine1: '6000 Pagac Land',
-                streetLine2: '',
-                city: 'Port Kirsten',
-                province: 'Avon',
-                postalCode: 'ZU32 9CP',
-                country: 'Cabo Verde',
-                phoneNumber: '',
-            },
-            payments: [],
-            pendingAdjustments: [],
-        }),
-    );
-
-    const contextConfig = defaultEmailTypes['order-confirmation'].createContext(event);
-    if (contextConfig) {
-        return new EmailContext({
-            ...contextConfig,
-            type: 'order-confirmation',
-            event,
-            templateVars: {},
-        });
-    }
-}
-
-export function getEmailVerificationContext():
-    | EmailContext<'email-verification', AccountRegistrationEvent>
-    | undefined {
-    const event = new AccountRegistrationEvent(
-        createRequestContext(),
-        new User({
-            verified: false,
-            verificationToken: 'MjAxOC0xMS0xM1QxNToxNToxNC42ODda_US2U6UK1WZC7NDAX',
-            identifier: 'Rhoda_Ebert@yahoo.com',
-        }),
-    );
-    const contextConfig = defaultEmailTypes['email-verification'].createContext(event);
-    if (contextConfig) {
-        return new EmailContext({
-            ...contextConfig,
-            type: 'email-verification',
-            event,
-            templateVars: {},
-        });
-    }
-}
-
-function createRequestContext(): RequestContext {
-    return new RequestContext({
-        languageCode: LanguageCode.en,
-        session: {} as any,
-        isAuthorized: false,
-        authorizedAsOwnerOnly: true,
-        channel: new Channel(),
-    });
-}

+ 0 - 63
packages/email-plugin/preview/generate-email-preview.ts

@@ -1,63 +0,0 @@
-import fs from 'fs-extra';
-import opn from 'opn';
-import path from 'path';
-
-import { NoopEmailGenerator } from '../../config/email/noop-email-generator';
-import { EmailOptions } from '../../config/vendure-config';
-import { defaultEmailTypes } from '../default-email-types';
-import { EmailContext } from '../email-context';
-import { HandlebarsMjmlGenerator } from '../handlebars-mjml-generator';
-import { TemplateLoader } from '../template-loader';
-
-import { getEmailVerificationContext, getOrderReceiptContext } from './email-contexts';
-// tslint:disable:no-console
-
-const generator = new HandlebarsMjmlGenerator();
-const emailOptions: EmailOptions<any> = {
-    emailTemplatePath: path.join(__dirname, '../templates', 'partials'),
-    emailTypes: defaultEmailTypes,
-    generator: new NoopEmailGenerator(),
-    transport: {
-        type: 'none',
-    },
-};
-const loader = new TemplateLoader({ emailOptions } as any);
-
-const emailType = process.argv[2];
-if (!emailType) {
-    failWith(`Please specify an emailType as the first argument. Example: order-confirmation`);
-}
-
-// tslint:disable-next-line
-generateEmailPreview(emailType);
-
-/**
- * Generates an .html file for the emailType specified as the first argument to the script.
- */
-async function generateEmailPreview(type: string) {
-    let emailContext: EmailContext | undefined;
-    switch (type) {
-        case 'order-confirmation':
-            emailContext = getOrderReceiptContext();
-            break;
-        case 'email-verification':
-            emailContext = getEmailVerificationContext();
-            break;
-    }
-    if (!emailContext) {
-        failWith(`Could not create a context for type "${type}"`);
-        return;
-    }
-    const { subject, body, templateContext } = await loader.loadTemplate(type, emailContext);
-    const generatedEmailContext = await generator.generate(subject, body, templateContext, emailContext);
-
-    const previewDir = path.join(__dirname, 'output');
-    await fs.ensureDir(previewDir);
-    await fs.writeFile(path.join(previewDir, `${type}.html`), generatedEmailContext.body);
-    await opn(path.join(previewDir, `${type}.html`));
-}
-
-function failWith(message: string) {
-    console.error(message);
-    process.exit(1);
-}

+ 7 - 3
packages/email-plugin/src/default-email-handlers.ts

@@ -2,13 +2,15 @@
 import { AccountRegistrationEvent, OrderStateTransitionEvent, PasswordResetEvent } from '@vendure/core';
 
 import { EmailEventListener } from './event-listener';
+import { mockAccountRegistrationEvent, mockOrderStateTransitionEvent, mockPasswordResetEvent } from './mock-events';
 
 export const orderConfirmationHandler = new EmailEventListener('order-confirmation')
     .on(OrderStateTransitionEvent)
     .filter(event => event.toState === 'PaymentSettled' && !!event.order.customer)
     .setRecipient(event => event.order.customer!.emailAddress)
     .setSubject(`Order confirmation for #{{ order.code }}`)
-    .setTemplateVars(event => ({ order: event.order }));
+    .setTemplateVars(event => ({ order: event.order }))
+    .setMockEvent(mockOrderStateTransitionEvent);
 
 export const emailVerificationHandler = new EmailEventListener('email-verification')
     .on(AccountRegistrationEvent)
@@ -17,7 +19,8 @@ export const emailVerificationHandler = new EmailEventListener('email-verificati
     .setTemplateVars(event => ({
         user: event.user,
         verifyUrl: 'verify',
-    }));
+    }))
+    .setMockEvent(mockAccountRegistrationEvent);
 
 export const passwordResetHandler = new EmailEventListener('password-reset')
     .on(PasswordResetEvent)
@@ -26,7 +29,8 @@ export const passwordResetHandler = new EmailEventListener('password-reset')
     .setTemplateVars(event => ({
         user: event.user,
         passwordResetUrl: 'reset-password',
-    }));
+    }))
+    .setMockEvent(mockPasswordResetEvent);
 
 export const defaultEmailHandlers = [
     orderConfirmationHandler,

+ 41 - 2
packages/email-plugin/src/dev-mailbox.ts

@@ -1,15 +1,23 @@
+import { Channel, RequestContext } from '@vendure/core';
 import express from 'express';
 import fs from 'fs-extra';
 import http from 'http';
 import path from 'path';
 
+import { LanguageCode } from '../../common/lib/generated-types';
+
+import { EmailEventHandler } from './event-listener';
+import { EmailPluginDevModeOptions, EventWithContext } from './types';
+
 /**
  * An email inbox application that serves the contents of the dev mode `outputPath` directory.
  */
 export class DevMailbox {
     server: http.Server;
+    private handleMockEventFn: (handler: EmailEventHandler<string, any>, event: EventWithContext) => void | undefined;
 
-    serve(port: number, outputPath: string) {
+    serve(options: EmailPluginDevModeOptions) {
+        const { outputPath, handlers, mailboxPort } = options;
         const server = express();
         server.get('/', (req, res) => {
             res.sendFile(path.join(__dirname, '../../dev-mailbox.html'));
@@ -19,12 +27,33 @@ export class DevMailbox {
             const contents = await this.getEmailList(outputPath);
             res.send(contents);
         });
+        server.get('/types', async (req, res) => {
+            res.send(handlers.map(h => h.type));
+        });
+        server.get('/generate/:type/:languageCode', async (req, res) => {
+            const { type, languageCode } = req.params;
+            if (this.handleMockEventFn) {
+                const handler = handlers.find(h => h.type === type);
+                if (!handler || !handler.mockEvent) {
+                    res.statusCode = 404;
+                    res.send({ success: false, error: `No mock event registered for type "${type}"`});
+                    return;
+                }
+                this.handleMockEventFn(handler, { ...handler.mockEvent, ctx: this.createRequestContext(languageCode) });
+                res.send({ success: true });
+            }
+            res.send({ success: false, error: `Mock email generation not set up.` });
+        });
         server.get('/item/:id', async (req, res) => {
             const fileName = req.params.id;
             const content = await this.getEmail(outputPath, fileName);
             res.send(content);
         });
-        this.server = server.listen(port);
+        this.server = server.listen(mailboxPort);
+    }
+
+    handleMockEvent(handler: (handler: EmailEventHandler<string, any>, event: EventWithContext) => void) {
+        this.handleMockEventFn = handler;
     }
 
     destroy() {
@@ -57,4 +86,14 @@ export class DevMailbox {
         const content = JSON.parse(json);
         return content;
     }
+
+    private createRequestContext(languageCode: LanguageCode): RequestContext {
+        return new RequestContext({
+            languageCode,
+            session: {} as any,
+            isAuthorized: false,
+            authorizedAsOwnerOnly: true,
+            channel: new Channel(),
+        });
+    }
 }

+ 0 - 37
packages/email-plugin/src/email-sender.ts

@@ -79,43 +79,6 @@ export class EmailSender {
             subject: email.subject,
             body: email.body,
         };
-        /*const content = `<html lang="en">
-            <head>
-                <title>${email.subject}</title>
-                <style>
-                    body {
-                        display: flex;
-                        flex-direction: column;
-                        font-family: Helvetica, Arial, sans-serif;
-                    }
-                    iframe {
-                        flex: 1;
-                        border: 1px solid #aaa;
-                    }
-                </style>
-            </head>
-            <body>
-            <div class="metadata">
-                <table>
-                    <tr>
-                        <td>Recipient:</td>
-                        <td>${email.recipient}</td>
-                    </tr>
-                    <tr>
-                        <td>Subject:</td>
-                        <td>${email.subject}</td>
-                    </tr>
-                    <tr>
-                        <td>Date:</td>
-                        <td>${new Date().toLocaleString()}</td>
-                    </tr>
-                </table>
-            </div>
-            <iframe srcdoc="${email.body.replace(/"/g, '&quot;')}"></iframe>
-            </body>
-            </html>
-        `;*/
-
         await fs.writeFile(pathWithoutExt + '.json', JSON.stringify(output, null, 2));
     }
 

+ 16 - 0
packages/email-plugin/src/event-listener.ts

@@ -1,3 +1,4 @@
+import { Omit } from '@vendure/common/lib/omit';
 import { Type } from '@vendure/common/lib/shared-types';
 import { LanguageCode } from '@vendure/common/src/generated-types';
 
@@ -103,6 +104,7 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
     private filterFns: Array<(event: Event) => boolean> = [];
     private configurations: EmailTemplateConfig[] = [];
     private defaultSubject: string;
+    private _mockEvent: Omit<Event, 'ctx'> | undefined;
 
     constructor(public listener: EmailEventListener<T>, public event: Type<Event>) {}
 
@@ -110,6 +112,10 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
         return this.listener.type;
     }
 
+    get mockEvent(): Omit<Event, 'ctx'> | undefined {
+        return this._mockEvent;
+    }
+
     /**
      * @description
      * Defines a predicate function which is used to determine whether the event will trigger an email.
@@ -185,6 +191,16 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
         };
     }
 
+    /**
+     * @description
+     * Optionally define a mock Event which is used by the dev mode mailbox app for generating mock emails
+     * from this handler.
+     */
+    setMockEvent(event: Omit<Event, 'ctx'>): EmailEventHandler<T, Event> {
+        this._mockEvent = event;
+        return this;
+    }
+
     private getBestConfiguration(channelCode: string, languageCode: LanguageCode): EmailTemplateConfig | undefined {
         if ( this.configurations.length === 0) {
             return;

+ 113 - 0
packages/email-plugin/src/mock-events.ts

@@ -0,0 +1,113 @@
+import { LanguageCode } from '@vendure/common/lib/generated-types';
+import {
+    AccountRegistrationEvent,
+    Channel,
+    Customer,
+    Order,
+    OrderItem,
+    OrderLine,
+    OrderStateTransitionEvent, PasswordResetEvent,
+    ProductVariant,
+    RequestContext,
+    User,
+} from '@vendure/core';
+
+export const mockOrderStateTransitionEvent = new OrderStateTransitionEvent(
+    'ArrangingPayment',
+    'PaymentSettled',
+    {} as any,
+    new Order({
+        id: '6',
+        createdAt: '2018-10-31T15:18:29.261Z',
+        updatedAt: '2018-10-31T15:24:17.000Z',
+        code: 'T3EPGJKTVZPBD6Z9',
+        state: 'ArrangingPayment',
+        active: true,
+        customer: new Customer({
+            id: '3',
+            firstName: 'Test',
+            lastName: 'Customer',
+            emailAddress: 'test.customer@email.com',
+        }),
+        lines: [
+            new OrderLine({
+                id: '5',
+                featuredAsset: {
+                    preview: 'http://localhost:3000/assets/mikkel-bech-748940-unsplash__49__preview.jpg',
+                },
+                productVariant: new ProductVariant({
+                    id: '3',
+                    name: 'en Intelligent Cotton Salad Small',
+                    sku: '5x7ss',
+                }),
+                items: [
+                    new OrderItem({
+                        id: '6',
+                        unitPrice: 745,
+                        unitPriceIncludesTax: false,
+                        taxRate: 20,
+                        pendingAdjustments: [],
+                    }),
+                ],
+            }),
+            new OrderLine({
+                id: '6',
+                featuredAsset: {
+                    preview: 'http://localhost:3000/assets/mikkel-bech-748940-unsplash__49__preview.jpg',
+                },
+                productVariant: new ProductVariant({
+                    id: '4',
+                    name: 'en Intelligent Cotton Salad Large',
+                    sku: '5x7ss',
+                }),
+                items: [
+                    new OrderItem({
+                        id: '7',
+                        unitPrice: 745,
+                        unitPriceIncludesTax: false,
+                        taxRate: 20,
+                        pendingAdjustments: [],
+                    }),
+                ],
+            }),
+        ],
+        subTotal: 1788,
+        subTotalBeforeTax: 1490,
+        shipping: 1000,
+        shippingMethod: {
+            code: 'express-flat-rate',
+            description: 'Express Shipping',
+            id: '2',
+        },
+        shippingAddress: {
+            fullName: 'Horacio Franecki',
+            company: '',
+            streetLine1: '6000 Pagac Land',
+            streetLine2: '',
+            city: 'Port Kirsten',
+            province: 'Avon',
+            postalCode: 'ZU32 9CP',
+            country: 'Cabo Verde',
+            phoneNumber: '',
+        },
+        payments: [],
+        pendingAdjustments: [],
+    }),
+);
+
+export const mockAccountRegistrationEvent = new AccountRegistrationEvent(
+    {} as any,
+    new User({
+        verified: false,
+        verificationToken: 'MjAxOC0xMS0xM1QxNToxNToxNC42ODda_US2U6UK1WZC7NDAX',
+        identifier: 'test@test.com',
+    }),
+);
+
+export const mockPasswordResetEvent = new PasswordResetEvent(
+    {} as any,
+    new User({
+        identifier: 'test@test.com',
+        passwordResetToken: 'MjAxOS0wNC0xNVQxMzozMDozOC43MjFa_MA2FR6HRZBW7JWD6',
+    }),
+);

+ 2 - 1
packages/email-plugin/src/plugin.ts

@@ -146,7 +146,8 @@ export class EmailPlugin implements VendurePlugin {
     configure(config: Required<VendureConfig>): Required<VendureConfig> | Promise<Required<VendureConfig>> {
         if (isDevModeOptions(this.options) && this.options.mailboxPort !== undefined) {
             this.devMailbox = new DevMailbox();
-            this.devMailbox.serve(this.options.mailboxPort, this.options.outputPath);
+            this.devMailbox.serve(this.options);
+            this.devMailbox.handleMockEvent((handler, event) => this.handleEvent(handler, event));
             const route = 'mailbox';
             config.middleware.push({
                 handler: createProxyHandler({ port: this.options.mailboxPort, route }, !config.silent),