Files
TREK/server/src/nest/common/rowShape.ts
T
Maurice 43a6c30325 feat(places): remember the Amap id a place was found by
The provider produced amap_poi_id, but nothing stored it, so a saved
Amap place could never be reopened against Amap. The column now travels
with google_place_id and osm_id through the shared contract, the place
insert and update, duplicate matching, the assignment projections, the
trip copy and the three place MCP tools.
2026-09-12 18:03:10 +02:00

73 lines
2.6 KiB
TypeScript

import type { AssignmentRow, Tag, Participant } from '../../types';
import type { PlaceRatingRow } from '../query-helpers/query-helpers.service';
/**
* Pure reshaping over rows the query helpers load. Neither function touches the
* database, so neither became a provider — they shipped in
* services/queryHelpers.ts next to the loaders only because the loaders were
* free functions too.
*/
/** Reshape a flat assignment+place DB row into the nested API response shape with embedded place, tags, and participants. */
export function formatAssignmentWithPlace(a: AssignmentRow, tags: Partial<Tag>[], participants: Participant[]) {
return {
id: a.id,
day_id: a.day_id,
place_id: a.place_id,
order_index: a.order_index,
notes: a.notes,
assignment_time: a.assignment_time ?? null,
assignment_end_time: a.assignment_end_time ?? null,
end_day: a.end_day === 1,
leg_transport_mode: a.leg_transport_mode ?? null,
incoming_leg_transport_mode: a.incoming_leg_transport_mode ?? null,
participants: participants || [],
created_at: a.created_at,
place: {
id: a.place_id,
name: a.place_name,
description: a.place_description,
lat: a.lat,
lng: a.lng,
address: a.address,
category_id: a.category_id,
price: a.price,
currency: a.place_currency,
place_time: a.place_time,
end_time: a.end_time,
duration_minutes: a.duration_minutes,
notes: a.place_notes,
image_url: a.image_url,
transport_mode: a.transport_mode,
google_place_id: a.google_place_id,
google_ftid: a.google_ftid,
osm_id: a.osm_id,
amap_poi_id: a.amap_poi_id,
website: a.website,
phone: a.phone,
// The rail marks a fuel stop as one; without it here every stop would need
// its own place request to find out what kind it is.
stop_type: a.stop_type ?? null,
// Same reason: the rail resets a range budget here and has to know how far this
// stop fills, not how far the traveller's default one does.
fill_percent: a.fill_percent ?? null,
category: a.category_id ? {
id: a.category_id,
name: a.category_name,
color: a.category_color,
icon: a.category_icon,
} : null,
tags: tags || [],
}
};
}
/** avg/count aggregate for a place's rating rows. */
export function ratingAggregate(ratings: PlaceRatingRow[] | undefined) {
const rows = ratings || [];
return {
rating_avg: rows.length > 0 ? rows.reduce((s, r) => s + r.rating, 0) / rows.length : null,
rating_count: rows.length,
};
}