Files
TREK/server/src/db/entities/Tours.entity.ts
T
Maurice f6fd2722d0 feat(tours): take a tour over from another trip, server and contract (LIB-02)
GET /api/tours/library lists the tours of every trip the user owns or is a
member of, with the trip each is in and how many tours each trip holds, to
choose one to take over into the trip being planned. It is narrowed by a
part of the name (matched ignoring case, a "%" or "_" just a character), a
type or one trip, and the asking trip can be left out; up to 100 tours, and
the answer says when there were more. GET /api/tours/library/:placeId
previews one: its facts and its line thinned to 400 points, its ends kept.
Neither is trip-scoped, so every answer is bounded by the user's own trips,
read anew each call: a tour of a trip out of reach is "not found", nothing
hints that it exists. Measures stay null where the tour has none.

POST /api/trips/:tripId/tours/take-over copies one into this trip, which the
guard checks with 'place_edit'; the source trip is checked against the
user's trips as they are at that moment, and the source is read again inside
the write. The copy is a tour of its own: its line, measures, settings and
control points, without days, without its stops' links to places of the
other trip and without the obstacle decisions taken there; it keeps the
import record of its line. A copy that fails leaves nothing behind.

Where it came from is kept as text (tours.taken_over_from_trip_title and
taken_over_from_tour_name, migration 045100), not as a pointer, and read
back in the provenance as takenOverFrom; a copy of such a copy names it too.
The in-trip copy (P2C-07) and the take-over share one copy routine.

MCP: list_tour_library and take_over_tour, over the same service and
checks. The repository tests follow the hiking preferences column of MF-08.
2026-10-19 14:53:08 +02:00

125 lines
4.8 KiB
TypeScript

import { Collection, EntityRepositoryType, type Opt, PrimaryKeyProp, type Ref, defineEntity, p } from '@mikro-orm/core';
import { ToursRepository } from '../repositories/Tours.repository';
import { Places } from './Places.entity';
import { TourObstacleReviews } from './TourObstacleReviews.entity';
import { TourSources } from './TourSources.entity';
import { TourTypes } from './TourTypes.entity';
import { TourWaypoints } from './TourWaypoints.entity';
export class Tours {
[EntityRepositoryType]?: ToursRepository;
[PrimaryKeyProp]?: 'place';
place?: Ref<Places> | null;
place_id?: number | null;
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;
created_at?: string | null;
max_hiking_difficulty?: number | null = 2;
planned_duration_minutes?: number | null;
break_additional_minutes?: number | null;
derivedFromPlace?: Ref<Places> | null;
derived_from_place_id?: number | null;
recorded_route_geometry?: string | null;
origin: TToursOrigin & Opt = ToursOrigin.TREK;
origin_file_name?: string | null;
boat_type?: TToursBoatType | null;
water_context?: TToursWaterContext | null;
paddle_speed_kmh?: number | null;
portage_speed_kmh?: number | null;
track_marks?: string | null;
paddle_lake_style?: TToursPaddleLakeStyle | null;
revision: number & Opt = 1;
hiking_preferences?: string | null;
taken_over_from_trip_title?: string | null;
taken_over_from_tour_name?: string | null;
tourTypeRef!: Ref<TourTypes>;
tour_obstacle_reviews_collection = new Collection<TourObstacleReviews>(this);
tour_sources: Ref<TourSources> | null = null;
tour_waypoints_collection = new Collection<TourWaypoints>(this);
}
export const ToursOrigin = {
TREK: 'trek',
GPX: 'gpx',
WANDERER: 'wanderer',
} as const;
export type TToursOrigin = (typeof ToursOrigin)[keyof typeof ToursOrigin];
export const ToursBoatType = {
KAYAK: 'kayak',
CANOE: 'canoe',
SUP: 'sup',
OTHER: 'other',
} as const;
export type TToursBoatType = (typeof ToursBoatType)[keyof typeof ToursBoatType];
export const ToursWaterContext = {
RIVER: 'river',
LAKE: 'lake',
} as const;
export type TToursWaterContext = (typeof ToursWaterContext)[keyof typeof ToursWaterContext];
export const ToursPaddleLakeStyle = {
DIRECT: 'direct',
SHORE: 'shore',
} as const;
export type TToursPaddleLakeStyle = (typeof ToursPaddleLakeStyle)[keyof typeof ToursPaddleLakeStyle];
export const ToursSchema = defineEntity({
class: Tours,
repository: () => ToursRepository,
indexes: [
{
name: 'idx_tours_derived_from_place_id',
where: 'derived_from_place_id IS NOT NULL',
properties: ['derived_from_place_id'],
},
],
checks: [{ name: 'tours_revision_check', expression: 'revision >= 1' }],
properties: {
place: () => p.oneToOne(Places).primary().ref().deleteRule('cascade').nullable().hidden(),
place_id: p.integer().nullable().persist(false),
tour_type: p.text().persist(false).index('idx_tours_tour_type'),
distance: p.double().nullable(),
elevation_gain: p.double().nullable(),
elevation_loss: p.double().nullable(),
duration: p.double().nullable(),
difficulty: p.text().nullable(),
wanderer_ref: p.text().nullable(),
match_confidence: p.double().nullable(),
created_at: p.text().nullable().defaultRaw(`CURRENT_TIMESTAMP`),
max_hiking_difficulty: p.integer().nullable(),
planned_duration_minutes: p.integer().nullable(),
break_additional_minutes: p.integer().nullable(),
derivedFromPlace: () => p.manyToOne(Places).ref().nullable().hidden(),
derived_from_place_id: p.integer().nullable().persist(false),
recorded_route_geometry: p.text().nullable(),
origin: p.enum(() => ToursOrigin).default('trek'),
origin_file_name: p.text().nullable(),
boat_type: p.enum(() => ToursBoatType).nullable(),
water_context: p.enum(() => ToursWaterContext).nullable(),
paddle_speed_kmh: p.double().nullable(),
portage_speed_kmh: p.double().nullable(),
track_marks: p.text().nullable(),
paddle_lake_style: p.enum(() => ToursPaddleLakeStyle).nullable(),
revision: p.integer().default(1),
hiking_preferences: p.text().nullable(),
taken_over_from_trip_title: p.text().nullable(),
taken_over_from_tour_name: p.text().nullable(),
tourTypeRef: () => p.manyToOne(TourTypes).ref().joinColumn('tour_type').hidden().index('idx_tours_tour_type'),
tour_obstacle_reviews_collection: () => p.oneToMany(TourObstacleReviews).mappedBy('place').hidden(),
tour_sources: () => p.oneToOne(TourSources).ref().mappedBy('place').hidden(),
tour_waypoints_collection: () => p.oneToMany(TourWaypoints).mappedBy('place').hidden(),
},
});