diff --git a/packages/notify-client/.eslintignore b/packages/notify-client/.eslintignore new file mode 100644 index 000000000..d75ac3167 --- /dev/null +++ b/packages/notify-client/.eslintignore @@ -0,0 +1,2 @@ +tsup.config.ts +dist/ diff --git a/packages/notify-client/README.md b/packages/notify-client/README.md new file mode 100644 index 000000000..0fe2ac146 --- /dev/null +++ b/packages/notify-client/README.md @@ -0,0 +1,149 @@ +# @manacore/notify-client + +Client SDK for the mana-notify notification service. + +## Installation + +```bash +pnpm add @manacore/notify-client +``` + +## Usage + +### Basic Usage + +```typescript +import { NotifyClient } from '@manacore/notify-client'; + +const notify = new NotifyClient({ + serviceUrl: 'http://localhost:3040', + serviceKey: process.env.MANA_NOTIFY_SERVICE_KEY, + appId: 'your-app-id', +}); + +// Send email +await notify.sendEmail({ + to: 'user@example.com', + template: 'auth-password-reset', + data: { resetUrl: 'https://...', userName: 'Max' }, +}); + +// Send push notification +await notify.sendPush({ + userId: 'user-uuid', + title: 'New Message', + body: 'You have a new message', + data: { messageId: 'xxx' }, +}); + +// Send to specific token +await notify.sendPush({ + token: 'ExponentPushToken[xxx]', + title: 'Hello', + body: 'World', +}); + +// Schedule notification +await notify.scheduleEmail({ + to: 'user@example.com', + template: 'calendar-reminder', + data: { eventTitle: 'Meeting' }, + scheduledFor: new Date('2024-12-20T13:45:00Z'), +}); +``` + +### NestJS Integration + +```typescript +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { NotifyModule } from '@manacore/notify-client/nestjs'; + +@Module({ + imports: [ + ConfigModule.forRoot(), + NotifyModule.forRootAsync({ + imports: [ConfigModule], + useFactory: (config: ConfigService) => ({ + serviceUrl: config.get('MANA_NOTIFY_URL', 'http://localhost:3040'), + serviceKey: config.get('MANA_NOTIFY_SERVICE_KEY'), + appId: config.get('APP_ID'), + }), + inject: [ConfigService], + }), + ], +}) +export class AppModule {} +``` + +Then inject the client: + +```typescript +import { Injectable, Inject } from '@nestjs/common'; +import { NOTIFY_CLIENT, NotifyClient } from '@manacore/notify-client/nestjs'; + +@Injectable() +export class NotificationService { + constructor(@Inject(NOTIFY_CLIENT) private readonly notify: NotifyClient) {} + + async sendWelcomeEmail(email: string, name: string) { + await this.notify.sendEmail({ + to: email, + template: 'auth-welcome', + data: { userName: name }, + }); + } +} +``` + +## API Reference + +### NotifyClient + +#### Constructor + +```typescript +new NotifyClient({ + serviceUrl: string; // mana-notify service URL + serviceKey: string; // Service authentication key + appId: string; // Your application ID + timeout?: number; // Request timeout in ms (default: 30000) +}); +``` + +#### Methods + +##### Email + +- `sendEmail(options)` - Send an email immediately +- `scheduleEmail(options)` - Schedule an email for later + +##### Push Notifications + +- `sendPush(options)` - Send a push notification +- `schedulePush(options)` - Schedule a push notification + +##### Other Channels + +- `sendMatrix(options)` - Send a Matrix message +- `sendWebhook(options)` - Send a webhook + +##### Batch & Management + +- `sendBatch(notifications)` - Send multiple notifications +- `getNotification(id)` - Get notification status +- `cancelNotification(id)` - Cancel a pending notification + +##### Templates + +- `listTemplates(appId?)` - List available templates +- `getTemplate(slug, locale?)` - Get a template +- `previewTemplate(slug, data, locale?)` - Preview a rendered template + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `MANA_NOTIFY_URL` | mana-notify service URL | +| `MANA_NOTIFY_SERVICE_KEY` | Service authentication key | +| `APP_ID` | Your application ID | diff --git a/packages/notify-client/package.json b/packages/notify-client/package.json new file mode 100644 index 000000000..71bbd6ee2 --- /dev/null +++ b/packages/notify-client/package.json @@ -0,0 +1,44 @@ +{ + "name": "@manacore/notify-client", + "version": "1.0.0", + "description": "Client SDK for mana-notify notification service", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./nestjs": { + "types": "./dist/nestjs/index.d.ts", + "import": "./dist/nestjs/index.mjs", + "require": "./dist/nestjs/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rm -rf dist" + }, + "dependencies": {}, + "devDependencies": { + "@nestjs/common": "^10.4.17", + "@types/node": "^22.10.5", + "tsup": "^8.3.5", + "typescript": "^5.7.2" + }, + "peerDependencies": { + "@nestjs/common": ">=10.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/common": { + "optional": true + } + } +} diff --git a/packages/notify-client/src/client.ts b/packages/notify-client/src/client.ts new file mode 100644 index 000000000..3ae108c61 --- /dev/null +++ b/packages/notify-client/src/client.ts @@ -0,0 +1,354 @@ +import type { + SendEmailOptions, + SendPushOptions, + SendMatrixOptions, + SendWebhookOptions, + ScheduleOptions, + NotificationResponse, + BatchNotificationResponse, + Template, + RenderedTemplate, +} from './types'; + +export interface NotifyClientOptions { + serviceUrl: string; + serviceKey: string; + appId: string; + timeout?: number; +} + +export class NotifyClient { + private readonly serviceUrl: string; + private readonly serviceKey: string; + private readonly appId: string; + private readonly timeout: number; + + constructor(options: NotifyClientOptions) { + this.serviceUrl = options.serviceUrl.replace(/\/$/, ''); + this.serviceKey = options.serviceKey; + this.appId = options.appId; + this.timeout = options.timeout || 30000; + } + + // ==================== Notifications ==================== + + /** + * Send an email notification + */ + async sendEmail(options: SendEmailOptions): Promise { + return this.send({ + channel: 'email', + appId: this.appId, + recipient: options.to, + template: options.template, + subject: options.subject, + body: options.body, + data: options.data, + emailOptions: { + from: options.from, + replyTo: options.replyTo, + }, + priority: options.priority, + externalId: options.externalId, + }); + } + + /** + * Send a push notification + */ + async sendPush(options: SendPushOptions): Promise { + return this.send({ + channel: 'push', + appId: this.appId, + userId: options.userId, + recipient: options.token, + recipients: options.tokens, + subject: options.title, + body: options.body, + data: options.data, + pushOptions: { + sound: options.sound, + badge: options.badge, + channelId: options.channelId, + }, + priority: options.priority, + externalId: options.externalId, + }); + } + + /** + * Send a Matrix message + */ + async sendMatrix(options: SendMatrixOptions): Promise { + return this.send({ + channel: 'matrix', + appId: this.appId, + recipient: options.roomId, + body: options.body, + matrixOptions: { + formattedBody: options.formattedBody, + msgtype: options.msgtype, + }, + priority: options.priority, + externalId: options.externalId, + }); + } + + /** + * Send a webhook notification + */ + async sendWebhook(options: SendWebhookOptions): Promise { + return this.send({ + channel: 'webhook', + appId: this.appId, + recipient: options.url, + data: options.body, + webhookOptions: { + method: options.method, + headers: options.headers, + timeout: options.timeout, + }, + priority: options.priority, + externalId: options.externalId, + }); + } + + /** + * Schedule an email notification + */ + async scheduleEmail(options: SendEmailOptions & ScheduleOptions): Promise { + return this.schedule({ + channel: 'email', + appId: this.appId, + recipient: options.to, + template: options.template, + subject: options.subject, + body: options.body, + data: options.data, + emailOptions: { + from: options.from, + replyTo: options.replyTo, + }, + priority: options.priority, + externalId: options.externalId, + scheduledFor: + options.scheduledFor instanceof Date + ? options.scheduledFor.toISOString() + : options.scheduledFor, + }); + } + + /** + * Schedule a push notification + */ + async schedulePush(options: SendPushOptions & ScheduleOptions): Promise { + return this.schedule({ + channel: 'push', + appId: this.appId, + userId: options.userId, + recipient: options.token, + recipients: options.tokens, + subject: options.title, + body: options.body, + data: options.data, + pushOptions: { + sound: options.sound, + badge: options.badge, + channelId: options.channelId, + }, + priority: options.priority, + externalId: options.externalId, + scheduledFor: + options.scheduledFor instanceof Date + ? options.scheduledFor.toISOString() + : options.scheduledFor, + }); + } + + /** + * Send multiple notifications in batch + */ + async sendBatch( + notifications: Array< + | ({ type: 'email' } & SendEmailOptions) + | ({ type: 'push' } & SendPushOptions) + | ({ type: 'matrix' } & SendMatrixOptions) + | ({ type: 'webhook' } & SendWebhookOptions) + > + ): Promise { + const mapped = notifications.map((n) => { + if (n.type === 'email') { + return { + channel: 'email' as const, + appId: this.appId, + recipient: n.to, + template: n.template, + subject: n.subject, + body: n.body, + data: n.data, + priority: n.priority, + externalId: n.externalId, + }; + } else if (n.type === 'push') { + return { + channel: 'push' as const, + appId: this.appId, + userId: n.userId, + recipient: n.token, + recipients: n.tokens, + subject: n.title, + body: n.body, + data: n.data, + priority: n.priority, + externalId: n.externalId, + }; + } else if (n.type === 'matrix') { + return { + channel: 'matrix' as const, + appId: this.appId, + recipient: n.roomId, + body: n.body, + priority: n.priority, + externalId: n.externalId, + }; + } else { + return { + channel: 'webhook' as const, + appId: this.appId, + recipient: n.url, + data: n.body, + priority: n.priority, + externalId: n.externalId, + }; + } + }); + + const response = await this.request('/notifications/batch', { + method: 'POST', + body: JSON.stringify({ notifications: mapped }), + }); + + return response; + } + + /** + * Get notification status + */ + async getNotification(id: string): Promise { + const response = await this.request<{ notification: NotificationResponse | null }>( + `/notifications/${id}` + ); + return response.notification; + } + + /** + * Cancel a pending notification + */ + async cancelNotification(id: string): Promise { + const response = await this.request<{ notification: NotificationResponse }>( + `/notifications/${id}`, + { method: 'DELETE' } + ); + return response.notification; + } + + // ==================== Templates ==================== + + /** + * List all templates + */ + async listTemplates(appId?: string): Promise { + const url = appId ? `/templates?appId=${encodeURIComponent(appId)}` : '/templates'; + const response = await this.request<{ templates: Template[] }>(url); + return response.templates; + } + + /** + * Get a template by slug + */ + async getTemplate(slug: string, locale = 'de-DE'): Promise