chore: move day-notes backend to proper DI

This commit is contained in:
jubnl
2026-07-26 17:44:12 +02:00
parent 7242812b27
commit 4e49e9c21e
18 changed files with 597 additions and 250 deletions
+2 -14
View File
@@ -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(
+2 -78
View File
@@ -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).
}
+6 -3
View File
@@ -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
+134
View File
@@ -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),
}],
};
}
}
+39 -21
View File
@@ -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);
}
}
+5 -2
View File
@@ -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 {}
+5 -4
View File
@@ -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 };
},
+2 -1
View File
@@ -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
-42
View File
@@ -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);
}