Comprehensive examples for implementing notifications with the Asterisms JS SDK Backend.
This guide provides detailed examples of notification implementation using the Asterisms JS SDK Backend. It covers various notification scenarios, from simple user notifications to complex multi-channel notification systems.
// Basic notification to a user
import { getAsterismsBackendSDK, createNotification } from '@asterisms/sdk-backend';
const sdk = getAsterismsBackendSDK(props);
await sdk.boot();
const notifications = sdk.notifications();
async function sendWelcomeNotification(accountId: string) {
try {
const notification = createNotification({
sender: 'your-app',
subject: 'Welcome to Our Platform!',
text: 'Thanks for joining us. Get started by exploring your dashboard.',
urgency: 'NONE'
});
await notifications.sendToAccount(accountId, notification);
console.log('Welcome notification sent successfully');
} catch (error) {
console.error('Failed to send welcome notification:', error);
}
}// Send notification to all users in a workspace
async function sendWorkspaceAnnouncement(workspaceId: string) {
const notifications = sdk.notifications();
try {
const notification = createNotification({
sender: 'your-app',
subject: 'System Maintenance Notice',
text: 'Scheduled maintenance will occur tonight from 2 AM to 4 AM EST.',
html: '<p>Scheduled maintenance will occur tonight from <strong>2 AM to 4 AM EST</strong>.</p>',
urgency: 'IMPORTANT'
});
await notifications.sendToWorkspace(workspaceId, notification);
console.log('Workspace announcement sent successfully');
} catch (error) {
console.error('Failed to send workspace announcement:', error);
}
}// src/routes/api/notifications/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ request, locals }) => {
try {
const { userId, workspaceId, notification } = await request.json();
if (!notification || !notification.subject || !notification.text) {
return json({ error: 'Notification subject and text are required' }, { status: 400 });
}
const sdk = locals.sdk;
if (!sdk) {
return json({ error: 'SDK not available' }, { status: 503 });
}
const notifications = sdk.notifications();
if (userId) {
const notificationPayload = createNotification({
sender: 'your-app',
subject: notification.subject,
text: notification.text,
html: notification.html,
urgency: notification.urgency || 'NONE'
});
await notifications.sendToAccount(userId, notificationPayload);
} else if (workspaceId) {
const notificationPayload = createNotification({
sender: 'your-app',
subject: notification.subject,
text: notification.text,
html: notification.html,
urgency: notification.urgency || 'NONE'
});
await notifications.sendToWorkspace(workspaceId, notificationPayload);
} else {
return json({ error: 'Either userId or workspaceId must be provided' }, { status: 400 });
}
return json({ success: true });
} catch (error) {
console.error('Notification sending error:', error);
return json({ error: 'Failed to send notification' }, { status: 500 });
}
};// src/routes/api/notifications/bulk/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
interface BulkNotificationRequest {
notifications: Array<{
userId: string;
subject: string;
text: string;
html?: string;
urgency?: 'NONE' | 'IMPORTANT' | 'ERROR' | 'CRITICAL';
}>;
}
export const POST: RequestHandler = async ({ request, locals }) => {
try {
const { notifications: notificationsData } = (await request.json()) as BulkNotificationRequest;
if (!Array.isArray(notificationsData) || notificationsData.length === 0) {
return json({ error: 'Notifications array is required' }, { status: 400 });
}
const sdk = locals.sdk;
if (!sdk) {
return json({ error: 'SDK not available' }, { status: 503 });
}
const notifications = sdk.notifications();
// Send notifications in batches to avoid overwhelming the system
const batchSize = 100;
const results = [];
for (let i = 0; i < notificationsData.length; i += batchSize) {
const batch = notificationsData.slice(i, i + batchSize);
const batchPromises = batch.map(async (notif) => {
try {
const notification = createNotification({
sender: 'your-app',
subject: notif.subject,
text: notif.text,
html: notif.html,
urgency: notif.urgency || 'NONE'
});
await notifications.sendToAccount(notif.userId, notification);
return { userId: notif.userId, success: true };
} catch (error) {
return { userId: notif.userId, success: false, error: error.message };
}
});
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
}
const successCount = results.filter((r) => r.success).length;
const failureCount = results.filter((r) => !r.success).length;
return json({
success: true,
results: {
total: notificationsData.length,
successful: successCount,
failed: failureCount,
details: results
}
});
} catch (error) {
console.error('Bulk notification error:', error);
return json({ error: 'Failed to send bulk notifications' }, { status: 500 });
}
};// src/lib/notifications/templates.ts
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';
export class NotificationTemplates {
constructor(private sdk: AsterismsBackendSDK) {}
async sendUserRegistrationNotification(accountId: string, userName: string) {
const notifications = this.sdk.notifications();
const notification = createNotification({
sender: 'your-app',
subject: `Welcome to Our Platform, ${userName}!`,
text: 'Your account has been created successfully. Start exploring our features.',
html: `<p>Your account has been created successfully. <a href="/dashboard">Start exploring our features</a>.</p>`,
urgency: 'NONE'
});
await notifications.sendToAccount(accountId, notification);
}
async sendPasswordResetNotification(accountId: string, resetToken: string) {
const notifications = this.sdk.notifications();
const notification = createNotification({
sender: 'your-app',
subject: 'Password Reset Request',
text: `Click the link below to reset your password. This link expires in 1 hour. ${process.env.APP_URL}/reset-password?token=${resetToken}`,
html: `<p>Click the link below to reset your password. This link expires in 1 hour.</p><p><a href="${process.env.APP_URL}/reset-password?token=${resetToken}">Reset Password</a></p>`,
urgency: 'IMPORTANT'
});
await notifications.sendToAccount(accountId, notification);
}
async sendOrderConfirmationNotification(accountId: string, orderId: string, amount: number) {
const notifications = this.sdk.notifications();
const notification = createNotification({
sender: 'your-app',
subject: 'Order Confirmation',
text: `Your order #${orderId} has been confirmed. Total: $${amount.toFixed(2)}. View order details at ${process.env.APP_URL}/orders/${orderId}`,
html: `<p>Your order #${orderId} has been confirmed. Total: $${amount.toFixed(2)}</p><p><a href="${process.env.APP_URL}/orders/${orderId}">View Order</a></p>`,
urgency: 'NONE'
});
await notifications.sendToAccount(accountId, notification);
}
async sendSystemMaintenanceNotification(workspaceId: string, startTime: string, endTime: string) {
const notifications = this.sdk.notifications();
const notification = createNotification({
sender: 'your-app',
subject: 'Scheduled System Maintenance',
text: `System maintenance is scheduled from ${startTime} to ${endTime}. Some services may be temporarily unavailable. Learn more at ${process.env.APP_URL}/maintenance-info`,
html: `<p>System maintenance is scheduled from ${startTime} to ${endTime}. Some services may be temporarily unavailable.</p><p><a href="${process.env.APP_URL}/maintenance-info">Learn More</a></p>`,
urgency: 'IMPORTANT'
});
await notifications.sendToWorkspace(workspaceId, notification);
}
async sendSecurityAlertNotification(accountId: string, event: string, location: string) {
const notifications = this.sdk.notifications();
const notification = createNotification({
sender: 'your-app',
subject: 'Security Alert',
text: `${event} detected from ${location}. If this wasn't you, please secure your account immediately. Review security settings at ${process.env.APP_URL}/security-settings`,
html: `<p>${event} detected from ${location}. If this wasn't you, please secure your account immediately.</p><p><a href="${process.env.APP_URL}/security-settings">Review Security</a></p>`,
urgency: 'CRITICAL'
});
await notifications.sendToAccount(accountId, notification);
}
}The notification service has built-in queueing and scheduling capabilities through the delivery directive. Here's how to use it:
// src/lib/notifications/scheduled.ts
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';
export class ScheduledNotifications {
constructor(private sdk: AsterismsBackendSDK) {}
async scheduleNotification(
accountId: string,
notification: {
subject: string;
text: string;
html?: string;
urgency?: 'NONE' | 'IMPORTANT' | 'ERROR' | 'CRITICAL';
},
scheduleFor: Date
): Promise<void> {
const notifications = this.sdk.notifications();
const notificationPayload = createNotification({
sender: 'your-app',
subject: notification.subject,
text: notification.text,
html: notification.html,
urgency: notification.urgency || 'NONE'
});
// Calculate delay from now until the scheduled time
const delayMs = scheduleFor.getTime() - Date.now();
const delaySeconds = Math.max(0, Math.floor(delayMs / 1000));
// Use the delivery directive to schedule the notification
// The delay field uses Java Duration format (ISO-8601 duration format)
const notificationWithDelivery = {
...notificationPayload,
delivery: {
schedule: 'WITH_DELAY' as const,
delay: `PT${delaySeconds}S` // ISO-8601 duration format: PT<seconds>S
}
};
await notifications.sendToAccount(accountId, notificationWithDelivery);
}
async scheduleWorkspaceNotification(
workspaceId: string,
notification: {
subject: string;
text: string;
html?: string;
urgency?: 'NONE' | 'IMPORTANT' | 'ERROR' | 'CRITICAL';
},
scheduleFor: Date
): Promise<void> {
const notifications = this.sdk.notifications();
const notificationPayload = createNotification({
sender: 'your-app',
subject: notification.subject,
text: notification.text,
html: notification.html,
urgency: notification.urgency || 'NONE'
});
// Calculate delay from now until the scheduled time
const delayMs = scheduleFor.getTime() - Date.now();
const delaySeconds = Math.max(0, Math.floor(delayMs / 1000));
// Use the delivery directive to schedule the notification
// The delay field uses Java Duration format (ISO-8601 duration format)
const notificationWithDelivery = {
...notificationPayload,
delivery: {
schedule: 'WITH_DELAY' as const,
delay: `PT${delaySeconds}S` // ISO-8601 duration format: PT<seconds>S
}
};
await notifications.sendToWorkspace(workspaceId, notificationWithDelivery);
}
// Helper method for common delay patterns
async scheduleWithDelay(
accountId: string,
notification: {
subject: string;
text: string;
html?: string;
urgency?: 'NONE' | 'IMPORTANT' | 'ERROR' | 'CRITICAL';
},
delay: {
seconds?: number;
minutes?: number;
hours?: number;
days?: number;
}
): Promise<void> {
const notifications = this.sdk.notifications();
const notificationPayload = createNotification({
sender: 'your-app',
subject: notification.subject,
text: notification.text,
html: notification.html,
urgency: notification.urgency || 'NONE'
});
// Build ISO-8601 duration string
const parts = [];
if (delay.days) parts.push(`${delay.days}D`);
const timeParts = [];
if (delay.hours) timeParts.push(`${delay.hours}H`);
if (delay.minutes) timeParts.push(`${delay.minutes}M`);
if (delay.seconds) timeParts.push(`${delay.seconds}S`);
let durationString = 'P';
if (parts.length > 0) durationString += parts.join('');
if (timeParts.length > 0) durationString += 'T' + timeParts.join('');
// If no components specified, default to 0 seconds
if (durationString === 'P') durationString = 'PT0S';
const notificationWithDelivery = {
...notificationPayload,
delivery: {
schedule: 'WITH_DELAY' as const,
delay: durationString // ISO-8601 duration format: P[n]DT[n]H[n]M[n]S
}
};
await notifications.sendToAccount(accountId, notificationWithDelivery);
}
}// src/lib/notifications/websocket.ts
import WebSocket from 'ws';
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';
interface NotificationClient {
userId: string;
ws: WebSocket;
subscriptions: Set<string>;
}
export class WebSocketNotificationService {
private clients = new Map<string, NotificationClient>();
constructor(private sdk: AsterismsBackendSDK) {}
addClient(userId: string, ws: WebSocket): void {
const client: NotificationClient = {
userId,
ws,
subscriptions: new Set()
};
this.clients.set(userId, client);
ws.on('message', (message) => {
this.handleClientMessage(userId, message.toString());
});
ws.on('close', () => {
this.clients.delete(userId);
});
// Send welcome message
this.sendToClient(userId, {
type: 'connection',
message: 'Connected to notification service'
});
}
private handleClientMessage(userId: string, message: string): void {
try {
const data = JSON.parse(message);
const client = this.clients.get(userId);
if (!client) return;
switch (data.type) {
case 'subscribe':
client.subscriptions.add(data.channel);
this.sendToClient(userId, {
type: 'subscribed',
channel: data.channel
});
break;
case 'unsubscribe':
client.subscriptions.delete(data.channel);
this.sendToClient(userId, {
type: 'unsubscribed',
channel: data.channel
});
break;
}
} catch (error) {
console.error('Invalid message from client:', error);
}
}
private sendToClient(userId: string, message: any): void {
const client = this.clients.get(userId);
if (client && client.ws.readyState === WebSocket.OPEN) {
client.ws.send(JSON.stringify(message));
}
}
async sendNotificationToUser(accountId: string, notification: any): Promise<void> {
// Send via WebSocket if client is connected
this.sendToClient(accountId, {
type: 'notification',
notification
});
// Also send via regular notification service
const notifications = this.sdk.notifications();
const notificationPayload = createNotification({
sender: 'your-app',
subject: notification.subject,
text: notification.text,
html: notification.html,
urgency: notification.urgency || 'NONE'
});
await notifications.sendToAccount(accountId, notificationPayload);
}
async sendNotificationToWorkspace(workspaceId: string, notification: any): Promise<void> {
// Send via WebSocket to all connected clients in workspace
const storage = this.sdk.storage();
const workspaceFiles = await storage.list({ bucketId: workspaceId });
for (const user of workspaceUsers) {
this.sendToClient(user.accessorId, {
type: 'notification',
notification
});
}
// Also send via regular notification service
const notifications = this.sdk.notifications();
const notificationPayload = createNotification({
sender: 'your-app',
subject: notification.subject,
text: notification.text,
html: notification.html,
urgency: notification.urgency || 'NONE'
});
await notifications.sendToWorkspace(workspaceId, notificationPayload);
}
broadcastToChannel(channel: string, message: any): void {
this.clients.forEach((client) => {
if (client.subscriptions.has(channel)) {
this.sendToClient(client.userId, {
type: 'channel_message',
channel,
message
});
}
});
}
}// src/lib/notifications/email.ts
import nodemailer from 'nodemailer';
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';
interface EmailNotification {
to: string;
subject: string;
html: string;
text?: string;
}
export class EmailNotificationService {
private transporter: nodemailer.Transporter;
constructor(private sdk: AsterismsBackendSDK) {
this.transporter = nodemailer.createTransporter({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT || '587'),
secure: process.env.SMTP_SECURE === 'true',
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});
}
async sendEmail(notification: EmailNotification): Promise<void> {
try {
await this.transporter.sendMail({
from: process.env.FROM_EMAIL,
to: notification.to,
subject: notification.subject,
html: notification.html,
text: notification.text
});
console.log(`Email sent to ${notification.to}`);
} catch (error) {
console.error('Email sending failed:', error);
throw error;
}
}
async sendWelcomeEmail(userEmail: string, userName: string): Promise<void> {
const html = `
<h1>Welcome to Our Platform, ${userName}!</h1>
<p>Thank you for joining us. We're excited to have you on board.</p>
<p>Get started by visiting your dashboard:</p>
<a href="${process.env.APP_URL}/dashboard" style="background: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Go to Dashboard</a>
`;
await this.sendEmail({
to: userEmail,
subject: 'Welcome to Our Platform!',
html,
text: `Welcome to Our Platform, ${userName}! Visit ${process.env.APP_URL}/dashboard to get started.`
});
}
async sendPasswordResetEmail(userEmail: string, resetToken: string): Promise<void> {
const resetUrl = `${process.env.APP_URL}/reset-password?token=${resetToken}`;
const html = `
<h1>Password Reset Request</h1>
<p>You requested a password reset. Click the link below to reset your password:</p>
<a href="${resetUrl}" style="background: #dc3545; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Reset Password</a>
<p>This link expires in 1 hour.</p>
<p>If you didn't request this, please ignore this email.</p>
`;
await this.sendEmail({
to: userEmail,
subject: 'Password Reset Request',
html,
text: `Password reset requested. Visit ${resetUrl} to reset your password. This link expires in 1 hour.`
});
}
}// src/lib/notifications/preferences.ts
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';
interface NotificationPreferences {
userId: string;
email: {
enabled: boolean;
types: string[];
};
push: {
enabled: boolean;
types: string[];
};
inApp: {
enabled: boolean;
types: string[];
};
frequency: 'immediate' | 'daily' | 'weekly';
}
export class NotificationPreferencesService {
constructor(private sdk: AsterismsBackendSDK) {}
async getUserPreferences(userId: string): Promise<NotificationPreferences> {
const storage = this.sdk.storage();
const preferencesFile = await storage.getData('notification_preferences_' + userId);
if (!preferences) {
// Return default preferences
return {
userId,
email: {
enabled: true,
types: ['security', 'system', 'updates']
},
push: {
enabled: true,
types: ['messages', 'security']
},
inApp: {
enabled: true,
types: ['all']
},
frequency: 'immediate'
};
}
return preferences;
}
async updateUserPreferences(
userId: string,
preferences: Partial<NotificationPreferences>
): Promise<void> {
const storage = this.sdk.storage();
const currentPreferences = await this.getUserPreferences(userId);
const updatedPreferences = {
...currentPreferences,
...preferences,
userId
};
await storage.upload(
new File([JSON.stringify(updatedPreferences)], 'notification_preferences_' + userId, {
type: 'application/json'
})
);
}
async shouldSendNotification(
userId: string,
notificationType: string,
channel: 'email' | 'push' | 'inApp'
): Promise<boolean> {
const preferences = await this.getUserPreferences(userId);
const channelPrefs = preferences[channel];
return (
channelPrefs.enabled &&
(channelPrefs.types.includes('all') || channelPrefs.types.includes(notificationType))
);
}
}// src/routes/api/notifications/preferences/+server.ts
import { json } from '@sveltejs/kit';
import { NotificationPreferencesService } from '$lib/notifications/preferences';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ locals, url }) => {
try {
const user = locals.user;
if (!user) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const sdk = locals.sdk;
const preferencesService = new NotificationPreferencesService(sdk);
const preferences = await preferencesService.getUserPreferences(user.id);
return json({ preferences });
} catch (error) {
console.error('Get preferences error:', error);
return json({ error: 'Failed to get preferences' }, { status: 500 });
}
};
export const PUT: RequestHandler = async ({ request, locals }) => {
try {
const user = locals.user;
if (!user) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
const preferences = await request.json();
const sdk = locals.sdk;
const preferencesService = new NotificationPreferencesService(sdk);
await preferencesService.updateUserPreferences(user.id, preferences);
return json({ success: true });
} catch (error) {
console.error('Update preferences error:', error);
return json({ error: 'Failed to update preferences' }, { status: 500 });
}
};// src/lib/notifications/push.ts
import webpush from 'web-push';
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';
interface PushSubscription {
userId: string;
endpoint: string;
keys: {
p256dh: string;
auth: string;
};
}
export class PushNotificationService {
constructor(private sdk: AsterismsBackendSDK) {
webpush.setVapidDetails(
process.env.VAPID_SUBJECT || 'mailto:admin@example.com',
process.env.VAPID_PUBLIC_KEY!,
process.env.VAPID_PRIVATE_KEY!
);
}
async saveSubscription(userId: string, subscription: PushSubscription): Promise<void> {
const storage = this.sdk.storage();
await storage.upload(
new File(
[JSON.stringify(subscription)],
`push_subscription_${userId}_${btoa(subscription.endpoint)}`,
{
type: 'application/json'
}
)
);
}
async removeSubscription(userId: string, endpoint: string): Promise<void> {
const storage = this.sdk.storage();
await storage.delete(`push_subscription_${userId}_${btoa(endpoint)}`);
}
async getUserSubscriptions(userId: string): Promise<PushSubscription[]> {
const storage = this.sdk.storage();
const allSubscriptions = await storage.list();
return allSubscriptions.filter((sub) => sub.userId === userId);
}
async sendPushNotification(userId: string, notification: any): Promise<void> {
const subscriptions = await this.getUserSubscriptions(userId);
const payload = JSON.stringify({
title: notification.title,
body: notification.message,
icon: '/icon-192x192.png',
badge: '/badge-72x72.png',
data: {
url: notification.actionUrl || '/',
type: notification.type
}
});
const sendPromises = subscriptions.map(async (subscription) => {
try {
await webpush.sendNotification(subscription, payload);
console.log(`Push notification sent to ${subscription.endpoint}`);
} catch (error) {
console.error('Push notification failed:', error);
// Remove invalid subscriptions
if (error.statusCode === 410) {
await this.removeSubscription(userId, subscription.endpoint);
}
}
});
await Promise.all(sendPromises);
}
}// src/lib/notifications/analytics.ts
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';
interface NotificationMetrics {
notificationId: string;
userId: string;
type: string;
channel: 'email' | 'push' | 'inApp' | 'websocket';
status: 'sent' | 'delivered' | 'opened' | 'clicked' | 'failed';
timestamp: Date;
metadata?: any;
}
export class NotificationAnalyticsService {
constructor(private sdk: AsterismsBackendSDK) {}
async trackNotification(metrics: NotificationMetrics): Promise<void> {
const storage = this.sdk.storage();
await storage.upload(
new File(
[
JSON.stringify({
...metrics,
id: crypto.randomUUID()
})
],
`notification_metric_${Date.now()}`,
{
type: 'application/json'
}
)
);
}
async getNotificationStats(userId?: string, startDate?: Date, endDate?: Date): Promise<any> {
const storage = this.sdk.storage();
let metrics = await storage.list('notification_metrics');
// Filter by user if specified
if (userId) {
metrics = metrics.filter((m) => m.userId === userId);
}
// Filter by date range if specified
if (startDate || endDate) {
metrics = metrics.filter((m) => {
const metricDate = new Date(m.timestamp);
return (!startDate || metricDate >= startDate) && (!endDate || metricDate <= endDate);
});
}
// Calculate statistics
const stats = {
total: metrics.length,
byChannel: {},
byType: {},
byStatus: {},
deliveryRate: 0,
openRate: 0,
clickRate: 0
};
metrics.forEach((metric) => {
// Count by channel
stats.byChannel[metric.channel] = (stats.byChannel[metric.channel] || 0) + 1;
// Count by type
stats.byType[metric.type] = (stats.byType[metric.type] || 0) + 1;
// Count by status
stats.byStatus[metric.status] = (stats.byStatus[metric.status] || 0) + 1;
});
// Calculate rates
const sentCount = stats.byStatus['sent'] || 0;
const deliveredCount = stats.byStatus['delivered'] || 0;
const openedCount = stats.byStatus['opened'] || 0;
const clickedCount = stats.byStatus['clicked'] || 0;
if (sentCount > 0) {
stats.deliveryRate = (deliveredCount / sentCount) * 100;
stats.openRate = (openedCount / sentCount) * 100;
stats.clickRate = (clickedCount / sentCount) * 100;
}
return stats;
}
}// src/lib/notifications/templates.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { NotificationTemplates } from './templates';
// Mock SDK for testing
function createMockSDK() {
return {
notifications: () => ({
sendToAccount: vi.fn().mockResolvedValue({ success: true }),
sendToRole: vi.fn().mockResolvedValue({ success: true }),
sendToAll: vi.fn().mockResolvedValue({ success: true }),
sendToChannel: vi.fn().mockResolvedValue({ success: true })
})
};
}
describe('NotificationTemplates', () => {
let templates: NotificationTemplates;
let mockSDK: ReturnType<typeof createMockSDK>;
beforeEach(() => {
mockSDK = createMockSDK();
templates = new NotificationTemplates(mockSDK);
});
describe('sendUserRegistrationNotification', () => {
it('should send welcome notification to user', async () => {
const accountId = 'user123';
const userName = 'John Doe';
await templates.sendUserRegistrationNotification(accountId, userName);
expect(mockSDK.notifications().sendToAccount).toHaveBeenCalledWith(
accountId,
expect.objectContaining({
sender: 'your-app',
message: expect.objectContaining({
subject: `Welcome to Our Platform, ${userName}!`,
text: expect.stringContaining('Your account has been created')
})
})
);
});
});
describe('sendPasswordResetNotification', () => {
it('should send password reset notification', async () => {
const accountId = 'user123';
const resetToken = 'reset-token-123';
await templates.sendPasswordResetNotification(accountId, resetToken);
expect(mockSDK.notifications().sendToAccount).toHaveBeenCalledWith(
accountId,
expect.objectContaining({
sender: 'your-app',
message: expect.objectContaining({
subject: 'Password Reset Request',
urgency: 'IMPORTANT'
})
})
);
});
});
});// src/routes/api/notifications/+server.test.ts
import { describe, it, expect } from 'vitest';
import { POST } from './+server';
// Mock SDK for testing
function createMockSDK() {
return {
notifications: () => ({
sendToAccount: vi.fn().mockResolvedValue({ success: true }),
sendToRole: vi.fn().mockResolvedValue({ success: true }),
sendToAll: vi.fn().mockResolvedValue({ success: true }),
sendToChannel: vi.fn().mockResolvedValue({ success: true })
})
};
}
describe('Notification API', () => {
it('should send notification to user', async () => {
const mockSDK = createMockSDK();
const mockEvent = {
request: {
json: async () => ({
userId: 'user123',
notification: {
subject: 'Test Notification',
text: 'This is a test message',
urgency: 'NONE'
}
})
},
locals: { sdk: mockSDK }
};
const response = await POST(mockEvent as any);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(mockSDK.notifications().sendToAccount).toHaveBeenCalledWith(
'user123',
expect.objectContaining({
sender: 'your-app',
message: expect.objectContaining({
subject: 'Test Notification',
text: 'This is a test message',
urgency: 'NONE'
})
})
);
});
it('should send notification to workspace', async () => {
const mockSDK = createMockSDK();
const mockEvent = {
request: {
json: async () => ({
workspaceId: 'workspace123',
notification: {
subject: 'Workspace Notification',
text: 'This is a workspace message',
urgency: 'IMPORTANT'
}
})
},
locals: { sdk: mockSDK }
};
const response = await POST(mockEvent as any);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(mockSDK.notifications().sendToWorkspace).toHaveBeenCalledWith(
'workspace123',
expect.objectContaining({
sender: 'your-app',
message: expect.objectContaining({
subject: 'Workspace Notification',
text: 'This is a workspace message',
urgency: 'IMPORTANT'
})
})
);
});
});// src/lib/notifications/rateLimiter.ts
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';
export class NotificationRateLimiter {
constructor(private sdk: AsterismsBackendSDK) {}
async canSendNotification(userId: string, type: string): Promise<boolean> {
const storage = this.sdk.storage();
const now = Date.now();
const windowMs = 60 * 60 * 1000; // 1 hour
const limits = {
security: 5,
marketing: 10,
system: 20,
default: 50
};
const limit = limits[type] || limits['default'];
const key = `rate_limit:${userId}:${type}`;
const requests = (await storage.get(key)) || [];
const recentRequests = requests.filter((time) => now - time < windowMs);
if (recentRequests.length >= limit) {
return false;
}
// Add current request
recentRequests.push(now);
await storage.set(key, recentRequests);
return true;
}
}// src/lib/notifications/errorHandler.ts
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';
export class NotificationErrorHandler {
constructor(private sdk: AsterismsBackendSDK) {}
async handleFailedNotification(
notificationId: string,
error: Error,
attempt: number
): Promise<void> {
const storage = this.sdk.storage();
const maxRetries = 3;
if (attempt < maxRetries) {
// Schedule retry
const retryDelay = Math.pow(2, attempt) * 1000; // Exponential backoff
const retryAt = new Date(Date.now() + retryDelay);
await storage.update('notification_queue', notificationId, {
status: 'pending',
scheduledFor: retryAt,
attempts: attempt + 1,
lastError: error.message
});
} else {
// Mark as permanently failed
await storage.update('notification_queue', notificationId, {
status: 'failed',
lastError: error.message
});
// Log to monitoring system
console.error(`Notification ${notificationId} failed permanently:`, error);
}
}
}The NotificationDelivery type structure mirrors the Java microservice implementation:
interface NotificationDelivery {
schedule: 'IMMEDIATE' | 'WITH_DELAY';
delay: string; // Java Duration format - ISO-8601 duration format
}Important Notes:
delay field uses Java Duration serialization format (ISO-8601 duration format)Duration type in the notification microservicePnDTnHnMn.nS where P is duration designator, T separates date/time components{ schedule: 'IMMEDIATE', delay: 'PT0S' }{ schedule: 'WITH_DELAY', delay: 'PT30M' } (30 minutes)Examples:
// Send immediately
{ schedule: 'IMMEDIATE', delay: 'PT0S' }
// Send after 30 minutes
{ schedule: 'WITH_DELAY', delay: 'PT30M' }
// Send after 1 hour and 30 minutes
{ schedule: 'WITH_DELAY', delay: 'PT1H30M' }
// Send after 2 days
{ schedule: 'WITH_DELAY', delay: 'P2D' }
// Send after 8 hours, 6 minutes, and 12.345 seconds
{ schedule: 'WITH_DELAY', delay: 'PT8H6M12.345S' }