@mostajs/notifications 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,444 @@
1
+ # @mostajs/notifications
2
+
3
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com>
4
+ **License** : AGPL-3.0-or-later
5
+ **Version** : 0.1.0
6
+
7
+ Moteur **générique** de notifications pour l'écosystème `@mostajs/*` — découple le **dispatch** (mailer + i18n + templating + idempotence) du **domaine métier** (booking, media-sfu, custom apps). Tout domaine peut envoyer des notifications en appelant `engine.notify(kind, context)` ; les adapters spécifiques (`@mostajs/booking-notifications`, `@mostajs/media-sfu-notifications`, …) sont de fins glues qui mappent leurs events internes en kinds génériques.
8
+
9
+ 📊 **Étude état de l'art** (Knock, Courier, Novu, SendGrid, Postmark, Resend, Customer.io, …) → [`docs/STATE-OF-THE-ART.md`](./docs/STATE-OF-THE-ART.md)
10
+
11
+ ---
12
+
13
+ ## Table des matières
14
+
15
+ 1. [Pourquoi un moteur générique](#1-pourquoi-un-moteur-générique)
16
+ 2. [Architecture](#2-architecture)
17
+ 3. [Quick start — how to use](#3-quick-start--how-to-use)
18
+ 4. [API détaillée](#4-api-détaillée)
19
+ 5. [Templates — patterns recommandés](#5-templates--patterns-recommandés)
20
+ 6. [Implémentation — how to impl](#6-implémentation--how-to-impl)
21
+ 7. [I18n & locales](#7-i18n--locales)
22
+ 8. [Idempotence](#8-idempotence)
23
+ 9. [Tests](#9-tests)
24
+ 10. [Modules liés](#10-modules-liés)
25
+
26
+ ---
27
+
28
+ ## 1. Pourquoi un moteur générique
29
+
30
+ Avant ce module, chaque domaine `@mostajs/*` qui voulait envoyer un mail dupliquait :
31
+ - Templates locale (FR / EN / AR)
32
+ - Dispatch via `@mostajs/mailer`
33
+ - Idempotence (`messageId`)
34
+ - Retry / fallback / callbacks
35
+
36
+ Avec ce module :
37
+ - `@mostajs/booking-notifications` : 100 LOC de glue (subscribe events booking → notify)
38
+ - `@mostajs/media-sfu-notifications` (futur) : 80 LOC de glue (subscribe events SFU → notify)
39
+ - `@mostajs/orphan-care-notifications` (futur) : idem
40
+ - Plomberie commune (templates registry, mailer, i18n, idempotence) : **un seul code à maintenir**
41
+
42
+ Cas où ne PAS utiliser ce module :
43
+ - App très simple avec 1 seul mail à envoyer → utilise `@mostajs/mailer` directement
44
+ - Domaine très spécifique avec templates DB-driven (CMS-like) → roll your own avec `@mostajs/mailer`
45
+
46
+ ---
47
+
48
+ ## 2. Architecture
49
+
50
+ ```
51
+ ┌──────────────────────────────────────────────────────────────┐
52
+ │ App / Adapter consumer │
53
+ │ - subscribe events @mostajs/booking, @mostajs/media-sfu, … │
54
+ │ - map event → { kind: 'booking.reservation.created', │
55
+ │ context: { reservation, slot, ... } } │
56
+ │ - appelle engine.notify(...) │
57
+ └──────────────────────────────────┬───────────────────────────┘
58
+
59
+
60
+ ┌──────────────────────────────────────────────────────────────┐
61
+ │ @mostajs/notifications — NotificationEngine │
62
+ │ │
63
+ │ ┌──────────────────┐ ┌──────────────────┐ │
64
+ │ │ Templates │ │ Locale picker │ │
65
+ │ │ registry │ ◀─│ ctx.locale → │ │
66
+ │ │ (kind → tpl) │ │ variant FR/EN/AR │ │
67
+ │ └────────┬─────────┘ └──────────────────┘ │
68
+ │ ▼ │
69
+ │ ┌──────────────────┐ ┌──────────────────┐ │
70
+ │ │ render(ctx) │ → │ Mail (subject, │ │
71
+ │ │ → RenderedMail │ │ to, html, text) │ │
72
+ │ └────────┬─────────┘ └────────┬─────────┘ │
73
+ │ ▼ ▼ │
74
+ │ ┌──────────────────────────────────────────┐ │
75
+ │ │ Idempotence (messageId) │ │
76
+ │ │ Callbacks (onSent / onError) │ │
77
+ │ └────────────────────┬─────────────────────┘ │
78
+ └────────────────────────┼─────────────────────────────────────┘
79
+
80
+
81
+ ┌──────────────────────────────────────────────────────────────┐
82
+ │ @mostajs/mailer — driver-based dispatch │
83
+ │ SMTP / Resend / Postmark / SES / Brevo / Console / Mock │
84
+ └──────────────────────────────────────────────────────────────┘
85
+ ```
86
+
87
+ ---
88
+
89
+ ## 3. Quick start — how to use
90
+
91
+ ### Installation
92
+
93
+ ```bash
94
+ npm install @mostajs/notifications @mostajs/mailer
95
+ ```
96
+
97
+ ### Bootstrap minimal
98
+
99
+ ```ts
100
+ import { createNotificationEngine, simpleTemplate, localizedTemplate } from '@mostajs/notifications'
101
+ import { createMailer, createConsoleDriver } from '@mostajs/mailer'
102
+
103
+ const mailer = createMailer({ driver: createConsoleDriver() })
104
+
105
+ const engine = createNotificationEngine({
106
+ mailer,
107
+ defaultFrom: 'noreply@example.com',
108
+ defaultLocale: 'fr',
109
+ templates: {
110
+ 'welcome': simpleTemplate({
111
+ to: ctx => (ctx.user as any)?.email ?? null,
112
+ subject: ctx => `Bienvenue ${(ctx.user as any)?.name}`,
113
+ html: ctx => `<h1>Bonjour ${(ctx.user as any)?.name}</h1><p>Heureux de vous accueillir !</p>`,
114
+ }),
115
+ },
116
+ })
117
+
118
+ await engine.notify('welcome', {
119
+ user: { name: 'Amina', email: 'amina@example.com' },
120
+ })
121
+ ```
122
+
123
+ ### Template multi-locale (FR + EN + AR)
124
+
125
+ ```ts
126
+ engine.register('appointment.reminder', localizedTemplate({
127
+ fr: {
128
+ subject: ctx => `Rappel : votre rendez-vous ${formatDate(ctx.startAt)}`,
129
+ html: ctx => `<p>Bonjour, n'oubliez pas votre rendez-vous demain à <strong>${formatDate(ctx.startAt)}</strong>.</p>`,
130
+ },
131
+ en: {
132
+ subject: ctx => `Reminder: your appointment ${formatDate(ctx.startAt)}`,
133
+ html: ctx => `<p>Hi, don't forget your appointment tomorrow at <strong>${formatDate(ctx.startAt)}</strong>.</p>`,
134
+ },
135
+ ar: {
136
+ subject: ctx => `تذكير: موعدكم ${formatDate(ctx.startAt)}`,
137
+ html: ctx => `<p>تذكير بموعدكم غداً في <strong>${formatDate(ctx.startAt)}</strong>.</p>`,
138
+ },
139
+ }, ctx => (ctx.user as any)?.email,
140
+ ctx => `appt-reminder-${(ctx as any).reservationId}-24h`, // idempotence
141
+ ))
142
+
143
+ // L'app envoie selon la locale du user :
144
+ await engine.notify('appointment.reminder', {
145
+ user: { email: 'patient@example.com' },
146
+ startAt: Date.now() + 86400_000,
147
+ reservationId: 'res-123',
148
+ locale: 'ar',
149
+ })
150
+ ```
151
+
152
+ ### Dans un adapter domain (@mostajs/booking-notifications, etc.)
153
+
154
+ ```ts
155
+ // @mostajs/booking-notifications fait :
156
+ manager.onEvent = async (ev) => {
157
+ if (ev.type !== 'reservation.created') return
158
+ const r = await manager.findReservation(ev.reservationId!)
159
+ const slot = await manager.findSlot(r.slotId)
160
+ await engine.notify('booking.reservation.created', {
161
+ reservation: r, slot, locale: r.metadata?.locale,
162
+ })
163
+ }
164
+ ```
165
+
166
+ ---
167
+
168
+ ## 4. API détaillée
169
+
170
+ ### `createNotificationEngine(opts) → NotificationEngine`
171
+
172
+ ```ts
173
+ interface NotificationEngineOptions {
174
+ mailer: { send: (mail: any) => Promise<{ messageId: string }> }
175
+ templates?: Record<string, NotificationTemplate>
176
+ defaultLocale?: string
177
+ defaultFrom?: string
178
+ disabled?: string[]
179
+ onSent?: (kind, context, result) => void | Promise<void>
180
+ onError?: (kind, context, error) => void | Promise<void>
181
+ }
182
+
183
+ interface NotificationEngine {
184
+ register(kind: string, template: NotificationTemplate): void
185
+ unregister(kind: string): void
186
+ registeredKinds(): string[]
187
+ setEnabled(kind: string, enabled: boolean): void
188
+
189
+ notify(kind: string, context: NotificationContext): Promise<{ messageId: string } | null>
190
+ notifyBatch(items: Array<{ kind, context }>): Promise<Array<{ messageId: string } | null>>
191
+ }
192
+ ```
193
+
194
+ ### `NotificationTemplate`
195
+
196
+ ```ts
197
+ interface NotificationTemplate {
198
+ render(context: NotificationContext): Promise<RenderedMail | null> | RenderedMail | null
199
+ }
200
+
201
+ interface RenderedMail {
202
+ to: string // requis (sinon skip)
203
+ cc?: string | string[]
204
+ bcc?: string | string[]
205
+ from?: string // override defaultFrom
206
+ subject: string
207
+ text?: string
208
+ html?: string
209
+ messageId?: string // idempotence (mailer skip si déjà sent)
210
+ metadata?: Record<string, unknown>
211
+ }
212
+ ```
213
+
214
+ ### Helpers
215
+
216
+ - `simpleTemplate({ to, subject, text?, html?, messageId? })` — factory function-based
217
+ - `localizedTemplate(variants, resolveTo, resolveMessageId?)` — multi-locale avec fallback fr → en
218
+
219
+ ---
220
+
221
+ ## 5. Templates — patterns recommandés
222
+
223
+ ### Pattern 1 — Templates inline (mini app)
224
+
225
+ ```ts
226
+ templates: {
227
+ 'welcome': simpleTemplate({
228
+ to: ctx => (ctx.user as any).email,
229
+ subject: () => 'Bienvenue',
230
+ html: ctx => `<h1>Hello ${(ctx.user as any).name}</h1>`,
231
+ }),
232
+ }
233
+ ```
234
+
235
+ ### Pattern 2 — Templates dans des fichiers séparés
236
+
237
+ ```
238
+ my-app/
239
+ ├── lib/
240
+ │ └── notifications.ts ← createNotificationEngine + register
241
+ └── templates/
242
+ ├── welcome.ts
243
+ ├── booking-created.ts
244
+ └── ...
245
+ ```
246
+
247
+ ```ts
248
+ // templates/welcome.ts
249
+ import { localizedTemplate } from '@mostajs/notifications'
250
+ export const welcomeTemplate = localizedTemplate({
251
+ fr: { subject: 'Bienvenue', html: ctx => `<h1>Hello ${(ctx.user as any).name}</h1>` },
252
+ en: { subject: 'Welcome', html: ctx => `<h1>Hello ${(ctx.user as any).name}</h1>` },
253
+ }, ctx => (ctx.user as any).email)
254
+ ```
255
+
256
+ ```ts
257
+ // lib/notifications.ts
258
+ import { welcomeTemplate } from '../templates/welcome.js'
259
+ const engine = createNotificationEngine({ mailer, templates: { welcome: welcomeTemplate } })
260
+ ```
261
+
262
+ ### Pattern 3 — Templates DB-driven (CMS)
263
+
264
+ Pour permettre à des non-devs d'éditer les templates :
265
+
266
+ ```ts
267
+ const engine = createNotificationEngine({ mailer })
268
+
269
+ async function loadTemplates() {
270
+ const rows = await db.select().from('notification_templates')
271
+ for (const row of rows) {
272
+ engine.register(row.kind, simpleTemplate({
273
+ to: ctx => evalExpr(row.toExpr, ctx),
274
+ subject: ctx => interpolate(row.subject, ctx),
275
+ html: ctx => interpolate(row.htmlBody, ctx),
276
+ }))
277
+ }
278
+ }
279
+
280
+ await loadTemplates()
281
+ db.on('change:notification_templates', loadTemplates) // hot-reload
282
+ ```
283
+
284
+ ### Pattern 4 — Moteur de template externe (Handlebars, EJS)
285
+
286
+ ```ts
287
+ import Handlebars from 'handlebars'
288
+
289
+ const subjectTpl = Handlebars.compile('Rappel : {{appointmentName}} le {{startAt}}')
290
+ const htmlTpl = Handlebars.compile(htmlTemplate)
291
+
292
+ engine.register('reminder', simpleTemplate({
293
+ to: ctx => (ctx.user as any).email,
294
+ subject: ctx => subjectTpl(ctx),
295
+ html: ctx => htmlTpl(ctx),
296
+ }))
297
+ ```
298
+
299
+ ---
300
+
301
+ ## 6. Implémentation — how to impl
302
+
303
+ ### Adapter pour un domaine (pattern type)
304
+
305
+ ```ts
306
+ // my-domain-notifications.ts
307
+ import type { NotificationEngine } from '@mostajs/notifications'
308
+ import { localizedTemplate } from '@mostajs/notifications'
309
+ import type { MyManager, MyEvent } from '@mostajs/my-domain'
310
+
311
+ export function createMyDomainNotifications(opts: {
312
+ engine: NotificationEngine
313
+ manager: MyManager
314
+ /** Prefix pour les kinds : 'my-domain' → 'my-domain.event-x'. Default = nom du domain. */
315
+ kindPrefix?: string
316
+ }) {
317
+ const prefix = opts.kindPrefix ?? 'my-domain'
318
+
319
+ // 1. Enregistrer les templates par défaut du domaine
320
+ opts.engine.register(`${prefix}.welcome`, localizedTemplate({
321
+ fr: { subject: 'Bienvenue dans mon domaine', html: ctx => `...` },
322
+ en: { subject: 'Welcome', html: ctx => `...` },
323
+ }, ctx => (ctx.entity as any)?.email))
324
+
325
+ // 2. Subscribe aux events du manager → notify
326
+ opts.manager.onEvent = async (ev: MyEvent) => {
327
+ switch (ev.type) {
328
+ case 'entity.created':
329
+ const e = await opts.manager.findEntity(ev.entityId!)
330
+ await opts.engine.notify(`${prefix}.welcome`, { entity: e, locale: e.locale })
331
+ break
332
+ // ...
333
+ }
334
+ }
335
+
336
+ return {
337
+ /** Permet à l'app d'override les templates par défaut. */
338
+ setTemplate(kind: string, template: NotificationTemplate) {
339
+ opts.engine.register(`${prefix}.${kind}`, template)
340
+ },
341
+ }
342
+ }
343
+ ```
344
+
345
+ ### Routes admin (Next.js)
346
+
347
+ ```ts
348
+ // app/api/admin/notifications/test/route.ts
349
+ import { engine } from '@/lib/notifications'
350
+
351
+ export async function POST(req: Request) {
352
+ const { kind, context } = await req.json()
353
+ const result = await engine.notify(kind, context)
354
+ return Response.json({ sent: !!result, messageId: result?.messageId })
355
+ }
356
+
357
+ // app/api/admin/notifications/kinds/route.ts
358
+ export async function GET() {
359
+ return Response.json({ kinds: engine.registeredKinds() })
360
+ }
361
+ ```
362
+
363
+ ---
364
+
365
+ ## 7. I18n & locales
366
+
367
+ Le `context.locale` détermine quel template variant est choisi. Helper `localizedTemplate(variants, ...)` :
368
+ - Cherche `variants[locale]` (ex. `fr-DZ`)
369
+ - Fallback sur `variants[locale.split('-')[0]]` (ex. `fr`)
370
+ - Fallback final sur `variants.fr` puis `variants.en`
371
+
372
+ **Résolution de la locale** : c'est à l'app de mettre `ctx.locale` (depuis `user.locale` DB, `Accept-Language` header, ou subdomain `fr.app.com`).
373
+
374
+ ```ts
375
+ await engine.notify('reminder', {
376
+ user: u,
377
+ locale: u.locale ?? extractFromHeaders(req) ?? 'fr',
378
+ })
379
+ ```
380
+
381
+ ---
382
+
383
+ ## 8. Idempotence
384
+
385
+ Pour éviter d'envoyer **2× le même reminder 24h** si un cron retrigger :
386
+ - Le template doit fournir un `messageId` déterministe : `reminder-24h-${reservation.id}`
387
+ - `@mostajs/mailer` (Phase 2 MailLog) persistera ce messageId en DB → 2e send avec même ID = no-op silencieux
388
+
389
+ ```ts
390
+ localizedTemplate(
391
+ { fr: { subject: 'Rappel', html: ... } },
392
+ ctx => (ctx.user as any).email,
393
+ ctx => `reminder-24h-${(ctx.reservation as any).id}`, // messageId
394
+ )
395
+ ```
396
+
397
+ ---
398
+
399
+ ## 9. Tests
400
+
401
+ ```ts
402
+ // tests/engine.test.ts
403
+ import { createNotificationEngine, simpleTemplate } from '@mostajs/notifications'
404
+
405
+ describe('NotificationEngine', () => {
406
+ it('dispatch template registered', async () => {
407
+ const sent: any[] = []
408
+ const mailer = { send: async (m: any) => { sent.push(m); return { messageId: 'x-1' } } }
409
+ const engine = createNotificationEngine({ mailer })
410
+ engine.register('welcome', simpleTemplate({
411
+ to: _ => 'user@x.com',
412
+ subject: _ => 'Hello',
413
+ text: _ => 'Welcome!',
414
+ }))
415
+
416
+ const r = await engine.notify('welcome', { user: 'x' })
417
+ expect(r?.messageId).toBe('x-1')
418
+ expect(sent[0]).toMatchObject({ to: 'user@x.com', subject: 'Hello' })
419
+ })
420
+
421
+ it('skip if template absent', async () => {
422
+ const engine = createNotificationEngine({ mailer: { send: async () => ({ messageId: '' }) } })
423
+ expect(await engine.notify('unknown', {})).toBeNull()
424
+ })
425
+
426
+ it('locale fallback', async () => {
427
+ // ... test localizedTemplate avec variant manquante
428
+ })
429
+ })
430
+ ```
431
+
432
+ ---
433
+
434
+ ## 10. Modules liés
435
+
436
+ - [`@mostajs/mailer`](../mosta-mailer) — peer dep, dispatch SMTP/Resend/SES/etc.
437
+ - [`@mostajs/booking-notifications`](../mosta-booking-stack/mosta-booking-notifications) — adapter booking events → notifications
438
+ - (futur) `@mostajs/media-sfu-notifications` — adapter SFU events
439
+ - [`@mostajs/url`](../mosta-url) — pour construire des liens dans les templates (`signedUrl` pour invite tokens)
440
+
441
+ ---
442
+
443
+ **License** : AGPL-3.0-or-later
444
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com>
@@ -0,0 +1,98 @@
1
+ export type Locale = string;
2
+ export interface NotificationContext {
3
+ /** Locale du destinataire — détermine quel template variant utiliser. */
4
+ locale?: Locale;
5
+ /** Champs libres consommés par le template (reservation, slot, session, etc.). */
6
+ [key: string]: unknown;
7
+ }
8
+ export interface RenderedMail {
9
+ to: string;
10
+ cc?: string | string[];
11
+ bcc?: string | string[];
12
+ from?: string;
13
+ subject: string;
14
+ text?: string;
15
+ html?: string;
16
+ /** Idempotence : mailer skip si déjà sent avec ce messageId. */
17
+ messageId?: string;
18
+ /** Metadata libre persistée dans MailLog. */
19
+ metadata?: Record<string, unknown>;
20
+ }
21
+ /** Template = fonction qui produit un Mail à partir du contexte.
22
+ * Retour null → skip (template applicable mais l'app a décidé de pas envoyer). */
23
+ export interface NotificationTemplate {
24
+ render(context: NotificationContext): Promise<RenderedMail | null> | RenderedMail | null;
25
+ }
26
+ export interface NotificationEngineOptions {
27
+ /** Instance @mostajs/mailer (ou compatible : tout objet avec `.send(mail)`). */
28
+ mailer: {
29
+ send: (mail: any) => Promise<{
30
+ messageId: string;
31
+ }>;
32
+ };
33
+ /** Templates initialement enregistrés (kind → template). */
34
+ templates?: Record<string, NotificationTemplate>;
35
+ /** Locale par défaut si le contexte n'en spécifie pas. */
36
+ defaultLocale?: Locale;
37
+ /** Default `from` (peut être overridé par chaque template). */
38
+ defaultFrom?: string;
39
+ /** Désactive certains kinds (e.g. mode dégradé). */
40
+ disabled?: string[];
41
+ /** Callback succès. */
42
+ onSent?: (kind: string, context: NotificationContext, result: {
43
+ messageId: string;
44
+ }) => void | Promise<void>;
45
+ /** Callback erreur. */
46
+ onError?: (kind: string, context: NotificationContext, error: Error) => void | Promise<void>;
47
+ }
48
+ export interface NotificationEngine {
49
+ /** Enregistre / override un template. */
50
+ register(kind: string, template: NotificationTemplate): void;
51
+ /** Retire un template. */
52
+ unregister(kind: string): void;
53
+ /** Liste les kinds enregistrés. */
54
+ registeredKinds(): string[];
55
+ /** Désactive ou réactive un kind sans le supprimer. */
56
+ setEnabled(kind: string, enabled: boolean): void;
57
+ /** Notifie : render + dispatch via mailer. Retourne le résultat (null = skipped). */
58
+ notify(kind: string, context: NotificationContext): Promise<{
59
+ messageId: string;
60
+ } | null>;
61
+ /** Notifie en parallèle plusieurs (kind, context). */
62
+ notifyBatch(items: Array<{
63
+ kind: string;
64
+ context: NotificationContext;
65
+ }>): Promise<Array<{
66
+ messageId: string;
67
+ } | null>>;
68
+ }
69
+ export declare function createNotificationEngine(opts: NotificationEngineOptions): NotificationEngine;
70
+ /**
71
+ * Template simple — function-based. Pas de moteur de template (pas de mustache,
72
+ * pas de handlebars). L'app peut construire ses chaines manuellement OU importer
73
+ * son moteur préféré (ejs, handlebars, etc.) et le wraper dans cette interface.
74
+ */
75
+ export declare function simpleTemplate(spec: {
76
+ to: (ctx: NotificationContext) => string | null;
77
+ subject: (ctx: NotificationContext) => string;
78
+ text?: (ctx: NotificationContext) => string;
79
+ html?: (ctx: NotificationContext) => string;
80
+ messageId?: (ctx: NotificationContext) => string;
81
+ }): NotificationTemplate;
82
+ /**
83
+ * Template multi-locale : utilise `ctx.locale` pour choisir la variant.
84
+ * Si la locale n'est pas dans les variants, fallback sur 'fr' puis 'en'.
85
+ */
86
+ export declare function localizedTemplate(variants: {
87
+ [locale: string]: {
88
+ subject: string | ((ctx: NotificationContext) => string);
89
+ text?: string | ((ctx: NotificationContext) => string);
90
+ html?: string | ((ctx: NotificationContext) => string);
91
+ };
92
+ }, resolveTo: (ctx: NotificationContext) => string | null, resolveMessageId?: (ctx: NotificationContext) => string): NotificationTemplate;
93
+ export declare const moduleInfo: {
94
+ name: string;
95
+ version: string;
96
+ scope: string;
97
+ };
98
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,118 @@
1
+ // @mostajs/notifications — Generic notification engine
2
+ // Author: Dr Hamid MADANI <drmdh@msn.com>
3
+ //
4
+ // Domain-agnostic : accepte n'importe quel `kind: string` (namespacé par convention
5
+ // `<domain>.<event>` : 'booking.reservation.created', 'media.session.started', etc.)
6
+ // et n'importe quel `context: Record<string, unknown>`. Les adapters spécifiques
7
+ // (booking-notifications, media-sfu-notifications) traduisent leurs events
8
+ // internes en `kind + context` et appellent `engine.notify(...)`.
9
+ // ─── Engine ────────────────────────────────────────────────────────────
10
+ export function createNotificationEngine(opts) {
11
+ const templates = new Map(Object.entries(opts.templates ?? {}));
12
+ const disabled = new Set(opts.disabled ?? []);
13
+ const defaultLocale = opts.defaultLocale ?? 'fr';
14
+ async function doNotify(kind, context) {
15
+ if (disabled.has(kind))
16
+ return null;
17
+ const tpl = templates.get(kind);
18
+ if (!tpl)
19
+ return null;
20
+ const ctx = { ...context, locale: context.locale ?? defaultLocale };
21
+ let rendered;
22
+ try {
23
+ rendered = await Promise.resolve(tpl.render(ctx));
24
+ }
25
+ catch (e) {
26
+ await Promise.resolve(opts.onError?.(kind, ctx, e));
27
+ return null;
28
+ }
29
+ if (!rendered)
30
+ return null;
31
+ if (!rendered.to) {
32
+ await Promise.resolve(opts.onError?.(kind, ctx, new Error('Template returned no `to` address')));
33
+ return null;
34
+ }
35
+ const mail = {
36
+ ...rendered,
37
+ from: rendered.from ?? opts.defaultFrom,
38
+ metadata: { ...(rendered.metadata ?? {}), kind, locale: ctx.locale },
39
+ };
40
+ try {
41
+ const result = await opts.mailer.send(mail);
42
+ await Promise.resolve(opts.onSent?.(kind, ctx, result));
43
+ return result;
44
+ }
45
+ catch (e) {
46
+ await Promise.resolve(opts.onError?.(kind, ctx, e));
47
+ return null;
48
+ }
49
+ }
50
+ return {
51
+ register(kind, template) { templates.set(kind, template); },
52
+ unregister(kind) { templates.delete(kind); },
53
+ registeredKinds() { return Array.from(templates.keys()); },
54
+ setEnabled(kind, enabled) {
55
+ if (enabled)
56
+ disabled.delete(kind);
57
+ else
58
+ disabled.add(kind);
59
+ },
60
+ notify: doNotify,
61
+ async notifyBatch(items) {
62
+ return Promise.all(items.map(it => doNotify(it.kind, it.context)));
63
+ },
64
+ };
65
+ }
66
+ // ─── Built-in template helpers ─────────────────────────────────────────
67
+ /**
68
+ * Template simple — function-based. Pas de moteur de template (pas de mustache,
69
+ * pas de handlebars). L'app peut construire ses chaines manuellement OU importer
70
+ * son moteur préféré (ejs, handlebars, etc.) et le wraper dans cette interface.
71
+ */
72
+ export function simpleTemplate(spec) {
73
+ return {
74
+ render(ctx) {
75
+ const to = spec.to(ctx);
76
+ if (!to)
77
+ return null;
78
+ return {
79
+ to,
80
+ subject: spec.subject(ctx),
81
+ text: spec.text?.(ctx),
82
+ html: spec.html?.(ctx),
83
+ messageId: spec.messageId?.(ctx),
84
+ };
85
+ },
86
+ };
87
+ }
88
+ /**
89
+ * Template multi-locale : utilise `ctx.locale` pour choisir la variant.
90
+ * Si la locale n'est pas dans les variants, fallback sur 'fr' puis 'en'.
91
+ */
92
+ export function localizedTemplate(variants, resolveTo, resolveMessageId) {
93
+ return {
94
+ render(ctx) {
95
+ const to = resolveTo(ctx);
96
+ if (!to)
97
+ return null;
98
+ const loc = ctx.locale ?? 'fr';
99
+ const v = variants[loc] ?? variants[loc.split('-')[0]] ?? variants.fr ?? variants.en;
100
+ if (!v)
101
+ return null;
102
+ const resolve = (s) => typeof s === 'function' ? s(ctx) : s;
103
+ return {
104
+ to,
105
+ subject: resolve(v.subject),
106
+ text: resolve(v.text),
107
+ html: resolve(v.html),
108
+ messageId: resolveMessageId?.(ctx),
109
+ };
110
+ },
111
+ };
112
+ }
113
+ export const moduleInfo = {
114
+ name: '@mostajs/notifications',
115
+ version: '0.1.0',
116
+ scope: 'Generic notification engine — domain-agnostic templating + mailer dispatch',
117
+ };
118
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@mostajs/notifications",
3
+ "version": "0.1.0",
4
+ "description": "Generic notification engine for @mostajs/* — domain-agnostic templating + i18n + mailer dispatch + idempotence. Consume any event kind from any source (booking, media-sfu, custom apps). Plug-and-play with @mostajs/mailer.",
5
+ "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
+ "license": "AGPL-3.0-or-later",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "peerDependencies": {
27
+ "@mostajs/mailer": "^0.1.1"
28
+ },
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^6.0.3"
34
+ }
35
+ }