mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-21 22:46:32 +00:00
chore: move day-notes backend to proper DI
This commit is contained in:
@@ -5,7 +5,6 @@ import { listDays, listAccommodations } from '../services/dayService';
|
||||
import { listPlaces } from '../services/placeService';
|
||||
import { listBudgetItems, getPerPersonSummary, calculateSettlement } from '../services/budgetService';
|
||||
import { listReservations } from '../services/reservationService';
|
||||
import { listNotes as listDayNotes } from '../services/dayNoteService';
|
||||
import { listNotes as listCollabNotes, listPolls, listMessages } from '../services/collabService';
|
||||
import { listBucketList, listVisitedCountries, getStats as getAtlasStats, listManuallyVisitedRegions } from '../services/atlasService';
|
||||
import { getNotifications } from '../services/inAppNotifications';
|
||||
@@ -132,19 +131,8 @@ export function registerResources(server: McpServer, userId: number, scopes: str
|
||||
}
|
||||
);
|
||||
|
||||
// Day notes
|
||||
if (canReadTrips(scopes)) server.registerResource(
|
||||
'day-notes',
|
||||
new ResourceTemplate('trek://trips/{tripId}/days/{dayId}/notes', { list: undefined }),
|
||||
{ description: 'Notes for a specific day in a trip', mimeType: 'application/json' },
|
||||
async (uri, { tripId, dayId }) => {
|
||||
const tId = parseId(tripId);
|
||||
const dId = parseId(dayId);
|
||||
if (tId === null || dId === null || !canAccessTrip(tId, userId)) return accessDenied(uri.href);
|
||||
const notes = listDayNotes(dId, tId);
|
||||
return jsonContent(uri.href, notes);
|
||||
}
|
||||
);
|
||||
// The day-notes resource moved to the DI-discovered
|
||||
// src/nest/days/day-notes.mcp.ts (@ResourceTemplate).
|
||||
|
||||
// Accommodations (hotels, rentals) per trip
|
||||
if (canReadTrips(scopes)) server.registerResource(
|
||||
|
||||
@@ -8,10 +8,6 @@ import {
|
||||
createAccommodation, getAccommodation, updateAccommodation, deleteAccommodation,
|
||||
} from '../../services/dayService';
|
||||
import { createPlace } from '../../services/placeService';
|
||||
import {
|
||||
createNote as createDayNote, getNote as getDayNote, updateNote as updateDayNote,
|
||||
deleteNote as deleteDayNote, dayExists as dayNoteExists,
|
||||
} from '../../services/dayNoteService';
|
||||
import {
|
||||
safeBroadcast, TOOL_ANNOTATIONS_WRITE, TOOL_ANNOTATIONS_DELETE,
|
||||
TOOL_ANNOTATIONS_NON_IDEMPOTENT,
|
||||
@@ -221,78 +217,6 @@ export function registerDayTools(server: McpServer, userId: number, scopes: stri
|
||||
}
|
||||
);
|
||||
|
||||
// --- DAY NOTES ---
|
||||
|
||||
server.registerTool(
|
||||
'create_day_note',
|
||||
{
|
||||
description: 'Add a note to a specific day in a trip.',
|
||||
inputSchema: {
|
||||
tripId: z.number().int().positive(),
|
||||
dayId: z.number().int().positive(),
|
||||
text: z.string().min(1).max(500),
|
||||
time: z.string().max(250).optional().describe('Time label (e.g. "09:00" or "Morning")'),
|
||||
icon: z.string().optional().describe('Emoji icon for the note'),
|
||||
},
|
||||
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
|
||||
},
|
||||
async ({ tripId, dayId, text, time, icon }) => {
|
||||
if (isDemoUser(userId)) return demoDenied();
|
||||
if (!canAccessTrip(tripId, userId)) return noAccess();
|
||||
if (!hasTripPermission('day_edit', tripId, userId)) return permissionDenied();
|
||||
if (!dayNoteExists(dayId, tripId)) return { content: [{ type: 'text' as const, text: 'Day not found.' }], isError: true };
|
||||
const note = createDayNote(dayId, tripId, text, time, icon);
|
||||
safeBroadcast(tripId, 'dayNote:created', { dayId, note });
|
||||
return ok({ note });
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'update_day_note',
|
||||
{
|
||||
description: 'Edit an existing note on a specific day.',
|
||||
inputSchema: {
|
||||
tripId: z.number().int().positive(),
|
||||
dayId: z.number().int().positive(),
|
||||
noteId: z.number().int().positive(),
|
||||
text: z.string().min(1).max(500).optional(),
|
||||
time: z.string().max(250).nullable().optional().describe('Time label (e.g. "09:00" or "Morning"), or null to clear'),
|
||||
icon: z.string().optional().describe('Emoji icon for the note'),
|
||||
},
|
||||
annotations: TOOL_ANNOTATIONS_WRITE,
|
||||
},
|
||||
async ({ tripId, dayId, noteId, text, time, icon }) => {
|
||||
if (isDemoUser(userId)) return demoDenied();
|
||||
if (!canAccessTrip(tripId, userId)) return noAccess();
|
||||
if (!hasTripPermission('day_edit', tripId, userId)) return permissionDenied();
|
||||
const existing = getDayNote(noteId, dayId, tripId);
|
||||
if (!existing) return { content: [{ type: 'text' as const, text: 'Note not found.' }], isError: true };
|
||||
const note = updateDayNote(noteId, existing, { text, time: time !== undefined ? time : undefined, icon });
|
||||
safeBroadcast(tripId, 'dayNote:updated', { dayId, note });
|
||||
return ok({ note });
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'delete_day_note',
|
||||
{
|
||||
description: 'Delete a note from a specific day.',
|
||||
inputSchema: {
|
||||
tripId: z.number().int().positive(),
|
||||
dayId: z.number().int().positive(),
|
||||
noteId: z.number().int().positive(),
|
||||
},
|
||||
annotations: TOOL_ANNOTATIONS_DELETE,
|
||||
},
|
||||
async ({ tripId, dayId, noteId }) => {
|
||||
if (isDemoUser(userId)) return demoDenied();
|
||||
if (!canAccessTrip(tripId, userId)) return noAccess();
|
||||
if (!hasTripPermission('day_edit', tripId, userId)) return permissionDenied();
|
||||
const note = getDayNote(noteId, dayId, tripId);
|
||||
if (!note) return { content: [{ type: 'text' as const, text: 'Note not found.' }], isError: true };
|
||||
deleteDayNote(noteId);
|
||||
safeBroadcast(tripId, 'dayNote:deleted', { noteId, dayId });
|
||||
return ok({ success: true });
|
||||
}
|
||||
);
|
||||
// The day-note tools (create/update/delete_day_note) moved to the
|
||||
// DI-discovered src/nest/days/day-notes.mcp.ts (@McpController).
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ mount to Nest and leaves the sibling trip routes (days, places, ...) on Express.
|
||||
categories, tags, notifications, atlas.
|
||||
- **Phase 2 (trip sub-domains):** vacay (addon), packing, todo.
|
||||
- **DI-native services (legacy `src/services/*` deleted):** tags, categories,
|
||||
todo, packing — see the migration recipe below.
|
||||
todo, packing, day-notes — see the migration recipe below.
|
||||
|
||||
## Cross-cutting Foundation pieces
|
||||
|
||||
@@ -101,8 +101,11 @@ first in-container consumer wiring: `TripsService` injects `TodoService` via
|
||||
`exports: [TodoService]` instead of using the bridge); packing followed (the
|
||||
largest port so far: a 17-tool + 2-resource `packing.mcp.ts` with inline admin
|
||||
gates, and the first `PluginHostDepsFactory` swap done as part of a service
|
||||
migration — no bridge entry for the plugin host). Repeat these steps per
|
||||
service (next up: dayNoteService). This is a **pure relocation** — byte-identical
|
||||
migration — no bridge entry for the plugin host); day-notes followed (the first
|
||||
migration needing **no bridge at all**: after its three tools + resource moved
|
||||
to `day-notes.mcp.ts` and the plugin host injection, nothing outside the
|
||||
container consumed it). Repeat these steps per
|
||||
service (next up: tripInviteService). This is a **pure relocation** — byte-identical
|
||||
SQL, statuses, bodies, and error strings. The plugin RPC host is **no longer a
|
||||
bridge consumer**: since Option A of `src/nest/plugins/DI-MIGRATION.md` it
|
||||
injects domain services via `PluginHostDepsFactory`, so a migrated domain adds
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import {
|
||||
McpController, Tool, ResourceTemplate, type McpContext,
|
||||
TOOL_ANNOTATIONS_WRITE, TOOL_ANNOTATIONS_DELETE, TOOL_ANNOTATIONS_NON_IDEMPOTENT,
|
||||
demoDenied, ok,
|
||||
} from '@trek/nest-mcp';
|
||||
import { z } from 'zod';
|
||||
import { isDemoUser } from '../../services/authService';
|
||||
import { safeBroadcast, noAccess, hasTripPermission, permissionDenied } from '../../mcp/tools/_shared';
|
||||
import { DayNotesService } from './day-notes.service';
|
||||
|
||||
function parseId(value: string | string[]): number | null {
|
||||
const n = Number(Array.isArray(value) ? value[0] : value);
|
||||
return Number.isInteger(n) && n > 0 ? n : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Day-note MCP surface — ported 1:1 from the legacy registrars: the three
|
||||
* tools from the DAY NOTES block of src/mcp/tools/days.ts and the
|
||||
* trek://trips/{tripId}/days/{dayId}/notes resource from src/mcp/resources.ts
|
||||
* (identical names, descriptions, schemas, annotations, error/payload shapes
|
||||
* and broadcasts). The legacy registration-time gates map to the declarative
|
||||
* trips write/read access markers (registerDayTools' whole-registrar
|
||||
* `canWrite(scopes, 'trips')` early return and the resource's canReadTrips
|
||||
* check, resolved by trekMcpAccessPolicy). No addon gate — day notes are core.
|
||||
*/
|
||||
@McpController()
|
||||
export class DayNotesMcp {
|
||||
constructor(private readonly notes: DayNotesService) {}
|
||||
|
||||
@Tool({
|
||||
name: 'create_day_note',
|
||||
description: 'Add a note to a specific day in a trip.',
|
||||
inputSchema: {
|
||||
tripId: z.number().int().positive(),
|
||||
dayId: z.number().int().positive(),
|
||||
text: z.string().min(1).max(500),
|
||||
time: z.string().max(250).optional().describe('Time label (e.g. "09:00" or "Morning")'),
|
||||
icon: z.string().optional().describe('Emoji icon for the note'),
|
||||
},
|
||||
annotations: TOOL_ANNOTATIONS_NON_IDEMPOTENT,
|
||||
access: { group: 'trips', mode: 'write' },
|
||||
})
|
||||
async createDayNote(
|
||||
{ tripId, dayId, text, time, icon }: { tripId: number; dayId: number; text: string; time?: string; icon?: string },
|
||||
ctx: McpContext,
|
||||
) {
|
||||
if (isDemoUser(ctx.userId)) return demoDenied();
|
||||
if (!this.notes.verifyTripAccess(tripId, ctx.userId)) return noAccess();
|
||||
if (!hasTripPermission('day_edit', tripId, ctx.userId)) return permissionDenied();
|
||||
if (!this.notes.dayExists(dayId, tripId)) return { content: [{ type: 'text' as const, text: 'Day not found.' }], isError: true };
|
||||
const note = this.notes.create(dayId, tripId, text, time, icon);
|
||||
safeBroadcast(tripId, 'dayNote:created', { dayId, note });
|
||||
return ok({ note });
|
||||
}
|
||||
|
||||
@Tool({
|
||||
name: 'update_day_note',
|
||||
description: 'Edit an existing note on a specific day.',
|
||||
inputSchema: {
|
||||
tripId: z.number().int().positive(),
|
||||
dayId: z.number().int().positive(),
|
||||
noteId: z.number().int().positive(),
|
||||
text: z.string().min(1).max(500).optional(),
|
||||
time: z.string().max(250).nullable().optional().describe('Time label (e.g. "09:00" or "Morning"), or null to clear'),
|
||||
icon: z.string().optional().describe('Emoji icon for the note'),
|
||||
},
|
||||
annotations: TOOL_ANNOTATIONS_WRITE,
|
||||
access: { group: 'trips', mode: 'write' },
|
||||
})
|
||||
async updateDayNote(
|
||||
{ tripId, dayId, noteId, text, time, icon }: { tripId: number; dayId: number; noteId: number; text?: string; time?: string | null; icon?: string },
|
||||
ctx: McpContext,
|
||||
) {
|
||||
if (isDemoUser(ctx.userId)) return demoDenied();
|
||||
if (!this.notes.verifyTripAccess(tripId, ctx.userId)) return noAccess();
|
||||
if (!hasTripPermission('day_edit', tripId, ctx.userId)) return permissionDenied();
|
||||
const existing = this.notes.getNote(noteId, dayId, tripId);
|
||||
if (!existing) return { content: [{ type: 'text' as const, text: 'Note not found.' }], isError: true };
|
||||
const note = this.notes.update(noteId, existing, { text, time: time !== undefined ? time : undefined, icon });
|
||||
safeBroadcast(tripId, 'dayNote:updated', { dayId, note });
|
||||
return ok({ note });
|
||||
}
|
||||
|
||||
@Tool({
|
||||
name: 'delete_day_note',
|
||||
description: 'Delete a note from a specific day.',
|
||||
inputSchema: {
|
||||
tripId: z.number().int().positive(),
|
||||
dayId: z.number().int().positive(),
|
||||
noteId: z.number().int().positive(),
|
||||
},
|
||||
annotations: TOOL_ANNOTATIONS_DELETE,
|
||||
access: { group: 'trips', mode: 'write' },
|
||||
})
|
||||
async deleteDayNote({ tripId, dayId, noteId }: { tripId: number; dayId: number; noteId: number }, ctx: McpContext) {
|
||||
if (isDemoUser(ctx.userId)) return demoDenied();
|
||||
if (!this.notes.verifyTripAccess(tripId, ctx.userId)) return noAccess();
|
||||
if (!hasTripPermission('day_edit', tripId, ctx.userId)) return permissionDenied();
|
||||
const note = this.notes.getNote(noteId, dayId, tripId);
|
||||
if (!note) return { content: [{ type: 'text' as const, text: 'Note not found.' }], isError: true };
|
||||
this.notes.remove(noteId);
|
||||
safeBroadcast(tripId, 'dayNote:deleted', { noteId, dayId });
|
||||
return ok({ success: true });
|
||||
}
|
||||
|
||||
@ResourceTemplate({
|
||||
name: 'day-notes',
|
||||
uriTemplate: 'trek://trips/{tripId}/days/{dayId}/notes',
|
||||
description: 'Notes for a specific day in a trip',
|
||||
mimeType: 'application/json',
|
||||
access: { group: 'trips', mode: 'read' },
|
||||
})
|
||||
async dayNotesResource(uri: URL, { tripId, dayId }: { tripId: string | string[]; dayId: string | string[] }, ctx: McpContext) {
|
||||
const tId = parseId(tripId);
|
||||
const dId = parseId(dayId);
|
||||
if (tId === null || dId === null || !this.notes.verifyTripAccess(tId, ctx.userId)) {
|
||||
return {
|
||||
contents: [{
|
||||
uri: uri.href,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify({ error: 'Trip not found or access denied' }),
|
||||
}],
|
||||
};
|
||||
}
|
||||
const notes = this.notes.list(dId, tId);
|
||||
return {
|
||||
contents: [{
|
||||
uri: uri.href,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(notes, null, 2),
|
||||
}],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,25 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { broadcast } from '../../websocket';
|
||||
import { checkPermission } from '../../services/permissions';
|
||||
import type { User } from '../../types';
|
||||
import * as dayNoteService from '../../services/dayNoteService';
|
||||
|
||||
type Trip = NonNullable<ReturnType<typeof dayNoteService.verifyTripAccess>>;
|
||||
import type { DayNote, User } from '../../types';
|
||||
import { DatabaseService, type TripAccess } from '../database/database.service';
|
||||
|
||||
/**
|
||||
* Thin Nest wrapper around the existing day-note service. Trip access + the
|
||||
* 'day_edit' permission reuse the legacy checks; the SQL is unchanged.
|
||||
* Day-notes domain service — the legacy dayNoteService SQL folded in over
|
||||
* the injected DatabaseService (byte-identical statements and
|
||||
* coercions). Trip access rides DatabaseService.canAccessTrip (absorbing the
|
||||
* legacy services/tripAccess re-export); the 'day_edit' permission reuses the
|
||||
* legacy check.
|
||||
*/
|
||||
@Injectable()
|
||||
export class DayNotesService {
|
||||
verifyTripAccess(tripId: string, userId: number) {
|
||||
return dayNoteService.verifyTripAccess(tripId, userId);
|
||||
constructor(private readonly dbs: DatabaseService) {}
|
||||
|
||||
verifyTripAccess(tripId: string | number, userId: number): TripAccess | undefined {
|
||||
return this.dbs.canAccessTrip(tripId, userId);
|
||||
}
|
||||
|
||||
canEdit(trip: Trip, user: User): boolean {
|
||||
canEdit(trip: TripAccess, user: User): boolean {
|
||||
return checkPermission('day_edit', user.role, trip.user_id, user.id, trip.user_id !== user.id);
|
||||
}
|
||||
|
||||
@@ -24,27 +27,42 @@ export class DayNotesService {
|
||||
broadcast(tripId, event, payload, socketId);
|
||||
}
|
||||
|
||||
list(dayId: string, tripId: string) {
|
||||
return dayNoteService.listNotes(dayId, tripId);
|
||||
list(dayId: string | number, tripId: string | number) {
|
||||
return this.dbs.all(
|
||||
'SELECT * FROM day_notes WHERE day_id = ? AND trip_id = ? ORDER BY sort_order ASC, created_at ASC',
|
||||
dayId, tripId,
|
||||
);
|
||||
}
|
||||
|
||||
dayExists(dayId: string, tripId: string) {
|
||||
return dayNoteService.dayExists(dayId, tripId);
|
||||
dayExists(dayId: string | number, tripId: string | number) {
|
||||
return this.dbs.get('SELECT id FROM days WHERE id = ? AND trip_id = ?', dayId, tripId);
|
||||
}
|
||||
|
||||
getNote(id: string, dayId: string, tripId: string) {
|
||||
return dayNoteService.getNote(id, dayId, tripId);
|
||||
getNote(id: string | number, dayId: string | number, tripId: string | number) {
|
||||
return this.dbs.get<DayNote>('SELECT * FROM day_notes WHERE id = ? AND day_id = ? AND trip_id = ?', id, dayId, tripId);
|
||||
}
|
||||
|
||||
create(dayId: string, tripId: string, text: string, time?: string, icon?: string, sortOrder?: number) {
|
||||
return dayNoteService.createNote(dayId, tripId, text, time, icon, sortOrder);
|
||||
create(dayId: string | number, tripId: string | number, text: string, time?: string | null, icon?: string | null, sortOrder?: number) {
|
||||
const result = this.dbs.run(
|
||||
'INSERT INTO day_notes (day_id, trip_id, text, time, icon, sort_order) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
dayId, tripId, text.trim(), time || null, icon || '📝', sortOrder ?? 9999,
|
||||
);
|
||||
return this.dbs.get('SELECT * FROM day_notes WHERE id = ?', result.lastInsertRowid);
|
||||
}
|
||||
|
||||
update(id: string, current: Parameters<typeof dayNoteService.updateNote>[1], fields: { text?: string; time?: string; icon?: string; sort_order?: number }) {
|
||||
return dayNoteService.updateNote(id, current, fields);
|
||||
update(id: string | number, current: DayNote, fields: { text?: string; time?: string | null; icon?: string | null; sort_order?: number }) {
|
||||
this.dbs.run(
|
||||
'UPDATE day_notes SET text = ?, time = ?, icon = ?, sort_order = ? WHERE id = ?',
|
||||
fields.text !== undefined ? fields.text.trim() : current.text,
|
||||
fields.time !== undefined ? fields.time : current.time,
|
||||
fields.icon !== undefined ? fields.icon : current.icon,
|
||||
fields.sort_order !== undefined ? fields.sort_order : current.sort_order,
|
||||
id,
|
||||
);
|
||||
return this.dbs.get('SELECT * FROM day_notes WHERE id = ?', id);
|
||||
}
|
||||
|
||||
remove(id: string): void {
|
||||
dayNoteService.deleteNote(id);
|
||||
remove(id: string | number): void {
|
||||
this.dbs.run('DELETE FROM day_notes WHERE id = ?', id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,17 @@ import { DaysController } from './days.controller';
|
||||
import { DaysService } from './days.service';
|
||||
import { DayNotesController } from './day-notes.controller';
|
||||
import { DayNotesService } from './day-notes.service';
|
||||
import { DayNotesMcp } from './day-notes.mcp';
|
||||
|
||||
/**
|
||||
* Days + day-notes domain (S6 — Phase 2 trip sub-domain). The single prefix
|
||||
* /api/trips/:tripId/days covers both the days mount and the nested
|
||||
* /days/:dayId/notes mount.
|
||||
* /days/:dayId/notes mount. DayNotesService is exported for the plugin RPC
|
||||
* host (PluginHostDepsFactory).
|
||||
*/
|
||||
@Module({
|
||||
controllers: [DaysController, DayNotesController],
|
||||
providers: [DaysService, DayNotesService],
|
||||
providers: [DaysService, DayNotesService, DayNotesMcp],
|
||||
exports: [DayNotesService],
|
||||
})
|
||||
export class DaysModule {}
|
||||
|
||||
@@ -98,8 +98,8 @@ is `@Global`, so no import needed for it.
|
||||
factory *until their own domain migrates*. Each future migration then swaps one
|
||||
import for one injected service (+ module export/import) — **no more bridge
|
||||
files for the plugin host, ever**. The factory's import list shrinks
|
||||
monotonically as the recipe in `src/nest/README.md` proceeds (packing swapped
|
||||
in 2026-07; next up: dayNoteService).
|
||||
monotonically as the recipe in `src/nest/README.md` proceeds (packing and
|
||||
day-notes swapped in 2026-07; next up: tripInviteService).
|
||||
|
||||
### Test impact (as landed)
|
||||
|
||||
@@ -215,8 +215,9 @@ none of it is throwaway.
|
||||
1. ~~**Now / opportunistically — Option A.**~~ **Done (2026-07).** Injectable
|
||||
factory, tags/categories bridges deleted, wiring test rewritten against
|
||||
stubs (see §2).
|
||||
2. **Per domain, as the service migrations proceed** (`packingService` done
|
||||
2026-07; `dayNoteService` next, per `src/nest/README.md`): migrate the
|
||||
2. **Per domain, as the service migrations proceed** (`packingService` and
|
||||
`dayNoteService` done 2026-07; `tripInviteService` next, per
|
||||
`src/nest/README.md`): migrate the
|
||||
service to DI as usual; the factory swaps one legacy import for one
|
||||
injected service. Optionally pilot `tags.rpc.ts` here — tags is small and
|
||||
already fully DI on HTTP + MCP.
|
||||
|
||||
@@ -28,7 +28,6 @@ import { ADDON_IDS } from '../../../addons';
|
||||
import { listJourneys, listEntries as listJournalEntriesSvc, createEntry as createJournalEntrySvc, updateEntry as updateJournalEntrySvc, deleteEntry as deleteJournalEntrySvc, createJourney as createJourneySvc, deleteJourney as deleteJourneySvc } from '../../../services/journeyService';
|
||||
import { listVisitedCountries, listManuallyVisitedRegions, listBucketList, markCountryVisited, unmarkCountryVisited, markRegionVisited, unmarkRegionVisited, createBucketItem as createBucketItemSvc, deleteBucketItem as deleteBucketItemSvc } from '../../../services/atlasService';
|
||||
import { getPlanData, getActivePlanId, toggleEntry as vacayToggleEntrySvc, toggleCompanyHoliday as vacayToggleCompanyHolidaySvc } from '../../../services/vacayService';
|
||||
import { listNotes, createNote, getNote, updateNote, deleteNote, dayExists as dayNoteDayExists } from '../../../services/dayNoteService';
|
||||
import { listCollections, getCollection, createCollection, updateCollection, savePlace as saveCollectionPlaceSvc, copyToTrip as copyCollectionToTripSvc, deletePlace as deleteCollectionPlaceSvc } from '../../../services/collectionsService';
|
||||
import { BudgetService } from '../../budget/budget.service';
|
||||
import { ReservationsService } from '../../reservations/reservations.service';
|
||||
@@ -36,6 +35,7 @@ import { TagsService } from '../../tags/tags.service';
|
||||
import { CategoriesService } from '../../categories/categories.service';
|
||||
import { TodoService } from '../../todo/todo.service';
|
||||
import { PackingService } from '../../packing/packing.service';
|
||||
import { DayNotesService } from '../../days/day-notes.service';
|
||||
import { notifyBookingChange } from '../../../services/reservationService';
|
||||
import { PluginRpcHost, ForbiddenResource, BadParams } from './rpc-host';
|
||||
import { appendAudit } from './plugin-audit';
|
||||
@@ -150,9 +150,10 @@ export interface PluginCallRouter {
|
||||
* `plugin:{id}:{event}` so a plugin can't forge a core event.
|
||||
*
|
||||
* DI-native domain services (budget, reservations, tags, categories, todo,
|
||||
* packing, oauth) are constructor-injected; legacy `services/*` domains stay
|
||||
* plain function imports until their own migration lands (DI-MIGRATION.md), at
|
||||
* which point each swaps one import for one injected service.
|
||||
* packing, day-notes, oauth) are constructor-injected; legacy `services/*`
|
||||
* domains stay plain function imports until their own migration lands
|
||||
* (DI-MIGRATION.md), at which point each swaps one import for one injected
|
||||
* service.
|
||||
*/
|
||||
@Injectable()
|
||||
export class PluginHostDepsFactory {
|
||||
@@ -164,6 +165,7 @@ export class PluginHostDepsFactory {
|
||||
private readonly todos: TodoService,
|
||||
private readonly packing: PackingService,
|
||||
private readonly oauth: PluginOAuthService,
|
||||
private readonly dayNotes: DayNotesService,
|
||||
) {}
|
||||
|
||||
create(id: string, granted: ReadonlySet<string>, router: PluginCallRouter): PluginRpcHost {
|
||||
@@ -690,27 +692,27 @@ export class PluginHostDepsFactory {
|
||||
return { deleted: true };
|
||||
},
|
||||
// Day notes are core (no addon) and trip-scoped; membership is enforced by the host.
|
||||
listDayNotes: (tripId, dayId) => listNotes(dayId, tripId),
|
||||
listDayNotes: (tripId, dayId) => this.dayNotes.list(dayId, tripId),
|
||||
// --- Day notes write (day_edit). The day must belong to the trip; broadcasts the
|
||||
// same dayNote:* events the REST controller emits so open sessions update live. ---
|
||||
createDayNote: (tripId, dayId, input) => {
|
||||
if (!dayNoteDayExists(dayId, tripId)) throw new ForbiddenResource(`no day ${dayId} on trip ${tripId}`);
|
||||
if (!this.dayNotes.dayExists(dayId, tripId)) throw new ForbiddenResource(`no day ${dayId} on trip ${tripId}`);
|
||||
const i = input as { text?: string; time?: string; icon?: string; sort_order?: number };
|
||||
const note = createNote(dayId, tripId, i.text ?? '', i.time, i.icon, i.sort_order);
|
||||
const note = this.dayNotes.create(dayId, tripId, i.text ?? '', i.time, i.icon, i.sort_order);
|
||||
broadcast(tripId, 'dayNote:created', { dayId, note }, undefined);
|
||||
return note;
|
||||
},
|
||||
updateDayNote: (tripId, dayId, noteId, input) => {
|
||||
const current = getNote(noteId, dayId, tripId);
|
||||
const current = this.dayNotes.getNote(noteId, dayId, tripId);
|
||||
if (!current) throw new ForbiddenResource(`no note ${noteId} on day ${dayId}`);
|
||||
const note = updateNote(noteId, current as never, input as { text?: string; time?: string; icon?: string; sort_order?: number });
|
||||
const note = this.dayNotes.update(noteId, current as never, input as { text?: string; time?: string; icon?: string; sort_order?: number });
|
||||
broadcast(tripId, 'dayNote:updated', { dayId, note }, undefined);
|
||||
return note;
|
||||
},
|
||||
deleteDayNote: (tripId, dayId, noteId) => {
|
||||
const current = getNote(noteId, dayId, tripId);
|
||||
const current = this.dayNotes.getNote(noteId, dayId, tripId);
|
||||
if (!current) throw new ForbiddenResource(`no note ${noteId} on day ${dayId}`);
|
||||
deleteNote(noteId);
|
||||
this.dayNotes.remove(noteId);
|
||||
broadcast(tripId, 'dayNote:deleted', { noteId, dayId }, undefined);
|
||||
return { deleted: true };
|
||||
},
|
||||
|
||||
@@ -30,6 +30,7 @@ import { BudgetModule } from '../budget/budget.module';
|
||||
import { ReservationsModule } from '../reservations/reservations.module';
|
||||
import { TodoModule } from '../todo/todo.module';
|
||||
import { PackingModule } from '../packing/packing.module';
|
||||
import { DaysModule } from '../days/days.module';
|
||||
|
||||
/**
|
||||
* Plugin system (#plugins). M0 read side + M2 isolated runtime + M3 frontend:
|
||||
@@ -41,7 +42,7 @@ import { PackingModule } from '../packing/packing.module';
|
||||
@Module({
|
||||
// The DI-native domain services the plugin host wiring injects
|
||||
// (PluginHostDepsFactory); DatabaseModule is @Global, so not listed.
|
||||
imports: [TagsModule, CategoriesModule, BudgetModule, ReservationsModule, TodoModule, PackingModule],
|
||||
imports: [TagsModule, CategoriesModule, BudgetModule, ReservationsModule, TodoModule, PackingModule, DaysModule],
|
||||
controllers: [PluginsController, PluginsFeedController, PluginsProxyController, PluginFrameController, PlaceDetailsController, TripWarningsController, ViewContributionsController, TripCardContributionsController, PluginPhotosController, PluginCalendarController, MapMarkersController, MapLayersController, PluginRoutesController, DayScheduleController, PdfSectionsController, AtlasLayersController, JournalEntryRowsController, PluginUserSettingsController, PluginOAuthController, PluginActivityController],
|
||||
providers: [PluginsService, PluginRuntimeService, PluginRegistryService, PluginOAuthService, PluginHostDepsFactory],
|
||||
// Exported so the admin addon-toggle handler can cascade-disable plugins whose
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { db } from '../db/database';
|
||||
import { DayNote } from '../types';
|
||||
|
||||
export { verifyTripAccess } from './tripAccess';
|
||||
|
||||
export function listNotes(dayId: string | number, tripId: string | number) {
|
||||
return db.prepare(
|
||||
'SELECT * FROM day_notes WHERE day_id = ? AND trip_id = ? ORDER BY sort_order ASC, created_at ASC'
|
||||
).all(dayId, tripId);
|
||||
}
|
||||
|
||||
export function dayExists(dayId: string | number, tripId: string | number) {
|
||||
return db.prepare('SELECT id FROM days WHERE id = ? AND trip_id = ?').get(dayId, tripId);
|
||||
}
|
||||
|
||||
export function createNote(dayId: string | number, tripId: string | number, text: string, time?: string, icon?: string, sort_order?: number) {
|
||||
const result = db.prepare(
|
||||
'INSERT INTO day_notes (day_id, trip_id, text, time, icon, sort_order) VALUES (?, ?, ?, ?, ?, ?)'
|
||||
).run(dayId, tripId, text.trim(), time || null, icon || '\uD83D\uDCDD', sort_order ?? 9999);
|
||||
return db.prepare('SELECT * FROM day_notes WHERE id = ?').get(result.lastInsertRowid);
|
||||
}
|
||||
|
||||
export function getNote(id: string | number, dayId: string | number, tripId: string | number) {
|
||||
return db.prepare('SELECT * FROM day_notes WHERE id = ? AND day_id = ? AND trip_id = ?').get(id, dayId, tripId) as DayNote | undefined;
|
||||
}
|
||||
|
||||
export function updateNote(id: string | number, current: DayNote, fields: { text?: string; time?: string; icon?: string; sort_order?: number }) {
|
||||
db.prepare(
|
||||
'UPDATE day_notes SET text = ?, time = ?, icon = ?, sort_order = ? WHERE id = ?'
|
||||
).run(
|
||||
fields.text !== undefined ? fields.text.trim() : current.text,
|
||||
fields.time !== undefined ? fields.time : current.time,
|
||||
fields.icon !== undefined ? fields.icon : current.icon,
|
||||
fields.sort_order !== undefined ? fields.sort_order : current.sort_order,
|
||||
id
|
||||
);
|
||||
return db.prepare('SELECT * FROM day_notes WHERE id = ?').get(id);
|
||||
}
|
||||
|
||||
export function deleteNote(id: string | number) {
|
||||
db.prepare('DELETE FROM day_notes WHERE id = ?').run(id);
|
||||
}
|
||||
Reference in New Issue
Block a user