mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 21:31:46 +00:00
refactor(mcp): replace direct DB access with service layer calls
Replace all db.prepare() calls in mcp/index.ts, mcp/resources.ts, and mcp/tools.ts with calls to the service layer. Add missing service functions: - authService: isDemoUser, verifyMcpToken, verifyJwtToken - adminService: isAddonEnabled - atlasService: listVisitedCountries - tripService: getTripSummary, listTrips with null archived param Also fix getAssignmentWithPlace and formatAssignmentWithPlace to expose place_id, assignment_time, and assignment_end_time at the top level, and fix updateDay to correctly handle null title for clearing. Add comprehensive unit and integration test suite for the MCP layer (821 tests all passing).
This commit is contained in:
@@ -27,7 +27,7 @@ export { isOwner };
|
||||
|
||||
// ── Day generation ────────────────────────────────────────────────────────
|
||||
|
||||
export function generateDays(tripId: number | bigint | string, startDate: string | null, endDate: string | null) {
|
||||
export function generateDays(tripId: number | bigint | string, startDate: string | null, endDate: string | null, maxDays?: number) {
|
||||
const existing = db.prepare('SELECT id, day_number, date FROM days WHERE trip_id = ?').all(tripId) as { id: number; day_number: number; date: string | null }[];
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
@@ -56,7 +56,7 @@ export function generateDays(tripId: number | bigint | string, startDate: string
|
||||
const [ey, em, ed] = endDate.split('-').map(Number);
|
||||
const startMs = Date.UTC(sy, sm - 1, sd);
|
||||
const endMs = Date.UTC(ey, em - 1, ed);
|
||||
const numDays = Math.min(Math.floor((endMs - startMs) / MS_PER_DAY) + 1, MAX_TRIP_DAYS);
|
||||
const numDays = Math.min(Math.floor((endMs - startMs) / MS_PER_DAY) + 1, maxDays ?? MAX_TRIP_DAYS);
|
||||
|
||||
const targetDates: string[] = [];
|
||||
for (let i = 0; i < numDays; i++) {
|
||||
@@ -99,7 +99,15 @@ export function generateDays(tripId: number | bigint | string, startDate: string
|
||||
|
||||
// ── Trip CRUD ─────────────────────────────────────────────────────────────
|
||||
|
||||
export function listTrips(userId: number, archived: number) {
|
||||
export function listTrips(userId: number, archived: number | null) {
|
||||
if (archived === null) {
|
||||
return db.prepare(`
|
||||
${TRIP_SELECT}
|
||||
LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = :userId
|
||||
WHERE (t.user_id = :userId OR m.user_id IS NOT NULL)
|
||||
ORDER BY t.created_at DESC
|
||||
`).all({ userId });
|
||||
}
|
||||
return db.prepare(`
|
||||
${TRIP_SELECT}
|
||||
LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = :userId
|
||||
@@ -117,7 +125,7 @@ interface CreateTripData {
|
||||
reminder_days?: number;
|
||||
}
|
||||
|
||||
export function createTrip(userId: number, data: CreateTripData) {
|
||||
export function createTrip(userId: number, data: CreateTripData, maxDays?: number) {
|
||||
const rd = data.reminder_days !== undefined
|
||||
? (Number(data.reminder_days) >= 0 && Number(data.reminder_days) <= 30 ? Number(data.reminder_days) : 3)
|
||||
: 3;
|
||||
@@ -128,7 +136,7 @@ export function createTrip(userId: number, data: CreateTripData) {
|
||||
`).run(userId, data.title, data.description || null, data.start_date || null, data.end_date || null, data.currency || 'EUR', rd);
|
||||
|
||||
const tripId = result.lastInsertRowid;
|
||||
generateDays(tripId, data.start_date || null, data.end_date || null);
|
||||
generateDays(tripId, data.start_date || null, data.end_date || null, maxDays);
|
||||
|
||||
const trip = db.prepare(`${TRIP_SELECT} WHERE t.id = :tripId`).get({ userId, tripId });
|
||||
return { trip, tripId: Number(tripId), reminderDays: rd };
|
||||
@@ -398,6 +406,101 @@ export function exportICS(tripId: string | number): { ics: string; filename: str
|
||||
return { ics, filename: `${safeFilename}.ics` };
|
||||
}
|
||||
|
||||
// ── Trip summary (used by MCP get_trip_summary tool) ──────────────────────
|
||||
|
||||
export function getTripSummary(tripId: number) {
|
||||
const trip = db.prepare('SELECT * FROM trips WHERE id = ?').get(tripId) as Record<string, unknown> | undefined;
|
||||
if (!trip) return null;
|
||||
|
||||
const owner = db.prepare('SELECT id, username, avatar FROM users WHERE id = ?').get(trip.user_id as number);
|
||||
const members = db.prepare(`
|
||||
SELECT u.id, u.username, u.avatar, tm.added_at
|
||||
FROM trip_members tm JOIN users u ON tm.user_id = u.id
|
||||
WHERE tm.trip_id = ?
|
||||
`).all(tripId);
|
||||
|
||||
const days = db.prepare('SELECT * FROM days WHERE trip_id = ? ORDER BY day_number ASC').all(tripId) as (Record<string, unknown> & { id: number })[];
|
||||
const dayIds = days.map(d => d.id);
|
||||
const assignmentsByDay: Record<number, unknown[]> = {};
|
||||
const dayNotesByDay: Record<number, unknown[]> = {};
|
||||
|
||||
if (dayIds.length > 0) {
|
||||
const placeholders = dayIds.map(() => '?').join(',');
|
||||
const assignments = db.prepare(`
|
||||
SELECT da.id, da.day_id, da.order_index, da.notes as assignment_notes,
|
||||
p.id as place_id, p.name, p.address, p.lat, p.lng,
|
||||
COALESCE(da.assignment_time, p.place_time) as place_time,
|
||||
c.name as category_name, c.icon as category_icon
|
||||
FROM day_assignments da
|
||||
JOIN places p ON da.place_id = p.id
|
||||
LEFT JOIN categories c ON p.category_id = c.id
|
||||
WHERE da.day_id IN (${placeholders})
|
||||
ORDER BY da.order_index ASC
|
||||
`).all(...dayIds) as (Record<string, unknown> & { day_id: number })[];
|
||||
for (const a of assignments) {
|
||||
if (!assignmentsByDay[a.day_id]) assignmentsByDay[a.day_id] = [];
|
||||
assignmentsByDay[a.day_id].push(a);
|
||||
}
|
||||
|
||||
const dayNotes = db.prepare(`
|
||||
SELECT * FROM day_notes WHERE day_id IN (${placeholders}) ORDER BY sort_order ASC
|
||||
`).all(...dayIds) as (Record<string, unknown> & { day_id: number })[];
|
||||
for (const n of dayNotes) {
|
||||
if (!dayNotesByDay[n.day_id]) dayNotesByDay[n.day_id] = [];
|
||||
dayNotesByDay[n.day_id].push(n);
|
||||
}
|
||||
}
|
||||
|
||||
const daysWithAssignments = days.map(d => ({
|
||||
...d,
|
||||
assignments: assignmentsByDay[d.id] || [],
|
||||
notes: dayNotesByDay[d.id] || [],
|
||||
}));
|
||||
|
||||
const accommodations = db.prepare(`
|
||||
SELECT da.*, p.name as place_name, ds.day_number as start_day_number, de.day_number as end_day_number
|
||||
FROM day_accommodations da
|
||||
JOIN places p ON da.place_id = p.id
|
||||
LEFT JOIN days ds ON da.start_day_id = ds.id
|
||||
LEFT JOIN days de ON da.end_day_id = de.id
|
||||
WHERE da.trip_id = ?
|
||||
ORDER BY ds.day_number ASC
|
||||
`).all(tripId);
|
||||
|
||||
const budgetStats = db.prepare(`
|
||||
SELECT COUNT(*) as item_count, COALESCE(SUM(total_price), 0) as total
|
||||
FROM budget_items WHERE trip_id = ?
|
||||
`).get(tripId) as { item_count: number; total: number };
|
||||
|
||||
const packingStats = db.prepare(`
|
||||
SELECT COUNT(*) as total, SUM(CASE WHEN checked = 1 THEN 1 ELSE 0 END) as checked
|
||||
FROM packing_items WHERE trip_id = ?
|
||||
`).get(tripId) as { total: number; checked: number };
|
||||
|
||||
const reservations = db.prepare(`
|
||||
SELECT r.*, d.day_number
|
||||
FROM reservations r
|
||||
LEFT JOIN days d ON r.day_id = d.id
|
||||
WHERE r.trip_id = ?
|
||||
ORDER BY r.reservation_time ASC, r.created_at ASC
|
||||
`).all(tripId);
|
||||
|
||||
const collabNotes = db.prepare(
|
||||
'SELECT * FROM collab_notes WHERE trip_id = ? ORDER BY pinned DESC, updated_at DESC'
|
||||
).all(tripId);
|
||||
|
||||
return {
|
||||
trip,
|
||||
members: { owner, collaborators: members },
|
||||
days: daysWithAssignments,
|
||||
accommodations,
|
||||
budget: { ...budgetStats, currency: trip.currency },
|
||||
packing: packingStats,
|
||||
reservations,
|
||||
collab_notes: collabNotes,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Custom error types ────────────────────────────────────────────────────
|
||||
|
||||
export class NotFoundError extends Error {
|
||||
|
||||
Reference in New Issue
Block a user