@b2y/email-service
v1.2.3
Published
A flexible, channel-agnostic notification delivery library. Ships a shared, versioned NotificationTemplate model, NotificationType/MediaType/TemplateCategory enums, and a Handlebars template engine that any notification channel (email, sms, whatsapp, push
Readme
@b2y/email-service
A reusable, channel-agnostic notification library. It is split into two layers:
- Shared, channel-agnostic layer - usable by any notification channel
(email, sms, whatsapp, push): the versioned
NotificationTemplatemodel, theNotificationType/MediaType/TemplateCategoryenums, and the Handlebars-basedTemplateEngine. - Concrete EMAIL delivery layer -
EmailServiceplus five email providers (Nodemailer, SendGrid, Postmark, Amazon SES, Mailgun).
Sibling packages such as @b2y/sms-service and @b2y/whatsapp-service are
expected to implement their own channel-specific delivery service (their own
SmsService/WhatsappService + their own carrier providers), but should
import layer 1 from this package via its subpath exports instead of
redefining their own template model/enums - see
"Building another channel"
below. This keeps exactly one source of truth for the template schema,
versioning rules, and rendering logic across every channel.
Features
- Channel-agnostic
NotificationTemplatemodel with real versioning (Version+IsActive, enforced at the DB level with a partial unique index so exactly one version is active per template/tenant/language/channel) NotificationType(EMAIL,SMS,PUSH,WHATSAPP),MediaType, andTemplateCategoryenums, shared by every consumer instead of being redefined per-service- Handlebars-based template rendering with placeholder substitution and a language-fallback lookup, independent of which channel is sending
- A concrete EMAIL implementation on top of that shared layer, with a common interface across five providers - switch providers via config, not code
- Zero
process.envreads anywhere in the library - every value the library needs (provider selection, credentials, from-address, etc.) is passed in explicitly by the consuming application
Architecture note: no database connection, no environment binding
This package does not open a database connection and does not read environment variables. Both concerns belong to the consuming application:
- Database connection & template CRUD/tenant logic: the application owns
its own
sequelizeinstance, instantiates the shared model against it, and is responsible for creating/versioning templates (seeNotificationTemplateModelbelow). This library only ever reads active templates. - Environment / secrets: the application reads its own
.env(however it already does that) and builds a plainconfigobject, which it passes intocreateEmailService/getEmailService/createEmailProvider. See.env.examplefor the exact shape each provider expects on that object.
Installation
npm install @b2y/email-servicesequelize is a peer dependency (see package.json) rather than a
bundled dependency - install whichever version your application already
uses, so there is only ever one sequelize instance in the dependency tree.
It is marked optional because a consumer that only needs the enums or the
email providers (no DB-backed templates) doesn't need it; importing the
package's root entrypoint (require('@b2y/email-service')), however, does
load model/NotificationTemplate.js, so sequelize must be present whenever
the root entrypoint is used.
Usage
const { getEmailService, NotificationTemplateModel } = require('@b2y/email-service');
const { sequelize } = require('./ConnectionDB/Connect'); // the app's own connection
// The application owns the model instance.
const NotificationTemplate = NotificationTemplateModel(sequelize);
// The application owns config - built from its own env, however it likes.
const config = {
emailProvider: process.env.EMAIL_PROVIDER, // nodemailer | sendgrid | postmark | amazonses | mailgun
fromEmail: process.env.FROM_EMAIL,
fromName: process.env.FROM_NAME,
defaultLanguage: process.env.DEFAULT_LANGUAGE, // optional - used when sendEmail gets no langCode (falls back to 'en')
// ...provider-specific fields, see .env.example
};
const emailService = await getEmailService({
dbClient: { NotificationTemplate },
config
});
await emailService.sendEmail({
tenantId: 1,
// langCode: 'en', // optional - defaults to config.defaultLanguage
templateName: 'WELCOME_EMAIL',
placeholders: { userName: 'Aarvi' },
recipients: { to: '[email protected]' }
});getEmailService memoizes a singleton on first call - dbClient/config
only need to be supplied once per process. Use createEmailService directly
if you need more than one independently-configured instance.
Building another channel (SMS / WhatsApp / Push)
A sibling service does not need its own copy of the template model or enums. Import just the shared layer via this package's subpath exports, which avoids pulling in any email provider SDK:
const NotificationTemplateModel = require('@b2y/email-service/model/NotificationTemplate');
const TemplateEngine = require('@b2y/email-service/utils/templateEngine');
const { NotificationType } = require('@b2y/email-service/enum/NotificationType');
const { MediaType } = require('@b2y/email-service/enum/MediaType');
const { TemplateCategory } = require('@b2y/email-service/enum/TemplateCategory');
const NotificationTemplate = NotificationTemplateModel(sequelize);
const templateEngine = new TemplateEngine({ NotificationTemplate }, config);
const { subject, body } = await templateEngine.compileTemplate(
'ORDER_SHIPPED',
{ orderId: '123' },
tenantId,
'en',
NotificationType.WHATSAPP // any NotificationType value works - the engine is channel-agnostic
);The new service then implements its own delivery layer (e.g. WhatsappService
- a
BaseProvider-style contract for its own carrier(s)), following the same config-injection pattern used here: noprocess.envreads inside the library, the host application passes in aconfigobject.
Breaking changes from 1.x
- The library no longer reads
process.envor its own.envfile (the oldConfig.jsis removed). Every provider/service constructor now takes an explicitconfigobject - see "Usage" above and.env.examplefor the field mapping. EmailTemplateis replaced by the shared, versionedNotificationTemplatemodel (addsNotificationType,MediaType,TemplateCategory,IsActive,Version; removesStyles). See the consuming application's migration scripts for the corresponding schema changes.sequelizemoved from a bundled dependency to a peer dependency - runnpm install sequelizein the consuming application if it doesn't already have it.
License
ISC
