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
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@notes/desktop",
"version": "0.4.1",
"version": "0.5.0",
"description": "Notes — local-first knowledge tool (Electron desktop wrapper)",
"author": "Notes Contributors",
"desktopName": "Notes",
Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@notes/server",
"version": "0.4.1",
"version": "0.5.0",
"private": true,
"type": "module",
"main": "./src/main.ts",
Expand Down
33 changes: 33 additions & 0 deletions apps/server/src/commands/calendar-event-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { EventBus } from "@notes/core";
import type { RichEvent } from "@notes/note-calendar";

// ---------------------------------------------------------------------------
// Calendar event command event map — emitted after mutating event.* commands
// ---------------------------------------------------------------------------

export type CalendarEventCommandEventMap = {
"calendar-event.created": { calendarPath: string; event: RichEvent };
"calendar-event.updated": { calendarPath: string; event: RichEvent };
"calendar-event.deleted": { calendarPath: string; eventId: string };
};

export function emitCalendarEventCreated(
events: EventBus<CalendarEventCommandEventMap>,
payload: CalendarEventCommandEventMap["calendar-event.created"],
): Promise<void> {
return events.emit("calendar-event.created", payload);
}

export function emitCalendarEventUpdated(
events: EventBus<CalendarEventCommandEventMap>,
payload: CalendarEventCommandEventMap["calendar-event.updated"],
): Promise<void> {
return events.emit("calendar-event.updated", payload);
}

export function emitCalendarEventDeleted(
events: EventBus<CalendarEventCommandEventMap>,
payload: CalendarEventCommandEventMap["calendar-event.deleted"],
): Promise<void> {
return events.emit("calendar-event.deleted", payload);
}
9 changes: 7 additions & 2 deletions apps/server/src/commands/card-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { RichCard } from "@notes/note-boards";
import { parseBoard, serializeBoard } from "@notes/note-boards";
import type { Tome } from "@notes/tome";
import { renameCompanionFolder } from "./companion-folder-rename";
import type { NoteLifecycleEventMap } from "./note-lifecycle";
import type { ServerEventMap } from "./server-events";
import { cardDotFolder, deleteCard, listCards, readCard, writeCard } from "./card-store";

interface GetTome {
Expand All @@ -30,7 +30,7 @@ async function saveBoardModel(
export function registerCardCommands(
bus: CommandBus,
getTome: GetTome,
events?: EventBus<NoteLifecycleEventMap>,
events?: EventBus<ServerEventMap>,
): void {
events?.on("note.path-moved", async ({ fromPath, toPath, noteType }) => {
if (noteType !== "board") {
Expand Down Expand Up @@ -73,6 +73,7 @@ export function registerCardCommands(
// ------------------------------------------------------------------
bus.register<{ boardPath: string; column: string }, RichCard>({
name: "card.create",
mutates: true,
handler: async ({ boardPath, column }) => {
const tome = getTome();
const model = await readBoardModel(boardPath, tome);
Expand All @@ -97,6 +98,7 @@ export function registerCardCommands(
// ------------------------------------------------------------------
bus.register<{ boardPath: string; card: RichCard }, RichCard>({
name: "card.update",
mutates: true,
handler: async ({ boardPath, card }) => {
const tome = getTome();
await writeCard(boardPath, card, tome);
Expand Down Expand Up @@ -129,6 +131,7 @@ export function registerCardCommands(
// ------------------------------------------------------------------
bus.register<{ boardPath: string; cardId: string }, { cardId: string }>({
name: "card.delete",
mutates: true,
handler: async ({ boardPath, cardId }) => {
const tome = getTome();
await deleteCard(boardPath, cardId, tome);
Expand All @@ -149,6 +152,7 @@ export function registerCardCommands(
// ------------------------------------------------------------------
bus.register<{ boardPath: string; cardId: string }, RichCard>({
name: "card.duplicate",
mutates: true,
handler: async ({ boardPath, cardId }) => {
const tome = getTome();
const source = await readCard(boardPath, cardId, tome);
Expand Down Expand Up @@ -182,6 +186,7 @@ export function registerCardCommands(
{ cardId: string; toColumn: string; toIndex: number }
>({
name: "card.move",
mutates: true,
handler: async ({ boardPath, cardId, toColumn, toIndex }) => {
const tome = getTome();
const model = await readBoardModel(boardPath, tome);
Expand Down
49 changes: 49 additions & 0 deletions apps/server/src/commands/card-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { EventBus } from "@notes/core";
import type { RichCard } from "@notes/note-boards";

// ---------------------------------------------------------------------------
// Card command event map — emitted after each mutating card.* command succeeds
// ---------------------------------------------------------------------------

export type CardCommandEventMap = {
"card.created": { boardPath: string; card: RichCard };
"card.updated": { boardPath: string; card: RichCard };
"card.deleted": { boardPath: string; cardId: string };
"card.moved": { boardPath: string; cardId: string; toColumn: string; toIndex: number };
"card.duplicated": { boardPath: string; card: RichCard };
};

export function emitCardCreated(
events: EventBus<CardCommandEventMap>,
payload: CardCommandEventMap["card.created"],
): Promise<void> {
return events.emit("card.created", payload);
}

export function emitCardUpdated(
events: EventBus<CardCommandEventMap>,
payload: CardCommandEventMap["card.updated"],
): Promise<void> {
return events.emit("card.updated", payload);
}

export function emitCardDeleted(
events: EventBus<CardCommandEventMap>,
payload: CardCommandEventMap["card.deleted"],
): Promise<void> {
return events.emit("card.deleted", payload);
}

export function emitCardMoved(
events: EventBus<CardCommandEventMap>,
payload: CardCommandEventMap["card.moved"],
): Promise<void> {
return events.emit("card.moved", payload);
}

export function emitCardDuplicated(
events: EventBus<CardCommandEventMap>,
payload: CardCommandEventMap["card.duplicated"],
): Promise<void> {
return events.emit("card.duplicated", payload);
}
127 changes: 127 additions & 0 deletions apps/server/src/commands/command-event-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import type { EventBus, Middleware } from "@notes/core";
import type { RichCard } from "@notes/note-boards";
import type { RichEvent } from "@notes/note-calendar";
import type { ServerEventMap } from "./server-events";

/**
* Command bus middleware that emits typed domain events after any command that
* has `mutates: true` succeeds. Plugins and internal listeners can subscribe
* to `EventBus<ServerEventMap>` to react to any mutation without coupling to
* specific command implementations.
*
* Event emission follows the `note-lifecycle.ts` pattern: typed event maps per
* domain, collected into a single `ServerEventMap` union.
*/
export function createCommandEventMiddleware(events: EventBus<ServerEventMap>): Middleware {
return async (invocation, next) => {
const result = await next();

if (!invocation.ctx) return result;

const command = invocation as { name: string; payload: unknown };
const { name, payload } = command;

switch (name) {
// ------------------------------------------------------------------
// file.* mutations
// ------------------------------------------------------------------
case "file.create":
case "file.createBinary": {
const p = payload as { path: string };
await events.emit("file.created", { path: p.path });
break;
}
case "file.write": {
const p = payload as { path: string };
await events.emit("file.written", { path: p.path });
break;
}
case "file.delete": {
const p = payload as { path: string };
await events.emit("file.deleted", { path: p.path });
break;
}
case "file.rename":
case "file.move": {
const p = payload as { from: string; to: string };
await events.emit("file.renamed", { from: p.from, to: p.to });
// note.path-moved is emitted separately inside file-commands.ts
break;
}
case "file.mkdir": {
const p = payload as { path: string };
await events.emit("file.mkdir", { path: p.path });
break;
}

// ------------------------------------------------------------------
// card.* mutations
// ------------------------------------------------------------------
case "card.create": {
const p = payload as { boardPath: string };
const card = result as RichCard;
await events.emit("card.created", { boardPath: p.boardPath, card });
break;
}
case "card.update": {
const p = payload as { boardPath: string; card: RichCard };
await events.emit("card.updated", { boardPath: p.boardPath, card: p.card });
break;
}
case "card.delete": {
const p = payload as { boardPath: string; cardId: string };
await events.emit("card.deleted", { boardPath: p.boardPath, cardId: p.cardId });
break;
}
case "card.move": {
const p = payload as {
boardPath: string;
cardId: string;
toColumn: string;
toIndex: number;
};
await events.emit("card.moved", {
boardPath: p.boardPath,
cardId: p.cardId,
toColumn: p.toColumn,
toIndex: p.toIndex,
});
break;
}
case "card.duplicate": {
const p = payload as { boardPath: string };
const card = result as RichCard;
await events.emit("card.duplicated", { boardPath: p.boardPath, card });
break;
}

// ------------------------------------------------------------------
// event.* (calendar events) mutations
// ------------------------------------------------------------------
case "event.create": {
const p = payload as { calendarPath: string };
const event = result as RichEvent;
await events.emit("calendar-event.created", { calendarPath: p.calendarPath, event });
break;
}
case "event.update": {
const p = payload as { calendarPath: string; event: RichEvent };
await events.emit("calendar-event.updated", {
calendarPath: p.calendarPath,
event: p.event,
});
break;
}
case "event.delete": {
const p = payload as { calendarPath: string; eventId: string };
await events.emit("calendar-event.deleted", {
calendarPath: p.calendarPath,
eventId: p.eventId,
});
break;
}
}

return result;
};
}
7 changes: 5 additions & 2 deletions apps/server/src/commands/event-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { RichEvent } from "@notes/note-calendar";
import { newEventId, parseCalendar, serializeCalendar } from "@notes/note-calendar";
import type { Tome } from "@notes/tome";
import { renameCompanionFolder } from "./companion-folder-rename";
import type { NoteLifecycleEventMap } from "./note-lifecycle";
import type { ServerEventMap } from "./server-events";
import { deleteEvent, eventDotFolder, listEvents, readEvent, writeEvent } from "./event-store";

interface GetTome {
Expand All @@ -30,7 +30,7 @@ async function saveCalendarModel(
export function registerEventCommands(
bus: CommandBus,
getTome: GetTome,
events?: EventBus<NoteLifecycleEventMap>,
events?: EventBus<ServerEventMap>,
): void {
events?.on("note.path-moved", async ({ fromPath, toPath, noteType }) => {
if (noteType !== "calendar") {
Expand Down Expand Up @@ -71,6 +71,7 @@ export function registerEventCommands(
// ------------------------------------------------------------------
bus.register<{ calendarPath: string; date: string }, RichEvent>({
name: "event.create",
mutates: true,
handler: async ({ calendarPath, date }) => {
const tome = getTome();
const model = await readCalendarModel(calendarPath, tome);
Expand All @@ -88,6 +89,7 @@ export function registerEventCommands(
// ------------------------------------------------------------------
bus.register<{ calendarPath: string; event: RichEvent }, RichEvent>({
name: "event.update",
mutates: true,
handler: async ({ calendarPath, event }) => {
await writeEvent(calendarPath, event, getTome());
return event;
Expand All @@ -99,6 +101,7 @@ export function registerEventCommands(
// ------------------------------------------------------------------
bus.register<{ calendarPath: string; eventId: string }, { eventId: string }>({
name: "event.delete",
mutates: true,
handler: async ({ calendarPath, eventId }) => {
const tome = getTome();
await deleteEvent(calendarPath, eventId, tome);
Expand Down
Loading
Loading