mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 16:03:02 +00:00
Merge dev into fix/prettier
Brings in the Tours addon and the MikroORM follow-ups. Conflicts were resolved to dev's content, then every file dev brought in was run through prettier and eslint --fix so the branch stays uniformly formatted.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { coalesce, columnIncrementedBy, columnRef } from '../dialect/sql-functions';
|
||||
import { caseWhenNotNull, coalesce, columnIncrementedBy, columnRef } from '../dialect/sql-functions';
|
||||
import type { DayAssignments } from '../entities/DayAssignments.entity';
|
||||
import { type AssertRowKeys } from './_shared/rows';
|
||||
import { TrekRepository } from './_shared/trek-repository';
|
||||
@@ -16,9 +16,12 @@ import type { Platform } from '@mikro-orm/core';
|
||||
* p.duration_minutes, p.notes as place_notes,
|
||||
* p.image_url, p.transport_mode, p.google_place_id, p.google_ftid, p.osm_id, p.amap_poi_id,
|
||||
* p.website, p.phone, p.stop_type, p.fill_percent,
|
||||
* t.place_id AS tour_place_id,
|
||||
* CASE WHEN t.place_id IS NOT NULL THEN p.route_geometry END AS tour_route_geometry,
|
||||
* c.name as category_name, c.color as category_color, c.icon as category_icon
|
||||
* FROM day_assignments da
|
||||
* JOIN places p ON da.place_id = p.id
|
||||
* LEFT JOIN tours t ON t.place_id = p.id
|
||||
* LEFT JOIN categories c ON p.category_id = c.id
|
||||
* WHERE <da.id = ? | da.day_id = ? | da.day_id IN (...)>
|
||||
* [ORDER BY da.order_index ASC, da.created_at ASC]
|
||||
@@ -58,6 +61,10 @@ export interface AssignmentWithPlaceRow extends DayAssignmentRow {
|
||||
phone: string | null;
|
||||
stop_type: string | null;
|
||||
fill_percent: number | null;
|
||||
/** The place's id when it is a Tour (its `tours` facet row), else null. */
|
||||
tour_place_id: number | null;
|
||||
/** The place's `route_geometry`, but only for a Tour; null for every other place. */
|
||||
tour_route_geometry: string | null;
|
||||
category_name: string | null;
|
||||
category_color: string | null;
|
||||
category_icon: string | null;
|
||||
@@ -66,11 +73,19 @@ export interface AssignmentWithPlaceRow extends DayAssignmentRow {
|
||||
/**
|
||||
* {@link DayAssignmentsRepository.listPublicForShare}'s projection (SH9) —
|
||||
* {@link AssignmentWithPlaceRow} minus the six columns a public share link
|
||||
* must not leak.
|
||||
* must not leak, and minus the two Tour facet columns the share projection
|
||||
* never selected.
|
||||
*/
|
||||
export type SharePublicAssignmentRow = Omit<
|
||||
AssignmentWithPlaceRow,
|
||||
'google_place_id' | 'google_ftid' | 'osm_id' | 'amap_poi_id' | 'stop_type' | 'fill_percent'
|
||||
| 'google_place_id'
|
||||
| 'google_ftid'
|
||||
| 'osm_id'
|
||||
| 'amap_poi_id'
|
||||
| 'stop_type'
|
||||
| 'fill_percent'
|
||||
| 'tour_place_id'
|
||||
| 'tour_route_geometry'
|
||||
>;
|
||||
|
||||
/**
|
||||
@@ -266,6 +281,10 @@ export class DayAssignmentsRepository extends TrekRepository<DayAssignments> {
|
||||
'p.phone',
|
||||
'p.stop_type',
|
||||
'p.fill_percent',
|
||||
// `place_id` on `Tours` is the `persist(false)` twin of its primary-key
|
||||
// relation, the same trap as `category_id` above, so `columnRef` again.
|
||||
columnRef(platform, 't.place_id').as('tour_place_id'),
|
||||
caseWhenNotNull(platform, 't.place_id', 'p.route_geometry').as('tour_route_geometry'),
|
||||
'c.name as category_name',
|
||||
'c.color as category_color',
|
||||
'c.icon as category_icon',
|
||||
@@ -280,6 +299,7 @@ export class DayAssignmentsRepository extends TrekRepository<DayAssignments> {
|
||||
const platform = this.getEntityManager().getPlatform();
|
||||
const row = await this.qb('da')
|
||||
.join('da.place', 'p')
|
||||
.leftJoin('p.tours', 't')
|
||||
.leftJoin('p.category', 'c')
|
||||
.select(this.assignmentWithPlaceSelect(platform))
|
||||
.where({ 'da.id': id })
|
||||
@@ -296,6 +316,7 @@ export class DayAssignmentsRepository extends TrekRepository<DayAssignments> {
|
||||
const platform = this.getEntityManager().getPlatform();
|
||||
return await this.qb('da')
|
||||
.join('da.place', 'p')
|
||||
.leftJoin('p.tours', 't')
|
||||
.leftJoin('p.category', 'c')
|
||||
.select(this.assignmentWithPlaceSelect(platform))
|
||||
.where({ 'da.day': day_id })
|
||||
@@ -313,6 +334,7 @@ export class DayAssignmentsRepository extends TrekRepository<DayAssignments> {
|
||||
const platform = this.getEntityManager().getPlatform();
|
||||
return await this.qb('da')
|
||||
.join('da.place', 'p')
|
||||
.leftJoin('p.tours', 't')
|
||||
.leftJoin('p.category', 'c')
|
||||
.select(this.assignmentWithPlaceSelect(platform))
|
||||
.where({ 'da.day': { $in: day_ids } })
|
||||
@@ -516,6 +538,20 @@ export class DayAssignmentsRepository extends TrekRepository<DayAssignments> {
|
||||
return row?.day_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* `moveAssignment`'s Tour check: `SELECT place_id FROM day_assignments
|
||||
* WHERE id = ?`, `columnRef` for the same `persist(false)` reason as
|
||||
* {@link getDayId} above.
|
||||
*/
|
||||
async getPlaceId(id: number): Promise<number | undefined> {
|
||||
const platform = this.getEntityManager().getPlatform();
|
||||
const row = await this.qb('da')
|
||||
.select([columnRef(platform, 'da.place_id').as('place_id')])
|
||||
.where({ id })
|
||||
.execute<{ place_id: number } | undefined>('get', false);
|
||||
return row?.place_id;
|
||||
}
|
||||
|
||||
/** AS14 — `UPDATE day_assignments SET day_id = ?, order_index = ? WHERE id = ?`. */
|
||||
async moveToDay(id: number, day_id: number, order_index: number): Promise<void> {
|
||||
await this.nativeUpdate({ id }, { day: day_id, order_index });
|
||||
|
||||
@@ -59,6 +59,8 @@ export interface PlaceWithCategoryRow extends PlaceRow {
|
||||
export interface PlaceListRow extends PlaceWithCategoryRow {
|
||||
country_code: string | null;
|
||||
region_name: string | null;
|
||||
/** The place's own id when it is a Tour (it has a `tours` facet row), else null. */
|
||||
tour_place_id: number | null;
|
||||
}
|
||||
|
||||
/** {@link PlacesRepository.findActiveTripFile}'s narrow `trip_files` shape. */
|
||||
@@ -344,6 +346,22 @@ export class PlacesRepository extends TrekRepository<Places> {
|
||||
.execute<{ google_place_id: string | null; image_url: string | null } | undefined>('get', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* `SELECT 1 FROM tours WHERE place_id = ?`, read through the place's
|
||||
* `tours` facet so the Places domain answers it without the Tours
|
||||
* repository. `remove`/`removeMany` call it inside their delete
|
||||
* transaction to report which deleted places were Tours; the facet row and
|
||||
* its waypoints go with the place through `ON DELETE CASCADE`.
|
||||
*/
|
||||
async isTour(id: number): Promise<boolean> {
|
||||
const row = await this.qb('p')
|
||||
.join('p.tours', 't')
|
||||
.select(['p.id'])
|
||||
.where({ 'p.id': id })
|
||||
.execute<{ id: number } | undefined>('get', false);
|
||||
return !!row;
|
||||
}
|
||||
|
||||
/** PL19/PL21 — `DELETE FROM places WHERE id = ?`, unscoped: every caller proved trip access first. */
|
||||
async deleteById(id: number): Promise<void> {
|
||||
await this.nativeDelete({ id });
|
||||
@@ -741,13 +759,19 @@ export class PlacesRepository extends TrekRepository<Places> {
|
||||
assignment?: 'all' | 'unassigned' | 'assigned';
|
||||
},
|
||||
): Promise<PlaceListRow[]> {
|
||||
const platform = this.getEntityManager().getPlatform();
|
||||
const qb = this.qb('p')
|
||||
.leftJoin('p.category', 'c')
|
||||
// #2537: the atlas' cached region rides along (`LEFT JOIN place_regions pr ON pr.place_id = p.id`).
|
||||
.leftJoin('p.place_regions', 'pr')
|
||||
// Tours: `LEFT JOIN tours t ON t.place_id = p.id` marks the Tour-backed
|
||||
// places. `columnRef`, because the bare `t.place_id` twin is
|
||||
// persist(false) and selects nothing.
|
||||
.leftJoin('p.tours', 't')
|
||||
.select(
|
||||
[
|
||||
'p.*',
|
||||
columnRef(platform, 't.place_id').as('tour_place_id'),
|
||||
'c.name as category_name',
|
||||
'c.color as category_color',
|
||||
'c.icon as category_icon',
|
||||
@@ -1455,6 +1479,56 @@ export class PlacesRepository extends TrekRepository<Places> {
|
||||
.execute();
|
||||
return rows as PublicApiAssignedPlaceRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* TO10 (`ToursService.createTour`) — `INSERT INTO places (trip_id, name,
|
||||
* lat, lng, transport_mode, route_geometry) VALUES (?, ?, ?, ?, 'walking',
|
||||
* ?)`. The place a drawn tour lives on starts at the route's first point;
|
||||
* every other column keeps its default. Returns the generated id.
|
||||
*/
|
||||
async insertTourPlace(input: {
|
||||
trip_id: number;
|
||||
name: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
route_geometry: string;
|
||||
}): Promise<number> {
|
||||
return await this.insert({
|
||||
trip: input.trip_id,
|
||||
name: input.name,
|
||||
lat: input.lat,
|
||||
lng: input.lng,
|
||||
transport_mode: 'walking',
|
||||
route_geometry: input.route_geometry,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* TO11 (`ToursService.updateTour`) — `UPDATE places SET name = ?, lat = ?,
|
||||
* lng = ?, transport_mode = 'walking', route_geometry = ?, updated_at =
|
||||
* CURRENT_TIMESTAMP WHERE id = ? AND trip_id = ?`. Returns whether a place
|
||||
* of that trip was updated, so a tour deleted or moved between the check
|
||||
* and the write answers 404 instead of writing nothing.
|
||||
*/
|
||||
async updateTourRoute(
|
||||
id: number,
|
||||
trip_id: number,
|
||||
input: { name: string; lat: number; lng: number; route_geometry: string },
|
||||
): Promise<boolean> {
|
||||
const platform = this.getEntityManager().getPlatform();
|
||||
const updated = await this.nativeUpdate(
|
||||
{ id, trip: trip_id },
|
||||
{
|
||||
name: input.name,
|
||||
lat: input.lat,
|
||||
lng: input.lng,
|
||||
transport_mode: 'walking',
|
||||
route_geometry: input.route_geometry,
|
||||
updated_at: currentTimestamp(platform),
|
||||
},
|
||||
);
|
||||
return updated > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** The `places` columns {@link PlacesRepository.listAssignedForPublicApi} selects, typed off {@link PlaceRow}. */
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { TourTypes } from '../entities/TourTypes.entity';
|
||||
import { TrekRepository } from './_shared/trek-repository';
|
||||
|
||||
/**
|
||||
* `tour_types` — the controlled vocabulary `tours.tour_type` points at. Only
|
||||
* `hike` exists, written by the migration that creates the table; nothing
|
||||
* reads the catalogue yet, so this repository has no methods of its own.
|
||||
*/
|
||||
export class TourTypesRepository extends TrekRepository<TourTypes> {}
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { TourWaypoints } from '../entities/TourWaypoints.entity';
|
||||
import { TrekRepository } from './_shared/trek-repository';
|
||||
|
||||
/** One saved routing control point of a tour, in route order. */
|
||||
export interface TourWaypointRow {
|
||||
lat: number;
|
||||
lng: number;
|
||||
role: 'start' | 'via' | 'end';
|
||||
sequence: number;
|
||||
}
|
||||
|
||||
/** {@link TourWaypointRow} plus the tour it belongs to, as the trip copy reads it. */
|
||||
export interface TourWaypointWithPlaceRow extends TourWaypointRow {
|
||||
place_id: number;
|
||||
}
|
||||
|
||||
interface TourWaypointsKyselyDB {
|
||||
tour_waypoints: {
|
||||
id: number;
|
||||
place_id: number;
|
||||
lat: number;
|
||||
lng: number;
|
||||
role: TourWaypointRow['role'];
|
||||
sequence: number;
|
||||
};
|
||||
places: { id: number; trip_id: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* `tour_waypoints` — the start, via and end points the route editor saved
|
||||
* for a tour, so reopening it restores the controls rather than only the
|
||||
* derived line. Rows hang off `tours(place_id)` and go with the tour.
|
||||
*/
|
||||
export class TourWaypointsRepository extends TrekRepository<TourWaypoints> {
|
||||
private db() {
|
||||
return this.kysely<TourWaypointsKyselyDB>();
|
||||
}
|
||||
|
||||
/** TW1 — `SELECT lat, lng, role, sequence FROM tour_waypoints WHERE place_id = ? ORDER BY sequence`. */
|
||||
async listForPlace(place_id: number): Promise<TourWaypointRow[]> {
|
||||
return await this.db()
|
||||
.selectFrom('tour_waypoints')
|
||||
.select(['lat', 'lng', 'role', 'sequence'])
|
||||
.where('place_id', '=', place_id)
|
||||
.orderBy('sequence', 'asc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
/** TW2 — every waypoint of a trip's tours, for the trip copy to remap by place. */
|
||||
async listForTrip(trip_id: number): Promise<TourWaypointWithPlaceRow[]> {
|
||||
return await this.db()
|
||||
.selectFrom('tour_waypoints as tw')
|
||||
.innerJoin('places as p', 'p.id', 'tw.place_id')
|
||||
.select(['tw.place_id', 'tw.lat', 'tw.lng', 'tw.role', 'tw.sequence'])
|
||||
.where('p.trip_id', '=', trip_id)
|
||||
.orderBy('tw.place_id', 'asc')
|
||||
.orderBy('tw.sequence', 'asc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* TW3 — `INSERT INTO tour_waypoints (place_id, lat, lng, role, sequence)
|
||||
* VALUES (...)` for each point. The caller owns the transaction: a create
|
||||
* writes the tour first, an edit clears the old points first.
|
||||
*/
|
||||
async insertForPlace(place_id: number, waypoints: readonly TourWaypointRow[]): Promise<void> {
|
||||
if (waypoints.length === 0) return;
|
||||
await this.insertMany(
|
||||
waypoints.map((w) => ({ place: place_id, lat: w.lat, lng: w.lng, role: w.role, sequence: w.sequence })),
|
||||
);
|
||||
}
|
||||
|
||||
/** TW4 — `DELETE FROM tour_waypoints WHERE place_id = ?`. */
|
||||
async deleteForPlace(place_id: number): Promise<void> {
|
||||
await this.nativeDelete({ place: place_id });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import type { Tours } from '../entities/Tours.entity';
|
||||
import type { AssertRowKeys } from './_shared/rows';
|
||||
import { TrekRepository } from './_shared/trek-repository';
|
||||
|
||||
/**
|
||||
* A tour as the Tours list and detail read it: the `tours` facet joined with
|
||||
* its owning place's name, plus two read-model flags (`planned`: the place
|
||||
* sits on at least one day; `has_waypoints`: the route editor saved control
|
||||
* points for it). SQLite hands both flags back as 0/1.
|
||||
*/
|
||||
export interface TourListRow {
|
||||
place_id: number;
|
||||
name: string;
|
||||
tour_type: string;
|
||||
distance: number | null;
|
||||
elevation_gain: number | null;
|
||||
elevation_loss: number | null;
|
||||
duration: number | null;
|
||||
difficulty: string | null;
|
||||
wanderer_ref: string | null;
|
||||
match_confidence: number | null;
|
||||
tour_group_id: number | null;
|
||||
max_hiking_difficulty: number;
|
||||
planned: number;
|
||||
has_waypoints: number;
|
||||
}
|
||||
|
||||
/** Every `tours` column, as the trip copy reads and re-inserts it. */
|
||||
export interface TourRow {
|
||||
place_id: number;
|
||||
tour_type: string;
|
||||
distance: number | null;
|
||||
elevation_gain: number | null;
|
||||
elevation_loss: number | null;
|
||||
duration: number | null;
|
||||
difficulty: string | null;
|
||||
wanderer_ref: string | null;
|
||||
match_confidence: number | null;
|
||||
tour_group_id: number | null;
|
||||
created_at: string | null;
|
||||
max_hiking_difficulty: number;
|
||||
}
|
||||
const _tourRowKeys: AssertRowKeys<TourRow, Tours> = true;
|
||||
|
||||
/** What a create or a GPX import writes; the unset metadata columns stay NULL. */
|
||||
export interface TourInsert {
|
||||
place_id: number;
|
||||
tour_type: string;
|
||||
distance: number | null;
|
||||
elevation_gain: number | null;
|
||||
elevation_loss: number | null;
|
||||
duration: number | null;
|
||||
match_confidence: number | null;
|
||||
max_hiking_difficulty: number;
|
||||
}
|
||||
|
||||
/** What a route edit replaces on an existing tour. */
|
||||
export type TourUpdate = Omit<TourInsert, 'place_id'>;
|
||||
|
||||
interface ToursKyselyDB {
|
||||
tours: TourRow;
|
||||
places: { id: number; trip_id: number; name: string };
|
||||
day_assignments: { id: number; day_id: number; place_id: number };
|
||||
tour_waypoints: { id: number; place_id: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* `tours` — the facet that makes a place a tour (#2586). Keyed on the owning
|
||||
* place, so it carries no trip of its own: every trip-scoped statement here
|
||||
* joins `places` for the predicate. Deleting the place cascades the row and
|
||||
* its `tour_waypoints`.
|
||||
*
|
||||
* Kysely throughout: the list projection needs two correlated `EXISTS`
|
||||
* subqueries, and the trip-scoped update and the day check join tables the
|
||||
* entity has no relation path to.
|
||||
*/
|
||||
export class ToursRepository extends TrekRepository<Tours> {
|
||||
private db() {
|
||||
return this.kysely<ToursKyselyDB>();
|
||||
}
|
||||
|
||||
/** The shared SELECT of {@link listForTrip} and {@link findInTrip}. */
|
||||
private listQuery() {
|
||||
return this.db()
|
||||
.selectFrom('tours as t')
|
||||
.innerJoin('places as p', 'p.id', 't.place_id')
|
||||
.select((eb) => [
|
||||
'p.id as place_id',
|
||||
'p.name as name',
|
||||
't.tour_type as tour_type',
|
||||
't.distance as distance',
|
||||
't.elevation_gain as elevation_gain',
|
||||
't.elevation_loss as elevation_loss',
|
||||
't.duration as duration',
|
||||
't.difficulty as difficulty',
|
||||
't.wanderer_ref as wanderer_ref',
|
||||
't.match_confidence as match_confidence',
|
||||
't.tour_group_id as tour_group_id',
|
||||
't.max_hiking_difficulty as max_hiking_difficulty',
|
||||
eb
|
||||
.exists(eb.selectFrom('day_assignments as da').select('da.id').whereRef('da.place_id', '=', 'p.id'))
|
||||
.as('planned'),
|
||||
eb
|
||||
.exists(eb.selectFrom('tour_waypoints as tw').select('tw.id').whereRef('tw.place_id', '=', 'p.id'))
|
||||
.as('has_waypoints'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* TO1 — `SELECT p.id AS place_id, p.name, t.*, EXISTS(day_assignments) AS
|
||||
* planned, EXISTS(tour_waypoints) AS has_waypoints FROM tours t JOIN places
|
||||
* p ON p.id = t.place_id WHERE p.trip_id = ? ORDER BY t.created_at DESC`,
|
||||
* newest first. The place id breaks ties inside one CURRENT_TIMESTAMP second.
|
||||
*/
|
||||
async listForTrip(trip_id: number): Promise<TourListRow[]> {
|
||||
const rows = await this.listQuery()
|
||||
.where('p.trip_id', '=', trip_id)
|
||||
.orderBy('t.created_at', 'desc')
|
||||
.orderBy('t.place_id', 'desc')
|
||||
.execute();
|
||||
return rows.map(toListRow);
|
||||
}
|
||||
|
||||
/** TO2 — {@link listForTrip}'s projection for one place, `undefined` unless it is a tour of that trip. */
|
||||
async findInTrip(trip_id: number, place_id: number): Promise<TourListRow | undefined> {
|
||||
const row = await this.listQuery().where('p.trip_id', '=', trip_id).where('p.id', '=', place_id).executeTakeFirst();
|
||||
return row ? toListRow(row) : undefined;
|
||||
}
|
||||
|
||||
/** TO3 — `SELECT 1 FROM tours WHERE place_id = ?`: whether the place is a tour. */
|
||||
async existsForPlace(place_id: number): Promise<boolean> {
|
||||
const row = await this.db()
|
||||
.selectFrom('tours')
|
||||
.select('place_id')
|
||||
.where('place_id', '=', place_id)
|
||||
.executeTakeFirst();
|
||||
return !!row;
|
||||
}
|
||||
|
||||
/** TO4 — `SELECT place_id FROM tours WHERE place_id IN (...)`: which of these places are tours. */
|
||||
async listTourPlaceIds(place_ids: number[]): Promise<number[]> {
|
||||
if (place_ids.length === 0) return [];
|
||||
const rows = await this.db().selectFrom('tours').select('place_id').where('place_id', 'in', place_ids).execute();
|
||||
return rows.map((r) => r.place_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* TO5 — `SELECT 1 FROM day_assignments da JOIN tours t ON t.place_id =
|
||||
* da.place_id WHERE da.day_id = ? AND da.place_id = ? [AND da.id != ?]`.
|
||||
*
|
||||
* A tour fills its day, so one day holds it at most once. Create and move
|
||||
* both ask this inside the transaction that writes the assignment; move
|
||||
* passes the assignment being moved so it does not collide with itself.
|
||||
* False for an ordinary place, which may sit on a day any number of times.
|
||||
*/
|
||||
async isOnDay(day_id: number, place_id: number, except_assignment_id?: number): Promise<boolean> {
|
||||
let query = this.db()
|
||||
.selectFrom('day_assignments as da')
|
||||
.innerJoin('tours as t', 't.place_id', 'da.place_id')
|
||||
.select('da.id')
|
||||
.where('da.day_id', '=', day_id)
|
||||
.where('da.place_id', '=', place_id);
|
||||
if (except_assignment_id !== undefined) query = query.where('da.id', '!=', except_assignment_id);
|
||||
return !!(await query.executeTakeFirst());
|
||||
}
|
||||
|
||||
/** TO6 — every `tours` row of a trip's places, for the trip copy to remap. */
|
||||
async listRowsForTrip(trip_id: number): Promise<TourRow[]> {
|
||||
return await this.db()
|
||||
.selectFrom('tours as t')
|
||||
.innerJoin('places as p', 'p.id', 't.place_id')
|
||||
.selectAll('t')
|
||||
.where('p.trip_id', '=', trip_id)
|
||||
.orderBy('t.place_id', 'asc')
|
||||
.execute();
|
||||
}
|
||||
|
||||
/** TO7 — `INSERT INTO tours (place_id, tour_type, distance, ...) VALUES (...)`. */
|
||||
async insertTour(input: TourInsert): Promise<void> {
|
||||
await this.insert({
|
||||
place: input.place_id,
|
||||
tourTypeRef: input.tour_type,
|
||||
distance: input.distance,
|
||||
elevation_gain: input.elevation_gain,
|
||||
elevation_loss: input.elevation_loss,
|
||||
duration: input.duration,
|
||||
match_confidence: input.match_confidence,
|
||||
max_hiking_difficulty: input.max_hiking_difficulty,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* TO8 — a copied tour keeps every column, `created_at` included, so the
|
||||
* copy lists its tours in the source's order.
|
||||
*/
|
||||
async insertCopy(place_id: number, row: Omit<TourRow, 'place_id'>): Promise<void> {
|
||||
await this.db()
|
||||
.insertInto('tours')
|
||||
.values({ ...row, place_id })
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* TO9 — `UPDATE tours SET tour_type = ?, distance = ?, ... WHERE place_id =
|
||||
* ? AND place_id IN (SELECT id FROM places WHERE trip_id = ?)`. Returns
|
||||
* whether a row of that trip was updated.
|
||||
*/
|
||||
async updateInTrip(trip_id: number, place_id: number, input: TourUpdate): Promise<boolean> {
|
||||
const result = await this.db()
|
||||
.updateTable('tours')
|
||||
.set({
|
||||
tour_type: input.tour_type,
|
||||
distance: input.distance,
|
||||
elevation_gain: input.elevation_gain,
|
||||
elevation_loss: input.elevation_loss,
|
||||
duration: input.duration,
|
||||
match_confidence: input.match_confidence,
|
||||
max_hiking_difficulty: input.max_hiking_difficulty,
|
||||
})
|
||||
.where('place_id', '=', place_id)
|
||||
.where('place_id', 'in', (eb) => eb.selectFrom('places').select('id').where('trip_id', '=', trip_id))
|
||||
.executeTakeFirst();
|
||||
return Number(result.numUpdatedRows ?? 0) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
function toListRow<
|
||||
R extends Omit<TourListRow, 'planned' | 'has_waypoints'> & { planned: unknown; has_waypoints: unknown },
|
||||
>(r: R): TourListRow {
|
||||
return { ...r, planned: Number(r.planned), has_waypoints: Number(r.has_waypoints) };
|
||||
}
|
||||
Reference in New Issue
Block a user