| name | add-resource-events |
| description | Add real-time event emission to a resource service. Use when adding SSE/real-time capabilities to a resource. Triggers on "add events", "real-time events", "SSE events". |
Add Resource Events
Enables real-time event emission for a resource service via Server-Sent Events (SSE).
Quick Reference
Event Infrastructure:
src/events/event-emitter.ts- SingletonAppEventEmittersrc/events/base.service.ts-BaseServicewithemitEvent()src/schemas/event.schema.ts- Event type definitionssrc/routes/events.router.ts- SSE endpoint
Event Pattern: {serviceName}:{action} (e.g., notes:created, notes:updated)
Prerequisites
To add events to a resource:
- Service must extend
BaseService(seecreate-resource-service) - Authorization methods for events in
AuthorizationService
How It Works
1. BaseService Provides emitEvent()
// src/events/base.service.ts
export abstract class BaseService {
constructor(protected serviceName: string) {}
protected emitEvent<T>(
action: ServiceEventType["action"],
data: T,
options?: {
id?: string;
user?: { userId: string; [key: string]: unknown };
},
) {
appEvents.emitServiceEvent(this.serviceName, {
id: options?.id || uuidv4(),
action,
data,
user: eventUser,
timestamp: new Date(),
resourceType: this.serviceName,
});
}
}
2. Resource Service Emits Events
export class NoteService extends BaseService {
constructor(...) {
super("notes"); // Service name for event routing
}
async create(data, user) {
// ... create logic ...
this.emitEvent("created", note, { id: note.id, user });
return note;
}
async update(id, data, user) {
// ... update logic ...
this.emitEvent("updated", updatedNote, { id: updatedNote.id, user });
return updatedNote;
}
async delete(id, user) {
// ... delete logic ...
this.emitEvent("deleted", note, { id: note.id, user });
return true;
}
}
3. Events Router Streams to Clients
// src/routes/events.router.ts
appEvents.on("notes:created", eventHandler);
appEvents.on("notes:updated", eventHandler);
appEvents.on("notes:deleted", eventHandler);
Adding Events to a New Resource
Step 1: Extend BaseService
Your service must extend BaseService:
import { BaseService } from "@/events/base.service";
export class {Entity}Service extends BaseService {
constructor(...) {
super("{entities}"); // Plural for event namespace
}
}
Step 2: Emit Events in CRUD Methods
Add emitEvent() calls after successful operations:
async create(data, user) {
const {entity} = await this.repository.create(data, user.userId);
this.emitEvent("created", {entity}, { id: {entity}.id, user });
return {entity};
}
async update(id, data, user) {
const updated = await this.repository.update(id, data);
if (!updated) return null;
this.emitEvent("updated", updated, { id: updated.id, user });
return updated;
}
async delete(id, user) {
const {entity} = await this.repository.findById(id);
const deleted = await this.repository.remove(id);
if (deleted) {
this.emitEvent("deleted", {entity}, { id: {entity}.id, user });
}
return deleted;
}
Step 3: Add Event Authorization
In src/services/authorization.service.ts:
async canReceive{Entity}Event(
user: AuthenticatedUserContextType,
{entity}Data: { createdBy: string; [key: string]: unknown },
): Promise<boolean> {
if (this.isAdmin(user)) return true;
if ({entity}Data.createdBy === user.userId) return true;
return false;
}
Step 4: Update Events Router
In src/routes/events.router.ts, add listeners:
// Listen to {entity} events
appEvents.on("{entities}:created", eventHandler);
appEvents.on("{entities}:updated", eventHandler);
appEvents.on("{entities}:deleted", eventHandler);
Update the shouldUserReceiveEvent function:
async function shouldUserReceiveEvent(
event: ServiceEventType,
user: AuthenticatedUserContextType,
authorizationService: AuthorizationService,
): Promise<boolean> {
switch (event.resourceType) {
case "notes":
// ... existing ...
case "{entities}":
if (
typeof event.data === "object" &&
event.data !== null &&
"createdBy" in event.data
) {
return await authorizationService.canReceive{Entity}Event(
user,
event.data as { createdBy: string; [key: string]: unknown },
);
}
return false;
default:
return false;
}
}
Step 5: Add Event Schema (Optional)
In src/schemas/event.schema.ts:
export const {entity}EventSchema = serviceEventSchema.extend({
data: z.object({
id: z.string(),
// ... entity-specific fields
createdAt: z.date().optional(),
updatedAt: z.date().optional(),
}),
});
export type {Entity}EventType = z.infer<typeof {entity}EventSchema>;
Event Flow
1. Client: POST /notes (create a note)
2. Controller: calls NoteService.create()
3. Service: creates note, calls this.emitEvent("created", note, ...)
4. BaseService: appEvents.emitServiceEvent("notes", { action: "created", ... })
5. EventEmitter: emits "notes:created" event
6. Events Router: catches event, checks authorization
7. SSE Stream: sends to authorized clients
8. Client: receives { event: "notes:created", data: {...} }
Client-Side Usage
const eventSource = new EventSource("/events", {
headers: { Authorization: `Bearer ${token}` },
});
eventSource.addEventListener("notes:created", (event) => {
const data = JSON.parse(event.data);
console.log("New note:", data);
});
eventSource.addEventListener("notes:updated", (event) => {
const data = JSON.parse(event.data);
console.log("Updated note:", data);
});
eventSource.addEventListener("notes:deleted", (event) => {
const data = JSON.parse(event.data);
console.log("Deleted note:", data);
});
What NOT to Do
- Do NOT emit events before confirming operation succeeded
- Do NOT emit events for failed/unauthorized operations
- Do NOT skip authorization checks in events router
- Do NOT forget to clean up event listeners on disconnect
See Also
create-resource-service- Creating services with event emissioncreate-routes- Events router exampletest-resource-service- Testing event emission