diff --git a/backend/.env.example b/backend/.env.example index 4e8c186..a97a59d 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -14,6 +14,10 @@ PLATFORM_SERVICE_URL=http://localhost:4003 EXTRACTION_SERVICE_URL=http://localhost:4005 MCP_SERVER_URL=http://localhost:4007 +# Observability +TELEMETRY_ENABLED=false +FEATURE_FLAGS_ENABLED=false + # LLM (@bytelyst/llm) LLM_PROVIDER=mock OPENAI_API_KEY= @@ -25,7 +29,12 @@ LLM_VISION_MODEL=gpt-4o LLM_EMBEDDING_MODEL=text-embedding-3-small # Field-level encryption +FIELD_ENCRYPT_ENABLED=true FIELD_ENCRYPT_KEY_PROVIDER=memory FIELD_ENCRYPT_KEY= FIELD_ENCRYPT_MEK_NAME=notelett-mek AZURE_KEYVAULT_URL= + +# Palace (MemPalace) +PALACE_ENABLED=true +PALACE_EXTRACTION_ENABLED=true diff --git a/backend/src/lib/event-bus.ts b/backend/src/lib/event-bus.ts new file mode 100644 index 0000000..03b8425 --- /dev/null +++ b/backend/src/lib/event-bus.ts @@ -0,0 +1,84 @@ +/** + * Domain event bus singleton for NoteLett backend. + * + * Lightweight typed pub/sub for domain events. Handlers run via + * Promise.allSettled — a failing handler never blocks others. + */ + +export interface NoteCreatedEvent { + noteId: string; + workspaceId: string; + userId: string; + title: string; +} + +export interface NoteUpdatedEvent { + noteId: string; + workspaceId: string; + userId: string; + title: string; +} + +export interface NoteDeletedEvent { + noteId: string; + workspaceId: string; + userId: string; +} + +export interface TaskCreatedEvent { + taskId: string; + noteId: string; + workspaceId: string; + userId: string; + title: string; +} + +export interface WorkspaceCreatedEvent { + workspaceId: string; + userId: string; + name: string; +} + +export type NoteLettEventMap = { + 'note.created': NoteCreatedEvent; + 'note.updated': NoteUpdatedEvent; + 'note.deleted': NoteDeletedEvent; + 'task.created': TaskCreatedEvent; + 'workspace.created': WorkspaceCreatedEvent; +}; + +type Handler = (payload: T) => void | Promise; + +class DomainEventBus { + private handlers = new Map>>(); + + on(event: K, handler: Handler): () => void { + if (!this.handlers.has(event)) this.handlers.set(event, new Set()); + this.handlers.get(event)!.add(handler as Handler); + return () => { this.handlers.get(event)?.delete(handler as Handler); }; + } + + async emit(event: K, payload: NoteLettEventMap[K]): Promise { + const fns = this.handlers.get(event); + if (!fns || fns.size === 0) return; + await Promise.allSettled([...fns].map(fn => fn(payload))); + } + + removeAll(): void { + this.handlers.clear(); + } +} + +let _bus: DomainEventBus | null = null; + +export function getEventBus(): DomainEventBus { + if (!_bus) { + _bus = new DomainEventBus(); + } + return _bus; +} + +/** @internal — for testing only. */ +export function _resetEventBus(): void { + _bus = null; +} diff --git a/backend/src/modules/notes/repository.ts b/backend/src/modules/notes/repository.ts index e2d14a3..4d62906 100644 --- a/backend/src/modules/notes/repository.ts +++ b/backend/src/modules/notes/repository.ts @@ -1,5 +1,6 @@ import { getCollection } from '../../lib/datastore.js'; import { getEncryptor } from '../../lib/field-encrypt.js'; +import { getEventBus } from '../../lib/event-bus.js'; import { isEncryptedField, type EncryptedField } from '@bytelyst/field-encrypt'; import type { NoteDoc, ListNotesQuery } from './types.js'; import type { FilterMap } from '@bytelyst/datastore'; @@ -78,7 +79,14 @@ export async function getNote(id: string, workspaceId: string): Promise { const encrypted = await encryptFields(doc); const created = await collection().create(encrypted); - return decryptFields(created); + const result = await decryptFields(created); + getEventBus().emit('note.created', { + noteId: result.id, + workspaceId: result.workspaceId, + userId: result.userId, + title: result.title, + }).catch(() => {}); + return result; } export async function deleteNote(id: string, workspaceId: string): Promise { @@ -93,6 +101,11 @@ export async function deleteNote(id: string, workspaceId: string): Promise {}); return true; } @@ -120,5 +133,12 @@ export async function updateNote( const encrypted = await encryptFields(merged); const result = await collection().upsert(encrypted); - return decryptFields(result); + const decryptedResult = await decryptFields(result); + getEventBus().emit('note.updated', { + noteId: decryptedResult.id, + workspaceId: decryptedResult.workspaceId, + userId: decryptedResult.userId, + title: decryptedResult.title, + }).catch(() => {}); + return decryptedResult; }