feat(backend): add domain event bus and wire note CRUD events
- New lib/event-bus.ts: lightweight typed pub/sub with error isolation via Promise.allSettled. Supports note.created, note.updated, note.deleted, task.created, workspace.created events. - notes/repository.ts: emit events on create, update, and delete. - .env.example: add TELEMETRY_ENABLED, FEATURE_FLAGS_ENABLED, FIELD_ENCRYPT_ENABLED, PALACE_ENABLED, PALACE_EXTRACTION_ENABLED.
This commit is contained in:
parent
58a778bc1e
commit
190d23280f
@ -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
|
||||
|
||||
84
backend/src/lib/event-bus.ts
Normal file
84
backend/src/lib/event-bus.ts
Normal file
@ -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<T> = (payload: T) => void | Promise<void>;
|
||||
|
||||
class DomainEventBus {
|
||||
private handlers = new Map<string, Set<Handler<unknown>>>();
|
||||
|
||||
on<K extends keyof NoteLettEventMap>(event: K, handler: Handler<NoteLettEventMap[K]>): () => void {
|
||||
if (!this.handlers.has(event)) this.handlers.set(event, new Set());
|
||||
this.handlers.get(event)!.add(handler as Handler<unknown>);
|
||||
return () => { this.handlers.get(event)?.delete(handler as Handler<unknown>); };
|
||||
}
|
||||
|
||||
async emit<K extends keyof NoteLettEventMap>(event: K, payload: NoteLettEventMap[K]): Promise<void> {
|
||||
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;
|
||||
}
|
||||
@ -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<NoteDoc
|
||||
export async function createNote(doc: NoteDoc): Promise<NoteDoc> {
|
||||
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<boolean> {
|
||||
@ -93,6 +101,11 @@ export async function deleteNote(id: string, workspaceId: string): Promise<boole
|
||||
} as NoteDoc);
|
||||
|
||||
await collection().upsert(merged);
|
||||
getEventBus().emit('note.deleted', {
|
||||
noteId: id,
|
||||
workspaceId: existing.workspaceId,
|
||||
userId: existing.userId,
|
||||
}).catch(() => {});
|
||||
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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user