@nathapp/nestjs-notification 2.1.1 → 2.2.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/UPGRADE-PLAN-PHASE7-8.md +307 -0
- package/UPGRADE-PLAN-PHASE9.md +361 -0
- package/UPGRADE-PLAN.md +425 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +20 -2
- package/dist/index.js.map +1 -1
- package/dist/interface/index.d.ts +3 -3
- package/dist/interface/message.interface.d.ts +10 -2
- package/dist/interface/payload.inteface.d.ts +51 -0
- package/dist/notification/errors.d.ts +3 -0
- package/dist/notification/errors.js +11 -0
- package/dist/notification/errors.js.map +1 -0
- package/dist/notification/index.d.ts +6 -2
- package/dist/notification/index.js +7 -1
- package/dist/notification/index.js.map +1 -1
- package/dist/notification/notification-provider.factory.d.ts +5 -0
- package/dist/notification/notification-provider.factory.js +33 -0
- package/dist/notification/notification-provider.factory.js.map +1 -0
- package/dist/notification/notification.interceptor.d.ts +18 -0
- package/dist/notification/notification.interceptor.js +5 -0
- package/dist/notification/notification.interceptor.js.map +1 -0
- package/dist/notification/notification.module.d.ts +28 -18
- package/dist/notification/notification.module.js +324 -63
- package/dist/notification/notification.module.js.map +1 -1
- package/dist/notification/notification.service.d.ts +18 -2
- package/dist/notification/notification.service.js +274 -13
- package/dist/notification/notification.service.js.map +1 -1
- package/dist/notification/provider-config.d.ts +17 -0
- package/dist/notification/provider-config.js +3 -0
- package/dist/notification/provider-config.js.map +1 -0
- package/dist/push-notification/push-notification.service.js +2 -1
- package/dist/push-notification/push-notification.service.js.map +1 -1
- package/dist/template/constant.d.ts +1 -0
- package/dist/template/constant.js +2 -1
- package/dist/template/constant.js.map +1 -1
- package/dist/template/engines/string-format.engine.d.ts +6 -0
- package/dist/template/engines/string-format.engine.js +17 -0
- package/dist/template/engines/string-format.engine.js.map +1 -0
- package/dist/template/index.d.ts +4 -2
- package/dist/template/index.js +4 -1
- package/dist/template/index.js.map +1 -1
- package/dist/template/template-engine.d.ts +5 -0
- package/dist/template/template-engine.js +3 -0
- package/dist/template/template-engine.js.map +1 -0
- package/dist/template/template.loader.d.ts +4 -1
- package/dist/template/template.loader.js +22 -14
- package/dist/template/template.loader.js.map +1 -1
- package/dist/whatsapp/constant.d.ts +1 -0
- package/dist/whatsapp/constant.js +5 -0
- package/dist/whatsapp/constant.js.map +1 -0
- package/dist/whatsapp/index.d.ts +2 -0
- package/dist/whatsapp/index.js +19 -0
- package/dist/whatsapp/index.js.map +1 -0
- package/dist/whatsapp/whatsapp.service.d.ts +7 -0
- package/dist/whatsapp/whatsapp.service.js +7 -0
- package/dist/whatsapp/whatsapp.service.js.map +1 -0
- package/package.json +2 -1
package/UPGRADE-PLAN.md
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# @nathapp/nestjs-notification Enterprise Upgrade Plan
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Upgrade `@nathapp/nestjs-notification` (v2.1.4) to be more enterprise-grade, extensible, and plugin-friendly while maintaining full backwards compatibility.
|
|
6
|
+
|
|
7
|
+
**Branch:** `feat/notification-enterprise-upgrade`
|
|
8
|
+
**Scope:** Core library only (`packages/nestjs-notification`). No business logic, no tenant context.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Phase 1: Pluggable Template Engine
|
|
13
|
+
|
|
14
|
+
### Problem
|
|
15
|
+
`TemplateLoader.resolve()` hardcodes `string-format` for variable interpolation. Apps needing conditionals, loops, or partials (e.g., Handlebars) must override the entire `resolve()` method.
|
|
16
|
+
|
|
17
|
+
### Changes
|
|
18
|
+
|
|
19
|
+
#### 1.1 Create `TemplateEngine` interface
|
|
20
|
+
**File:** `lib/template/template-engine.ts` (NEW)
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
export interface TemplateEngine {
|
|
24
|
+
/**
|
|
25
|
+
* Compile and render a template string with the given context.
|
|
26
|
+
* @param template - Raw template string (e.g., "Hello {name}" or "Hello {{name}}")
|
|
27
|
+
* @param context - Key-value pairs for interpolation
|
|
28
|
+
* @returns Rendered string
|
|
29
|
+
*/
|
|
30
|
+
render(template: string, context: Record<string, any>): string;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
#### 1.2 Create `StringFormatEngine` (default)
|
|
35
|
+
**File:** `lib/template/engines/string-format.engine.ts` (NEW)
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import Format from 'string-format';
|
|
39
|
+
import { TemplateEngine } from '../template-engine';
|
|
40
|
+
|
|
41
|
+
export class StringFormatEngine implements TemplateEngine {
|
|
42
|
+
render(template: string, context: Record<string, any>): string {
|
|
43
|
+
return Format(template, context);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### 1.3 Create `TEMPLATE_ENGINE` injection token
|
|
49
|
+
**File:** `lib/template/constant.ts` (UPDATE)
|
|
50
|
+
|
|
51
|
+
Add: `export const TEMPLATE_ENGINE = 'TEMPLATE_ENGINE';`
|
|
52
|
+
|
|
53
|
+
#### 1.4 Update `TemplateLoader.resolve()` to use engine
|
|
54
|
+
**File:** `lib/template/template.loader.ts` (UPDATE)
|
|
55
|
+
|
|
56
|
+
- Inject `TemplateEngine` (optional, defaults to `StringFormatEngine`)
|
|
57
|
+
- Replace direct `Format()` calls with `this.engine.render()`
|
|
58
|
+
- `CachableTemplateLoader` same treatment
|
|
59
|
+
- **Backwards compat:** If no engine injected, fall back to `StringFormatEngine` internally
|
|
60
|
+
|
|
61
|
+
#### 1.5 Update `NotificationModule` to accept `templateEngine` option
|
|
62
|
+
**File:** `lib/notification/notification.module.ts` (UPDATE)
|
|
63
|
+
|
|
64
|
+
- Add `templateEngine?: Type<TemplateEngine> | ProviderAsyncOptions<TemplateEngine>` to module options
|
|
65
|
+
- Default to `StringFormatEngine` if not provided
|
|
66
|
+
- Register as provider with `TEMPLATE_ENGINE` token
|
|
67
|
+
|
|
68
|
+
#### 1.6 Export new types
|
|
69
|
+
**File:** `lib/template/index.ts` (UPDATE)
|
|
70
|
+
**File:** `lib/index.ts` (UPDATE)
|
|
71
|
+
|
|
72
|
+
Export `TemplateEngine`, `StringFormatEngine`, `TEMPLATE_ENGINE`.
|
|
73
|
+
|
|
74
|
+
### Unit Tests
|
|
75
|
+
|
|
76
|
+
**File:** `test/template/string-format.engine.spec.ts` (NEW)
|
|
77
|
+
- `render()` with simple variables
|
|
78
|
+
- `render()` with nested objects
|
|
79
|
+
- `render()` with missing variables (should not throw, leave placeholder)
|
|
80
|
+
|
|
81
|
+
**File:** `test/template/template-loader.spec.ts` (NEW)
|
|
82
|
+
- `resolve()` uses injected `TemplateEngine`
|
|
83
|
+
- `resolve()` defaults to `StringFormatEngine` when no engine provided
|
|
84
|
+
- `CachableTemplateLoader.resolve()` uses engine after cache hit
|
|
85
|
+
- `CachableTemplateLoader.resolve()` uses engine after cache miss
|
|
86
|
+
|
|
87
|
+
### E2E Tests
|
|
88
|
+
|
|
89
|
+
**File:** `test/module/notification-custom-engine.module.e2e.spec.ts` (NEW)
|
|
90
|
+
- Register `NotificationModule.forRoot()` with a custom `TemplateEngine` (e.g., a mock Handlebars-like engine)
|
|
91
|
+
- Verify `NotificationService.sendEmail()` renders templates through the custom engine
|
|
92
|
+
- Verify default `StringFormatEngine` works when no custom engine specified
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Phase 2: Configurable Strict Mode
|
|
97
|
+
|
|
98
|
+
### Problem
|
|
99
|
+
When `EmailService` or `PushNotificationService` isn't configured, `NotificationService` silently returns `{ success: false }`. This hides configuration bugs.
|
|
100
|
+
|
|
101
|
+
### Changes
|
|
102
|
+
|
|
103
|
+
#### 2.1 Add `strict` option to module options
|
|
104
|
+
**File:** `lib/notification/notification.module.ts` (UPDATE)
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
export interface NotificationModuleOptions {
|
|
108
|
+
// ... existing
|
|
109
|
+
strict?: boolean; // default: true
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### 2.2 Update `NotificationService` to respect strict mode
|
|
114
|
+
**File:** `lib/notification/notification.service.ts` (UPDATE)
|
|
115
|
+
|
|
116
|
+
- Inject `NOTIFICATION_OPTIONS` to read `strict` flag
|
|
117
|
+
- When `strict: true` (default) and service not configured → throw `Error('EmailService is not configured. Configure emailOptions in NotificationModule or set strict: false.')`
|
|
118
|
+
- When `strict: false` → keep current behaviour (return `{ success: false }`)
|
|
119
|
+
|
|
120
|
+
#### 2.3 Create custom error class
|
|
121
|
+
**File:** `lib/notification/errors.ts` (NEW)
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
export class NotificationConfigError extends Error {
|
|
125
|
+
constructor(service: string) {
|
|
126
|
+
super(`${service} is not configured. Configure it in NotificationModule or set strict: false.`);
|
|
127
|
+
this.name = 'NotificationConfigError';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Unit Tests
|
|
133
|
+
|
|
134
|
+
**File:** `test/notification/notification-strict.service.spec.ts` (NEW)
|
|
135
|
+
- `strict: true` + no email configured → `sendEmail()` throws `NotificationConfigError`
|
|
136
|
+
- `strict: true` + no push configured → `sendPushNotification()` throws `NotificationConfigError`
|
|
137
|
+
- `strict: false` + no email configured → `sendEmail()` returns `{ success: false }`
|
|
138
|
+
- `strict: false` + no push configured → `sendPushNotification()` returns `{ success: false }`
|
|
139
|
+
- Default (no strict option) → behaves as `strict: true`
|
|
140
|
+
|
|
141
|
+
### E2E Tests
|
|
142
|
+
|
|
143
|
+
**File:** `test/module/notification-strict.module.e2e.spec.ts` (NEW)
|
|
144
|
+
- Module with `strict: true`, no email options → calling `sendEmail` throws
|
|
145
|
+
- Module with `strict: false`, no email options → calling `sendEmail` returns `{ success: false }`
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Phase 3: Unified `send()` Method
|
|
150
|
+
|
|
151
|
+
### Problem
|
|
152
|
+
Callers must use separate methods (`sendEmail`, `sendPushNotification`) per channel. No way to send to multiple channels in one call.
|
|
153
|
+
|
|
154
|
+
### Changes
|
|
155
|
+
|
|
156
|
+
#### 3.1 Create unified payload interface
|
|
157
|
+
**File:** `lib/interface/payload.inteface.ts` (UPDATE - add to existing)
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
export type NotificationChannel = 'email' | 'push' | 'sms';
|
|
161
|
+
|
|
162
|
+
export interface NotificationPayload {
|
|
163
|
+
templateCode: string;
|
|
164
|
+
channels: NotificationChannel[];
|
|
165
|
+
options?: {
|
|
166
|
+
locale?: string;
|
|
167
|
+
fallbackLocale?: string;
|
|
168
|
+
args?: any;
|
|
169
|
+
};
|
|
170
|
+
// Channel-specific recipient data
|
|
171
|
+
email?: string;
|
|
172
|
+
userId?: string | number;
|
|
173
|
+
deviceToken?: DeviceToken;
|
|
174
|
+
phoneNumber?: string;
|
|
175
|
+
// Optional
|
|
176
|
+
attachments?: EmailAttachment[];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface NotificationResult {
|
|
180
|
+
results: Record<NotificationChannel, SendResult>;
|
|
181
|
+
/** True only if ALL channels succeeded */
|
|
182
|
+
success: boolean;
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### 3.2 Add `send()` to `NotificationService`
|
|
187
|
+
**File:** `lib/notification/notification.service.ts` (UPDATE)
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
async send(payload: NotificationPayload): Promise<NotificationResult> {
|
|
191
|
+
const results: Record<string, SendResult> = {};
|
|
192
|
+
|
|
193
|
+
for (const channel of payload.channels) {
|
|
194
|
+
switch (channel) {
|
|
195
|
+
case 'email':
|
|
196
|
+
results.email = await this.sendEmail({
|
|
197
|
+
templateCode: payload.templateCode,
|
|
198
|
+
email: payload.email,
|
|
199
|
+
options: payload.options,
|
|
200
|
+
attachments: payload.attachments,
|
|
201
|
+
});
|
|
202
|
+
break;
|
|
203
|
+
case 'push':
|
|
204
|
+
results.push = await this.sendPushNotification({
|
|
205
|
+
templateCode: payload.templateCode,
|
|
206
|
+
userId: payload.userId,
|
|
207
|
+
deviceToken: payload.deviceToken,
|
|
208
|
+
options: payload.options,
|
|
209
|
+
});
|
|
210
|
+
break;
|
|
211
|
+
case 'sms':
|
|
212
|
+
// Delegate to SmsService when available
|
|
213
|
+
results.sms = { success: false, message: 'SMS not configured' };
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
results: results as Record<NotificationChannel, SendResult>,
|
|
220
|
+
success: Object.values(results).every(r => r.success),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
#### 3.3 Export new types
|
|
226
|
+
**File:** `lib/interface/index.ts` (UPDATE)
|
|
227
|
+
|
|
228
|
+
Export `NotificationPayload`, `NotificationResult`, `NotificationChannel`.
|
|
229
|
+
|
|
230
|
+
### Unit Tests
|
|
231
|
+
|
|
232
|
+
**File:** `test/notification/notification-unified-send.service.spec.ts` (NEW)
|
|
233
|
+
- `send()` with `channels: ['email']` → calls `sendEmail`, returns result
|
|
234
|
+
- `send()` with `channels: ['push']` → calls `sendPushNotification`, returns result
|
|
235
|
+
- `send()` with `channels: ['email', 'push']` → calls both, aggregates results
|
|
236
|
+
- `send()` with `channels: ['sms']` → returns `{ success: false }` for sms
|
|
237
|
+
- `send()` overall `success` is `true` only when all channels succeed
|
|
238
|
+
- `send()` overall `success` is `false` when any channel fails
|
|
239
|
+
|
|
240
|
+
### E2E Tests
|
|
241
|
+
|
|
242
|
+
**File:** `test/module/notification-unified-send.module.e2e.spec.ts` (NEW)
|
|
243
|
+
- Full module with email configured → `send({ channels: ['email'] })` works end-to-end
|
|
244
|
+
- Full module with email + push → `send({ channels: ['email', 'push'] })` dispatches to both
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Phase 4: Backwards-Compatible `platform` Alias
|
|
249
|
+
|
|
250
|
+
### Problem
|
|
251
|
+
`PushMessage.paltform` is a typo. Fixing it directly is a breaking change.
|
|
252
|
+
|
|
253
|
+
### Changes
|
|
254
|
+
|
|
255
|
+
#### 4.1 Add `platform` property, deprecate `paltform`
|
|
256
|
+
**File:** `lib/interface/message.interface.ts` (UPDATE)
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
export interface PushMessage extends BaseMessage {
|
|
260
|
+
/** @deprecated Use `platform` instead */
|
|
261
|
+
paltform?: PlatformOS;
|
|
262
|
+
platform?: PlatformOS;
|
|
263
|
+
subject: string;
|
|
264
|
+
data?: { [key: string]: string };
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
#### 4.2 Update all internal usages to prefer `platform`
|
|
269
|
+
**Files:** `lib/push-notification/push-notification.service.ts`, `lib/notification/notification.service.ts`
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
// Resolve with fallback
|
|
273
|
+
const platform = message.platform || message.paltform;
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Unit Tests
|
|
277
|
+
|
|
278
|
+
**File:** `test/push-notification/platform-alias.spec.ts` (NEW)
|
|
279
|
+
- Message with `platform: 'android'` → uses android provider
|
|
280
|
+
- Message with `paltform: 'android'` (legacy) → still works
|
|
281
|
+
- Message with both `platform` and `paltform` → `platform` takes precedence
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Phase 5: SMS Integration Point
|
|
286
|
+
|
|
287
|
+
### Problem
|
|
288
|
+
`SmsService` is abstract with no integration into `NotificationModule` or `NotificationService`.
|
|
289
|
+
|
|
290
|
+
### Changes
|
|
291
|
+
|
|
292
|
+
#### 5.1 Add `SmsService` injection to `NotificationService`
|
|
293
|
+
**File:** `lib/notification/notification.service.ts` (UPDATE)
|
|
294
|
+
|
|
295
|
+
- Add `@Optional() @Inject(SmsService) private readonly smsService: SmsService` to constructor
|
|
296
|
+
- Implement `sendSms(data: SmsNotificationPayload): Promise<SendResult>` using template resolution + `smsService.send()`
|
|
297
|
+
- Update `send()` unified method to use `sendSms` for SMS channel
|
|
298
|
+
|
|
299
|
+
#### 5.2 Add SMS payload interface
|
|
300
|
+
**File:** `lib/interface/payload.inteface.ts` (UPDATE)
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
export interface SmsNotificationPayload {
|
|
304
|
+
phoneNumber: string;
|
|
305
|
+
templateCode: string;
|
|
306
|
+
options?: {
|
|
307
|
+
locale?: string;
|
|
308
|
+
fallbackLocale?: string;
|
|
309
|
+
args?: any;
|
|
310
|
+
};
|
|
311
|
+
countryIso2?: string;
|
|
312
|
+
dialCode?: string;
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
#### 5.3 Add SMS options to `NotificationModule`
|
|
317
|
+
**File:** `lib/notification/notification.module.ts` (UPDATE)
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
export interface NotificationModuleOptions {
|
|
321
|
+
// ... existing
|
|
322
|
+
smsService?: Type<SmsService>;
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
#### 5.4 Export new types
|
|
327
|
+
**File:** `lib/sms/index.ts` (UPDATE)
|
|
328
|
+
**File:** `lib/interface/index.ts` (UPDATE)
|
|
329
|
+
|
|
330
|
+
### Unit Tests
|
|
331
|
+
|
|
332
|
+
**File:** `test/notification/notification-sms.service.spec.ts` (NEW)
|
|
333
|
+
- `sendSms()` with configured SMS service → resolves template, calls `smsService.send()`
|
|
334
|
+
- `sendSms()` without SMS service + `strict: true` → throws
|
|
335
|
+
- `sendSms()` without SMS service + `strict: false` → returns `{ success: false }`
|
|
336
|
+
- `send({ channels: ['sms'] })` routes to `sendSms()`
|
|
337
|
+
|
|
338
|
+
### E2E Tests
|
|
339
|
+
|
|
340
|
+
**File:** `test/module/notification-sms.module.e2e.spec.ts` (NEW)
|
|
341
|
+
- Register module with a mock `SmsService` implementation → `sendSms()` works
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Phase 6: Interceptors / Hooks (beforeSend, afterSend)
|
|
346
|
+
|
|
347
|
+
### Problem
|
|
348
|
+
No way to globally intercept notifications for logging, auditing, or message modification.
|
|
349
|
+
|
|
350
|
+
### Changes
|
|
351
|
+
|
|
352
|
+
#### 6.1 Create `NotificationInterceptor` interface
|
|
353
|
+
**File:** `lib/notification/notification.interceptor.ts` (NEW)
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
export interface NotificationContext {
|
|
357
|
+
channel: NotificationChannel;
|
|
358
|
+
templateCode: string;
|
|
359
|
+
recipient: string;
|
|
360
|
+
resolvedMessage?: { subject?: string; content: string };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export interface NotificationInterceptor {
|
|
364
|
+
beforeSend?(context: NotificationContext): Promise<NotificationContext> | NotificationContext;
|
|
365
|
+
afterSend?(context: NotificationContext, result: SendResult): Promise<void> | void;
|
|
366
|
+
onError?(context: NotificationContext, error: Error): Promise<void> | void;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export const NOTIFICATION_INTERCEPTORS = 'NOTIFICATION_INTERCEPTORS';
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
#### 6.2 Update `NotificationService` to run interceptors
|
|
373
|
+
**File:** `lib/notification/notification.service.ts` (UPDATE)
|
|
374
|
+
|
|
375
|
+
- Inject `@Optional() @Inject(NOTIFICATION_INTERCEPTORS) interceptors: NotificationInterceptor[]`
|
|
376
|
+
- Before each send: run all `beforeSend` hooks (allows modifying context)
|
|
377
|
+
- After each send: run all `afterSend` hooks
|
|
378
|
+
- On error: run all `onError` hooks
|
|
379
|
+
|
|
380
|
+
#### 6.3 Add `interceptors` option to module
|
|
381
|
+
**File:** `lib/notification/notification.module.ts` (UPDATE)
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
export interface NotificationModuleOptions {
|
|
385
|
+
// ... existing
|
|
386
|
+
interceptors?: Type<NotificationInterceptor>[];
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Unit Tests
|
|
391
|
+
|
|
392
|
+
**File:** `test/notification/notification-interceptor.spec.ts` (NEW)
|
|
393
|
+
- `beforeSend` is called before provider send
|
|
394
|
+
- `beforeSend` can modify context (e.g., append unsubscribe link)
|
|
395
|
+
- `afterSend` is called after successful send with result
|
|
396
|
+
- `onError` is called when send fails
|
|
397
|
+
- Multiple interceptors run in order
|
|
398
|
+
- No interceptors registered → sends normally (no errors)
|
|
399
|
+
|
|
400
|
+
### E2E Tests
|
|
401
|
+
|
|
402
|
+
**File:** `test/module/notification-interceptor.module.e2e.spec.ts` (NEW)
|
|
403
|
+
- Register module with a logging interceptor → verify hooks called during `sendEmail()`
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## Implementation Order
|
|
408
|
+
|
|
409
|
+
1. **Phase 1** (Template Engine) — Foundation, no breaking changes
|
|
410
|
+
2. **Phase 2** (Strict Mode) — Defaults change but existing `strict: false` opt-out available
|
|
411
|
+
3. **Phase 4** (Platform Alias) — Quick fix, no breaking changes
|
|
412
|
+
4. **Phase 3** (Unified Send) — New method, additive
|
|
413
|
+
5. **Phase 5** (SMS Integration) — Additive
|
|
414
|
+
6. **Phase 6** (Interceptors) — Additive
|
|
415
|
+
|
|
416
|
+
## Breaking Changes
|
|
417
|
+
|
|
418
|
+
**None.** All changes are additive or have backwards-compatible defaults:
|
|
419
|
+
- `strict` defaults to `true` which IS a behaviour change, but it only surfaces existing bugs (calling unconfigured services). Can be set to `false` to restore old behaviour.
|
|
420
|
+
- `paltform` is deprecated but still works.
|
|
421
|
+
- `string-format` remains the default template engine.
|
|
422
|
+
|
|
423
|
+
## Version
|
|
424
|
+
|
|
425
|
+
This should be released as **v3.0.0** due to the `strict: true` default change (even though technically backwards compat, it changes runtime behaviour).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { EMAIL_OPTIONS, EMAIL_PROVIDER, EMAIL_SERVICE, EmailOption, SmtpOption, SmtpHostOption, SmtpUriOption, CustomEmailOption, EmailService, EmailProvider, SmtpEmailProvider, EmailProviderFactory, } from "./email";
|
|
2
|
+
export { PushNotificationService, FCMPushNotificationOption, PushNotificationOption, FCMPushNotificationProvider, PushNotificationProvider, PushNotificationPlatform, PlatformOS, ProviderManager, PUSH_NOTIFICATION_PROVIDERS, DefaultProviderManager, DeviceToken, TokenId, IUserTokenStore, UserTokenStore, InMemoryUserTokenStore, } from "./push-notification";
|
|
2
3
|
export * from "./interface";
|
|
3
4
|
export * from "./notification";
|
|
4
|
-
export * from "./push-notification";
|
|
5
5
|
export * from "./sms";
|
|
6
6
|
export * from "./template";
|
|
7
|
+
export * from "./whatsapp";
|
package/dist/index.js
CHANGED
|
@@ -14,10 +14,28 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
|
|
17
|
+
exports.InMemoryUserTokenStore = exports.UserTokenStore = exports.DefaultProviderManager = exports.PUSH_NOTIFICATION_PROVIDERS = exports.ProviderManager = exports.PlatformOS = exports.PushNotificationProvider = exports.FCMPushNotificationProvider = exports.PushNotificationService = exports.EmailProviderFactory = exports.SmtpEmailProvider = exports.EmailProvider = exports.EmailService = exports.EMAIL_SERVICE = exports.EMAIL_PROVIDER = exports.EMAIL_OPTIONS = void 0;
|
|
18
|
+
var email_1 = require("./email");
|
|
19
|
+
Object.defineProperty(exports, "EMAIL_OPTIONS", { enumerable: true, get: function () { return email_1.EMAIL_OPTIONS; } });
|
|
20
|
+
Object.defineProperty(exports, "EMAIL_PROVIDER", { enumerable: true, get: function () { return email_1.EMAIL_PROVIDER; } });
|
|
21
|
+
Object.defineProperty(exports, "EMAIL_SERVICE", { enumerable: true, get: function () { return email_1.EMAIL_SERVICE; } });
|
|
22
|
+
Object.defineProperty(exports, "EmailService", { enumerable: true, get: function () { return email_1.EmailService; } });
|
|
23
|
+
Object.defineProperty(exports, "EmailProvider", { enumerable: true, get: function () { return email_1.EmailProvider; } });
|
|
24
|
+
Object.defineProperty(exports, "SmtpEmailProvider", { enumerable: true, get: function () { return email_1.SmtpEmailProvider; } });
|
|
25
|
+
Object.defineProperty(exports, "EmailProviderFactory", { enumerable: true, get: function () { return email_1.EmailProviderFactory; } });
|
|
26
|
+
var push_notification_1 = require("./push-notification");
|
|
27
|
+
Object.defineProperty(exports, "PushNotificationService", { enumerable: true, get: function () { return push_notification_1.PushNotificationService; } });
|
|
28
|
+
Object.defineProperty(exports, "FCMPushNotificationProvider", { enumerable: true, get: function () { return push_notification_1.FCMPushNotificationProvider; } });
|
|
29
|
+
Object.defineProperty(exports, "PushNotificationProvider", { enumerable: true, get: function () { return push_notification_1.PushNotificationProvider; } });
|
|
30
|
+
Object.defineProperty(exports, "PlatformOS", { enumerable: true, get: function () { return push_notification_1.PlatformOS; } });
|
|
31
|
+
Object.defineProperty(exports, "ProviderManager", { enumerable: true, get: function () { return push_notification_1.ProviderManager; } });
|
|
32
|
+
Object.defineProperty(exports, "PUSH_NOTIFICATION_PROVIDERS", { enumerable: true, get: function () { return push_notification_1.PUSH_NOTIFICATION_PROVIDERS; } });
|
|
33
|
+
Object.defineProperty(exports, "DefaultProviderManager", { enumerable: true, get: function () { return push_notification_1.DefaultProviderManager; } });
|
|
34
|
+
Object.defineProperty(exports, "UserTokenStore", { enumerable: true, get: function () { return push_notification_1.UserTokenStore; } });
|
|
35
|
+
Object.defineProperty(exports, "InMemoryUserTokenStore", { enumerable: true, get: function () { return push_notification_1.InMemoryUserTokenStore; } });
|
|
18
36
|
__exportStar(require("./interface"), exports);
|
|
19
37
|
__exportStar(require("./notification"), exports);
|
|
20
|
-
__exportStar(require("./push-notification"), exports);
|
|
21
38
|
__exportStar(require("./sms"), exports);
|
|
22
39
|
__exportStar(require("./template"), exports);
|
|
40
|
+
__exportStar(require("./whatsapp"), exports);
|
|
23
41
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AACA,iCAaiB;AAZf,sGAAA,aAAa,OAAA;AACb,uGAAA,cAAc,OAAA;AACd,sGAAA,aAAa,OAAA;AAMb,qGAAA,YAAY,OAAA;AACZ,sGAAA,aAAa,OAAA;AACb,0GAAA,iBAAiB,OAAA;AACjB,6GAAA,oBAAoB,OAAA;AAItB,yDAgB6B;AAf3B,4HAAA,uBAAuB,OAAA;AAGvB,gIAAA,2BAA2B,OAAA;AAC3B,6HAAA,wBAAwB,OAAA;AAExB,+GAAA,UAAU,OAAA;AACV,oHAAA,eAAe,OAAA;AACf,gIAAA,2BAA2B,OAAA;AAC3B,2HAAA,sBAAsB,OAAA;AAItB,mHAAA,cAAc,OAAA;AACd,2HAAA,sBAAsB,OAAA;AAIxB,8CAA4B;AAC5B,iDAA+B;AAC/B,wCAAsB;AACtB,6CAA2B;AAC3B,6CAA2B"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseMessage, SmsMessage, EmailMessage, PushMessage, Message, PushMessageOnly, EmailAttachment } from "./message.interface";
|
|
2
|
-
import { EmailNotificationPayload, PushNotificationPayload } from "./payload.inteface";
|
|
1
|
+
import { BaseMessage, SmsMessage, EmailMessage, PushMessage, WhatsAppMessage, Message, PushMessageOnly, EmailAttachment } from "./message.interface";
|
|
2
|
+
import { EmailNotificationPayload, PushNotificationPayload, SmsNotificationPayload, WhatsAppNotificationPayload, NotificationPayload, NotificationResult, NotificationChannel } from "./payload.inteface";
|
|
3
3
|
import { BatchSendResult, SendResult } from "./result.interface";
|
|
4
|
-
export { BaseMessage, SmsMessage, EmailMessage, PushMessage, PushMessageOnly, Message, SendResult, BatchSendResult, EmailAttachment, EmailNotificationPayload, PushNotificationPayload, };
|
|
4
|
+
export { BaseMessage, SmsMessage, EmailMessage, PushMessage, WhatsAppMessage, PushMessageOnly, Message, SendResult, BatchSendResult, EmailAttachment, EmailNotificationPayload, PushNotificationPayload, SmsNotificationPayload, WhatsAppNotificationPayload, NotificationPayload, NotificationResult, NotificationChannel, };
|
|
@@ -19,10 +19,18 @@ export interface EmailMessage extends BaseMessage {
|
|
|
19
19
|
}
|
|
20
20
|
export interface PushMessage extends BaseMessage {
|
|
21
21
|
paltform?: PlatformOS;
|
|
22
|
+
platform?: PlatformOS;
|
|
22
23
|
subject: string;
|
|
23
24
|
data?: {
|
|
24
25
|
[key: string]: string;
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
|
-
export
|
|
28
|
-
|
|
28
|
+
export interface WhatsAppMessage extends BaseMessage {
|
|
29
|
+
templateName?: string;
|
|
30
|
+
languageCode?: string;
|
|
31
|
+
headerParams?: string[];
|
|
32
|
+
bodyParams?: string[];
|
|
33
|
+
mediaUrl?: string;
|
|
34
|
+
}
|
|
35
|
+
export type PushMessageOnly = Omit<PushMessage, "recipient" | "paltform" | "platform">;
|
|
36
|
+
export type Message = SmsMessage | EmailMessage | PushMessage | WhatsAppMessage;
|
|
@@ -20,3 +20,54 @@ export interface PushNotificationPayload {
|
|
|
20
20
|
args?: any;
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
+
export type NotificationChannel = 'email' | 'push' | 'sms' | 'whatsapp';
|
|
24
|
+
export interface NotificationPayload {
|
|
25
|
+
templateCode: string;
|
|
26
|
+
channels: NotificationChannel[];
|
|
27
|
+
fallback?: NotificationChannel[];
|
|
28
|
+
options?: {
|
|
29
|
+
locale?: string;
|
|
30
|
+
fallbackLocale?: string;
|
|
31
|
+
args?: any;
|
|
32
|
+
};
|
|
33
|
+
email?: string;
|
|
34
|
+
userId?: string | number;
|
|
35
|
+
deviceToken?: DeviceToken;
|
|
36
|
+
phoneNumber?: string;
|
|
37
|
+
whatsappTemplateName?: string;
|
|
38
|
+
mediaUrl?: string;
|
|
39
|
+
attachments?: EmailAttachment[];
|
|
40
|
+
}
|
|
41
|
+
export interface SendResult {
|
|
42
|
+
success: boolean;
|
|
43
|
+
message?: string;
|
|
44
|
+
error?: any;
|
|
45
|
+
}
|
|
46
|
+
export interface SmsNotificationPayload {
|
|
47
|
+
phoneNumber: string;
|
|
48
|
+
templateCode: string;
|
|
49
|
+
options?: {
|
|
50
|
+
locale?: string;
|
|
51
|
+
fallbackLocale?: string;
|
|
52
|
+
args?: any;
|
|
53
|
+
};
|
|
54
|
+
countryIso2?: string;
|
|
55
|
+
dialCode?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface WhatsAppNotificationPayload {
|
|
58
|
+
phoneNumber: string;
|
|
59
|
+
templateCode: string;
|
|
60
|
+
options?: {
|
|
61
|
+
locale?: string;
|
|
62
|
+
fallbackLocale?: string;
|
|
63
|
+
args?: any;
|
|
64
|
+
};
|
|
65
|
+
whatsappTemplateName?: string;
|
|
66
|
+
mediaUrl?: string;
|
|
67
|
+
}
|
|
68
|
+
export interface NotificationResult {
|
|
69
|
+
results: Partial<Record<NotificationChannel, SendResult>>;
|
|
70
|
+
success: boolean;
|
|
71
|
+
fallbackAttempted?: NotificationChannel[];
|
|
72
|
+
fallbackSucceeded?: NotificationChannel;
|
|
73
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NotificationConfigError = void 0;
|
|
4
|
+
class NotificationConfigError extends Error {
|
|
5
|
+
constructor(service) {
|
|
6
|
+
super(`${service} is not configured. Configure it in NotificationModule or set strict: false.`);
|
|
7
|
+
this.name = 'NotificationConfigError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.NotificationConfigError = NotificationConfigError;
|
|
11
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../lib/notification/errors.ts"],"names":[],"mappings":";;;AAAA,MAAa,uBAAwB,SAAQ,KAAK;IAChD,YAAY,OAAe;QACzB,KAAK,CAAC,GAAG,OAAO,8EAA8E,CAAC,CAAC;QAChG,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AALD,0DAKC"}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { NOTIFICATION_OPTIONS, NOTIFICATION_SERVICE, TEMPLATE_SERVICE } from "./constant";
|
|
2
|
-
import { NotificationModule, NotificationModuleOptions } from "./notification.module";
|
|
2
|
+
import { NotificationModule, NotificationModuleAsyncOptions, NotificationModuleOptions, NotificationModuleOptionsFactory } from "./notification.module";
|
|
3
3
|
import { NotificationService } from "./notification.service";
|
|
4
|
-
|
|
4
|
+
import { NotificationConfigError } from "./errors";
|
|
5
|
+
import { NotificationInterceptor, NotificationContext, NOTIFICATION_INTERCEPTORS } from "./notification.interceptor";
|
|
6
|
+
import { ProviderConfig, ChannelProviderMap } from "./provider-config";
|
|
7
|
+
import { NotificationProviderFactory } from "./notification-provider.factory";
|
|
8
|
+
export { NOTIFICATION_OPTIONS, NOTIFICATION_SERVICE, TEMPLATE_SERVICE, NotificationModule, NotificationModuleOptions, NotificationModuleAsyncOptions, NotificationModuleOptionsFactory, NotificationService, NotificationConfigError, NotificationInterceptor, NotificationContext, NOTIFICATION_INTERCEPTORS, ProviderConfig, ChannelProviderMap, NotificationProviderFactory, };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.NotificationService = exports.NotificationModule = exports.TEMPLATE_SERVICE = exports.NOTIFICATION_SERVICE = exports.NOTIFICATION_OPTIONS = void 0;
|
|
3
|
+
exports.NotificationProviderFactory = exports.NOTIFICATION_INTERCEPTORS = exports.NotificationConfigError = exports.NotificationService = exports.NotificationModule = exports.TEMPLATE_SERVICE = exports.NOTIFICATION_SERVICE = exports.NOTIFICATION_OPTIONS = void 0;
|
|
4
4
|
const constant_1 = require("./constant");
|
|
5
5
|
Object.defineProperty(exports, "NOTIFICATION_OPTIONS", { enumerable: true, get: function () { return constant_1.NOTIFICATION_OPTIONS; } });
|
|
6
6
|
Object.defineProperty(exports, "NOTIFICATION_SERVICE", { enumerable: true, get: function () { return constant_1.NOTIFICATION_SERVICE; } });
|
|
@@ -9,4 +9,10 @@ const notification_module_1 = require("./notification.module");
|
|
|
9
9
|
Object.defineProperty(exports, "NotificationModule", { enumerable: true, get: function () { return notification_module_1.NotificationModule; } });
|
|
10
10
|
const notification_service_1 = require("./notification.service");
|
|
11
11
|
Object.defineProperty(exports, "NotificationService", { enumerable: true, get: function () { return notification_service_1.NotificationService; } });
|
|
12
|
+
const errors_1 = require("./errors");
|
|
13
|
+
Object.defineProperty(exports, "NotificationConfigError", { enumerable: true, get: function () { return errors_1.NotificationConfigError; } });
|
|
14
|
+
const notification_interceptor_1 = require("./notification.interceptor");
|
|
15
|
+
Object.defineProperty(exports, "NOTIFICATION_INTERCEPTORS", { enumerable: true, get: function () { return notification_interceptor_1.NOTIFICATION_INTERCEPTORS; } });
|
|
16
|
+
const notification_provider_factory_1 = require("./notification-provider.factory");
|
|
17
|
+
Object.defineProperty(exports, "NotificationProviderFactory", { enumerable: true, get: function () { return notification_provider_factory_1.NotificationProviderFactory; } });
|
|
12
18
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../lib/notification/index.ts"],"names":[],"mappings":";;;AAAA,yCAA0F;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../lib/notification/index.ts"],"names":[],"mappings":";;;AAAA,yCAA0F;AASxF,qGATO,+BAAoB,OASP;AACpB,qGAV6B,+BAAoB,OAU7B;AACpB,iGAXmD,2BAAgB,OAWnD;AAVlB,+DAAwJ;AAWtJ,mGAXO,wCAAkB,OAWP;AAVpB,iEAA6D;AAc3D,oGAdO,0CAAmB,OAcP;AAbrB,qCAAmD;AAcjD,wGAdO,gCAAuB,OAcP;AAbzB,yEAAqH;AAgBnH,0GAhBqD,oDAAyB,OAgBrD;AAd3B,mFAA8E;AAiB5E,4GAjBO,2DAA2B,OAiBP"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NotificationProviderFactory = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
const email_1 = require("../email");
|
|
6
|
+
const push_notification_1 = require("../push-notification");
|
|
7
|
+
const sms_1 = require("../sms");
|
|
8
|
+
const whatsapp_1 = require("../whatsapp");
|
|
9
|
+
const CHANNEL_BASE_CLASSES = {
|
|
10
|
+
email: email_1.EmailProvider,
|
|
11
|
+
push: push_notification_1.PushNotificationProvider,
|
|
12
|
+
sms: sms_1.SmsService,
|
|
13
|
+
whatsapp: whatsapp_1.WhatsAppService,
|
|
14
|
+
};
|
|
15
|
+
class NotificationProviderFactory {
|
|
16
|
+
static create(config) {
|
|
17
|
+
const { channel, provider, options } = config;
|
|
18
|
+
const baseClass = CHANNEL_BASE_CLASSES[channel];
|
|
19
|
+
if (!baseClass) {
|
|
20
|
+
throw new Error(`Unknown notification channel: ${channel}`);
|
|
21
|
+
}
|
|
22
|
+
const instance = new provider(options);
|
|
23
|
+
if (!(instance instanceof baseClass)) {
|
|
24
|
+
throw new Error(`${provider.name} is not a valid ${channel} provider. ` +
|
|
25
|
+
`Expected ${baseClass.name} subclass.`);
|
|
26
|
+
}
|
|
27
|
+
this.logger.log(`Created ${channel} provider: ${provider.name}`);
|
|
28
|
+
return instance;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.NotificationProviderFactory = NotificationProviderFactory;
|
|
32
|
+
NotificationProviderFactory.logger = new common_1.Logger(NotificationProviderFactory.name);
|
|
33
|
+
//# sourceMappingURL=notification-provider.factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-provider.factory.js","sourceRoot":"","sources":["../../lib/notification/notification-provider.factory.ts"],"names":[],"mappings":";;;AAAA,2CAAwC;AAExC,oCAAyC;AACzC,4DAAgE;AAChE,gCAAoC;AACpC,0CAA8C;AAK9C,MAAM,oBAAoB,GAA6B;IACrD,KAAK,EAAE,qBAAa;IACpB,IAAI,EAAE,4CAAwB;IAC9B,GAAG,EAAE,gBAAU;IACf,QAAQ,EAAE,0BAAe;CAC1B,CAAC;AAKF,MAAa,2BAA2B;IAoBtC,MAAM,CAAC,MAAM,CACX,MAAyB;QAEzB,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;QAC9C,MAAM,SAAS,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;QAEhD,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,iCAAiC,OAAO,EAAE,CAAC,CAAC;QAC9D,CAAC;QAGD,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAC;QAGvC,IAAI,CAAC,CAAC,QAAQ,YAAY,SAAS,CAAC,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CACb,GAAG,QAAQ,CAAC,IAAI,mBAAmB,OAAO,aAAa;gBACvD,YAAY,SAAS,CAAC,IAAI,YAAY,CACvC,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW,OAAO,cAAc,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;QACjE,OAAO,QAAiC,CAAC;IAC3C,CAAC;;AA3CH,kEA4CC;AA3CyB,kCAAM,GAAG,IAAI,eAAM,CAAC,2BAA2B,CAAC,IAAI,CAAC,CAAC"}
|