diff --git a/server/tests/unit/nest/calendar.service.test.ts b/server/tests/unit/nest/calendar.service.test.ts index a3fe1eb0d..66ba56400 100644 --- a/server/tests/unit/nest/calendar.service.test.ts +++ b/server/tests/unit/nest/calendar.service.test.ts @@ -18,14 +18,6 @@ vi.mock('../../../src/db/database', async () => { closeDb: () => {}, reinitialize: () => {}, getPlaceWithTags: () => null, - canAccessTrip: (tripId: any, userId: number) => - db.prepare(` - SELECT t.id, t.user_id FROM trips t - LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? - WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL) - `).get(userId, tripId, userId), - isOwner: (tripId: any, userId: number) => - !!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId), }; return mock; }); @@ -56,10 +48,20 @@ import { accommodationsOver } from '../../helpers/accommodations-service'; import { createTestUnitOfWork, createTestAppSettingsRepo, createTestReservationsRepo, createTestReservationEndpointsRepo, createTestReservationTravelersRepo, createTestReservationDayPositionsRepo, createTestDayAccommodationsRepo, createTestDaysRepo, createTestDayNotesRepo, createTestPlacesRepo, createTestDayAssignmentsRepo, createTestTripMembersRepo, createTestUsersRepo, createTestTripsRepo } from '../../helpers/test-uow'; import { createTestBudgetItemsRepo } from '../../helpers/files-repos'; import { budgetRepoArgs } from '../../helpers/budget-repos'; +import { sharedTestOrm } from '../../helpers/test-uow'; +import type { TestOrm } from '../../helpers/test-orm'; +import { findRow, findRows, insertRow, insertRows, updateRows } from '../../helpers/factories/rows'; +import { DayAccommodations } from '../../../src/db/entities/DayAccommodations.entity'; +import { DayAssignments } from '../../../src/db/entities/DayAssignments.entity'; +import { Days } from '../../../src/db/entities/Days.entity'; +import { Places } from '../../../src/db/entities/Places.entity'; +import { ReservationEndpoints } from '../../../src/db/entities/ReservationEndpoints.entity'; +import { Reservations } from '../../../src/db/entities/Reservations.entity'; // Named `svc` so the moved cases below read exactly as they did on TripsService. let budgetSvc: BudgetService; let svc: CalendarService; +let orm: TestOrm; beforeAll(async () => { budgetSvc = new BudgetService(new PermissionsService(await createTestAppSettingsRepo(testDb), await createTestUnitOfWork(testDb)), new ExchangeRatesService(), new RealtimeService(), await createTestUnitOfWork(testDb), ...(await budgetRepoArgs(testDb))); svc = new CalendarService( @@ -68,8 +70,9 @@ beforeAll(async () => { ); }); -beforeEach(() => { +beforeEach(async () => { resetTestDb(testDb); + orm = await sharedTestOrm(testDb); }); afterAll(() => { @@ -156,9 +159,7 @@ describe('exportICS', () => { title: 'Morning Flight', type: 'flight', }); - testDb - .prepare('UPDATE reservations SET reservation_time=? WHERE id=?') - .run('2025-06-02T09:00', reservation.id); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: '2025-06-02T09:00' }); const { ics } = await svc.exportICS(trip.id); @@ -173,9 +174,7 @@ describe('exportICS', () => { title: 'Hotel Check-in', type: 'hotel', }); - testDb - .prepare('UPDATE reservations SET reservation_time=? WHERE id=?') - .run('2025-06-02', reservation.id); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: '2025-06-02' }); const { ics } = await svc.exportICS(trip.id); @@ -189,18 +188,12 @@ describe('exportICS', () => { title: 'CDG to JFK', type: 'flight', }); - testDb - .prepare('UPDATE reservations SET reservation_time=?, metadata=? WHERE id=?') - .run( - '2025-06-02T09:00', - JSON.stringify({ + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: '2025-06-02T09:00', metadata: JSON.stringify({ airline: 'Air Test', flight_number: 'AT100', departure_airport: 'CDG', arrival_airport: 'JFK', - }), - reservation.id - ); + }) }); const { ics } = await svc.exportICS(trip.id); @@ -253,9 +246,7 @@ describe('exportICS', () => { title: 'Afternoon Tour', type: 'activity', }); - testDb - .prepare('UPDATE reservations SET reservation_time=?, reservation_end_time=? WHERE id=?') - .run('2025-06-02T14:00', '2025-06-02T16:00', reservation.id); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: '2025-06-02T14:00', reservation_end_time: '2025-06-02T16:00' }); const { ics } = await svc.exportICS(trip.id); @@ -270,12 +261,9 @@ describe('exportICS', () => { type: 'flight', }); // Confirmed flights store times per endpoint, never as reservation_time. - testDb.prepare('UPDATE reservations SET reservation_time=NULL, reservation_end_time=NULL WHERE id=?').run(reservation.id); - const insertEp = testDb.prepare( - 'INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, code, lat, lng, timezone, local_time, local_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' - ); - insertEp.run(reservation.id, 'from', 0, 'Paris CDG', 'CDG', 49.0, 2.5, 'Europe/Paris', '09:00', '2025-06-02'); - insertEp.run(reservation.id, 'to', 1, 'New York JFK', 'JFK', 40.6, -73.8, 'America/New_York', '12:00', '2025-06-02'); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: null, reservation_end_time: null }); + await insertRow(orm, ReservationEndpoints, { reservation: reservation.id, role: 'from', sequence: 0, name: 'Paris CDG', code: 'CDG', lat: 49.0, lng: 2.5, timezone: 'Europe/Paris', local_time: '09:00', local_date: '2025-06-02' }); + await insertRow(orm, ReservationEndpoints, { reservation: reservation.id, role: 'to', sequence: 1, name: 'New York JFK', code: 'JFK', lat: 40.6, lng: -73.8, timezone: 'America/New_York', local_time: '12:00', local_date: '2025-06-02' }); const { ics } = await svc.exportICS(trip.id); @@ -301,13 +289,9 @@ describe('exportICS', () => { // TransportModal stamps reservation_time/_end_time (departure/arrival) alongside // the endpoints. That branch's only zone is the linked place — a flight has none — // so it floated both ends in the subscribed feed; endpoints must win (#1453). - testDb.prepare('UPDATE reservations SET reservation_time=?, reservation_end_time=? WHERE id=?') - .run('2025-06-02T09:00', '2025-06-02T12:00', reservation.id); - const insertEp = testDb.prepare( - 'INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, code, lat, lng, timezone, local_time, local_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' - ); - insertEp.run(reservation.id, 'from', 0, 'Paris CDG', 'CDG', 49.0, 2.5, 'Europe/Paris', '09:00', '2025-06-02'); - insertEp.run(reservation.id, 'to', 1, 'New York JFK', 'JFK', 40.6, -73.8, 'America/New_York', '12:00', '2025-06-02'); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: '2025-06-02T09:00', reservation_end_time: '2025-06-02T12:00' }); + await insertRow(orm, ReservationEndpoints, { reservation: reservation.id, role: 'from', sequence: 0, name: 'Paris CDG', code: 'CDG', lat: 49.0, lng: 2.5, timezone: 'Europe/Paris', local_time: '09:00', local_date: '2025-06-02' }); + await insertRow(orm, ReservationEndpoints, { reservation: reservation.id, role: 'to', sequence: 1, name: 'New York JFK', code: 'JFK', lat: 40.6, lng: -73.8, timezone: 'America/New_York', local_time: '12:00', local_date: '2025-06-02' }); const { ics } = await svc.exportICS(trip.id); @@ -325,11 +309,8 @@ describe('exportICS', () => { // An imported rental geocodes the pickup only, so there is no second endpoint // to carry the return. Before the endpoint branch took precedence, the return // came from reservation_end_time; it still has to. - testDb.prepare('UPDATE reservations SET reservation_time=?, reservation_end_time=? WHERE id=?') - .run('2025-06-02T10:00', '2025-06-09T10:00', reservation.id); - testDb.prepare( - 'INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, code, lat, lng, timezone, local_time, local_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' - ).run(reservation.id, 'from', 0, 'Berlin', null, 52.5, 13.4, 'Europe/Berlin', '10:00', '2025-06-02'); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: '2025-06-02T10:00', reservation_end_time: '2025-06-09T10:00' }); + await insertRow(orm, ReservationEndpoints, { reservation: reservation.id, role: 'from', sequence: 0, name: 'Berlin', code: null, lat: 52.5, lng: 13.4, timezone: 'Europe/Berlin', local_time: '10:00', local_date: '2025-06-02' }); const { ics } = await svc.exportICS(trip.id); @@ -341,15 +322,11 @@ describe('exportICS', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Train Trip' }); const reservation = createReservation(testDb, trip.id, { title: 'ICE 1234', type: 'train' }); - testDb.prepare('UPDATE reservations SET reservation_time=?, reservation_end_time=? WHERE id=?') - .run('2025-06-02T08:00', '2025-06-02T14:30', reservation.id); - const insertEp = testDb.prepare( - 'INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, code, lat, lng, timezone, local_time, local_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' - ); - insertEp.run(reservation.id, 'from', 0, 'Berlin Hbf', null, 52.5, 13.4, 'Europe/Berlin', '08:00', '2025-06-02'); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: '2025-06-02T08:00', reservation_end_time: '2025-06-02T14:30' }); + await insertRow(orm, ReservationEndpoints, { reservation: reservation.id, role: 'from', sequence: 0, name: 'Berlin Hbf', code: null, lat: 52.5, lng: 13.4, timezone: 'Europe/Berlin', local_time: '08:00', local_date: '2025-06-02' }); // The destination was added without a clock — the arrival time only exists // on the reservation itself. - insertEp.run(reservation.id, 'to', 1, 'Wien Hbf', null, 48.2, 16.4, 'Europe/Vienna', null, '2025-06-02'); + await insertRow(orm, ReservationEndpoints, { reservation: reservation.id, role: 'to', sequence: 1, name: 'Wien Hbf', code: null, lat: 48.2, lng: 16.4, timezone: 'Europe/Vienna', local_time: null, local_date: '2025-06-02' }); const { ics } = await svc.exportICS(trip.id); @@ -361,11 +338,8 @@ describe('exportICS', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Ferry Trip' }); const reservation = createReservation(testDb, trip.id, { title: 'Ferry', type: 'ferry' }); - testDb.prepare('UPDATE reservations SET reservation_time=?, reservation_end_time=NULL WHERE id=?') - .run('2025-06-02T07:00', reservation.id); - testDb.prepare( - 'INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, code, lat, lng, timezone, local_time, local_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' - ).run(reservation.id, 'from', 0, 'Dover', null, 51.1, 1.3, 'Europe/London', '07:00', '2025-06-02'); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: '2025-06-02T07:00', reservation_end_time: null }); + await insertRow(orm, ReservationEndpoints, { reservation: reservation.id, role: 'from', sequence: 0, name: 'Dover', code: null, lat: 51.1, lng: 1.3, timezone: 'Europe/London', local_time: '07:00', local_date: '2025-06-02' }); const { ics } = await svc.exportICS(trip.id); @@ -379,14 +353,11 @@ describe('exportICS', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Bad TZ Trip' }); const reservation = createReservation(testDb, trip.id, { title: 'CDG → JFK', type: 'flight' }); - testDb.prepare('UPDATE reservations SET reservation_time=NULL, reservation_end_time=NULL WHERE id=?').run(reservation.id); - const insertEp = testDb.prepare( - 'INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, code, lat, lng, timezone, local_time, local_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' - ); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: null, reservation_end_time: null }); // A stored/plugin-written timezone can be any string; it must never reach Intl. // The bogus zone takes precedence over the coordinates (first.timezone || resolveZone). - insertEp.run(reservation.id, 'from', 0, 'Paris CDG', 'CDG', 49.0, 2.5, 'Not/AZone', '09:00', '2025-06-02'); - insertEp.run(reservation.id, 'to', 1, 'New York JFK', 'JFK', 40.6, -73.8, 'garbage', '12:00', '2025-06-02'); + await insertRow(orm, ReservationEndpoints, { reservation: reservation.id, role: 'from', sequence: 0, name: 'Paris CDG', code: 'CDG', lat: 49.0, lng: 2.5, timezone: 'Not/AZone', local_time: '09:00', local_date: '2025-06-02' }); + await insertRow(orm, ReservationEndpoints, { reservation: reservation.id, role: 'to', sequence: 1, name: 'New York JFK', code: 'JFK', lat: 40.6, lng: -73.8, timezone: 'garbage', local_time: '12:00', local_date: '2025-06-02' }); const ics = (await svc.exportICS(trip.id)).ics; // Falls back to a floating local time (no TZID) and never emits a bogus VTIMEZONE. @@ -402,10 +373,8 @@ describe('exportICS', () => { title: 'Timeless Flight', type: 'flight', }); - testDb.prepare('UPDATE reservations SET reservation_time=NULL WHERE id=?').run(reservation.id); - testDb.prepare( - 'INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, code, lat, lng, timezone, local_time, local_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' - ).run(reservation.id, 'from', 0, 'Origin', 'AAA', 1.0, 1.0, null, '09:00', null); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: null }); + await insertRow(orm, ReservationEndpoints, { reservation: reservation.id, role: 'from', sequence: 0, name: 'Origin', code: 'AAA', lat: 1.0, lng: 1.0, timezone: null, local_time: '09:00', local_date: null }); const { ics } = await svc.exportICS(trip.id); @@ -419,9 +388,7 @@ describe('exportICS', () => { // Tokyo coordinates → Asia/Tokyo via tz-lookup. const place = createPlace(testDb, trip.id, { name: 'Senso-ji', lat: 35.7148, lng: 139.7967 }); const assignment = createDayAssignment(testDb, day.id, place.id); - testDb - .prepare('UPDATE day_assignments SET assignment_time=? WHERE id=?') - .run('09:00', assignment.id); + await updateRows(orm, DayAssignments, { id: assignment.id }, { assignment_time: '09:00' }); const { ics } = await svc.exportICS(trip.id); @@ -437,9 +404,7 @@ describe('exportICS', () => { // Importers write both shapes: "…T14:00" (padded to seconds) and "…T14:00:00" // (already 15 chars). Padding the second one again would produce a 17-char // value that no client parses. - testDb - .prepare('UPDATE reservations SET reservation_time=?, reservation_end_time=? WHERE id=?') - .run('2025-06-02T14:00:00', '2025-06-03', reservation.id); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: '2025-06-02T14:00:00', reservation_end_time: '2025-06-03' }); const { ics } = await svc.exportICS(trip.id); @@ -456,10 +421,10 @@ describe('exportICS', () => { // Relative "Day N" trips store a clock time without a date; there is nothing // to anchor it to, so the reservation must not reach the calendar at all. const timeOnly = createReservation(testDb, trip.id, { title: 'Floating Dinner', type: 'restaurant' }); - testDb.prepare('UPDATE reservations SET reservation_time=? WHERE id=?').run('19:30', timeOnly.id); + await updateRows(orm, Reservations, { id: timeOnly.id }, { reservation_time: '19:30' }); // A transport row whose endpoints were never imported has no fallback time either. const noEndpoints = createReservation(testDb, trip.id, { title: 'Endpointless Train', type: 'transport' }); - testDb.prepare('UPDATE reservations SET reservation_time=NULL WHERE id=?').run(noEndpoints.id); + await updateRows(orm, Reservations, { id: noEndpoints.id }, { reservation_time: null }); const { ics } = await svc.exportICS(trip.id); @@ -474,15 +439,12 @@ describe('exportICS', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'No Stored Zone' }); const reservation = createReservation(testDb, trip.id, { title: 'CDG to JFK', type: 'flight' }); - testDb.prepare('UPDATE reservations SET reservation_time=NULL WHERE id=?').run(reservation.id); - const insertEp = testDb.prepare( - 'INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, code, lat, lng, timezone, local_time, local_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' - ); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: null }); // Endpoints created before the importer learned to store IANA zones have only // coordinates. Without the lookup fallback both ends would go floating and the // subscriber would see the flight in their own zone (#1453). - insertEp.run(reservation.id, 'from', 0, 'Paris CDG', 'CDG', 49.0, 2.5, null, '09:00', '2025-06-02'); - insertEp.run(reservation.id, 'to', 1, 'New York JFK', 'JFK', 40.6, -73.8, null, '12:00', '2025-06-02'); + await insertRow(orm, ReservationEndpoints, { reservation: reservation.id, role: 'from', sequence: 0, name: 'Paris CDG', code: 'CDG', lat: 49.0, lng: 2.5, timezone: null, local_time: '09:00', local_date: '2025-06-02' }); + await insertRow(orm, ReservationEndpoints, { reservation: reservation.id, role: 'to', sequence: 1, name: 'New York JFK', code: 'JFK', lat: 40.6, lng: -73.8, timezone: null, local_time: '12:00', local_date: '2025-06-02' }); const { ics } = await svc.exportICS(trip.id); @@ -494,12 +456,10 @@ describe('exportICS', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'One Way' }); const reservation = createReservation(testDb, trip.id, { title: 'Airport Transfer', type: 'transport' }); - testDb.prepare('UPDATE reservations SET reservation_time=NULL WHERE id=?').run(reservation.id); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: null }); // Only the departure was imported. Using it for DTEND as well would emit a // zero-length event; leaving DTEND out lets clients apply their default duration. - testDb.prepare( - 'INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, code, lat, lng, timezone, local_time, local_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' - ).run(reservation.id, 'from', 0, 'Paris CDG', 'CDG', 49.0, 2.5, 'Europe/Paris', '09:00', '2025-06-02'); + await insertRow(orm, ReservationEndpoints, { reservation: reservation.id, role: 'from', sequence: 0, name: 'Paris CDG', code: 'CDG', lat: 49.0, lng: 2.5, timezone: 'Europe/Paris', local_time: '09:00', local_date: '2025-06-02' }); const { ics } = await svc.exportICS(trip.id); @@ -512,11 +472,11 @@ describe('exportICS', () => { const trip = createTrip(testDb, user.id, { title: 'Guided Day' }); const day = createDay(testDb, trip.id, { date: '2025-06-02' }); const place = createPlace(testDb, trip.id, { name: 'Louvre', lat: 48.8606, lng: 2.3376 }); - testDb.prepare("UPDATE places SET address = 'Rue de Rivoli' WHERE id = ?").run(place.id); + await updateRows(orm, Places, { id: place.id }, { address: 'Rue de Rivoli' }); const withNotes = createDayAssignment(testDb, day.id, place.id, { notes: 'meet the guide' }); - testDb.prepare('UPDATE day_assignments SET assignment_time=? WHERE id=?').run('09:00', withNotes.id); + await updateRows(orm, DayAssignments, { id: withNotes.id }, { assignment_time: '09:00' }); const withoutNotes = createDayAssignment(testDb, day.id, place.id); - testDb.prepare('UPDATE day_assignments SET assignment_time=? WHERE id=?').run('11:00', withoutNotes.id); + await updateRows(orm, DayAssignments, { id: withoutNotes.id }, { assignment_time: '11:00' }); const ics = (await svc.exportICS(trip.id)).ics.replace(/\r\n /g, ''); @@ -555,9 +515,7 @@ describe('exportICS', () => { const reservation = createReservation(testDb, trip.id, { title: 'Bare Booking', type: 'hotel' }); // Rows imported before `type` became mandatory still exist in prod. An empty // DESCRIPTION line is invalid enough for some clients to reject the file. - testDb - .prepare('UPDATE reservations SET type=NULL, reservation_time=? WHERE id=?') - .run('2025-06-02', reservation.id); + await updateRows(orm, Reservations, { id: reservation.id }, { type: null, reservation_time: '2025-06-02' }); const { ics } = await svc.exportICS(trip.id); @@ -570,11 +528,10 @@ describe('exportICS', () => { const trip = createTrip(testDb, user.id, { title: 'Half Routes' }); const inbound = createReservation(testDb, trip.id, { title: 'Inbound', type: 'flight' }); const outbound = createReservation(testDb, trip.id, { title: 'Outbound', type: 'flight' }); - const setMeta = testDb.prepare('UPDATE reservations SET reservation_time=?, metadata=? WHERE id=?'); // A hand-entered flight often has one airport only; the missing side must be // left out instead of printing "To: undefined". - setMeta.run('2025-06-02T09:00', JSON.stringify({ arrival_airport: 'JFK' }), inbound.id); - setMeta.run('2025-06-09T09:00', JSON.stringify({ departure_airport: 'CDG' }), outbound.id); + await updateRows(orm, Reservations, { id: inbound.id }, { reservation_time: '2025-06-02T09:00', metadata: JSON.stringify({ arrival_airport: 'JFK' }) }); + await updateRows(orm, Reservations, { id: outbound.id }, { reservation_time: '2025-06-09T09:00', metadata: JSON.stringify({ departure_airport: 'CDG' }) }); const ics = (await svc.exportICS(trip.id)).ics.replace(/\r\n /g, ''); @@ -588,17 +545,13 @@ describe('exportICS', () => { const legs = createReservation(testDb, trip.id, { title: 'Codeless Legs', type: 'flight' }); // Multi-leg metadata whose legs carry no airports would otherwise render // "Route: " with nothing after it. - testDb.prepare('UPDATE reservations SET reservation_time=?, metadata=? WHERE id=?') - .run('2025-06-02T09:00', JSON.stringify({ legs: [{}, {}] }), legs.id); + await updateRows(orm, Reservations, { id: legs.id }, { reservation_time: '2025-06-02T09:00', metadata: JSON.stringify({ legs: [{}, {}] }) }); const ferry = createReservation(testDb, trip.id, { title: 'Nameless Ferry', type: 'transport' }); - testDb.prepare('UPDATE reservations SET reservation_time=NULL WHERE id=?').run(ferry.id); - const insertEp = testDb.prepare( - 'INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, code, lat, lng, timezone, local_time, local_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' - ); + await updateRows(orm, Reservations, { id: ferry.id }, { reservation_time: null }); // Same for endpoints with neither a code nor a name: the derived route would // collapse to a single arrow between two blanks. - insertEp.run(ferry.id, 'from', 0, '', null, 1.0, 1.0, null, null, '2025-06-03'); - insertEp.run(ferry.id, 'to', 1, '', null, 1.1, 1.1, null, null, '2025-06-03'); + await insertRow(orm, ReservationEndpoints, { reservation: ferry.id, role: 'from', sequence: 0, name: '', code: null, lat: 1.0, lng: 1.0, timezone: null, local_time: null, local_date: '2025-06-03' }); + await insertRow(orm, ReservationEndpoints, { reservation: ferry.id, role: 'to', sequence: 1, name: '', code: null, lat: 1.1, lng: 1.1, timezone: null, local_time: null, local_date: '2025-06-03' }); const ics = (await svc.exportICS(trip.id)).ics.replace(/\r\n /g, ''); @@ -611,14 +564,10 @@ describe('exportICS', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Layover' }); const flight = createReservation(testDb, trip.id, { title: 'FRA to HND', type: 'flight' }); - testDb.prepare('UPDATE reservations SET reservation_time=?, confirmation_number=?, metadata=? WHERE id=?').run( - '2025-06-02T09:00', 'BOOK1', - JSON.stringify({ legs: [ + await updateRows(orm, Reservations, { id: flight.id }, { reservation_time: '2025-06-02T09:00', confirmation_number: 'BOOK1', metadata: JSON.stringify({ legs: [ { from: 'FRA', to: 'BER', confirmation_number: 'ABC123' }, { from: 'BER', to: 'HND', confirmation_number: 'XYZ789' }, - ] }), - flight.id, - ); + ] }) }); const ics = (await svc.exportICS(trip.id)).ics.replace(/\r\n /g, ''); @@ -632,11 +581,7 @@ describe('exportICS', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Layover' }); const flight = createReservation(testDb, trip.id, { title: 'FRA to HND', type: 'flight' }); - testDb.prepare('UPDATE reservations SET reservation_time=?, confirmation_number=?, metadata=? WHERE id=?').run( - '2025-06-02T09:00', 'BOOK1', - JSON.stringify({ legs: [{ from: 'FRA', to: 'BER' }, { from: 'BER', to: 'HND' }] }), - flight.id, - ); + await updateRows(orm, Reservations, { id: flight.id }, { reservation_time: '2025-06-02T09:00', confirmation_number: 'BOOK1', metadata: JSON.stringify({ legs: [{ from: 'FRA', to: 'BER' }, { from: 'BER', to: 'HND' }] }) }); const ics = (await svc.exportICS(trip.id)).ics.replace(/\r\n /g, ''); @@ -651,14 +596,10 @@ describe('exportICS', () => { const d1 = createDay(testDb, trip.id, { date: '2025-06-02' }); const d2 = createDay(testDb, trip.id, { date: '2025-06-03' }); const flight = createReservation(testDb, trip.id, { title: 'FRA to HND', type: 'flight' }); - testDb.prepare('UPDATE reservations SET reservation_time=?, confirmation_number=?, metadata=? WHERE id=?').run( - '2025-06-02T09:00', 'BOOK1', - JSON.stringify({ legs: [ + await updateRows(orm, Reservations, { id: flight.id }, { reservation_time: '2025-06-02T09:00', confirmation_number: 'BOOK1', metadata: JSON.stringify({ legs: [ { from: 'FRA', to: 'BER', airline: 'LH', flight_number: '1', dep_day_id: d1.id, dep_time: '09:00', arr_day_id: d1.id, arr_time: '10:10' }, { from: 'BER', to: 'HND', airline: 'LH', flight_number: '2', confirmation_number: 'SEG2', dep_day_id: d1.id, dep_time: '12:30', arr_day_id: d2.id, arr_time: '07:45' }, - ] }), - flight.id, - ); + ] }) }); const ics = (await svc.exportICS(trip.id)).ics.replace(/\r\n /g, ''); @@ -678,14 +619,10 @@ describe('exportICS', () => { const d1 = createDay(testDb, trip.id, { date: '2025-06-02' }); const d2 = createDay(testDb, trip.id, { date: '2025-06-03' }); const cruise = createReservation(testDb, trip.id, { title: 'Baltic cruise', type: 'cruise' }); - testDb.prepare('UPDATE reservations SET reservation_time=?, metadata=? WHERE id=?').run( - '2025-06-02T17:00', - JSON.stringify({ legs: [ + await updateRows(orm, Reservations, { id: cruise.id }, { reservation_time: '2025-06-02T17:00', metadata: JSON.stringify({ legs: [ { from: 'Kiel', to: 'Tallinn', dep_day_id: d1.id, dep_time: '17:00', arr_day_id: d2.id, arr_time: '09:00' }, { from: 'Tallinn', to: 'Kiel', dep_day_id: d2.id, dep_time: '18:00', arr_day_id: d2.id, arr_time: '23:00' }, - ] }), - cruise.id, - ); + ] }) }); const ics = (await svc.exportICS(trip.id)).ics.replace(/\r\n /g, ''); expect(ics).toContain('SUMMARY:Baltic cruise: Kiel → Tallinn'); expect(ics).toContain('SUMMARY:Baltic cruise: Tallinn → Kiel'); @@ -697,21 +634,14 @@ describe('exportICS', () => { const trip = createTrip(testDb, user.id, { title: 'Layover' }); const d1 = createDay(testDb, trip.id, { date: '2025-06-02' }); const flight = createReservation(testDb, trip.id, { title: 'Via Frankfurt', type: 'flight' }); - testDb.prepare('UPDATE reservations SET reservation_time=?, metadata=? WHERE id=?').run( - '2025-06-02T09:00', - JSON.stringify({ legs: [ + await updateRows(orm, Reservations, { id: flight.id }, { reservation_time: '2025-06-02T09:00', metadata: JSON.stringify({ legs: [ { dep_day_id: d1.id, dep_time: '09:00', arr_day_id: d1.id, arr_time: '10:00' }, { dep_day_id: d1.id, dep_time: '12:00', arr_day_id: d1.id, arr_time: '15:00' }, - ] }), - flight.id, - ); - const insertEp = testDb.prepare( - 'INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, code, lat, lng, timezone, local_time, local_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' - ); - insertEp.run(flight.id, 'from', 0, 'Paris CDG', 'CDG', 49.0, 2.5, 'Europe/Paris', '09:00', '2025-06-02'); + ] }) }); + await insertRow(orm, ReservationEndpoints, { reservation: flight.id, role: 'from', sequence: 0, name: 'Paris CDG', code: 'CDG', lat: 49.0, lng: 2.5, timezone: 'Europe/Paris', local_time: '09:00', local_date: '2025-06-02' }); // No stored zone here: it is worked out from the coordinates. - insertEp.run(flight.id, 'stop', 1, 'Frankfurt', 'FRA', 50.03, 8.57, null, '12:00', '2025-06-02'); - insertEp.run(flight.id, 'to', 2, 'London LHR', 'LHR', 51.47, -0.45, 'Europe/London', '15:00', '2025-06-02'); + await insertRow(orm, ReservationEndpoints, { reservation: flight.id, role: 'stop', sequence: 1, name: 'Frankfurt', code: 'FRA', lat: 50.03, lng: 8.57, timezone: null, local_time: '12:00', local_date: '2025-06-02' }); + await insertRow(orm, ReservationEndpoints, { reservation: flight.id, role: 'to', sequence: 2, name: 'London LHR', code: 'LHR', lat: 51.47, lng: -0.45, timezone: 'Europe/London', local_time: '15:00', local_date: '2025-06-02' }); const { ics } = await svc.exportICS(trip.id); @@ -728,14 +658,10 @@ describe('exportICS', () => { const trip = createTrip(testDb, user.id, { title: 'Layover' }); const d1 = createDay(testDb, trip.id, { date: '2025-06-02' }); const flight = createReservation(testDb, trip.id, { title: 'FRA to HND', type: 'flight' }); - testDb.prepare('UPDATE reservations SET reservation_time=?, metadata=? WHERE id=?').run( - '2025-06-02T09:00', - JSON.stringify({ legs: [ + await updateRows(orm, Reservations, { id: flight.id }, { reservation_time: '2025-06-02T09:00', metadata: JSON.stringify({ legs: [ { from: 'FRA', to: 'BER', dep_day_id: d1.id, dep_time: '09:00' }, { from: 'BER', to: 'HND', dep_day_id: d1.id }, - ] }), - flight.id, - ); + ] }) }); const ics = (await svc.exportICS(trip.id)).ics; expect(ics).toContain(`UID:trek-res-${flight.id}@trek`); @@ -765,7 +691,7 @@ describe('exportICS', () => { const day = createDay(testDb, trip.id, { date: '2025-13-45' }); const place = createPlace(testDb, trip.id, { name: 'Senso-ji', lat: 35.7148, lng: 139.7967 }); const assignment = createDayAssignment(testDb, day.id, place.id); - testDb.prepare('UPDATE day_assignments SET assignment_time=? WHERE id=?').run('09:00', assignment.id); + await updateRows(orm, DayAssignments, { id: assignment.id }, { assignment_time: '09:00' }); const ics = (await svc.exportICS(trip.id)).ics; @@ -780,19 +706,16 @@ describe('exportICS', () => { const bulkTrip = createTrip(testDb, user.id, { title: 'Cache Filler' }); const realTrip = createTrip(testDb, user.id, { title: 'After The Bound' }); - const insertRes = testDb.prepare('INSERT INTO reservations (trip_id, title, type, reservation_time) VALUES (?, ?, ?, NULL)'); - const insertEp = testDb.prepare( - 'INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, code, lat, lng, timezone, local_time, local_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' - ); // Endpoint timezones are free-form strings written by importers and plugins, so // the validity cache is keyed by attacker-ish input and has to be bounded. Push // it past the bound in one export. - testDb.transaction(() => { - for (let i = 0; i < 1010; i++) { - const resId = insertRes.run(bulkTrip.id, `Bulk ${i}`, 'transport').lastInsertRowid as number; - insertEp.run(resId, 'from', 0, `Stop ${i}`, null, 49.0, 2.5, `Bogus/Zone-${i}`, '09:00', '2025-06-02'); - } - })(); + const bulkIds = await insertRows(orm, Reservations, Array.from({ length: 1010 }, (_, i) => ( + { trip: bulkTrip.id, title: `Bulk ${i}`, type: 'transport', reservation_time: null } + ))); + await insertRows(orm, ReservationEndpoints, bulkIds.map((resId, i) => ({ + reservation: resId, role: 'from', sequence: 0, name: `Stop ${i}`, code: null, lat: 49.0, lng: 2.5, + timezone: `Bogus/Zone-${i}`, local_time: '09:00', local_date: '2025-06-02', + }))); const bulk = (await svc.exportICS(bulkTrip.id)).ics; // Every one of them has to reach the zone check — otherwise the bound is never @@ -803,8 +726,8 @@ describe('exportICS', () => { expect(bulk).not.toContain('TZID=Bogus/Zone-0:'); const reservation = createReservation(testDb, realTrip.id, { title: 'CDG to JFK', type: 'flight' }); - testDb.prepare('UPDATE reservations SET reservation_time=NULL WHERE id=?').run(reservation.id); - insertEp.run(reservation.id, 'from', 0, 'Paris CDG', 'CDG', 49.0, 2.5, 'Europe/Paris', '09:00', '2025-06-02'); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: null }); + await insertRow(orm, ReservationEndpoints, { reservation: reservation.id, role: 'from', sequence: 0, name: 'Paris CDG', code: 'CDG', lat: 49.0, lng: 2.5, timezone: 'Europe/Paris', local_time: '09:00', local_date: '2025-06-02' }); // Clearing the cache must make later zones be re-checked. A regression that // evicted by writing `false` instead would silently strip the TZID from every @@ -821,9 +744,7 @@ describe('exportICS', () => { // A date-only start/end pair is what the booking import writes for an all-day // multi-day event. Without a DTEND the RFC reads the event as a single day, so // the booking collapsed onto its first day (#1869). - testDb - .prepare('UPDATE reservations SET reservation_time=?, reservation_end_time=? WHERE id=?') - .run('2026-08-26', '2026-08-30', reservation.id); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: '2026-08-26', reservation_end_time: '2026-08-30' }); const { ics } = await svc.exportICS(trip.id); @@ -837,9 +758,7 @@ describe('exportICS', () => { const reservation = createReservation(testDb, trip.id, { title: 'Cottage', type: 'other' }); // Mixing DTSTART;VALUE=DATE with a date-time DTEND violates RFC 5545 §3.8.2.2 // (both ends must share a value type), so only the date part may be used. - testDb - .prepare('UPDATE reservations SET reservation_time=?, reservation_end_time=? WHERE id=?') - .run('2026-08-26', '2026-08-30T11:00', reservation.id); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: '2026-08-26', reservation_end_time: '2026-08-30T11:00' }); const { ics } = await svc.exportICS(trip.id); @@ -853,9 +772,7 @@ describe('exportICS', () => { const reservation = createReservation(testDb, trip.id, { title: 'Swapped Dates', type: 'event' }); // Clients either drop an event whose DTEND precedes its DTSTART or render it // with zero duration; leaving the end out keeps the single-day fallback. - testDb - .prepare('UPDATE reservations SET reservation_time=?, reservation_end_time=? WHERE id=?') - .run('2026-08-26', '2026-08-20', reservation.id); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: '2026-08-26', reservation_end_time: '2026-08-20' }); const { ics } = await svc.exportICS(trip.id); @@ -869,9 +786,7 @@ describe('exportICS', () => { const reservation = createReservation(testDb, trip.id, { title: 'Workshop', type: 'other' }); // Relative "Day N" trips store a bare clock time; there is no date to anchor it // to, so it cannot become the end of an all-day range. - testDb - .prepare('UPDATE reservations SET reservation_time=?, reservation_end_time=? WHERE id=?') - .run('2026-08-26', '11:00', reservation.id); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: '2026-08-26', reservation_end_time: '11:00' }); const { ics } = await svc.exportICS(trip.id); @@ -884,7 +799,7 @@ describe('exportICS', () => { describe('accommodations', () => { /** A stay with its linked hotel reservation, the shape createAccommodation writes. */ - const createStay = ( + const createStay = async ( tripId: number, opts: { start: string | null; @@ -907,29 +822,15 @@ describe('accommodations', () => { lat: opts.lat ?? 48.8566, lng: opts.lng ?? 2.3522, }); - testDb.prepare('UPDATE places SET address = ? WHERE id = ?') - .run(opts.address === undefined ? '1 Rue de Rivoli' : opts.address, place.id); + await updateRows(orm, Places, { id: place.id }, { address: opts.address === undefined ? '1 Rue de Rivoli' : opts.address }); const startDay = createDay(testDb, tripId, { date: opts.start ?? undefined }); const endDay = opts.end === undefined ? startDay : createDay(testDb, tripId, { date: opts.end ?? undefined }); - const stayId = testDb.prepare(` - INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, check_in, check_in_end, check_out) - VALUES (?, ?, ?, ?, ?, ?, ?) - `).run( - tripId, place.id, startDay.id, endDay.id, - opts.check_in ?? null, opts.check_in_end ?? null, opts.check_out ?? null, - ).lastInsertRowid as number; + const stayId = (await insertRow(orm, DayAccommodations, { trip: tripId, place: place.id, startDay: startDay.id, endDay: endDay.id, check_in: opts.check_in ?? null, check_in_end: opts.check_in_end ?? null, check_out: opts.check_out ?? null })) as number; if (opts.withReservation !== false) { - testDb.prepare(` - INSERT INTO reservations (trip_id, day_id, title, reservation_time, status, type, accommodation_id, - confirmation_number, notes, location) - VALUES (?, ?, ?, ?, 'confirmed', 'hotel', ?, ?, ?, ?) - `).run( - tripId, startDay.id, opts.title ?? 'Hotel Bellevue', opts.start, String(stayId), - opts.confirmation ?? null, opts.notes ?? null, opts.location ?? null, - ); + await insertRow(orm, Reservations, { trip: tripId, day: startDay.id, title: opts.title ?? 'Hotel Bellevue', reservation_time: opts.start, status: 'confirmed', type: 'hotel', accommodation_id: String(stayId), confirmation_number: opts.confirmation ?? null, notes: opts.notes ?? null, location: opts.location ?? null }); } return { stayId, placeId: place.id }; }; @@ -937,7 +838,7 @@ describe('accommodations', () => { it('CAL-025: a stay covers every night as one all-day event, not just the arrival day', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris' }); - createStay(trip.id, { start: '2026-07-07', end: '2026-07-12' }); + await createStay(trip.id, { start: '2026-07-07', end: '2026-07-12' }); const { ics } = await svc.exportICS(trip.id); @@ -952,7 +853,7 @@ describe('accommodations', () => { // under a bar that repeats what the markers say. const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris' }); - createStay(trip.id, { + await createStay(trip.id, { start: '2026-07-07', end: '2026-07-12', check_in: '15:00', check_out: '11:00', }); @@ -973,15 +874,12 @@ describe('accommodations', () => { // booking, so dropping every block would leave the second one with nothing. const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris' }); - const { stayId } = createStay(trip.id, { + const { stayId } = (await createStay(trip.id, { start: '2026-07-07', end: '2026-07-12', check_in: '15:00', check_out: '11:00', - }); - const day = testDb.prepare('SELECT id FROM days WHERE trip_id = ? ORDER BY id ASC LIMIT 1').get(trip.id) as { id: number }; - testDb.prepare(` - INSERT INTO reservations (trip_id, day_id, title, reservation_time, status, type, accommodation_id) - VALUES (?, ?, 'Bellevue second room', NULL, 'confirmed', 'hotel', ?) - `).run(trip.id, day.id, String(stayId)); + })); + const [day] = await findRows(orm, Days, { trip: trip.id }, { id: 'asc' }); + await insertRow(orm, Reservations, { trip: trip.id, day: day.id, title: 'Bellevue second room', reservation_time: null, status: 'confirmed', type: 'hotel', accommodation_id: String(stayId) }); const { ics } = await svc.exportICS(trip.id); @@ -994,7 +892,7 @@ describe('accommodations', () => { // it, the same handover the split window bookings do (#2068). const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris' }); - createStay(trip.id, { + await createStay(trip.id, { start: '2026-07-07', end: '2026-07-12', check_in: '15:00', check_out: '11:00', confirmation: 'HTL-77291', notes: 'Key box code 4711', @@ -1014,7 +912,7 @@ describe('accommodations', () => { // without the fallback the only two events left name no address at all. const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris' }); - createStay(trip.id, { + await createStay(trip.id, { start: '2026-07-07', end: '2026-07-12', check_in: '15:00', check_out: '11:00', address: null, location: '12 Hotel Street', @@ -1028,7 +926,7 @@ describe('accommodations', () => { it('CAL-025g: a stay that keeps its block does not repeat the description on its markers', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris' }); - createStay(trip.id, { + await createStay(trip.id, { start: '2026-07-07', end: '2026-07-12', check_in: '15:00', confirmation: 'HTL-77291', }); @@ -1044,7 +942,7 @@ describe('accommodations', () => { it('CAL-025c: knowing only one end keeps the block, since nothing else carries the other', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris' }); - createStay(trip.id, { start: '2026-07-07', end: '2026-07-12', check_in: '15:00' }); + await createStay(trip.id, { start: '2026-07-07', end: '2026-07-12', check_in: '15:00' }); const { ics } = await svc.exportICS(trip.id); @@ -1056,7 +954,7 @@ describe('accommodations', () => { it('CAL-026: check-in and check-out become their own timed events in the stay zone', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris' }); - createStay(trip.id, { + await createStay(trip.id, { start: '2026-07-07', end: '2026-07-12', check_in: '15:00', check_in_end: '22:00', check_out: '11:00', }); @@ -1075,7 +973,7 @@ describe('accommodations', () => { it('CAL-026z: a stay across the change to summer time spells out both offsets', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris in spring' }); - createStay(trip.id, { start: '2026-03-27', end: '2026-04-02', check_in: '15:00', check_out: '11:00' }); + await createStay(trip.id, { start: '2026-03-27', end: '2026-04-02', check_in: '15:00', check_out: '11:00' }); const { ics } = await svc.exportICS(trip.id); const vtz = ics.slice(ics.indexOf('BEGIN:VTIMEZONE'), ics.indexOf('END:VTIMEZONE')); @@ -1091,7 +989,7 @@ describe('accommodations', () => { it('CAL-026b: a check-in without an until-clock reads as one hour, not a point (#2136)', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris' }); - createStay(trip.id, { start: '2026-07-07', end: '2026-07-12', check_in: '15:00' }); + await createStay(trip.id, { start: '2026-07-07', end: '2026-07-12', check_in: '15:00' }); const { ics } = await svc.exportICS(trip.id); @@ -1105,7 +1003,7 @@ describe('accommodations', () => { it('CAL-026c: the check-out marker carries the same default hour', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris' }); - createStay(trip.id, { start: '2026-07-07', end: '2026-07-12', check_out: '11:00' }); + await createStay(trip.id, { start: '2026-07-07', end: '2026-07-12', check_out: '11:00' }); const { ics } = await svc.exportICS(trip.id); @@ -1116,7 +1014,7 @@ describe('accommodations', () => { it('CAL-026d: a late check-out rolls the DTEND over midnight', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris' }); - createStay(trip.id, { start: '2026-07-07', end: '2026-07-12', check_out: '23:30' }); + await createStay(trip.id, { start: '2026-07-07', end: '2026-07-12', check_out: '23:30' }); const { ics } = await svc.exportICS(trip.id); @@ -1129,7 +1027,7 @@ describe('accommodations', () => { it('CAL-026e: a check-in window that ends before it starts runs past midnight (#2136)', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris' }); - createStay(trip.id, { + await createStay(trip.id, { start: '2026-07-07', end: '2026-07-12', check_in: '22:00', check_in_end: '02:00', check_out: '11:00', }); @@ -1146,7 +1044,7 @@ describe('accommodations', () => { it('CAL-027: a stay without times emits the all-day range and nothing else', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris' }); - createStay(trip.id, { start: '2026-07-07', end: '2026-07-12' }); + await createStay(trip.id, { start: '2026-07-07', end: '2026-07-12' }); const { ics } = await svc.exportICS(trip.id); @@ -1157,7 +1055,7 @@ describe('accommodations', () => { it('CAL-028: a stay whose end day lost its date falls back to the arrival day', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris' }); - createStay(trip.id, { start: '2026-07-07', end: null, check_out: '11:00' }); + await createStay(trip.id, { start: '2026-07-07', end: null, check_out: '11:00' }); const { ics } = await svc.exportICS(trip.id); @@ -1169,7 +1067,7 @@ describe('accommodations', () => { it('CAL-029: a stay with no reservation still contributes its check-in event', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris' }); - createStay(trip.id, { start: '2026-07-07', end: '2026-07-09', check_in: '15:00', withReservation: false }); + await createStay(trip.id, { start: '2026-07-07', end: '2026-07-09', check_in: '15:00', withReservation: false }); const { ics } = await svc.exportICS(trip.id); @@ -1183,7 +1081,7 @@ describe('accommodations', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris' }); const reservation = createReservation(testDb, trip.id, { title: 'Airbnb', type: 'hotel' }); - testDb.prepare('UPDATE reservations SET reservation_time=? WHERE id=?').run('2026-07-07', reservation.id); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: '2026-07-07' }); const { ics } = await svc.exportICS(trip.id); @@ -1194,7 +1092,7 @@ describe('accommodations', () => { it('CAL-031: a dateless stay is skipped instead of emitting a broken event', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Someday' }); - createStay(trip.id, { start: null, end: null, check_in: '15:00', check_out: '11:00' }); + await createStay(trip.id, { start: null, end: null, check_in: '15:00', check_out: '11:00' }); const { ics } = await svc.exportICS(trip.id); @@ -1206,17 +1104,14 @@ describe('accommodations', () => { it('CAL-036: two bookings on one stay emit the check-in/check-out markers once', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris' }); - const { stayId } = createStay(trip.id, { + const { stayId } = (await createStay(trip.id, { start: '2026-07-07', end: '2026-07-12', check_in: '15:00', check_out: '11:00', - }); + })); // Nothing stops a second booking from pointing at the same accommodation. The // markers are keyed by the stay, so joining the reservations in fanned the stay // out into two VEVENTs carrying the same UID, and clients then show whichever // one they saw last (#1869). - testDb.prepare(` - INSERT INTO reservations (trip_id, title, reservation_time, status, type, accommodation_id) - VALUES (?, 'Bellevue second room', NULL, 'confirmed', 'hotel', ?) - `).run(trip.id, String(stayId)); + await insertRow(orm, Reservations, { trip: trip.id, title: 'Bellevue second room', reservation_time: null, status: 'confirmed', type: 'hotel', accommodation_id: String(stayId) }); const { ics } = await svc.exportICS(trip.id); @@ -1232,14 +1127,12 @@ describe('accommodations', () => { it('CAL-060: the stop a booking puts on its check-in day does not list the hotel a second time', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Paris' }); - const { stayId, placeId } = createStay(trip.id, { start: '2026-07-07', end: '2026-07-12' }); - const stay = testDb.prepare('SELECT start_day_id FROM day_accommodations WHERE id = ?') - .get(stayId) as { start_day_id: number }; + const { stayId, placeId } = await createStay(trip.id, { start: '2026-07-07', end: '2026-07-12' }); + const stay = (await findRow(orm, DayAccommodations, { id: stayId }))!; // The stop a booked night puts on its check-in day, so the route can reach // the hotel. It belongs to the stay, not to the day's plan, and the stay // block already carries the hotel. - testDb.prepare('INSERT INTO day_assignments (day_id, place_id, order_index, accommodation_id) VALUES (?, ?, 0, ?)') - .run(stay.start_day_id, placeId, stayId); + await insertRow(orm, DayAssignments, { day: stay.start_day_id, place: placeId, order_index: 0, accommodation_id: stayId }); // A place the traveller planned by hand on the same day stays on it. const museum = createPlace(testDb, trip.id, { name: 'Louvre' }); createDayAssignment(testDb, stay.start_day_id, museum.id); @@ -1256,7 +1149,7 @@ describe('accommodations', () => { // ── Car rental pickup/drop-off in the feed (#1721) ────────────────────────── describe('car rentals', () => { - const insertEndpoint = ( + const insertEndpoint = async ( reservationId: number, role: string, sequence: number, @@ -1267,19 +1160,16 @@ describe('car rentals', () => { local_time: string | null, local_date: string | null, ) => { - testDb.prepare( - 'INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, code, lat, lng, timezone, local_time, local_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' - ).run(reservationId, role, sequence, name, null, lat, lng, timezone, local_time, local_date); + await insertRow(orm, ReservationEndpoints, { reservation: reservationId, role, sequence, name, code: null, lat, lng, timezone, local_time, local_date }); }; it('CAL-037: a rental with from/to endpoints produces a pickup and a drop-off event at the right local times and zones', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Road Trip' }); const reservation = createReservation(testDb, trip.id, { title: 'Hertz Rental', type: 'car' }); - testDb.prepare('UPDATE reservations SET reservation_time=NULL, reservation_end_time=NULL, location=? WHERE id=?') - .run('Hertz Downtown', reservation.id); - insertEndpoint(reservation.id, 'from', 0, 'Paris Office', 48.8566, 2.3522, 'Europe/Paris', '09:00', '2026-07-07'); - insertEndpoint(reservation.id, 'to', 1, 'Berlin Office', 52.5, 13.4, 'Europe/Berlin', '10:30', '2026-07-14'); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: null, reservation_end_time: null, location: 'Hertz Downtown' }); + await insertEndpoint(reservation.id, 'from', 0, 'Paris Office', 48.8566, 2.3522, 'Europe/Paris', '09:00', '2026-07-07'); + await insertEndpoint(reservation.id, 'to', 1, 'Berlin Office', 52.5, 13.4, 'Europe/Berlin', '10:30', '2026-07-14'); const { ics } = await svc.exportICS(trip.id); @@ -1296,10 +1186,10 @@ describe('car rentals', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Road Trip' }); const reservation = createReservation(testDb, trip.id, { title: 'Avis Rental', type: 'car' }); - testDb.prepare('UPDATE reservations SET reservation_time=NULL, reservation_end_time=NULL WHERE id=?').run(reservation.id); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: null, reservation_end_time: null }); // Neither endpoint carries a from/to role — an older import shape. - insertEndpoint(reservation.id, 'stop', 0, 'Paris Office', 48.8566, 2.3522, 'Europe/Paris', '09:00', '2026-07-07'); - insertEndpoint(reservation.id, 'stop', 1, 'Berlin Office', 52.5, 13.4, 'Europe/Berlin', '10:30', '2026-07-14'); + await insertEndpoint(reservation.id, 'stop', 0, 'Paris Office', 48.8566, 2.3522, 'Europe/Paris', '09:00', '2026-07-07'); + await insertEndpoint(reservation.id, 'stop', 1, 'Berlin Office', 52.5, 13.4, 'Europe/Berlin', '10:30', '2026-07-14'); const { ics } = await svc.exportICS(trip.id); @@ -1314,8 +1204,7 @@ describe('car rentals', () => { const trip = createTrip(testDb, user.id, { title: 'Road Trip' }); const place = createPlace(testDb, trip.id, { name: 'Rental Desk', lat: 48.8566, lng: 2.3522 }); const reservation = createReservation(testDb, trip.id, { title: 'Budget Rental', type: 'car' }); - testDb.prepare('UPDATE reservations SET reservation_time=?, reservation_end_time=?, place_id=? WHERE id=?') - .run('2026-07-07T09:00', '2026-07-14T10:30', place.id, reservation.id); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: '2026-07-07T09:00', reservation_end_time: '2026-07-14T10:30', place: place.id }); const { ics } = await svc.exportICS(trip.id); @@ -1329,10 +1218,10 @@ describe('car rentals', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Road Trip' }); const reservation = createReservation(testDb, trip.id, { title: 'Sixt Rental', type: 'car' }); - testDb.prepare('UPDATE reservations SET reservation_time=NULL, reservation_end_time=NULL WHERE id=?').run(reservation.id); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: null, reservation_end_time: null }); // Only the pickup was geocoded — the common shape for a partially-imported // booking. The lone endpoint must not be reused as the drop-off too. - insertEndpoint(reservation.id, 'from', 0, 'Paris Office', 48.8566, 2.3522, 'Europe/Paris', '09:00', '2026-07-07'); + await insertEndpoint(reservation.id, 'from', 0, 'Paris Office', 48.8566, 2.3522, 'Europe/Paris', '09:00', '2026-07-07'); const { ics } = await svc.exportICS(trip.id); @@ -1344,10 +1233,10 @@ describe('car rentals', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Road Trip' }); const reservation = createReservation(testDb, trip.id, { title: 'Untimed Rental', type: 'car' }); - testDb.prepare('UPDATE reservations SET reservation_time=NULL, reservation_end_time=NULL WHERE id=?').run(reservation.id); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: null, reservation_end_time: null }); // A date but no clock — the pre-existing "untimed transport" all-day // fallback still applies and must be unaffected by the new marker events. - insertEndpoint(reservation.id, 'from', 0, 'Paris Office', 48.8566, 2.3522, 'Europe/Paris', null, '2026-07-07'); + await insertEndpoint(reservation.id, 'from', 0, 'Paris Office', 48.8566, 2.3522, 'Europe/Paris', null, '2026-07-07'); const { ics } = await svc.exportICS(trip.id); @@ -1365,10 +1254,9 @@ describe('car rentals', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Road Trip' }); const reservation = createReservation(testDb, trip.id, { title: 'Seconds Rental', type: 'car' }); - testDb.prepare('UPDATE reservations SET reservation_time=?, reservation_end_time=? WHERE id=?') - .run('2026-07-07T09:00:00', '18:30:00', reservation.id); - insertEndpoint(reservation.id, 'from', 0, 'Paris Office', 48.8566, 2.3522, 'Europe/Paris', '09:00:00', '2026-07-07'); - insertEndpoint(reservation.id, 'to', 1, 'Lyon Office', 45.764, 4.8357, 'Europe/Paris', '18:30:00', '2026-07-07'); + await updateRows(orm, Reservations, { id: reservation.id }, { reservation_time: '2026-07-07T09:00:00', reservation_end_time: '18:30:00' }); + await insertEndpoint(reservation.id, 'from', 0, 'Paris Office', 48.8566, 2.3522, 'Europe/Paris', '09:00:00', '2026-07-07'); + await insertEndpoint(reservation.id, 'to', 1, 'Lyon Office', 45.764, 4.8357, 'Europe/Paris', '18:30:00', '2026-07-07'); const { ics } = await svc.exportICS(trip.id); @@ -1384,8 +1272,7 @@ describe('car rentals', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Road Trip' }); const parking = createReservation(testDb, trip.id, { title: 'Airport P4', type: 'parking' }); - testDb.prepare('UPDATE reservations SET reservation_time=?, reservation_end_time=? WHERE id=?') - .run('2026-07-01T06:30', '2026-07-10T22:15', parking.id); + await updateRows(orm, Reservations, { id: parking.id }, { reservation_time: '2026-07-01T06:30', reservation_end_time: '2026-07-10T22:15' }); const { ics } = await svc.exportICS(trip.id); @@ -1405,8 +1292,7 @@ describe('car rentals', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Road Trip' }); const parking = createReservation(testDb, trip.id, { title: 'Garage', type: 'parking' }); - testDb.prepare('UPDATE reservations SET reservation_time=?, reservation_end_time=? WHERE id=?') - .run('2026-07-01T08:00', '2026-07-01T18:30', parking.id); + await updateRows(orm, Reservations, { id: parking.id }, { reservation_time: '2026-07-01T08:00', reservation_end_time: '2026-07-01T18:30' }); const { ics } = await svc.exportICS(trip.id); @@ -1425,8 +1311,7 @@ describe('car rentals', () => { const rental = createReservation(testDb, trip.id, { title: 'Sixt', type: 'car' }); // The planner writes reservation_time = NULL when the optional time pickers // are left blank, which is what made this booking invisible. - testDb.prepare('UPDATE reservations SET reservation_time=NULL, reservation_end_time=NULL, day_id=?, end_day_id=? WHERE id=?') - .run(first.id, last.id, rental.id); + await updateRows(orm, Reservations, { id: rental.id }, { reservation_time: null, reservation_end_time: null, day: first.id, endDay: last.id }); const { ics } = await svc.exportICS(trip.id); @@ -1440,10 +1325,9 @@ describe('car rentals', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Road Trip' }); const rental = createReservation(testDb, trip.id, { title: 'Hertz', type: 'car' }); - testDb.prepare('UPDATE reservations SET reservation_time=NULL, reservation_end_time=NULL, confirmation_number=?, location=? WHERE id=?') - .run('HZ-4471', 'Hertz Downtown', rental.id); - insertEndpoint(rental.id, 'from', 0, 'Paris Office', 48.8566, 2.3522, 'Europe/Paris', '09:00', '2026-07-07'); - insertEndpoint(rental.id, 'to', 1, 'Berlin Office', 52.5, 13.4, 'Europe/Berlin', '10:30', '2026-07-14'); + await updateRows(orm, Reservations, { id: rental.id }, { reservation_time: null, reservation_end_time: null, confirmation_number: 'HZ-4471', location: 'Hertz Downtown' }); + await insertEndpoint(rental.id, 'from', 0, 'Paris Office', 48.8566, 2.3522, 'Europe/Paris', '09:00', '2026-07-07'); + await insertEndpoint(rental.id, 'to', 1, 'Berlin Office', 52.5, 13.4, 'Europe/Berlin', '10:30', '2026-07-14'); const { ics } = await svc.exportICS(trip.id); @@ -1467,8 +1351,7 @@ describe('car rentals', () => { const trip = createTrip(testDb, user.id, { title: 'Road Trip' }); const place = createPlace(testDb, trip.id, { name: 'Rental Desk', lat: 48.8566, lng: 2.3522 }); const rental = createReservation(testDb, trip.id, { title: 'Avis', type: 'car' }); - testDb.prepare('UPDATE reservations SET reservation_time=?, reservation_end_time=?, place_id=? WHERE id=?') - .run('2026-07-02T10:00', '2026-07-09T10:00', place.id, rental.id); + await updateRows(orm, Reservations, { id: rental.id }, { reservation_time: '2026-07-02T10:00', reservation_end_time: '2026-07-09T10:00', place: place.id }); const { ics } = await svc.exportICS(trip.id); @@ -1483,8 +1366,8 @@ describe('car rentals', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Road Trip' }); const rental = createReservation(testDb, trip.id, { title: 'Solo', type: 'car' }); - testDb.prepare('UPDATE reservations SET reservation_time=NULL, reservation_end_time=NULL WHERE id=?').run(rental.id); - insertEndpoint(rental.id, 'from', 0, 'Paris Office', 48.8566, 2.3522, 'Europe/Paris', '09:00', '2026-07-07'); + await updateRows(orm, Reservations, { id: rental.id }, { reservation_time: null, reservation_end_time: null }); + await insertEndpoint(rental.id, 'from', 0, 'Paris Office', 48.8566, 2.3522, 'Europe/Paris', '09:00', '2026-07-07'); const { ics } = await svc.exportICS(trip.id); @@ -1499,8 +1382,7 @@ describe('car rentals', () => { const first = createDay(testDb, trip.id, { date: '2026-07-03' }); const last = createDay(testDb, trip.id, { date: '2026-07-04' }); const bus = createReservation(testDb, trip.id, { title: 'Night Bus', type: 'bus' }); - testDb.prepare('UPDATE reservations SET reservation_time=NULL, reservation_end_time=NULL, day_id=?, end_day_id=? WHERE id=?') - .run(first.id, last.id, bus.id); + await updateRows(orm, Reservations, { id: bus.id }, { reservation_time: null, reservation_end_time: null, day: first.id, endDay: last.id }); const { ics } = await svc.exportICS(trip.id); @@ -1515,10 +1397,10 @@ describe('car rentals', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Road Trip' }); const rental = createReservation(testDb, trip.id, { title: 'Europcar', type: 'car' }); - testDb.prepare('UPDATE reservations SET reservation_time=NULL, reservation_end_time=NULL WHERE id=?').run(rental.id); + await updateRows(orm, Reservations, { id: rental.id }, { reservation_time: null, reservation_end_time: null }); // An older import that geocoded both ends but stored no IANA zone. - insertEndpoint(rental.id, 'from', 0, 'Paris Office', 48.8566, 2.3522, null, '09:00', '2026-07-07'); - insertEndpoint(rental.id, 'to', 1, 'Berlin Office', 52.5, 13.4, null, '10:30', '2026-07-14'); + await insertEndpoint(rental.id, 'from', 0, 'Paris Office', 48.8566, 2.3522, null, '09:00', '2026-07-07'); + await insertEndpoint(rental.id, 'to', 1, 'Berlin Office', 52.5, 13.4, null, '10:30', '2026-07-14'); const { ics } = await svc.exportICS(trip.id); @@ -1532,8 +1414,7 @@ describe('car rentals', () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Road Trip' }); const parking = createReservation(testDb, trip.id, { title: 'Street Bay', type: 'parking' }); - testDb.prepare('UPDATE reservations SET reservation_time=?, reservation_end_time=? WHERE id=?') - .run('2026-07-01T08:00', '18:30', parking.id); + await updateRows(orm, Reservations, { id: parking.id }, { reservation_time: '2026-07-01T08:00', reservation_end_time: '18:30' }); const { ics } = await svc.exportICS(trip.id); @@ -1549,8 +1430,7 @@ describe('car rentals', () => { const rental = createReservation(testDb, trip.id, { title: 'Dayless', type: 'car' }); // A day but no end day and no clock: one side resolves, and it has no time, // so there is nothing to place — the all-day block carries it instead. - testDb.prepare('UPDATE reservations SET reservation_time=NULL, reservation_end_time=NULL, day_id=?, end_day_id=NULL WHERE id=?') - .run(day.id, rental.id); + await updateRows(orm, Reservations, { id: rental.id }, { reservation_time: null, reservation_end_time: null, day: day.id, endDay: null }); const { ics } = await svc.exportICS(trip.id); @@ -1563,20 +1443,14 @@ describe('car rentals', () => { describe('staged bookings', () => { /** A stay plus, optionally, the bookings that point at it. */ - const stayWith = (tripId: number, states: Array<{ state: string; title: string }>) => { + const stayWith = async (tripId: number, states: Array<{ state: string; title: string }>) => { const place = createPlace(testDb, tripId, { name: 'Hotel Bellevue', lat: 48.8566, lng: 2.3522 }); const startDay = createDay(testDb, tripId, { date: '2026-09-01' }); const endDay = createDay(testDb, tripId, { date: '2026-09-04' }); - const stayId = testDb.prepare(` - INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, check_in, check_out) - VALUES (?, ?, ?, ?, '15:00', '11:00') - `).run(tripId, place.id, startDay.id, endDay.id).lastInsertRowid as number; + const stayId = (await insertRow(orm, DayAccommodations, { trip: tripId, place: place.id, startDay: startDay.id, endDay: endDay.id, check_in: '15:00', check_out: '11:00' })) as number; for (const s of states) { - testDb.prepare(` - INSERT INTO reservations (trip_id, day_id, title, reservation_time, status, type, accommodation_id, ingest_state) - VALUES (?, ?, ?, '2026-09-01T15:00', 'confirmed', 'hotel', ?, ?) - `).run(tripId, startDay.id, s.title, String(stayId), s.state); + await insertRow(orm, Reservations, { trip: tripId, day: startDay.id, title: s.title, reservation_time: '2026-09-01T15:00', status: 'confirmed', type: 'hotel', accommodation_id: String(stayId), ingest_state: s.state }); } return { stayId, placeId: place.id }; }; @@ -1586,10 +1460,9 @@ describe('staged bookings', () => { const trip = createTrip(testDb, user.id, { title: 'Kyoto' }); const day = createDay(testDb, trip.id, { date: '2026-09-01' }); const staged = createReservation(testDb, trip.id, { title: 'Parked Flight', type: 'flight', day_id: day.id }); - testDb.prepare(`UPDATE reservations SET ingest_state='staged', reservation_time='2026-09-01T08:00', - confirmation_number='ABC123' WHERE id=?`).run(staged.id); + await updateRows(orm, Reservations, { id: staged.id }, { ingest_state: 'staged', reservation_time: '2026-09-01T08:00', confirmation_number: 'ABC123' }); const live = createReservation(testDb, trip.id, { title: 'Booked Flight', type: 'flight', day_id: day.id }); - testDb.prepare("UPDATE reservations SET reservation_time='2026-09-01T12:00' WHERE id=?").run(live.id); + await updateRows(orm, Reservations, { id: live.id }, { reservation_time: '2026-09-01T12:00' }); const { ics } = await svc.exportICS(trip.id); @@ -1606,7 +1479,7 @@ describe('staged bookings', () => { // No ingest_state named on the insert: exactly what every writer does today, // and what the ALTER backfilled onto every pre-existing row. const old = createReservation(testDb, trip.id, { title: 'Legacy Booking', type: 'flight', day_id: day.id }); - testDb.prepare("UPDATE reservations SET reservation_time='2026-09-02T09:00' WHERE id=?").run(old.id); + await updateRows(orm, Reservations, { id: old.id }, { reservation_time: '2026-09-02T09:00' }); const { ics } = await svc.exportICS(trip.id); @@ -1616,7 +1489,7 @@ describe('staged bookings', () => { it('CAL-057: an accommodation whose only booking is staged emits no stay, check-in or check-out', async () => { const { user } = createUser(testDb); const trip = createTrip(testDb, user.id, { title: 'Osaka' }); - stayWith(trip.id, [{ state: 'staged', title: 'Parked Hotel' }]); + await stayWith(trip.id, [{ state: 'staged', title: 'Parked Hotel' }]); const { ics } = await svc.exportICS(trip.id); @@ -1631,7 +1504,7 @@ describe('staged bookings', () => { // The regression this pins: a plain EXISTS(live) instead of // NOT EXISTS(any) OR EXISTS(live) would drop every hand-added hotel out of // trips that are shared today. - stayWith(trip.id, []); + await stayWith(trip.id, []); const { ics } = await svc.exportICS(trip.id); @@ -1644,7 +1517,7 @@ describe('staged bookings', () => { const trip = createTrip(testDb, user.id, { title: 'Kobe' }); // The staged row gets the lower id, so the subquery's ORDER BY r.id ASC // would pick it without the predicate. - stayWith(trip.id, [{ state: 'staged', title: 'Parked Name' }, { state: 'live', title: 'Real Name' }]); + await stayWith(trip.id, [{ state: 'staged', title: 'Parked Name' }, { state: 'live', title: 'Real Name' }]); const { ics } = await svc.exportICS(trip.id); @@ -1659,27 +1532,20 @@ describe('folded quirk branches', () => { const trip = createTrip(testDb, user.id, { title: 'Branchy' }); const day = createDay(testDb, trip.id, { date: '2025-06-02' }); const place = createPlace(testDb, trip.id, { name: 'Untimed Spot' }); - testDb.prepare("UPDATE places SET address = '1 Rue Test' WHERE id = ?").run(place.id); + await updateRows(orm, Places, { id: place.id }, { address: '1 Rue Test' }); const assignment = createDayAssignment(testDb, day.id, place.id); - testDb.prepare("UPDATE day_assignments SET notes = 'bring hat' WHERE id = ?").run(assignment.id); + await updateRows(orm, DayAssignments, { id: assignment.id }, { notes: 'bring hat' }); createDayNote(testDb, day.id, trip.id, { text: 'timed note', time: '10:00' }); createDayNote(testDb, day.id, trip.id, { text: 'plain note' }); // Multi-leg flight metadata → Route: A → B → C, plus train + notes + location. const flight = createReservation(testDb, trip.id, { title: 'Legs', type: 'flight' }); - testDb.prepare('UPDATE reservations SET reservation_time=?, metadata=?, notes=?, location=?, confirmation_number=? WHERE id=?').run( - '2025-06-02T09:00', - JSON.stringify({ legs: [{ from: 'FRA', to: 'BER' }, { to: 'HND' }], train_number: 'ICE 100' }), - 'window seat', 'Gate 4', 'ABC123', flight.id, - ); + await updateRows(orm, Reservations, { id: flight.id }, { reservation_time: '2025-06-02T09:00', metadata: JSON.stringify({ legs: [{ from: 'FRA', to: 'BER' }, { to: 'HND' }], train_number: 'ICE 100' }), notes: 'window seat', location: 'Gate 4', confirmation_number: 'ABC123' }); // Endpoint-derived route (no route metadata) with a date-only endpoint fallback. const transport = createReservation(testDb, trip.id, { title: 'Ferry', type: 'transport' }); - testDb.prepare('UPDATE reservations SET reservation_time=NULL WHERE id=?').run(transport.id); - const insertEp = testDb.prepare( - 'INSERT INTO reservation_endpoints (reservation_id, role, sequence, name, code, lat, lng, timezone, local_time, local_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' - ); - insertEp.run(transport.id, 'from', 0, 'Pier A', null, 1.0, 1.0, null, null, '2025-06-03'); - insertEp.run(transport.id, 'to', 1, 'Pier B', 'PB', 1.1, 1.1, null, null, '2025-06-03'); + await updateRows(orm, Reservations, { id: transport.id }, { reservation_time: null }); + await insertRow(orm, ReservationEndpoints, { reservation: transport.id, role: 'from', sequence: 0, name: 'Pier A', code: null, lat: 1.0, lng: 1.0, timezone: null, local_time: null, local_date: '2025-06-03' }); + await insertRow(orm, ReservationEndpoints, { reservation: transport.id, role: 'to', sequence: 1, name: 'Pier B', code: 'PB', lat: 1.1, lng: 1.1, timezone: null, local_time: null, local_date: '2025-06-03' }); // Unfold the RFC 5545 75-octet folding so substring assertions see whole lines. const ics = (await svc.exportICS(trip.id)).ics.replace(/\r\n /g, ''); @@ -1780,8 +1646,7 @@ describe('serialised output', () => { // placeable time. const place = createPlace(testDb, trip.id, { name: 'Tokyo Tower', lat: 35.6586, lng: 139.7454 }); const assignment = createDayAssignment(testDb, day.id, place.id); - testDb.prepare('UPDATE day_assignments SET assignment_time=?, assignment_end_time=? WHERE id=?') - .run('09:00', '10:30', assignment.id); + await updateRows(orm, DayAssignments, { id: assignment.id }, { assignment_time: '09:00', assignment_end_time: '10:30' }); createDayNote(testDb, day.id, trip.id, { text: 'Bring the tickets', time: '08:00' }); createReservation(testDb, trip.id, { title: 'NH 203', type: 'flight' });