Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions listener/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
"lint": "node ./node_modules/typescript/bin/tsc --noEmit",
"test": "node ./node_modules/jest/bin/jest.js",
"migrate": "ts-node src/scripts/migrate-db.ts",
"migrate:templates": "ts-node src/scripts/migrate-templates.ts"
"typecheck": "node ./node_modules/typescript/bin/tsc --noEmit",
"lint": "node ./node_modules/typescript/bin/tsc --noEmit",
"migrate": "ts-node src/scripts/migrate-db.ts",
"check-migrations": "ts-node src/scripts/check-migrations.ts",
"validate:batch": "ts-node src/utils/batch-validator.ts"
},
Expand Down
24 changes: 24 additions & 0 deletions listener/src/api/events-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { PreferencesUpdateInput } from '../types/preferences';
import { NotificationAPI } from '../services/notification-api';
import { NotificationType } from '../types/scheduled-notification';
import logger from '../utils/logger';
import { generateRequestId } from '../utils/request-id';
import { TemplateService } from '../services/template-service';
import { handleTemplateRoutes } from './template-routes';
import { generateRequestId, resolveCorrelationId } from '../utils/request-id';
import { NotificationHistoryService } from '../services/notification-history';
import { SearchSuggestionService } from '../services/search-suggestion';
Expand Down Expand Up @@ -55,6 +58,7 @@ export interface EventsServerOptions {
webhookSecrets?: WebhookSecret[];
apiKeys?: Array<{ key: string; name?: string }>;
notificationAPI?: NotificationAPI | null;
templateService?: TemplateService | null;
rateLimit?: RateLimitConfig;
/**
* Optional override for the analytics aggregator. Tests use this to inject
Expand Down Expand Up @@ -376,6 +380,8 @@ export function createEventsServer(options: EventsServerOptions): http.Server {

res.setHeader('Access-Control-Allow-Origin', corsOrigin);
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-API-Key, Authorization, X-Correlation-Id');
res.setHeader('X-Request-Id', requestId);
res.setHeader('X-Correlation-Id', correlationId);
Expand All @@ -399,6 +405,24 @@ export function createEventsServer(options: EventsServerOptions): http.Server {
return;
}

// Template API routes (handled first for priority)
if (options.templateService && req.url?.startsWith('/api/templates')) {
handleTemplateRoutes(req, res, requestId, options.templateService)
.then((handled) => {
if (!handled) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not found' }));
}
})
.catch((error) => {
logger.error('Template route handler error', { error, requestId });
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Internal server error' }));
});
return;
}

if (req.method === 'GET' && req.url === '/health') {
// GET /health
if (req.method === 'GET' && url.pathname === '/health') {
buildHealthResponse(options).then((health) => {
Expand Down
73 changes: 73 additions & 0 deletions listener/src/database/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,79 @@ BEGIN
WHERE id = NEW.id;
END;

-- ===============================================
-- NOTIFICATION TEMPLATE SYSTEM SCHEMA
-- ===============================================

-- Main table for notification templates
CREATE TABLE IF NOT EXISTS notification_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,

-- Template identification
unique_key VARCHAR(100) NOT NULL UNIQUE, -- e.g., 'welcome_email', 'payment_confirmation'
name VARCHAR(255) NOT NULL, -- Human-readable name
description TEXT, -- Template purpose/usage description

-- Template content
channel_type VARCHAR(50) NOT NULL, -- EMAIL, SMS, DISCORD, PUSH, WEBHOOK
subject_template TEXT, -- Optional subject (for EMAIL, PUSH)
body_template TEXT NOT NULL, -- Main template content with {{placeholders}}

-- Variable definitions
variables TEXT NOT NULL, -- JSON array of required variable names
default_values TEXT, -- JSON object with default values for optional variables

-- Metadata
is_active BOOLEAN NOT NULL DEFAULT 1,
version INTEGER NOT NULL DEFAULT 1, -- Template versioning for A/B testing
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(100), -- User/system that created template

-- Validation
last_validated_at DATETIME,
validation_status VARCHAR(20) DEFAULT 'PENDING' -- VALID, INVALID, PENDING
);

-- Indexes for template lookups
CREATE INDEX IF NOT EXISTS idx_templates_unique_key
ON notification_templates(unique_key);

CREATE INDEX IF NOT EXISTS idx_templates_channel_type
ON notification_templates(channel_type, is_active)
WHERE is_active = 1;

CREATE INDEX IF NOT EXISTS idx_templates_active
ON notification_templates(is_active, created_at);

-- Template usage tracking for analytics
CREATE TABLE IF NOT EXISTS template_usage_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
template_id INTEGER NOT NULL,
rendered_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
context_hash VARCHAR(64), -- Hash of the context data for deduplication
success BOOLEAN NOT NULL DEFAULT 1,
error_message TEXT,
render_duration_ms INTEGER,

FOREIGN KEY (template_id) REFERENCES notification_templates(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_template_usage_template_id
ON template_usage_log(template_id, rendered_at);

CREATE INDEX IF NOT EXISTS idx_template_usage_rendered_at
ON template_usage_log(rendered_at);

-- Trigger to update template updated_at timestamp
CREATE TRIGGER IF NOT EXISTS update_notification_templates_timestamp
AFTER UPDATE ON notification_templates
FOR EACH ROW
BEGIN
UPDATE notification_templates
SET updated_at = CURRENT_TIMESTAMP
WHERE id = NEW.id;
END;
-- Rate limit events table for auditing
CREATE TABLE IF NOT EXISTS rate_limit_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
Expand Down
9 changes: 9 additions & 0 deletions listener/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,17 @@ dotenv.config();
async function main() {
const config = loadConfig();

// Initialize database for scheduled notifications and templates
// Initialize database for templates, scheduler, and rate limiting
let scheduler: NotificationScheduler | null = null;
let retryScheduler: RetryScheduler | null = null;
let notificationAPI: NotificationAPI | null = null;
let templateService: TemplateService | null = null;

if (config.scheduler?.enabled) {
try {
logger.info('Initializing database for scheduled notifications and templates');
const db = await initializeDatabase(config.databasePath);
let templateService: NotificationTemplateService | null = null;
let cleanupService: CleanupService | null = null;
let reconciliationEngine: IndexingReconciliationEngine | null = null;
Expand Down Expand Up @@ -147,6 +154,8 @@ async function main() {
stellarNetworkPassphrase: config.stellarNetworkPassphrase,
contractAddresses: config.contractAddresses,
discordWebhookUrl: config.discord?.webhookUrl,
notificationAPI, // Pass API to events server for scheduling endpoints
templateService, // Pass template service for template endpoints
webhookSecrets: config.webhookSecrets,
apiKeys: config.apiKeys,
notificationAPI,
Expand Down
Loading