From a2aeeb32c899e0f53d9d8fe7fde484557027da4c Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 7 Nov 2026 12:07:29 +0100 Subject: [PATCH] fix(ci): keep the reservations repository at its size baseline The wallet pass commit had reformatted the whole repository file, which put it 200 lines over its size baseline. It is back to its old form, in the format baseline again, and the pass-context query lives in a helper of its own. --- server/scripts/format-baseline.json | 1 + .../repositories/Reservations.repository.ts | 478 +++++------------- .../_shared/reservation-pass-context.ts | 46 ++ .../reservations/reservations-read.service.ts | 3 +- 4 files changed, 163 insertions(+), 365 deletions(-) create mode 100644 server/src/db/repositories/_shared/reservation-pass-context.ts diff --git a/server/scripts/format-baseline.json b/server/scripts/format-baseline.json index 10980f381..e1ce926b3 100644 --- a/server/scripts/format-baseline.json +++ b/server/scripts/format-baseline.json @@ -1,6 +1,7 @@ [ "src/db/repositories/DayAssignments.repository.ts", "src/db/repositories/Places.repository.ts", + "src/db/repositories/Reservations.repository.ts", "src/db/repositories/Trips.repository.ts", "src/db/repositories/Users.repository.ts", "src/nest/atlas/atlas-geo.ts", diff --git a/server/src/db/repositories/Reservations.repository.ts b/server/src/db/repositories/Reservations.repository.ts index fb006d980..a61a5ce47 100644 --- a/server/src/db/repositories/Reservations.repository.ts +++ b/server/src/db/repositories/Reservations.repository.ts @@ -1,16 +1,14 @@ -import { castIntegerKysely, concatKysely, startsWithIsoDateKysely, substringKysely } from '../dialect/kysely-functions'; -import { coalesceOverride, columnRef, dayDistance } from '../dialect/sql-functions'; +import type { Reservations } from '../entities/Reservations.entity'; import { DayAssignments } from '../entities/DayAssignments.entity'; import { Days } from '../entities/Days.entity'; -import type { Reservations } from '../entities/Reservations.entity'; -import type { DB } from '../kysely/db'; -import { - publicReservationExpr, - publicStayExists, - type ReservationVisibilityKyselyDB, -} from './_shared/reservation-visibility'; -import { TrekRepository } from './_shared/trek-repository'; +import { coalesceOverride, columnRef, dayDistance } from '../dialect/sql-functions'; +import { castIntegerKysely, concatKysely, startsWithIsoDateKysely, substringKysely } from '../dialect/kysely-functions'; +import { publicReservationExpr, publicStayExists, type ReservationVisibilityKyselyDB } from './_shared/reservation-visibility'; import type { DayAssignmentRow } from './day-assignment-rows'; +import { TrekRepository } from './_shared/trek-repository'; +import { passContextQuery, type PassContextDB } from './_shared/reservation-pass-context'; +import type { DB } from '../kysely/db'; +import { ROADTRIP_RESERVATION_TYPES } from '@trek/shared'; import type { PublicApiAccommodationKyselyDB, PublicApiAccommodationRow, @@ -20,7 +18,6 @@ import type { PublicApiUnplannedPlaceRow, PublicApiUnscheduledReservationRow, } from './reservation-public-api-rows'; -import { ROADTRIP_RESERVATION_TYPES } from '@trek/shared'; // The public API row types moved to their own module; re-exported so callers keep one import path. export type { @@ -68,9 +65,7 @@ type ReservationJoinKyselyDB = Pick & { - accommodation_id: string | number | null; -}; +type ReservationsNumericAccommodationBind = Omit & { accommodation_id: string | number | null }; /** * Kysely typing for `restampLinkedReservation` (DY23). `accommodation_id: @@ -224,8 +219,7 @@ export interface CalendarStayRow { reservation_title: string | null; } -type CalendarStayKyselyDB = ReservationVisibilityKyselyDB & - Pick; +type CalendarStayKyselyDB = ReservationVisibilityKyselyDB & Pick; /** * RS20 (`ReservationsService.listUpcoming`) — the CTE + `UNION ALL` @@ -275,10 +269,7 @@ interface EntriesRow extends UpcomingReservationRow { at_time: string | null; } -type UpcomingReservationsKyselyDB = Pick< - DB, - 'trips' | 'trip_members' | 'reservations' | 'days' | 'places' | 'day_accommodations' ->; +type UpcomingReservationsKyselyDB = Pick; /** * `share.service.ts:251` (`publicEndpointsByReservation`) — `SELECT @@ -381,51 +372,12 @@ export interface SharePublicAccommodationRow { type SharePublicAccommodationKyselyDB = ReservationVisibilityKyselyDB & Pick; -/** - * Plan 3h Task 4 (`AirportsService.backfillFlightEndpoints`, AIR1) — - * additive. The `reservations`/`reservation_endpoints` tables the - * `NOT EXISTS` guard reads. - */ +/** `AirportsService.backfillFlightEndpoints` (AIR1): the tables its `NOT EXISTS` guard reads. */ type FlightsMissingEndpointsKyselyDB = Pick; -/** `findPassContext`: the booking's trip title and the coordinates of its place or stay. */ -type PassContextKyselyDB = Pick; - -/** What a booking's Wallet pass needs beyond the booking: where it is, and the trip's name. */ -export interface ReservationPassContext { - trip_title: string | null; - place_lat: number | null; - place_lng: number | null; - place_name: string | null; -} - export class ReservationsRepository extends TrekRepository { - /** - * The trip's title and the place a booking is at (its own place, else its - * stay's), for its Apple Wallet pass (WalletPassService). Scoped to the trip, - * so a booking of another trip reads as none. - */ - async findPassContext(id: number, trip_id: number): Promise { - const row = await this.kysely() - .selectFrom('reservations as r') - .innerJoin('trips as t', 't.id', 'r.trip_id') - .leftJoin('places as p', 'p.id', 'r.place_id') - // accommodation_id is TEXT; compared as a number so Postgres takes it as well as SQLite ("14" and "14.0" alike). - .leftJoin('day_accommodations as ap', (join) => - join.on((eb) => eb(eb.cast('r.accommodation_id', 'numeric'), '=', eb.ref('ap.id'))), - ) - .leftJoin('places as acc_p', 'acc_p.id', 'ap.place_id') - .where('r.id', '=', id) - .where('r.trip_id', '=', trip_id) - .select([ - 't.title as trip_title', - (eb) => eb.fn.coalesce('p.lat', 'acc_p.lat').as('place_lat'), - (eb) => eb.fn.coalesce('p.lng', 'acc_p.lng').as('place_lng'), - (eb) => eb.fn.coalesce('p.name', 'acc_p.name').as('place_name'), - ]) - .executeTakeFirst(); - return row as ReservationPassContext | undefined; - } + // For its Wallet pass (_shared/reservation-pass-context.ts). + findPassContext(id: number, tripId: number) { return passContextQuery(this.kysely(), id, tripId); } private joinedQuery() { return this.kysely() @@ -463,9 +415,7 @@ export class ReservationsRepository extends TrekRepository { * projection for one reservation, no `ORDER BY` (a single row). */ async findWithJoins(id: number | string): Promise { - const row = await this.joinedQuery() - .where('r.id', '=', id as number) - .executeTakeFirst(); + const row = await this.joinedQuery().where('r.id', '=', id as number).executeTakeFirst(); return row as ReservationJoinRow | undefined; } @@ -485,17 +435,11 @@ export class ReservationsRepository extends TrekRepository { } /** RS45 (`ReservationsService.remove`'s pre-image read) — `SELECT id, title, type, accommodation_id FROM reservations WHERE id = ? AND trip_id = ?`. */ - async findHeaderInTrip( - id: number, - trip_id: number, - ): Promise<{ id: number; title: string; type: string | null; accommodation_id: string | null } | undefined> { + async findHeaderInTrip(id: number, trip_id: number): Promise<{ id: number; title: string; type: string | null; accommodation_id: string | null } | undefined> { return this.qb('r') .select(['r.id', 'r.title', 'r.type', 'r.accommodation_id']) .where({ id, trip: trip_id }) - .execute<{ id: number; title: string; type: string | null; accommodation_id: string | null } | undefined>( - 'get', - false, - ); + .execute<{ id: number; title: string; type: string | null; accommodation_id: string | null } | undefined>('get', false); } /** @@ -504,38 +448,19 @@ export class ReservationsRepository extends TrekRepository { * reservations WHERE trip_id = ? AND (type != 'hotel' OR accommodation_id * IS NULL) AND reservation_time IS NOT NULL`. */ - async listResyncCandidates(trip_id: number): Promise< - { - id: number; - reservation_time: string | null; - reservation_end_time: string | null; - day_id: number | null; - end_day_id: number | null; - }[] - > { + async listResyncCandidates(trip_id: number): Promise<{ + id: number; reservation_time: string | null; reservation_end_time: string | null; + day_id: number | null; end_day_id: number | null; + }[]> { const platform = this.getEntityManager().getPlatform(); return this.qb('r') - .select([ - 'r.id', - 'r.reservation_time', - 'r.reservation_end_time', - columnRef(platform, 'r.day_id').as('day_id'), - columnRef(platform, 'r.end_day_id').as('end_day_id'), - ]) + .select(['r.id', 'r.reservation_time', 'r.reservation_end_time', columnRef(platform, 'r.day_id').as('day_id'), columnRef(platform, 'r.end_day_id').as('end_day_id')]) .where({ trip: trip_id, $or: [{ type: { $ne: 'hotel' } }, { accommodation_id: null }], reservation_time: { $ne: null }, }) - .execute< - { - id: number; - reservation_time: string | null; - reservation_end_time: string | null; - day_id: number | null; - end_day_id: number | null; - }[] - >('all', false); + .execute<{ id: number; reservation_time: string | null; reservation_end_time: string | null; day_id: number | null; end_day_id: number | null }[]>('all', false); } /** RS13 (`resyncReservationDays`'s per-row write) — `UPDATE reservations SET day_id = ?, end_day_id = ? WHERE id = ?`. */ @@ -605,48 +530,42 @@ export class ReservationsRepository extends TrekRepository { * is handed — no SQL-side COALESCE. `id: number` (rule 21 — the SAME * `toRowId`-parsed value `findInTrip`'s gate read used). */ - async updateReservation( - id: number, - write: { - title: string; - reservation_time: string | null; - reservation_end_time: string | null; - location: string | null; - confirmation_number: string | null; - notes: string | null; - url: string | null; - day_id: number | null; - end_day_id: number | null; - place_id: number | null; - assignment_id: number | null; - status: string; - type: string; - accommodation_id: string | null; - metadata: string | null; - needs_review: number; - }, - ): Promise { - await this.nativeUpdate( - { id }, - { - title: write.title, - reservation_time: write.reservation_time, - reservation_end_time: write.reservation_end_time, - location: write.location, - confirmation_number: write.confirmation_number, - notes: write.notes, - url: write.url, - day: write.day_id, - endDay: write.end_day_id, - place: write.place_id, - assignment: write.assignment_id, - status: write.status, - type: write.type, - accommodation_id: write.accommodation_id, - metadata: write.metadata, - needs_review: write.needs_review, - }, - ); + async updateReservation(id: number, write: { + title: string; + reservation_time: string | null; + reservation_end_time: string | null; + location: string | null; + confirmation_number: string | null; + notes: string | null; + url: string | null; + day_id: number | null; + end_day_id: number | null; + place_id: number | null; + assignment_id: number | null; + status: string; + type: string; + accommodation_id: string | null; + metadata: string | null; + needs_review: number; + }): Promise { + await this.nativeUpdate({ id }, { + title: write.title, + reservation_time: write.reservation_time, + reservation_end_time: write.reservation_end_time, + location: write.location, + confirmation_number: write.confirmation_number, + notes: write.notes, + url: write.url, + day: write.day_id, + endDay: write.end_day_id, + place: write.place_id, + assignment: write.assignment_id, + status: write.status, + type: write.type, + accommodation_id: write.accommodation_id, + metadata: write.metadata, + needs_review: write.needs_review, + }); } /** @@ -666,34 +585,15 @@ export class ReservationsRepository extends TrekRepository { } /** DY14 (`DaysService.restampReservationDates`) — `SELECT id, day_id, end_day_id, reservation_time, reservation_end_time FROM reservations WHERE trip_id = ?`. */ - async listForRestamp(trip_id: number): Promise< - { - id: number; - day_id: number | null; - end_day_id: number | null; - reservation_time: string | null; - reservation_end_time: string | null; - }[] - > { + async listForRestamp(trip_id: number): Promise<{ + id: number; day_id: number | null; end_day_id: number | null; + reservation_time: string | null; reservation_end_time: string | null; + }[]> { const platform = this.getEntityManager().getPlatform(); return this.qb('r') - .select([ - 'r.id', - columnRef(platform, 'r.day_id').as('day_id'), - columnRef(platform, 'r.end_day_id').as('end_day_id'), - 'r.reservation_time', - 'r.reservation_end_time', - ]) + .select(['r.id', columnRef(platform, 'r.day_id').as('day_id'), columnRef(platform, 'r.end_day_id').as('end_day_id'), 'r.reservation_time', 'r.reservation_end_time']) .where({ trip: trip_id }) - .execute< - { - id: number; - day_id: number | null; - end_day_id: number | null; - reservation_time: string | null; - reservation_end_time: string | null; - }[] - >('all', false); + .execute<{ id: number; day_id: number | null; end_day_id: number | null; reservation_time: string | null; reservation_end_time: string | null }[]>('all', false); } /** DY15 — `UPDATE reservations SET reservation_time = ? WHERE id = ?`. */ @@ -732,14 +632,7 @@ export class ReservationsRepository extends TrekRepository { .case() .when('reservation_time', 'is', null) .then(eb.val(date)) - .else( - concatKysely( - platform, - eb, - { value: date }, - { expression: substringKysely(platform, eb, 'reservation_time', 11) }, - ), - ) + .else(concatKysely(platform, eb, { value: date }, { expression: substringKysely(platform, eb, 'reservation_time', 11) })) .end(), })) .where('accommodation_id', '=', accommodation_id) @@ -1087,12 +980,7 @@ export class ReservationsRepository extends TrekRepository { } /** RS20 — see {@link UpcomingReservationRow}'s docstring. */ - async listUpcomingForUser( - user_id: number, - today: string, - now_hhmm: string, - limit: number, - ): Promise { + async listUpcomingForUser(user_id: number, today: string, now_hhmm: string, limit: number): Promise { const platform = this.getEntityManager().getPlatform(); // The `COALESCE(place name, nameless-stay's-linked-booking title, trip @@ -1107,15 +995,10 @@ export class ReservationsRepository extends TrekRepository { .with('visible_trips', (qb) => qb .selectFrom('trips as t') - .leftJoin('trip_members as tm', (join) => - join.onRef('tm.trip_id', '=', 't.id').on('tm.user_id', '=', user_id), - ) + .leftJoin('trip_members as tm', (join) => join.onRef('tm.trip_id', '=', 't.id').on('tm.user_id', '=', user_id)) .select(['t.id', 't.title', 't.cover_image']) .where((eb) => - eb.and([ - eb.or([eb('t.user_id', '=', user_id), eb('tm.user_id', 'is not', null)]), - eb('t.is_archived', '=', 0), - ]), + eb.and([eb.or([eb('t.user_id', '=', user_id), eb('tm.user_id', 'is not', null)]), eb('t.is_archived', '=', 0)]), ), ) .with('entries', (qb) => { @@ -1165,8 +1048,8 @@ export class ReservationsRepository extends TrekRepository { .select((eb) => [ 'a.id as id', 'a.trip_id as trip_id', - eb.fn - .coalesce( + eb + .fn.coalesce( eb.ref('p.name'), eb .selectFrom('reservations as res') @@ -1206,8 +1089,8 @@ export class ReservationsRepository extends TrekRepository { .select((eb) => [ 'a.id as id', 'a.trip_id as trip_id', - eb.fn - .coalesce( + eb + .fn.coalesce( eb.ref('p.name'), eb .selectFrom('reservations as res') @@ -1242,21 +1125,7 @@ export class ReservationsRepository extends TrekRepository { return bookings.unionAll(checkins).unionAll(checkouts); }) .selectFrom('entries') - .select([ - 'id', - 'trip_id', - 'title', - 'type', - 'status', - 'location', - 'reservation_time', - 'confirmation_number', - 'trip_title', - 'trip_cover', - 'day_date', - 'place_name', - 'place_image', - ]) + .select(['id', 'trip_id', 'title', 'type', 'status', 'location', 'reservation_time', 'confirmation_number', 'trip_title', 'trip_cover', 'day_date', 'place_name', 'place_image']) .where('at_date', 'is not', null) .where((eb) => eb.or([ @@ -1277,18 +1146,7 @@ export class ReservationsRepository extends TrekRepository { const rows = await this.kysely() .selectFrom('reservation_endpoints as e') .innerJoin('reservations as r', 'r.id', 'e.reservation_id') - .select([ - 'e.reservation_id', - 'e.role', - 'e.sequence', - 'e.name', - 'e.code', - 'e.lat', - 'e.lng', - 'e.timezone', - 'e.local_date', - 'e.local_time', - ]) + .select(['e.reservation_id', 'e.role', 'e.sequence', 'e.name', 'e.code', 'e.lat', 'e.lng', 'e.timezone', 'e.local_date', 'e.local_time']) .where('r.trip_id', '=', trip_id as number) .orderBy('e.reservation_id', 'asc') .orderBy('e.sequence', 'asc') @@ -1312,22 +1170,9 @@ export class ReservationsRepository extends TrekRepository { const rows = await this.kysely() .selectFrom('reservations as r') .select([ - 'r.id', - 'r.trip_id', - 'r.day_id', - 'r.end_day_id', - 'r.place_id', - 'r.accommodation_id', - 'r.title', - 'r.type', - 'r.status', - 'r.location', - 'r.reservation_time', - 'r.reservation_end_time', - 'r.notes', - 'r.url', - 'r.metadata', - 'r.created_at', + 'r.id', 'r.trip_id', 'r.day_id', 'r.end_day_id', 'r.place_id', 'r.accommodation_id', + 'r.title', 'r.type', 'r.status', 'r.location', 'r.reservation_time', 'r.reservation_end_time', + 'r.notes', 'r.url', 'r.metadata', 'r.created_at', ]) .where('r.trip_id', '=', trip_id as number) .where((eb) => publicReservationExpr(eb, 'r.ingest_state')) @@ -1344,19 +1189,9 @@ export class ReservationsRepository extends TrekRepository { .where((eb) => publicStayExists(eb)) .innerJoin('places as p', 'p.id', 'a.place_id') .select([ - 'a.id', - 'a.trip_id', - 'a.place_id', - 'a.start_day_id', - 'a.end_day_id', - 'a.check_in', - 'a.check_in_end', - 'a.check_out', - 'a.notes', - 'p.name as place_name', - 'p.address as place_address', - 'p.lat as place_lat', - 'p.lng as place_lng', + 'a.id', 'a.trip_id', 'a.place_id', 'a.start_day_id', 'a.end_day_id', + 'a.check_in', 'a.check_in_end', 'a.check_out', 'a.notes', + 'p.name as place_name', 'p.address as place_address', 'p.lat as place_lat', 'p.lng as place_lng', ]) .execute(); return rows as SharePublicAccommodationRow[]; @@ -1412,17 +1247,7 @@ export class ReservationsRepository extends TrekRepository { .leftJoin('places as p', 'p.id', 'a.place_id') .leftJoin('days as ds', 'ds.id', 'a.start_day_id') .leftJoin('days as de', 'de.id', 'a.end_day_id') - .select([ - 'p.name', - 'p.address', - 'p.lat', - 'p.lng', - 'ds.date as start_date', - 'de.date as end_date', - 'a.check_in', - 'a.check_out', - 'a.notes', - ]) + .select(['p.name', 'p.address', 'p.lat', 'p.lng', 'ds.date as start_date', 'de.date as end_date', 'a.check_in', 'a.check_out', 'a.notes']) .where('a.trip_id', '=', trip_id) .orderBy('ds.date', 'asc') .execute(); @@ -1435,24 +1260,12 @@ export class ReservationsRepository extends TrekRepository { .selectFrom('places as p') .leftJoin('categories as c', 'c.id', 'p.category_id') .select((eb) => [ - 'p.name', - 'p.address', - 'p.lat', - 'p.lng', - 'p.place_time', - 'p.end_time', - 'p.duration_minutes', - 'p.notes', - 'p.transport_mode', - 'c.name as category', + 'p.name', 'p.address', 'p.lat', 'p.lng', 'p.place_time', 'p.end_time', + 'p.duration_minutes', 'p.notes', 'p.transport_mode', 'c.name as category', ]) .where('p.trip_id', '=', trip_id) - .where((eb) => - eb.not(eb.exists(eb.selectFrom('day_assignments as da').select('da.id').whereRef('da.place_id', '=', 'p.id'))), - ) - .where((eb) => - eb.not(eb.exists(eb.selectFrom('day_accommodations as a').select('a.id').whereRef('a.place_id', '=', 'p.id'))), - ) + .where((eb) => eb.not(eb.exists(eb.selectFrom('day_assignments as da').select('da.id').whereRef('da.place_id', '=', 'p.id')))) + .where((eb) => eb.not(eb.exists(eb.selectFrom('day_accommodations as a').select('a.id').whereRef('a.place_id', '=', 'p.id')))) .orderBy('p.created_at', 'asc') .orderBy('p.id', 'asc') .execute(); @@ -1492,10 +1305,7 @@ export class ReservationsRepository extends TrekRepository { // --------------------------------------------------------------------------- /** BG54/BG85 — `SELECT id, metadata FROM reservations WHERE id = ? AND trip_id = ?`. */ - async getIdAndMetadata( - id: number | string, - trip_id: number | string, - ): Promise<{ id: number; metadata: string | null } | undefined> { + async getIdAndMetadata(id: number | string, trip_id: number | string): Promise<{ id: number; metadata: string | null } | undefined> { return await this.kysely>() .selectFrom('reservations') .select(['id', 'metadata']) @@ -1542,9 +1352,7 @@ export class ReservationsRepository extends TrekRepository { } /** ATI2 (`airtrail-import.service.ts#importAirtrailFlights`, existing-flight signature dedup) — `SELECT r.id, r.reservation_time, r.metadata FROM reservations r WHERE r.trip_id = ? AND r.type = 'flight'`. */ - async listFlightReservationsForTrip( - trip_id: number, - ): Promise<{ id: number; reservation_time: string | null; metadata: string | null }[]> { + async listFlightReservationsForTrip(trip_id: number): Promise<{ id: number; reservation_time: string | null; metadata: string | null }[]> { return this.qb('r') .select(['r.id', 'r.reservation_time', 'r.metadata']) .where({ trip: trip_id, type: 'flight' }) @@ -1562,16 +1370,8 @@ export class ReservationsRepository extends TrekRepository { * untracked reservation. Preserved exactly, not "fixed" with an added * `uow.transactional` wrap. */ - async linkAirtrailMultiLeg( - id: number, - external_id: string, - external_owner_user_id: number, - external_synced_at: string, - ): Promise { - await this.nativeUpdate( - { id }, - { external_source: 'airtrail', external_id, external_owner_user_id, sync_enabled: 0, external_synced_at }, - ); + async linkAirtrailMultiLeg(id: number, external_id: string, external_owner_user_id: number, external_synced_at: string): Promise { + await this.nativeUpdate({ id }, { external_source: 'airtrail', external_id, external_owner_user_id, sync_enabled: 0, external_synced_at }); } /** @@ -1581,24 +1381,8 @@ export class ReservationsRepository extends TrekRepository { * external_hash = ?, external_synced_at = ? WHERE id = ?`. Same * pre-existing tx gap as {@link linkAirtrailMultiLeg} — report don't fix. */ - async linkAirtrailSingleFlight( - id: number, - external_id: string, - external_owner_user_id: number, - external_hash: string, - external_synced_at: string, - ): Promise { - await this.nativeUpdate( - { id }, - { - external_source: 'airtrail', - external_id, - external_owner_user_id, - sync_enabled: 1, - external_hash, - external_synced_at, - }, - ); + async linkAirtrailSingleFlight(id: number, external_id: string, external_owner_user_id: number, external_hash: string, external_synced_at: string): Promise { + await this.nativeUpdate({ id }, { external_source: 'airtrail', external_id, external_owner_user_id, sync_enabled: 1, external_hash, external_synced_at }); } /** ATL2 (`airtrail-link.service.ts#detach`) — `UPDATE reservations SET sync_enabled = 0 WHERE id = ?`. */ @@ -1613,36 +1397,12 @@ export class ReservationsRepository extends TrekRepository { * via `columnRef` (the `listResyncCandidates` precedent above) — the * mirror column, not a bare select. */ - async findAirtrailLinked(id: number): Promise< - | { - id: number; - trip_id: number; - external_id: string | null; - external_owner_user_id: number | null; - sync_enabled: number | null; - } - | undefined - > { + async findAirtrailLinked(id: number): Promise<{ id: number; trip_id: number; external_id: string | null; external_owner_user_id: number | null; sync_enabled: number | null } | undefined> { const platform = this.getEntityManager().getPlatform(); return this.qb('r') - .select([ - 'r.id', - columnRef(platform, 'r.trip_id').as('trip_id'), - 'r.external_id', - 'r.external_owner_user_id', - 'r.sync_enabled', - ]) + .select(['r.id', columnRef(platform, 'r.trip_id').as('trip_id'), 'r.external_id', 'r.external_owner_user_id', 'r.sync_enabled']) .where({ id, external_source: 'airtrail' }) - .execute< - | { - id: number; - trip_id: number; - external_id: string | null; - external_owner_user_id: number | null; - sync_enabled: number | null; - } - | undefined - >('get', false); + .execute<{ id: number; trip_id: number; external_id: string | null; external_owner_user_id: number | null; sync_enabled: number | null } | undefined>('get', false); } /** @@ -1660,17 +1420,12 @@ export class ReservationsRepository extends TrekRepository { * external_id, external_hash FROM reservations WHERE external_source = * 'airtrail' AND sync_enabled = 1 AND external_owner_user_id = ?`. */ - async listAirtrailSyncCandidatesForOwner( - owner_user_id: number, - ): Promise<{ id: number; trip_id: number; external_id: string | null; external_hash: string | null }[]> { + async listAirtrailSyncCandidatesForOwner(owner_user_id: number): Promise<{ id: number; trip_id: number; external_id: string | null; external_hash: string | null }[]> { const platform = this.getEntityManager().getPlatform(); return this.qb('r') .select(['r.id', columnRef(platform, 'r.trip_id').as('trip_id'), 'r.external_id', 'r.external_hash']) .where({ external_source: 'airtrail', sync_enabled: 1, external_owner_user_id: owner_user_id }) - .execute<{ id: number; trip_id: number; external_id: string | null; external_hash: string | null }[]>( - 'all', - false, - ); + .execute<{ id: number; trip_id: number; external_id: string | null; external_hash: string | null }[]>('all', false); } /** @@ -1697,20 +1452,12 @@ export class ReservationsRepository extends TrekRepository { * EXISTS guard, same shape as `Reservations.repository.ts`'s own * `listUnplannedPlacesForPublicApi` NOT EXISTS precedent (`eb.not(eb.exists(...))`). */ - async listFlightsMissingEndpoints(): Promise< - { id: number; metadata: string | null; reservation_time: string | null; reservation_end_time: string | null }[] - > { + async listFlightsMissingEndpoints(): Promise<{ id: number; metadata: string | null; reservation_time: string | null; reservation_end_time: string | null }[]> { const rows = await this.kysely() .selectFrom('reservations as r') .select(['r.id', 'r.metadata', 'r.reservation_time', 'r.reservation_end_time']) .where('r.type', '=', 'flight') - .where((eb) => - eb.not( - eb.exists( - eb.selectFrom('reservation_endpoints as e').select('e.id').whereRef('e.reservation_id', '=', 'r.id'), - ), - ), - ) + .where((eb) => eb.not(eb.exists(eb.selectFrom('reservation_endpoints as e').select('e.id').whereRef('e.reservation_id', '=', 'r.id')))) .execute(); return rows; } @@ -1723,17 +1470,18 @@ export class ReservationsRepository extends TrekRepository { /** RPL4/RPL5's shared select list, the columns of `RoadtripCarrierRow`. */ private roadtripCarrierQuery() { const platform = this.getEntityManager().getPlatform(); - return this.qb('r').select([ - 'r.id', - 'r.type', - 'r.title', - columnRef(platform, 'r.day_id').as('day_id'), - columnRef(platform, 'r.end_day_id').as('end_day_id'), - 'r.reservation_time', - 'r.reservation_end_time', - 'r.metadata', - 'r.day_plan_position', - ]); + return this.qb('r') + .select([ + 'r.id', + 'r.type', + 'r.title', + columnRef(platform, 'r.day_id').as('day_id'), + columnRef(platform, 'r.end_day_id').as('end_day_id'), + 'r.reservation_time', + 'r.reservation_end_time', + 'r.metadata', + 'r.day_plan_position', + ]); } /** @@ -1756,7 +1504,9 @@ export class ReservationsRepository extends TrekRepository { * NULL`. Which of them count as rides is `undatedRides` (shared) alone. */ async listRoadtripUndated(trip_id: number): Promise { - return this.roadtripCarrierQuery().where({ trip: trip_id, day: null }).execute('all', false); + return this.roadtripCarrierQuery() + .where({ trip: trip_id, day: null }) + .execute('all', false); } } diff --git a/server/src/db/repositories/_shared/reservation-pass-context.ts b/server/src/db/repositories/_shared/reservation-pass-context.ts new file mode 100644 index 000000000..05da299e4 --- /dev/null +++ b/server/src/db/repositories/_shared/reservation-pass-context.ts @@ -0,0 +1,46 @@ +import type { DB } from '../../kysely/db'; + +import type { Kysely } from 'kysely'; + +/** The tables `passContextQuery` reads. */ +export type PassContextDB = Pick; + +/** What a booking's Wallet pass needs beyond the booking: where it is, and the trip's name. */ +export interface ReservationPassContext { + trip_title: string | null; + place_lat: number | null; + place_lng: number | null; + place_name: string | null; +} + +/** + * The trip's title and the place a booking is at (its own place, else its + * stay's), for its Apple Wallet pass (`ReservationsRepository.findPassContext`). + * Scoped to the trip, so a booking of another trip reads as none. Kept out of + * the repository file, which is at its size baseline. + */ +export async function passContextQuery( + kysely: Kysely, + id: number, + trip_id: number, +): Promise { + const row = await kysely + .selectFrom('reservations as r') + .innerJoin('trips as t', 't.id', 'r.trip_id') + .leftJoin('places as p', 'p.id', 'r.place_id') + // accommodation_id is TEXT; compared as a number so Postgres takes it as well as SQLite ("14" and "14.0" alike). + .leftJoin('day_accommodations as ap', (join) => + join.on((eb) => eb(eb.cast('r.accommodation_id', 'numeric'), '=', eb.ref('ap.id'))), + ) + .leftJoin('places as acc_p', 'acc_p.id', 'ap.place_id') + .where('r.id', '=', id) + .where('r.trip_id', '=', trip_id) + .select([ + 't.title as trip_title', + (eb) => eb.fn.coalesce('p.lat', 'acc_p.lat').as('place_lat'), + (eb) => eb.fn.coalesce('p.lng', 'acc_p.lng').as('place_lng'), + (eb) => eb.fn.coalesce('p.name', 'acc_p.name').as('place_name'), + ]) + .executeTakeFirst(); + return row as ReservationPassContext | undefined; +} diff --git a/server/src/nest/reservations/reservations-read.service.ts b/server/src/nest/reservations/reservations-read.service.ts index b2060a60a..79e1976d1 100644 --- a/server/src/nest/reservations/reservations-read.service.ts +++ b/server/src/nest/reservations/reservations-read.service.ts @@ -5,7 +5,8 @@ import { Reservations } from '../../db/entities/Reservations.entity'; import type { ReservationDayPositionsRepository } from '../../db/repositories/ReservationDayPositions.repository'; import type { ReservationEndpointsRepository } from '../../db/repositories/ReservationEndpoints.repository'; import type { ReservationTravelersRepository } from '../../db/repositories/ReservationTravelers.repository'; -import type { ReservationPassContext, ReservationsRepository } from '../../db/repositories/Reservations.repository'; +import type { ReservationsRepository } from '../../db/repositories/Reservations.repository'; +import type { ReservationPassContext } from '../../db/repositories/_shared/reservation-pass-context'; import { avatarUrl } from '../common/avatarUrl'; import type { ReservationRow, ReservationEndpoint, ReservationTraveler } from './reservations.service'; import { InjectRepository } from '@mikro-orm/nestjs';