From 40c5eb5c5474ca9940088fe495d281e3ff18eb5e Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 8 Oct 2026 17:45:40 +0200 Subject: [PATCH] test(server): seed the calendar service spec through the factories Converts the raw prepare() fixtures in the calendar service spec to the ORM factories: reservation times, metadata and endpoints, stays, stops and place addresses are written through insertRow and updateRows. The stay helpers become async, so their callers await them. The timezone cache case seeds its 1010 bulk bookings and their endpoints with two insertRows batches instead of a raw transaction. The unused canAccessTrip and isOwner entries are dropped from the database mock. --- .../tests/unit/nest/calendar.service.test.ts | 455 ++++++------------ 1 file changed, 160 insertions(+), 295 deletions(-) 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' });