mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 22:16:26 +00:00
refactor(assignments, public-api, accommodations, places): async DB-touching methods; transactions through UnitOfWork
This commit is contained in:
@@ -68,8 +68,8 @@ export class AccommodationsController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
list(@CurrentUser() user: User, @Param('tripId') tripId: string) {
|
||||
return { accommodations: this.accommodations.list(tripId) };
|
||||
async list(@CurrentUser() user: User, @Param('tripId') tripId: string) {
|
||||
return { accommodations: await this.accommodations.list(tripId) };
|
||||
}
|
||||
|
||||
@RequirePermission('day_edit')
|
||||
@@ -85,7 +85,7 @@ export class AccommodationsController {
|
||||
if (!place_id || !start_day_id || !end_day_id) {
|
||||
throw new HttpException({ error: 'place_id, start_day_id, and end_day_id are required' }, 400);
|
||||
}
|
||||
const errors = this.accommodations.validateRefs(tripId, place_id, start_day_id, end_day_id);
|
||||
const errors = await this.accommodations.validateRefs(tripId, place_id, start_day_id, end_day_id);
|
||||
if (errors.length > 0) {
|
||||
throw new HttpException({ error: errors[0].message }, 404);
|
||||
}
|
||||
@@ -108,12 +108,12 @@ export class AccommodationsController {
|
||||
@Headers('x-socket-id') socketId?: string,
|
||||
) {
|
||||
const body = rawBody as AccommodationBody;
|
||||
const existing = this.accommodations.get(id, tripId);
|
||||
const existing = await this.accommodations.get(id, tripId);
|
||||
if (!existing) {
|
||||
throw new HttpException({ error: 'Accommodation not found' }, 404);
|
||||
}
|
||||
const { place_id, start_day_id, end_day_id, check_in, check_in_end, check_out, confirmation, notes } = body;
|
||||
const errors = this.accommodations.validateRefs(tripId, place_id, start_day_id, end_day_id);
|
||||
const errors = await this.accommodations.validateRefs(tripId, place_id, start_day_id, end_day_id);
|
||||
if (errors.length > 0) {
|
||||
throw new HttpException({ error: errors[0].message }, 404);
|
||||
}
|
||||
@@ -132,7 +132,7 @@ export class AccommodationsController {
|
||||
@Headers('x-socket-id') socketId?: string,
|
||||
@Query('keepStop') keepStop?: string,
|
||||
) {
|
||||
if (!this.accommodations.get(id, tripId)) {
|
||||
if (!(await this.accommodations.get(id, tripId))) {
|
||||
throw new HttpException({ error: 'Accommodation not found' }, 404);
|
||||
}
|
||||
// Turning a night back into a pause in road trip mode: the booking goes, the
|
||||
|
||||
@@ -69,9 +69,9 @@ export class AccommodationsMcp {
|
||||
ctx: McpContext,
|
||||
) {
|
||||
if (await this.auth.isDemoUser(ctx.userId)) return demoDenied();
|
||||
if (!this.accommodations.verifyTripAccess(tripId, ctx.userId)) return noAccess();
|
||||
if (!(await this.accommodations.verifyTripAccess(tripId, ctx.userId))) return noAccess();
|
||||
if (!(await this.guards.hasTripPermission('day_edit', tripId, ctx.userId))) return permissionDenied();
|
||||
const errors = this.accommodations.validateAccommodationRefs(tripId, place_id, start_day_id, end_day_id);
|
||||
const errors = await this.accommodations.validateAccommodationRefs(tripId, place_id, start_day_id, end_day_id);
|
||||
if (errors.length > 0) return { content: [{ type: 'text' as const, text: errors.map(e => e.message).join(', ') }], isError: true };
|
||||
const { accommodation, mirror } = await this.accommodations.createAccommodation(tripId, { place_id, start_day_id, end_day_id, check_in, check_in_end, check_out, confirmation, notes });
|
||||
this.guards.safeBroadcast(tripId, 'accommodation:created', { accommodation });
|
||||
@@ -120,13 +120,13 @@ export class AccommodationsMcp {
|
||||
ctx: McpContext,
|
||||
) {
|
||||
if (await this.auth.isDemoUser(ctx.userId)) return demoDenied();
|
||||
if (!this.accommodations.verifyTripAccess(tripId, ctx.userId)) return noAccess();
|
||||
if (!(await this.accommodations.verifyTripAccess(tripId, ctx.userId))) return noAccess();
|
||||
if (!(await this.guards.hasTripPermission('day_edit', tripId, ctx.userId))) return permissionDenied();
|
||||
const dayErrors = this.accommodations.validateAccommodationRefs(tripId, undefined, start_day_id, end_day_id);
|
||||
const dayErrors = await this.accommodations.validateAccommodationRefs(tripId, undefined, start_day_id, end_day_id);
|
||||
if (dayErrors.length > 0) return { content: [{ type: 'text' as const, text: dayErrors.map(e => e.message).join(', ') }], isError: true };
|
||||
try {
|
||||
const result = await this.uow.transactional(async () => {
|
||||
const place = this.places.create(String(tripId), { name, description, lat, lng, address, category_id, google_place_id, google_ftid, osm_id, notes: place_notes, website, phone, price, currency });
|
||||
const place = await this.places.create(String(tripId), { name, description, lat, lng, address, category_id, google_place_id, google_ftid, osm_id, notes: place_notes, website, phone, price, currency });
|
||||
const { accommodation, mirror } = await this.accommodations.createAccommodation(tripId, { place_id: place.id, start_day_id, end_day_id, check_in, check_in_end, check_out, confirmation, notes: accommodation_notes });
|
||||
return { place, accommodation, mirror };
|
||||
});
|
||||
@@ -168,9 +168,9 @@ export class AccommodationsMcp {
|
||||
ctx: McpContext,
|
||||
) {
|
||||
if (await this.auth.isDemoUser(ctx.userId)) return demoDenied();
|
||||
if (!this.accommodations.verifyTripAccess(tripId, ctx.userId)) return noAccess();
|
||||
if (!(await this.accommodations.verifyTripAccess(tripId, ctx.userId))) return noAccess();
|
||||
if (!(await this.guards.hasTripPermission('day_edit', tripId, ctx.userId))) return permissionDenied();
|
||||
const existing = this.accommodations.getAccommodation(accommodationId, tripId);
|
||||
const existing = await this.accommodations.getAccommodation(accommodationId, tripId);
|
||||
if (!existing) return { content: [{ type: 'text' as const, text: 'Accommodation not found.' }], isError: true };
|
||||
const { accommodation, mirror } = await this.accommodations.updateAccommodation(accommodationId, existing, { place_id, start_day_id, end_day_id, check_in, check_in_end, check_out, confirmation, notes });
|
||||
this.guards.safeBroadcast(tripId, 'accommodation:updated', { accommodation });
|
||||
@@ -192,9 +192,9 @@ export class AccommodationsMcp {
|
||||
})
|
||||
async deleteAccommodation({ tripId, accommodationId }: { tripId: number; accommodationId: number }, ctx: McpContext) {
|
||||
if (await this.auth.isDemoUser(ctx.userId)) return demoDenied();
|
||||
if (!this.accommodations.verifyTripAccess(tripId, ctx.userId)) return noAccess();
|
||||
if (!(await this.accommodations.verifyTripAccess(tripId, ctx.userId))) return noAccess();
|
||||
if (!(await this.guards.hasTripPermission('day_edit', tripId, ctx.userId))) return permissionDenied();
|
||||
if (!this.accommodations.getAccommodation(accommodationId, tripId)) return { content: [{ type: 'text' as const, text: 'Accommodation not found.' }], isError: true };
|
||||
if (!(await this.accommodations.getAccommodation(accommodationId, tripId))) return { content: [{ type: 'text' as const, text: 'Accommodation not found.' }], isError: true };
|
||||
// linkedReservationId stays the first one so the tool's answer keeps its shape;
|
||||
// linkedReservationIds carries the rest for a block that had more than one booking.
|
||||
const { linkedReservationId, linkedReservationIds, mirror } = await this.accommodations.deleteAccommodation(accommodationId);
|
||||
@@ -212,7 +212,7 @@ export class AccommodationsMcp {
|
||||
})
|
||||
async tripAccommodationsResource(uri: URL, { tripId }: { tripId: string | string[] }, ctx: McpContext) {
|
||||
const id = parseId(tripId);
|
||||
if (id === null || !this.accommodations.verifyTripAccess(id, ctx.userId)) {
|
||||
if (id === null || !(await this.accommodations.verifyTripAccess(id, ctx.userId))) {
|
||||
return {
|
||||
contents: [{
|
||||
uri: uri.href,
|
||||
@@ -221,7 +221,7 @@ export class AccommodationsMcp {
|
||||
}],
|
||||
};
|
||||
}
|
||||
const accommodations = this.accommodations.listAccommodations(id);
|
||||
const accommodations = await this.accommodations.listAccommodations(id);
|
||||
return {
|
||||
contents: [{
|
||||
uri: uri.href,
|
||||
|
||||
@@ -54,7 +54,7 @@ export class AccommodationsRpc {
|
||||
const endDayId = Math.trunc(Number(input.end_day_id));
|
||||
if (!placeId || !startDayId || !endDayId) throw new BadParams('place_id, start_day_id, and end_day_id are required');
|
||||
// Verifies the place and both days belong to this trip.
|
||||
const errors = this.days.validateAccommodationRefs(tripId, placeId, startDayId, endDayId);
|
||||
const errors = await this.days.validateAccommodationRefs(tripId, placeId, startDayId, endDayId);
|
||||
if (errors.length > 0) throw new ForbiddenResource(errors[0].message);
|
||||
const { accommodation, mirror } = await this.days.createAccommodation(tripId, {
|
||||
place_id: placeId,
|
||||
@@ -81,10 +81,10 @@ export class AccommodationsRpc {
|
||||
const parsed = accommodationUpdateRequestSchema.safeParse(params.input);
|
||||
if (!parsed.success) throw new BadParams(`invalid accommodation: ${schemaMessage(parsed.error)}`);
|
||||
await this.guards.requireTripEdit(tripId, actor, ACCOMMODATION_EDIT_ACTION);
|
||||
const existing = this.days.getAccommodation(accommodationId, tripId);
|
||||
const existing = await this.days.getAccommodation(accommodationId, tripId);
|
||||
if (!existing) throw new ForbiddenResource(`no accommodation ${accommodationId} on trip ${tripId}`);
|
||||
const input = parsed.data as { place_id?: number; start_day_id?: number; end_day_id?: number; check_in?: string; check_in_end?: string; check_out?: string; confirmation?: string; notes?: string };
|
||||
const errors = this.days.validateAccommodationRefs(tripId, input.place_id, input.start_day_id, input.end_day_id);
|
||||
const errors = await this.days.validateAccommodationRefs(tripId, input.place_id, input.start_day_id, input.end_day_id);
|
||||
if (errors.length > 0) throw new ForbiddenResource(errors[0].message);
|
||||
const { accommodation, mirror } = await this.days.updateAccommodation(accommodationId, existing, input);
|
||||
this.realtime.broadcast(tripId, 'accommodation:updated', { accommodation });
|
||||
@@ -98,7 +98,7 @@ export class AccommodationsRpc {
|
||||
const accommodationId = num(params.accommodationId, 'accommodationId');
|
||||
const actor = this.guards.requireActor(ctx, 'accommodation');
|
||||
await this.guards.requireTripEdit(tripId, actor, ACCOMMODATION_EDIT_ACTION);
|
||||
if (!this.days.getAccommodation(accommodationId, tripId)) {
|
||||
if (!(await this.days.getAccommodation(accommodationId, tripId))) {
|
||||
throw new ForbiddenResource(`no accommodation ${accommodationId} on trip ${tripId}`);
|
||||
}
|
||||
// Deleting a block can take its partner reservation and budget item with it.
|
||||
|
||||
@@ -105,7 +105,7 @@ export class AccommodationsService {
|
||||
|
||||
/** Owner or member, returning the trip. Takes a number too: the MCP tools pass
|
||||
* the parsed id, the REST path the raw param. */
|
||||
verifyTripAccess(tripId: string | number, userId: number) {
|
||||
async verifyTripAccess(tripId: string | number, userId: number) {
|
||||
return this.dbs.canAccessTrip(Number(tripId), userId);
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ export class AccommodationsService {
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
|
||||
private getAccommodationWithPlace(id: number | bigint) {
|
||||
private async getAccommodationWithPlace(id: number | bigint) {
|
||||
return this.db.get(`
|
||||
SELECT a.*, p.name as place_name, p.address as place_address, p.image_url as place_image, p.lat as place_lat, p.lng as place_lng
|
||||
FROM day_accommodations a
|
||||
@@ -244,7 +244,7 @@ export class AccommodationsService {
|
||||
`, id);
|
||||
}
|
||||
|
||||
listAccommodations(tripId: string | number) {
|
||||
async listAccommodations(tripId: string | number) {
|
||||
return this.db.all(`
|
||||
SELECT a.*, p.name as place_name, p.address as place_address, p.image_url as place_image, p.lat as place_lat, p.lng as place_lng,
|
||||
r.title as reservation_title
|
||||
@@ -256,7 +256,7 @@ export class AccommodationsService {
|
||||
`, tripId);
|
||||
}
|
||||
|
||||
validateAccommodationRefs(tripId: string | number, placeId?: number, startDayId?: number, endDayId?: number) {
|
||||
async validateAccommodationRefs(tripId: string | number, placeId?: number, startDayId?: number, endDayId?: number) {
|
||||
const errors: { field: string; message: string }[] = [];
|
||||
if (placeId !== undefined) {
|
||||
const place = this.db.get('SELECT id FROM places WHERE id = ? AND trip_id = ?', placeId, tripId);
|
||||
@@ -294,7 +294,7 @@ export class AccommodationsService {
|
||||
* pulled it forward, and a night booked for ten in the morning sat behind a whole
|
||||
* day of unpinned stops, reached at a quarter past twelve.
|
||||
*/
|
||||
private positionForCheckIn(dayId: number, checkIn: string | null | undefined, excludeId?: number): number {
|
||||
private async positionForCheckIn(dayId: number, checkIn: string | null | undefined, excludeId?: number): Promise<number> {
|
||||
if (!checkIn) return 0;
|
||||
// excludeId leaves the row being re-seated out of the chain it is measured
|
||||
// against. Without it a night parked at the end of the day can find itself.
|
||||
@@ -321,7 +321,7 @@ export class AccommodationsService {
|
||||
* without an hour are passed over in both directions: the night may have been
|
||||
* dragged past them on purpose, and a change of notes is no reason to undo that.
|
||||
*/
|
||||
private seatedByCheckIn(dayId: number, ownId: number, checkIn: string | null | undefined): boolean {
|
||||
private async seatedByCheckIn(dayId: number, ownId: number, checkIn: string | null | undefined): Promise<boolean> {
|
||||
if (!checkIn) return true;
|
||||
const rows = this.db.all<{ id: number; at: string | null }>(`
|
||||
SELECT da.id, COALESCE(da.assignment_time, p.place_time, other.check_in) AS at
|
||||
@@ -345,7 +345,7 @@ export class AccommodationsService {
|
||||
* road trip. Only ever filled in when it is empty: a type the traveller picked
|
||||
* (a campsite, say) is theirs.
|
||||
*/
|
||||
private stampLodging(placeId: number): PlaceWithTags | null {
|
||||
private async stampLodging(placeId: number): Promise<PlaceWithTags | null> {
|
||||
const place = this.db.get<{ stop_type: string | null }>('SELECT stop_type FROM places WHERE id = ?', placeId);
|
||||
if (!place || place.stop_type) return null;
|
||||
this.db.run("UPDATE places SET stop_type = 'hotel' WHERE id = ?", placeId);
|
||||
@@ -359,11 +359,15 @@ export class AccommodationsService {
|
||||
* the n-th stop that has coordinates, not behind a row id. Stops without
|
||||
* coordinates are never routed and so never counted.
|
||||
*/
|
||||
private stopOrders(dayIds: number[]): Map<number, number[]> {
|
||||
return new Map([...new Set(dayIds)].map((dayId): [number, number[]] => [dayId, this.locatedStopIds(dayId)]));
|
||||
private async stopOrders(dayIds: number[]): Promise<Map<number, number[]>> {
|
||||
const result = new Map<number, number[]>();
|
||||
for (const dayId of new Set(dayIds)) {
|
||||
result.set(dayId, await this.locatedStopIds(dayId));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private locatedStopIds(dayId: number): number[] {
|
||||
private async locatedStopIds(dayId: number): Promise<number[]> {
|
||||
return this.db.all<{ id: number }>(`
|
||||
SELECT da.id FROM day_assignments da JOIN places p ON p.id = da.place_id
|
||||
WHERE da.day_id = ? AND p.lat IS NOT NULL AND p.lng IS NOT NULL
|
||||
@@ -385,9 +389,9 @@ export class AccommodationsService {
|
||||
*
|
||||
* Runs inside the caller's transaction.
|
||||
*/
|
||||
private reanchorVias(mirror: AccommodationMirror, before: Map<number, number[]>): void {
|
||||
private async reanchorVias(mirror: AccommodationMirror, before: Map<number, number[]>): Promise<void> {
|
||||
for (const [dayId, previousIds] of before) {
|
||||
const nextIds = this.locatedStopIds(dayId);
|
||||
const nextIds = await this.locatedStopIds(dayId);
|
||||
if (previousIds.length === nextIds.length && previousIds.every((id, i) => id === nextIds[i])) continue;
|
||||
const vias = this.db.all<PinnedVia>('SELECT id, after_order_index, sequence FROM roadtrip_vias WHERE day_id = ?', dayId);
|
||||
if (!vias.length) continue;
|
||||
@@ -411,7 +415,7 @@ export class AccommodationsService {
|
||||
|
||||
for (const viaId of remove) this.db.run('DELETE FROM roadtrip_vias WHERE id = ? AND day_id = ?', viaId, dayId);
|
||||
for (const via of moved) this.db.run('UPDATE roadtrip_vias SET after_order_index = ? WHERE id = ? AND day_id = ?', via.after_order_index, via.id, dayId);
|
||||
this.renumberMergedLegs(dayId, vias.filter(via => !remove.includes(via.id)), moved);
|
||||
await this.renumberMergedLegs(dayId, vias.filter(via => !remove.includes(via.id)), moved);
|
||||
this.noteVias(mirror, {
|
||||
dayId,
|
||||
vias: this.db.all<RoadtripVia>(
|
||||
@@ -445,7 +449,7 @@ export class AccommodationsService {
|
||||
* `RoadtripService.reanchor` does it, the earlier leg's points first, then by
|
||||
* their old sequence, then by id. Only a leg that received a via is touched.
|
||||
*/
|
||||
private renumberMergedLegs(dayId: number, kept: PinnedVia[], moved: { id: number; after_order_index: number }[]): void {
|
||||
private async renumberMergedLegs(dayId: number, kept: PinnedVia[], moved: { id: number; after_order_index: number }[]): Promise<void> {
|
||||
const landed = new Map(moved.map(via => [via.id, via.after_order_index]));
|
||||
const byLeg = new Map<number, PinnedVia[]>();
|
||||
for (const via of kept) {
|
||||
@@ -506,7 +510,7 @@ export class AccommodationsService {
|
||||
const end = (max.max !== null ? max.max : -1) + 1;
|
||||
this.db.run('UPDATE day_assignments SET day_id = ?, place_id = ?, order_index = ? WHERE id = ?', dayId, placeId, end, stop.id);
|
||||
|
||||
const seat = this.positionForCheckIn(dayId, checkIn, stop.id);
|
||||
const seat = await this.positionForCheckIn(dayId, checkIn, stop.id);
|
||||
if (seat < end) {
|
||||
this.db.run(
|
||||
'UPDATE day_assignments SET order_index = order_index + 1 WHERE day_id = ? AND order_index >= ? AND id != ?',
|
||||
@@ -524,7 +528,7 @@ export class AccommodationsService {
|
||||
// form writes stays that never had one. Nothing to put on the map then.
|
||||
if (!placeId) return mirror;
|
||||
|
||||
mirror.stamped = this.stampLodging(placeId);
|
||||
mirror.stamped = await this.stampLodging(placeId);
|
||||
|
||||
// The road-trip flow assigns the place to the day and only then books the night.
|
||||
// Claiming that row would make cancelling the booking delete a stop the traveller
|
||||
@@ -532,7 +536,7 @@ export class AccommodationsService {
|
||||
// answer for a place already planned for that day by hand.
|
||||
if (this.db.get('SELECT id FROM day_assignments WHERE day_id = ? AND place_id = ?', dayId, placeId)) return mirror;
|
||||
|
||||
const before = this.stopOrders([dayId]);
|
||||
const before = await this.stopOrders([dayId]);
|
||||
// Through AssignmentsService, seated where the check-in says (see
|
||||
// positionForCheckIn), with everything behind it moved up one.
|
||||
//
|
||||
@@ -543,14 +547,14 @@ export class AccommodationsService {
|
||||
// time until the next reload.
|
||||
mirror.created = await this.assignments.createAssignment(dayId, placeId, null, {
|
||||
accommodationId,
|
||||
orderIndex: this.positionForCheckIn(dayId, checkIn),
|
||||
orderIndex: await this.positionForCheckIn(dayId, checkIn),
|
||||
});
|
||||
this.reanchorVias(mirror, before);
|
||||
await this.reanchorVias(mirror, before);
|
||||
return mirror;
|
||||
}
|
||||
|
||||
/** The day stops this booking, and only this booking, put on the plan. */
|
||||
private ownStops(accommodationId: number) {
|
||||
private async ownStops(accommodationId: number) {
|
||||
return this.db.all<{ id: number; day_id: number; place_id: number; order_index: number }>(
|
||||
'SELECT id, day_id, place_id, order_index FROM day_assignments WHERE accommodation_id = ?', accommodationId
|
||||
);
|
||||
@@ -572,8 +576,8 @@ export class AccommodationsService {
|
||||
*/
|
||||
private async releaseStops(accommodationId: number, opts: { keepStop?: boolean }): Promise<AccommodationMirror> {
|
||||
const mirror = noMirror();
|
||||
const own = this.ownStops(accommodationId);
|
||||
const before = this.stopOrders(own.map(stop => stop.day_id));
|
||||
const own = await this.ownStops(accommodationId);
|
||||
const before = await this.stopOrders(own.map(stop => stop.day_id));
|
||||
for (const stop of own) {
|
||||
if (opts.keepStop) {
|
||||
this.db.run('UPDATE day_assignments SET accommodation_id = NULL WHERE id = ?', stop.id);
|
||||
@@ -587,7 +591,7 @@ export class AccommodationsService {
|
||||
this.db.run('DELETE FROM day_assignments WHERE id = ?', stop.id);
|
||||
mirror.removed.push({ id: stop.id, dayId: stop.day_id });
|
||||
}
|
||||
this.reanchorVias(mirror, before);
|
||||
await this.reanchorVias(mirror, before);
|
||||
return mirror;
|
||||
}
|
||||
|
||||
@@ -610,7 +614,7 @@ export class AccommodationsService {
|
||||
checkIn?: string | null,
|
||||
opts: { checkInChanged?: boolean } = {},
|
||||
): Promise<AccommodationMirror> {
|
||||
const own = this.ownStops(accommodationId);
|
||||
const own = await this.ownStops(accommodationId);
|
||||
if (own.length === 0) return noMirror();
|
||||
if (own.length === 1 && own[0].day_id === dayId && own[0].place_id === placeId) {
|
||||
// Same place, same day. A check-in given a new hour seats the night afresh, the
|
||||
@@ -618,8 +622,8 @@ export class AccommodationsService {
|
||||
// it is unless the clocks around it say it is in the wrong place, so a night the
|
||||
// traveller dragged somewhere stays there through a change of notes.
|
||||
const settled = opts.checkInChanged
|
||||
? this.positionForCheckIn(dayId, checkIn, own[0].id) === own[0].order_index
|
||||
: this.seatedByCheckIn(dayId, own[0].id, checkIn);
|
||||
? (await this.positionForCheckIn(dayId, checkIn, own[0].id)) === own[0].order_index
|
||||
: await this.seatedByCheckIn(dayId, own[0].id, checkIn);
|
||||
if (settled) return noMirror();
|
||||
}
|
||||
|
||||
@@ -630,22 +634,22 @@ export class AccommodationsService {
|
||||
// note, its hour, its end-of-day flag and the road-trip day boundary anchored
|
||||
// on its id, all of which a DELETE takes with it.
|
||||
if (own.length === 1 && placeId) {
|
||||
const before = this.stopOrders([own[0].day_id, dayId]);
|
||||
const before = await this.stopOrders([own[0].day_id, dayId]);
|
||||
const moved = await this.relocateOwnStop(own[0], placeId, dayId, checkIn);
|
||||
if (moved) {
|
||||
mirror.moved = { assignment: moved, oldDayId: own[0].day_id };
|
||||
mirror.stamped = this.stampLodging(placeId);
|
||||
this.reanchorVias(mirror, before);
|
||||
mirror.stamped = await this.stampLodging(placeId);
|
||||
await this.reanchorVias(mirror, before);
|
||||
return mirror;
|
||||
}
|
||||
}
|
||||
|
||||
const beforeRebuild = this.stopOrders(own.map(stop => stop.day_id));
|
||||
const beforeRebuild = await this.stopOrders(own.map(stop => stop.day_id));
|
||||
for (const stop of own) {
|
||||
this.db.run('DELETE FROM day_assignments WHERE id = ?', stop.id);
|
||||
mirror.removed.push({ id: stop.id, dayId: stop.day_id });
|
||||
}
|
||||
this.reanchorVias(mirror, beforeRebuild);
|
||||
await this.reanchorVias(mirror, beforeRebuild);
|
||||
const fresh = await this.mirrorStay(accommodationId, placeId, dayId, checkIn);
|
||||
mirror.created = fresh.created;
|
||||
mirror.stamped = fresh.stamped;
|
||||
@@ -685,10 +689,10 @@ export class AccommodationsService {
|
||||
return { accommodationId: newId, mirror: await this.mirrorStay(Number(newId), place_id ?? null, start_day_id, check_in) };
|
||||
});
|
||||
|
||||
return { accommodation: this.getAccommodationWithPlace(written.accommodationId), mirror: written.mirror };
|
||||
return { accommodation: await this.getAccommodationWithPlace(written.accommodationId), mirror: written.mirror };
|
||||
}
|
||||
|
||||
getAccommodation(id: string | number, tripId: string | number) {
|
||||
async getAccommodation(id: string | number, tripId: string | number) {
|
||||
return this.db.get<DayAccommodation>('SELECT * FROM day_accommodations WHERE id = ? AND trip_id = ?', id, tripId);
|
||||
}
|
||||
|
||||
@@ -731,7 +735,7 @@ export class AccommodationsService {
|
||||
JSON.stringify(meta), newConfirmation || null, res.id);
|
||||
}
|
||||
|
||||
return { accommodation: this.getAccommodationWithPlace(Number(id)), mirror };
|
||||
return { accommodation: await this.getAccommodationWithPlace(Number(id)), mirror };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,7 +49,7 @@ export class DayAssignmentsController {
|
||||
|
||||
@Get()
|
||||
async list(@CurrentUser() user: User, @Param('tripId') tripId: string, @Param('dayId') dayId: string) {
|
||||
if (!this.assignments.dayExists(dayId, tripId)) {
|
||||
if (!await this.assignments.dayExists(dayId, tripId)) {
|
||||
throw new HttpException({ error: 'Day not found' }, 404);
|
||||
}
|
||||
return { assignments: await this.assignments.listDayAssignments(dayId) };
|
||||
@@ -64,10 +64,10 @@ export class DayAssignmentsController {
|
||||
@Body() body: AssignmentCreateDto,
|
||||
@Headers('x-socket-id') socketId?: string,
|
||||
) {
|
||||
if (!this.assignments.dayExists(dayId, tripId)) {
|
||||
if (!await this.assignments.dayExists(dayId, tripId)) {
|
||||
throw new HttpException({ error: 'Day not found' }, 404);
|
||||
}
|
||||
if (!this.assignments.placeExists(body.place_id, tripId)) {
|
||||
if (!await this.assignments.placeExists(body.place_id, tripId)) {
|
||||
throw new HttpException({ error: 'Place not found' }, 404);
|
||||
}
|
||||
const assignment = await this.assignments.createAssignment(dayId, body.place_id, body.notes);
|
||||
@@ -78,17 +78,17 @@ export class DayAssignmentsController {
|
||||
|
||||
@RequirePermission('day_edit')
|
||||
@Put('reorder')
|
||||
reorder(
|
||||
async reorder(
|
||||
@CurrentUser() user: User,
|
||||
@Param('tripId') tripId: string,
|
||||
@Param('dayId') dayId: string,
|
||||
@Body() body: AssignmentReorderDto,
|
||||
@Headers('x-socket-id') socketId?: string,
|
||||
) {
|
||||
if (!this.assignments.dayExists(dayId, tripId)) {
|
||||
if (!await this.assignments.dayExists(dayId, tripId)) {
|
||||
throw new HttpException({ error: 'Day not found' }, 404);
|
||||
}
|
||||
this.assignments.reorderAssignments(dayId, body.orderedIds);
|
||||
await this.assignments.reorderAssignments(dayId, body.orderedIds);
|
||||
this.assignments.broadcast(tripId, 'assignment:reordered', { dayId: Number(dayId), orderedIds: body.orderedIds }, socketId);
|
||||
return { success: true };
|
||||
}
|
||||
@@ -102,10 +102,10 @@ export class DayAssignmentsController {
|
||||
@Param('id') id: string,
|
||||
@Headers('x-socket-id') socketId?: string,
|
||||
) {
|
||||
if (!this.assignments.assignmentExistsInDay(id, dayId, tripId)) {
|
||||
if (!await this.assignments.assignmentExistsInDay(id, dayId, tripId)) {
|
||||
throw new HttpException({ error: 'Assignment not found' }, 404);
|
||||
}
|
||||
this.assignments.deleteAssignment(id);
|
||||
await this.assignments.deleteAssignment(id);
|
||||
this.assignments.broadcast(tripId, 'assignment:deleted', { assignmentId: Number(id), dayId: Number(dayId) }, socketId);
|
||||
await this.assignments.reconcile(tripId, socketId);
|
||||
return { success: true };
|
||||
@@ -135,10 +135,10 @@ export class AssignmentOpsController {
|
||||
@Body() body: AssignmentMoveDto,
|
||||
@Headers('x-socket-id') socketId?: string,
|
||||
) {
|
||||
if (!this.assignments.getAssignmentForTrip(id, tripId)) {
|
||||
if (!await this.assignments.getAssignmentForTrip(id, tripId)) {
|
||||
throw new HttpException({ error: 'Assignment not found' }, 404);
|
||||
}
|
||||
if (!this.assignments.dayExists(String(body.new_day_id), tripId)) {
|
||||
if (!await this.assignments.dayExists(String(body.new_day_id), tripId)) {
|
||||
throw new HttpException({ error: 'Target day not found' }, 404);
|
||||
}
|
||||
const { assignment, oldDayId } = await this.assignments.moveAssignment(id, body.new_day_id, body.order_index);
|
||||
@@ -148,11 +148,11 @@ export class AssignmentOpsController {
|
||||
}
|
||||
|
||||
@Get(':id/participants')
|
||||
participants(@CurrentUser() user: User, @Param('tripId') tripId: string, @Param('id') id: string) {
|
||||
if (!this.assignments.getAssignmentForTrip(id, tripId)) {
|
||||
async participants(@CurrentUser() user: User, @Param('tripId') tripId: string, @Param('id') id: string) {
|
||||
if (!await this.assignments.getAssignmentForTrip(id, tripId)) {
|
||||
throw new HttpException({ error: 'Assignment not found' }, 404);
|
||||
}
|
||||
return { participants: this.assignments.getParticipants(id) };
|
||||
return { participants: await this.assignments.getParticipants(id) };
|
||||
}
|
||||
|
||||
@RequirePermission('day_edit')
|
||||
@@ -164,7 +164,7 @@ export class AssignmentOpsController {
|
||||
@Body() body: AssignmentTimeDto,
|
||||
@Headers('x-socket-id') socketId?: string,
|
||||
) {
|
||||
if (!this.assignments.getAssignmentForTrip(id, tripId)) {
|
||||
if (!await this.assignments.getAssignmentForTrip(id, tripId)) {
|
||||
throw new HttpException({ error: 'Assignment not found' }, 404);
|
||||
}
|
||||
const { assignment, reordered, vias } = await this.assignments.updateTime(id, body.place_time, body.end_time);
|
||||
@@ -190,7 +190,7 @@ export class AssignmentOpsController {
|
||||
@Body() body: AssignmentEndDayDto,
|
||||
@Headers('x-socket-id') socketId?: string,
|
||||
) {
|
||||
if (!this.assignments.getAssignmentForTrip(id, tripId)) {
|
||||
if (!await this.assignments.getAssignmentForTrip(id, tripId)) {
|
||||
throw new HttpException({ error: 'Assignment not found' }, 404);
|
||||
}
|
||||
const assignment = await this.assignments.setEndDay(id, body.end_day);
|
||||
@@ -210,7 +210,7 @@ export class AssignmentOpsController {
|
||||
@Body() body: AssignmentNotesDto,
|
||||
@Headers('x-socket-id') socketId?: string,
|
||||
) {
|
||||
if (!this.assignments.getAssignmentForTrip(id, tripId)) {
|
||||
if (!await this.assignments.getAssignmentForTrip(id, tripId)) {
|
||||
throw new HttpException({ error: 'Assignment not found' }, 404);
|
||||
}
|
||||
const assignment = await this.assignments.updateNotes(id, body.notes);
|
||||
@@ -227,7 +227,7 @@ export class AssignmentOpsController {
|
||||
@Body() body: AssignmentTransportDto,
|
||||
@Headers('x-socket-id') socketId?: string,
|
||||
) {
|
||||
if (!this.assignments.getAssignmentForTrip(id, tripId)) {
|
||||
if (!await this.assignments.getAssignmentForTrip(id, tripId)) {
|
||||
throw new HttpException({ error: 'Assignment not found' }, 404);
|
||||
}
|
||||
const assignment = body.direction === 'incoming'
|
||||
@@ -239,17 +239,17 @@ export class AssignmentOpsController {
|
||||
|
||||
@RequirePermission('day_edit')
|
||||
@Put(':id/participants')
|
||||
setParticipants(
|
||||
async setParticipants(
|
||||
@CurrentUser() user: User,
|
||||
@Param('tripId') tripId: string,
|
||||
@Param('id') id: string,
|
||||
@Body() body: AssignmentParticipantsDto,
|
||||
@Headers('x-socket-id') socketId?: string,
|
||||
) {
|
||||
if (!this.assignments.getAssignmentForTrip(id, tripId)) {
|
||||
if (!await this.assignments.getAssignmentForTrip(id, tripId)) {
|
||||
throw new HttpException({ error: 'Assignment not found' }, 404);
|
||||
}
|
||||
const participants = this.assignments.setParticipants(id, body.user_ids, tripId);
|
||||
const participants = await this.assignments.setParticipants(id, body.user_ids, tripId);
|
||||
this.assignments.broadcast(tripId, 'assignment:participants', { assignmentId: Number(id), participants }, socketId);
|
||||
return { participants };
|
||||
}
|
||||
|
||||
@@ -47,10 +47,10 @@ export class AssignmentsMcp {
|
||||
ctx: McpContext,
|
||||
) {
|
||||
if (await this.auth.isDemoUser(ctx.userId)) return demoDenied();
|
||||
if (!this.assignments.verifyTripAccess(tripId, ctx.userId)) return noAccess();
|
||||
if (!(await this.assignments.verifyTripAccess(tripId, ctx.userId))) return noAccess();
|
||||
if (!(await this.guards.hasTripPermission('day_edit', tripId, ctx.userId))) return permissionDenied();
|
||||
if (!this.assignments.dayExists(dayId, tripId)) return errorResult('Day not found.');
|
||||
if (!this.assignments.placeExists(placeId, tripId)) return errorResult('Place not found.');
|
||||
if (!(await this.assignments.dayExists(dayId, tripId))) return errorResult('Day not found.');
|
||||
if (!(await this.assignments.placeExists(placeId, tripId))) return errorResult('Place not found.');
|
||||
const assignment = await this.assignments.createAssignment(dayId, placeId, notes || null);
|
||||
this.guards.safeBroadcast(tripId, 'assignment:created', { assignment });
|
||||
await this.assignments.reconcile(tripId);
|
||||
@@ -73,9 +73,9 @@ export class AssignmentsMcp {
|
||||
ctx: McpContext,
|
||||
) {
|
||||
if (await this.auth.isDemoUser(ctx.userId)) return demoDenied();
|
||||
if (!this.assignments.verifyTripAccess(tripId, ctx.userId)) return noAccess();
|
||||
if (!(await this.assignments.verifyTripAccess(tripId, ctx.userId))) return noAccess();
|
||||
if (!(await this.guards.hasTripPermission('day_edit', tripId, ctx.userId))) return permissionDenied();
|
||||
if (!this.assignments.getAssignmentForTrip(assignmentId, tripId)) return errorResult('Assignment not found.');
|
||||
if (!(await this.assignments.getAssignmentForTrip(assignmentId, tripId))) return errorResult('Assignment not found.');
|
||||
const assignment = await this.assignments.setEndDay(assignmentId, end_day);
|
||||
this.guards.safeBroadcast(tripId, 'assignment:updated', { assignment });
|
||||
return ok({ assignment });
|
||||
@@ -97,11 +97,11 @@ export class AssignmentsMcp {
|
||||
ctx: McpContext,
|
||||
) {
|
||||
if (await this.auth.isDemoUser(ctx.userId)) return demoDenied();
|
||||
if (!this.assignments.verifyTripAccess(tripId, ctx.userId)) return noAccess();
|
||||
if (!(await this.assignments.verifyTripAccess(tripId, ctx.userId))) return noAccess();
|
||||
if (!(await this.guards.hasTripPermission('day_edit', tripId, ctx.userId))) return permissionDenied();
|
||||
if (!this.assignments.assignmentExistsInDay(assignmentId, dayId, tripId))
|
||||
if (!(await this.assignments.assignmentExistsInDay(assignmentId, dayId, tripId)))
|
||||
return errorResult('Assignment not found.');
|
||||
this.assignments.deleteAssignment(assignmentId);
|
||||
await this.assignments.deleteAssignment(assignmentId);
|
||||
this.guards.safeBroadcast(tripId, 'assignment:deleted', { assignmentId, dayId });
|
||||
await this.assignments.reconcile(tripId);
|
||||
return ok({ success: true });
|
||||
@@ -126,9 +126,9 @@ export class AssignmentsMcp {
|
||||
ctx: McpContext,
|
||||
) {
|
||||
if (await this.auth.isDemoUser(ctx.userId)) return demoDenied();
|
||||
if (!this.assignments.verifyTripAccess(tripId, ctx.userId)) return noAccess();
|
||||
if (!(await this.assignments.verifyTripAccess(tripId, ctx.userId))) return noAccess();
|
||||
if (!(await this.guards.hasTripPermission('day_edit', tripId, ctx.userId))) return permissionDenied();
|
||||
const existing = this.assignments.getAssignmentForTrip(assignmentId, tripId);
|
||||
const existing = await this.assignments.getAssignmentForTrip(assignmentId, tripId);
|
||||
if (!existing) return errorResult('Assignment not found.');
|
||||
const { assignment, reordered, vias } = await this.assignments.updateTime(
|
||||
assignmentId,
|
||||
@@ -162,9 +162,9 @@ export class AssignmentsMcp {
|
||||
ctx: McpContext,
|
||||
) {
|
||||
if (await this.auth.isDemoUser(ctx.userId)) return demoDenied();
|
||||
if (!this.assignments.verifyTripAccess(tripId, ctx.userId)) return noAccess();
|
||||
if (!(await this.assignments.verifyTripAccess(tripId, ctx.userId))) return noAccess();
|
||||
if (!(await this.guards.hasTripPermission('day_edit', tripId, ctx.userId))) return permissionDenied();
|
||||
if (!this.assignments.getAssignmentForTrip(assignmentId, tripId)) return errorResult('Assignment not found.');
|
||||
if (!(await this.assignments.getAssignmentForTrip(assignmentId, tripId))) return errorResult('Assignment not found.');
|
||||
const assignment = await this.assignments.updateNotes(assignmentId, notes);
|
||||
this.guards.safeBroadcast(tripId, 'assignment:updated', { assignment });
|
||||
return ok({ assignment });
|
||||
@@ -189,9 +189,9 @@ export class AssignmentsMcp {
|
||||
ctx: McpContext,
|
||||
) {
|
||||
if (await this.auth.isDemoUser(ctx.userId)) return demoDenied();
|
||||
if (!this.assignments.verifyTripAccess(tripId, ctx.userId)) return noAccess();
|
||||
if (!(await this.assignments.verifyTripAccess(tripId, ctx.userId))) return noAccess();
|
||||
if (!(await this.guards.hasTripPermission('day_edit', tripId, ctx.userId))) return permissionDenied();
|
||||
if (!this.assignments.getAssignmentForTrip(assignmentId, tripId)) return errorResult('Assignment not found.');
|
||||
if (!(await this.assignments.getAssignmentForTrip(assignmentId, tripId))) return errorResult('Assignment not found.');
|
||||
const assignment = direction === 'incoming'
|
||||
? await this.assignments.setIncomingLegTransportMode(assignmentId, transport_mode ?? null)
|
||||
: await this.assignments.setLegTransportMode(assignmentId, transport_mode ?? null);
|
||||
@@ -219,9 +219,9 @@ export class AssignmentsMcp {
|
||||
ctx: McpContext,
|
||||
) {
|
||||
if (await this.auth.isDemoUser(ctx.userId)) return demoDenied();
|
||||
if (!this.assignments.verifyTripAccess(tripId, ctx.userId)) return noAccess();
|
||||
if (!(await this.assignments.verifyTripAccess(tripId, ctx.userId))) return noAccess();
|
||||
if (!(await this.guards.hasTripPermission('day_edit', tripId, ctx.userId))) return permissionDenied();
|
||||
if (!this.assignments.getAssignmentForTrip(assignmentId, tripId)) return errorResult('Assignment not found.');
|
||||
if (!(await this.assignments.getAssignmentForTrip(assignmentId, tripId))) return errorResult('Assignment not found.');
|
||||
if (!(await this.days.getDay(newDayId, tripId))) return errorResult('Day not found.');
|
||||
const result = await this.assignments.moveAssignment(assignmentId, newDayId, orderIndex ?? 0);
|
||||
// REST parity shape ({ assignment, oldDayId, newDayId }) — the client keys its
|
||||
@@ -246,9 +246,9 @@ export class AssignmentsMcp {
|
||||
{ tripId, assignmentId }: { tripId: number; assignmentId: number },
|
||||
ctx: McpContext,
|
||||
) {
|
||||
if (!this.assignments.verifyTripAccess(tripId, ctx.userId)) return noAccess();
|
||||
if (!this.assignments.getAssignmentForTrip(assignmentId, tripId)) return errorResult('Assignment not found.');
|
||||
const participants = this.assignments.getParticipants(assignmentId);
|
||||
if (!(await this.assignments.verifyTripAccess(tripId, ctx.userId))) return noAccess();
|
||||
if (!(await this.assignments.getAssignmentForTrip(assignmentId, tripId))) return errorResult('Assignment not found.');
|
||||
const participants = await this.assignments.getParticipants(assignmentId);
|
||||
return ok({ participants });
|
||||
}
|
||||
|
||||
@@ -268,10 +268,10 @@ export class AssignmentsMcp {
|
||||
ctx: McpContext,
|
||||
) {
|
||||
if (await this.auth.isDemoUser(ctx.userId)) return demoDenied();
|
||||
if (!this.assignments.verifyTripAccess(tripId, ctx.userId)) return noAccess();
|
||||
if (!(await this.assignments.verifyTripAccess(tripId, ctx.userId))) return noAccess();
|
||||
if (!(await this.guards.hasTripPermission('day_edit', tripId, ctx.userId))) return permissionDenied();
|
||||
if (!this.assignments.getAssignmentForTrip(assignmentId, tripId)) return errorResult('Assignment not found.');
|
||||
const participants = this.assignments.setParticipants(assignmentId, userIds, tripId);
|
||||
if (!(await this.assignments.getAssignmentForTrip(assignmentId, tripId))) return errorResult('Assignment not found.');
|
||||
const participants = await this.assignments.setParticipants(assignmentId, userIds, tripId);
|
||||
this.guards.safeBroadcast(tripId, 'assignment:participants', { assignmentId, participants });
|
||||
return ok({ participants });
|
||||
}
|
||||
@@ -292,10 +292,10 @@ export class AssignmentsMcp {
|
||||
ctx: McpContext,
|
||||
) {
|
||||
if (await this.auth.isDemoUser(ctx.userId)) return demoDenied();
|
||||
if (!this.assignments.verifyTripAccess(tripId, ctx.userId)) return noAccess();
|
||||
if (!(await this.assignments.verifyTripAccess(tripId, ctx.userId))) return noAccess();
|
||||
if (!(await this.guards.hasTripPermission('day_edit', tripId, ctx.userId))) return permissionDenied();
|
||||
if (!(await this.days.getDay(dayId, tripId))) return errorResult('Day not found.');
|
||||
this.assignments.reorderAssignments(dayId, assignmentIds);
|
||||
await this.assignments.reorderAssignments(dayId, assignmentIds);
|
||||
// REST parity shape ({ dayId, orderedIds }) — the client only reads orderedIds,
|
||||
// so broadcasting the tool-input key name emptied the day for collaborators.
|
||||
this.guards.safeBroadcast(tripId, 'assignment:reordered', { dayId, orderedIds: assignmentIds });
|
||||
|
||||
@@ -8,6 +8,7 @@ import { QueryHelpersService } from '../query-helpers/query-helpers.service';
|
||||
import { formatAssignmentWithPlace } from '../common/rowShape';
|
||||
import type { AssignmentRow, DayAssignment, User, Participant } from '../../types';
|
||||
import { JourneyDomainService } from '../journey/journey-domain.service';
|
||||
import { UnitOfWork } from '../database/unit-of-work';
|
||||
|
||||
type Trip = TripAccess;
|
||||
|
||||
@@ -64,9 +65,10 @@ export class AssignmentsService {
|
||||
private readonly realtime: RealtimeService,
|
||||
private readonly queryHelpers: QueryHelpersService,
|
||||
private readonly journey: JourneyDomainService,
|
||||
private readonly uow: UnitOfWork,
|
||||
) {}
|
||||
|
||||
verifyTripAccess(tripId: string | number, userId: number) {
|
||||
async verifyTripAccess(tripId: string | number, userId: number) {
|
||||
return this.dbs.canAccessTrip(Number(tripId), userId);
|
||||
}
|
||||
|
||||
@@ -157,11 +159,11 @@ export class AssignmentsService {
|
||||
});
|
||||
}
|
||||
|
||||
dayExists(dayId: string | number, tripId: string | number) {
|
||||
async dayExists(dayId: string | number, tripId: string | number) {
|
||||
return !!this.dbs.get('SELECT id FROM days WHERE id = ? AND trip_id = ?', dayId, tripId);
|
||||
}
|
||||
|
||||
placeExists(placeId: unknown, tripId: string | number) {
|
||||
async placeExists(placeId: unknown, tripId: string | number) {
|
||||
return !!this.dbs.get('SELECT id FROM places WHERE id = ? AND trip_id = ?', placeId, tripId);
|
||||
}
|
||||
|
||||
@@ -172,8 +174,8 @@ export class AssignmentsService {
|
||||
* stop that reaches it without its booking id is one the day list cannot tell
|
||||
* from a place the traveller added, so it draws the hotel a second time.
|
||||
*/
|
||||
createAssignment(dayId: string | number, placeId: unknown, notes?: string | null, opts: { accommodationId?: number; orderIndex?: number } = {}) {
|
||||
const result = this.dbs.transaction(() => {
|
||||
async createAssignment(dayId: string | number, placeId: unknown, notes?: string | null, opts: { accommodationId?: number; orderIndex?: number } = {}) {
|
||||
const result = await this.uow.transactional(async () => {
|
||||
const maxOrder = this.dbs.get<{ max: number | null }>('SELECT MAX(order_index) as max FROM day_assignments WHERE day_id = ?', dayId)!;
|
||||
const end = (maxOrder.max !== null ? maxOrder.max : -1) + 1;
|
||||
// Somewhere in the middle when the caller says so, which means everything from
|
||||
@@ -190,30 +192,30 @@ export class AssignmentsService {
|
||||
);
|
||||
});
|
||||
|
||||
return this.getAssignmentWithPlace(result.lastInsertRowid);
|
||||
return await this.getAssignmentWithPlace(result.lastInsertRowid);
|
||||
}
|
||||
|
||||
assignmentExistsInDay(id: string | number, dayId: string | number, tripId: string | number) {
|
||||
async assignmentExistsInDay(id: string | number, dayId: string | number, tripId: string | number) {
|
||||
return !!this.dbs.get(
|
||||
'SELECT da.id FROM day_assignments da JOIN days d ON da.day_id = d.id WHERE da.id = ? AND da.day_id = ? AND d.trip_id = ?',
|
||||
id, dayId, tripId
|
||||
);
|
||||
}
|
||||
|
||||
deleteAssignment(id: string | number): void {
|
||||
async deleteAssignment(id: string | number): Promise<void> {
|
||||
this.dbs.run('DELETE FROM day_assignments WHERE id = ?', id);
|
||||
}
|
||||
|
||||
reorderAssignments(dayId: string | number, orderedIds: number[]): void {
|
||||
async reorderAssignments(dayId: string | number, orderedIds: number[]): Promise<void> {
|
||||
const update = this.dbs.prepare('UPDATE day_assignments SET order_index = ? WHERE id = ? AND day_id = ?');
|
||||
this.dbs.transaction(() => {
|
||||
await this.uow.transactional(async () => {
|
||||
orderedIds.forEach((id: number, index: number) => {
|
||||
update.run(index, id, dayId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getAssignmentForTrip(id: string | number, tripId: string | number) {
|
||||
async getAssignmentForTrip(id: string | number, tripId: string | number) {
|
||||
return this.dbs.get<DayAssignment>(`
|
||||
SELECT da.* FROM day_assignments da
|
||||
JOIN days d ON da.day_id = d.id
|
||||
@@ -224,7 +226,7 @@ export class AssignmentsService {
|
||||
async moveAssignment(id: string | number, newDayId: unknown, orderIndex: number | null | undefined) {
|
||||
// The source day comes from the row, not the caller — callers can't lie
|
||||
// about (or race on) where the assignment was.
|
||||
const oldDayId = this.dbs.transaction(() => {
|
||||
const oldDayId = await this.uow.transactional(async () => {
|
||||
const row = this.dbs.get<{ day_id: number }>('SELECT day_id FROM day_assignments WHERE id = ?', id);
|
||||
this.dbs.run('UPDATE day_assignments SET day_id = ?, order_index = ? WHERE id = ?', newDayId, orderIndex ?? 0, id);
|
||||
return row?.day_id;
|
||||
@@ -233,7 +235,7 @@ export class AssignmentsService {
|
||||
return { assignment: updated, oldDayId };
|
||||
}
|
||||
|
||||
getParticipants(assignmentId: string | number) {
|
||||
async getParticipants(assignmentId: string | number) {
|
||||
return this.dbs.all(`
|
||||
SELECT ap.user_id, COALESCE(u.display_name, u.username) AS username, u.avatar
|
||||
FROM assignment_participants ap
|
||||
@@ -256,7 +258,7 @@ export class AssignmentsService {
|
||||
* same.
|
||||
*/
|
||||
async updateTime(id: string | number, placeTime: unknown, endTime: unknown): Promise<AssignmentTimeUpdate> {
|
||||
const sorted = this.dbs.transaction(() => {
|
||||
const sorted = await this.uow.transactional(async () => {
|
||||
const stored = this.dbs.get<{ day_id: number; start: string | null }>(`
|
||||
SELECT da.day_id, COALESCE(da.assignment_time, p.place_time, acc.check_in) AS start
|
||||
FROM day_assignments da
|
||||
@@ -278,13 +280,15 @@ export class AssignmentsService {
|
||||
// cleared start leaves the day alone too.
|
||||
if (!placeTime || !stored) return null;
|
||||
if (sortMinutes(String(placeTime)) === sortMinutes(stored.start)) return null;
|
||||
return this.sortDayByTime(stored.day_id);
|
||||
return await this.sortDayByTime(stored.day_id);
|
||||
});
|
||||
|
||||
const vias = sorted?.viasMoved ? { dayId: sorted.dayId, vias: await this.listDayVias(sorted.dayId) } : null;
|
||||
|
||||
return {
|
||||
assignment: await this.getAssignmentWithPlace(Number(id)),
|
||||
reordered: sorted ? { dayId: sorted.dayId, orderedIds: sorted.orderedIds } : null,
|
||||
vias: sorted?.viasMoved ? { dayId: sorted.dayId, vias: this.listDayVias(sorted.dayId) } : null,
|
||||
vias,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -292,7 +296,7 @@ export class AssignmentsService {
|
||||
* Puts one day in time order. Writes nothing when it already is, which is the usual
|
||||
* case: most starts are typed in the order the day is planned.
|
||||
*/
|
||||
private sortDayByTime(dayId: number): { dayId: number; orderedIds: number[]; viasMoved: boolean } | null {
|
||||
private async sortDayByTime(dayId: number): Promise<{ dayId: number; orderedIds: number[]; viasMoved: boolean } | null> {
|
||||
// A booked night's hour lives on the booking, not on the stop: nobody types a
|
||||
// time into a hotel row, they type a check-in. Left out of this, the night
|
||||
// counted as untimed and stayed wherever it had been dropped, so pinning an
|
||||
@@ -321,7 +325,7 @@ export class AssignmentsService {
|
||||
if (row.order_index !== i) update.run(i, row.id);
|
||||
});
|
||||
|
||||
return { dayId, orderedIds: sorted.map(row => row.id), viasMoved: this.reanchorVias(dayId, rows, sorted) };
|
||||
return { dayId, orderedIds: sorted.map(row => row.id), viasMoved: await this.reanchorVias(dayId, rows, sorted) };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -334,7 +338,7 @@ export class AssignmentsService {
|
||||
* No sequence renumbering, unlike RoadtripService.reanchor: a reorder maps each leg
|
||||
* onto a different one, so two legs' vias never end up on the same leg.
|
||||
*/
|
||||
private reanchorVias(dayId: number, before: DayStopRow[], after: DayStopRow[]): boolean {
|
||||
private async reanchorVias(dayId: number, before: DayStopRow[], after: DayStopRow[]): Promise<boolean> {
|
||||
const located = (rows: DayStopRow[]) => rows.filter(row => row.located).map(row => row.id);
|
||||
const previousIds = located(before);
|
||||
const nextIds = located(after);
|
||||
@@ -364,7 +368,7 @@ export class AssignmentsService {
|
||||
* The day's vias in the shape the road trip routes broadcast them. RoadtripService
|
||||
* has this query too, but its module imports this one, so it cannot be injected here.
|
||||
*/
|
||||
private listDayVias(dayId: number): RoadtripVia[] {
|
||||
private async listDayVias(dayId: number): Promise<RoadtripVia[]> {
|
||||
return this.dbs.all<RoadtripVia>(
|
||||
`SELECT id, day_id, after_order_index, sequence, lat, lng, created_at
|
||||
FROM roadtrip_vias
|
||||
@@ -374,9 +378,9 @@ export class AssignmentsService {
|
||||
);
|
||||
}
|
||||
|
||||
setEndDay(id: string | number, endDay: boolean) {
|
||||
async setEndDay(id: string | number, endDay: boolean) {
|
||||
this.dbs.run('UPDATE day_assignments SET end_day = ? WHERE id = ?', endDay ? 1 : 0, id);
|
||||
return this.getAssignmentWithPlace(Number(id));
|
||||
return await this.getAssignmentWithPlace(Number(id));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -387,9 +391,9 @@ export class AssignmentsService {
|
||||
* journey reconcile: the note affects neither the day order nor the skeleton
|
||||
* mirror (same as the transport-mode writes).
|
||||
*/
|
||||
updateNotes(id: string | number, notes: string | null | undefined) {
|
||||
async updateNotes(id: string | number, notes: string | null | undefined) {
|
||||
this.dbs.run('UPDATE day_assignments SET notes = ? WHERE id = ?', notes || null, id);
|
||||
return this.getAssignmentWithPlace(Number(id));
|
||||
return await this.getAssignmentWithPlace(Number(id));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -398,9 +402,9 @@ export class AssignmentsService {
|
||||
* sticky by design: changing the whole-day default never touches a leg that
|
||||
* carries its own explicit mode.
|
||||
*/
|
||||
setLegTransportMode(id: string | number, mode: string | null) {
|
||||
async setLegTransportMode(id: string | number, mode: string | null) {
|
||||
this.dbs.run('UPDATE day_assignments SET leg_transport_mode = ? WHERE id = ?', mode ?? null, id);
|
||||
return this.getAssignmentWithPlace(Number(id));
|
||||
return await this.getAssignmentWithPlace(Number(id));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -409,9 +413,9 @@ export class AssignmentsService {
|
||||
* when the previous timeline element is a place (the column is only read for
|
||||
* non-place origins like a booking arrival or a morning hotel departure).
|
||||
*/
|
||||
setIncomingLegTransportMode(id: string | number, mode: string | null) {
|
||||
async setIncomingLegTransportMode(id: string | number, mode: string | null) {
|
||||
this.dbs.run('UPDATE day_assignments SET incoming_leg_transport_mode = ? WHERE id = ?', mode ?? null, id);
|
||||
return this.getAssignmentWithPlace(Number(id));
|
||||
return await this.getAssignmentWithPlace(Number(id));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -421,10 +425,10 @@ export class AssignmentsService {
|
||||
* the participants box sends the whole list back on every edit, so rejecting
|
||||
* the request would strand a trip whose membership changed underneath it.
|
||||
*/
|
||||
setParticipants(assignmentId: string | number, userIds: number[], tripId: string | number) {
|
||||
async setParticipants(assignmentId: string | number, userIds: number[], tripId: string | number) {
|
||||
const roster = this.dbs.rosterUserIds(tripId);
|
||||
const scoped = userIds.filter(id => roster.has(id));
|
||||
this.dbs.transaction(() => {
|
||||
await this.uow.transactional(async () => {
|
||||
this.dbs.run('DELETE FROM assignment_participants WHERE assignment_id = ?', assignmentId);
|
||||
if (scoped.length > 0) {
|
||||
const insert = this.dbs.prepare('INSERT OR IGNORE INTO assignment_participants (assignment_id, user_id) VALUES (?, ?)');
|
||||
|
||||
@@ -32,8 +32,8 @@ export class ItineraryRpc {
|
||||
const actor = this.guards.requireActor(ctx, 'itinerary');
|
||||
const notes = params.notes === undefined || params.notes === null ? null : str(params.notes, 'notes');
|
||||
await this.guards.requireTripEdit(tripId, actor, DAY_EDIT_ACTION);
|
||||
if (!this.assignments.dayExists(dayId, tripId)) throw new ForbiddenResource(`no day ${dayId} on trip ${tripId}`);
|
||||
if (!this.assignments.placeExists(placeId, tripId)) throw new ForbiddenResource(`no place ${placeId} on trip ${tripId}`);
|
||||
if (!(await this.assignments.dayExists(dayId, tripId))) throw new ForbiddenResource(`no day ${dayId} on trip ${tripId}`);
|
||||
if (!(await this.assignments.placeExists(placeId, tripId))) throw new ForbiddenResource(`no place ${placeId} on trip ${tripId}`);
|
||||
const assignment = await this.assignments.createAssignment(dayId, placeId, notes);
|
||||
this.realtime.broadcast(tripId, 'assignment:created', { assignment });
|
||||
await this.assignments.reconcile(tripId);
|
||||
@@ -46,9 +46,9 @@ export class ItineraryRpc {
|
||||
const assignmentId = num(params.assignmentId, 'assignmentId');
|
||||
const actor = this.guards.requireActor(ctx, 'itinerary');
|
||||
await this.guards.requireTripEdit(tripId, actor, DAY_EDIT_ACTION);
|
||||
const existing = this.assignments.getAssignmentForTrip(assignmentId, tripId);
|
||||
const existing = await this.assignments.getAssignmentForTrip(assignmentId, tripId);
|
||||
if (!existing) throw new ForbiddenResource(`no assignment ${assignmentId} on trip ${tripId}`);
|
||||
this.assignments.deleteAssignment(assignmentId);
|
||||
await this.assignments.deleteAssignment(assignmentId);
|
||||
// The dayId is what the client reducer keys the eviction on. Without it nothing
|
||||
// leaves the day, so keep the payload shape identical to the REST/MCP delete.
|
||||
this.realtime.broadcast(tripId, 'assignment:deleted', { assignmentId, dayId: existing.day_id });
|
||||
|
||||
@@ -252,7 +252,7 @@ export class BookingImportService {
|
||||
}
|
||||
}
|
||||
|
||||
const place = this.places.create(tripId, {
|
||||
const place = await this.places.create(tripId, {
|
||||
name: _venue.name,
|
||||
lat,
|
||||
lng,
|
||||
|
||||
@@ -225,7 +225,7 @@ export class DawarichSuggestionsService {
|
||||
|
||||
if (body.dayId !== undefined) {
|
||||
await this.requirePermission('day_edit', access.user_id, userId);
|
||||
if (!this.assignments.dayExists(body.dayId, tripId)) {
|
||||
if (!(await this.assignments.dayExists(body.dayId, tripId))) {
|
||||
throw new AcceptError('day_not_on_trip', 'Day does not belong to this trip', 400);
|
||||
}
|
||||
}
|
||||
@@ -238,7 +238,7 @@ export class DawarichSuggestionsService {
|
||||
// worse than none: the stay would come back as unhandled while the place it
|
||||
// already produced sat on the trip.
|
||||
const { created, placeId, assignment } = await this.uow.transactional(async () => {
|
||||
const place = this.places.create(String(tripId), {
|
||||
const place = await this.places.create(String(tripId), {
|
||||
name: body.name?.trim() || row.name,
|
||||
lat,
|
||||
lng,
|
||||
|
||||
@@ -118,15 +118,15 @@ export class PlacesController {
|
||||
private readonly storage: StorageService,
|
||||
) {}
|
||||
|
||||
private requireTrip(tripId: string, user: User) {
|
||||
const trip = this.places.verifyTripAccess(tripId, user.id);
|
||||
private async requireTrip(tripId: string, user: User) {
|
||||
const trip = await this.places.verifyTripAccess(tripId, user.id);
|
||||
if (!trip) {
|
||||
throw new HttpException({ error: 'Trip not found' }, 404);
|
||||
}
|
||||
return trip;
|
||||
}
|
||||
|
||||
private async requireEdit(trip: NonNullable<ReturnType<PlacesService['verifyTripAccess']>>, user: User): Promise<void> {
|
||||
private async requireEdit(trip: NonNullable<Awaited<ReturnType<PlacesService['verifyTripAccess']>>>, user: User): Promise<void> {
|
||||
if (!(await this.places.canEdit(trip, user))) {
|
||||
throw new HttpException({ error: 'No permission' }, 403);
|
||||
}
|
||||
@@ -151,12 +151,12 @@ export class PlacesController {
|
||||
@Body() body: PlaceCreateDto,
|
||||
@Headers('x-socket-id') socketId?: string,
|
||||
) {
|
||||
const trip = this.requireTrip(tripId, user);
|
||||
const trip = await this.requireTrip(tripId, user);
|
||||
validateLengths(body);
|
||||
validateRouteColor(body);
|
||||
validateUrlFields(body);
|
||||
await this.requireEdit(trip, user);
|
||||
const place = this.places.create(tripId, body as never);
|
||||
const place = await this.places.create(tripId, body as never);
|
||||
this.places.broadcast(tripId, 'place:created', { place }, socketId);
|
||||
await this.places.onCreated(tripId, place.id);
|
||||
return { place };
|
||||
@@ -171,7 +171,7 @@ export class PlacesController {
|
||||
@Body() body: PlaceImportGpxDto,
|
||||
@Headers('x-socket-id') socketId?: string,
|
||||
) {
|
||||
const trip = this.requireTrip(tripId, user);
|
||||
const trip = await this.requireTrip(tripId, user);
|
||||
await this.requireEdit(trip, user);
|
||||
if (!file) {
|
||||
throw new HttpException({ error: 'No file uploaded' }, 400);
|
||||
@@ -182,7 +182,7 @@ export class PlacesController {
|
||||
if (!importWaypoints && !importRoutes && !importTracks) {
|
||||
throw new HttpException({ error: 'No import types selected' }, 400);
|
||||
}
|
||||
const result = this.places.importGpx(tripId, file.buffer, { importWaypoints, importRoutes, importTracks, defaultName: file.originalname });
|
||||
const result = await this.places.importGpx(tripId, file.buffer, { importWaypoints, importRoutes, importTracks, defaultName: file.originalname });
|
||||
if (!result) {
|
||||
throw new HttpException({ error: 'No matching places found in GPX file' }, 400);
|
||||
}
|
||||
@@ -198,20 +198,20 @@ export class PlacesController {
|
||||
* before the `:id` routes so the literal path wins.
|
||||
*/
|
||||
@Get('export.gpx')
|
||||
exportGpx(
|
||||
async exportGpx(
|
||||
@CurrentUser() user: User,
|
||||
@Param('tripId') tripId: string,
|
||||
@Query() query: PlaceExportGpxDto,
|
||||
@Res() res: Response,
|
||||
) {
|
||||
this.requireTrip(tripId, user);
|
||||
await this.requireTrip(tripId, user);
|
||||
const waypoints = parseBool(query.waypoints, true);
|
||||
const tracks = parseBool(query.tracks, true);
|
||||
const dayRoutes = parseBool(query.dayRoutes, true);
|
||||
if (!waypoints && !tracks && !dayRoutes) {
|
||||
throw new HttpException({ error: 'No export types selected' }, 400);
|
||||
}
|
||||
const result = this.places.exportGpx(tripId, { waypoints, tracks, dayRoutes });
|
||||
const result = await this.places.exportGpx(tripId, { waypoints, tracks, dayRoutes });
|
||||
if (!result) {
|
||||
throw new HttpException({ error: 'Nothing to export' }, 404);
|
||||
}
|
||||
@@ -229,7 +229,7 @@ export class PlacesController {
|
||||
@Body() body: PlaceImportMapDto,
|
||||
@Headers('x-socket-id') socketId?: string,
|
||||
) {
|
||||
const trip = this.requireTrip(tripId, user);
|
||||
const trip = await this.requireTrip(tripId, user);
|
||||
await this.requireEdit(trip, user);
|
||||
if (!file) {
|
||||
throw new HttpException({ error: 'No file uploaded' }, 400);
|
||||
@@ -267,7 +267,7 @@ export class PlacesController {
|
||||
|
||||
/** Shared google/naver list import — identical flow, different provider + error string. */
|
||||
private async importList(provider: 'google' | 'naver', user: User, tripId: string, body: PlaceImportListDto, socketId?: string) {
|
||||
const trip = this.requireTrip(tripId, user);
|
||||
const trip = await this.requireTrip(tripId, user);
|
||||
await this.requireEdit(trip, user);
|
||||
const { url, enrich } = body;
|
||||
// Opt-in: re-resolve each imported place via the Places API to fill in
|
||||
@@ -306,7 +306,7 @@ export class PlacesController {
|
||||
@Body() body: PlaceBulkDeleteDto,
|
||||
@Headers('x-socket-id') socketId?: string,
|
||||
) {
|
||||
const trip = this.requireTrip(tripId, user);
|
||||
const trip = await this.requireTrip(tripId, user);
|
||||
await this.requireEdit(trip, user);
|
||||
const { ids } = body;
|
||||
if (ids.length === 0) {
|
||||
@@ -317,10 +317,10 @@ export class PlacesController {
|
||||
// journey entries even though removeMany below refuses it. Still ahead of
|
||||
// the DELETE — journey_entries.source_place_id is ON DELETE SET NULL, so
|
||||
// afterwards there is nothing left to detach.
|
||||
const scoped = this.places.scopedIds(tripId, ids);
|
||||
const scoped = await this.places.scopedIds(tripId, ids);
|
||||
for (const id of scoped) await this.places.onDeleted(id);
|
||||
// Read the linked expenses before the delete — afterwards the link is gone (#1298).
|
||||
const expenseIds = this.places.linkedExpenseIds(tripId, scoped);
|
||||
const expenseIds = await this.places.linkedExpenseIds(tripId, scoped);
|
||||
const { deleted, cancelled } = await this.places.removeMany(tripId, ids);
|
||||
for (const id of deleted) {
|
||||
this.places.broadcast(tripId, 'place:deleted', { placeId: id }, socketId);
|
||||
@@ -350,7 +350,7 @@ export class PlacesController {
|
||||
@Body() body: PlaceBulkUpdateDto,
|
||||
@Headers('x-socket-id') socketId?: string,
|
||||
) {
|
||||
const trip = this.requireTrip(tripId, user);
|
||||
const trip = await this.requireTrip(tripId, user);
|
||||
await this.requireEdit(trip, user);
|
||||
const { ids } = body;
|
||||
if (ids.length === 0) {
|
||||
@@ -371,8 +371,8 @@ export class PlacesController {
|
||||
|
||||
@Get(':id')
|
||||
@UseGuards(TripAccessGuard)
|
||||
get(@CurrentUser() user: User, @Param('tripId') tripId: string, @Param('id') id: string) {
|
||||
const place = this.places.get(tripId, id);
|
||||
async get(@CurrentUser() user: User, @Param('tripId') tripId: string, @Param('id') id: string) {
|
||||
const place = await this.places.get(tripId, id);
|
||||
if (!place) {
|
||||
throw new HttpException({ error: 'Place not found' }, 404);
|
||||
}
|
||||
@@ -389,7 +389,7 @@ export class PlacesController {
|
||||
@UploadedFile() file: Express.Multer.File | undefined,
|
||||
@Headers('x-socket-id') socketId?: string,
|
||||
) {
|
||||
const trip = this.requireTrip(tripId, user);
|
||||
const trip = await this.requireTrip(tripId, user);
|
||||
await this.requireEdit(trip, user);
|
||||
// Inline rather than DemoWriteGuard: requireEdit above answers 404/403 for a
|
||||
// trip the caller cannot reach, and a guard would run before it.
|
||||
@@ -415,7 +415,7 @@ export class PlacesController {
|
||||
}
|
||||
|
||||
@Put(':id/rating')
|
||||
rate(
|
||||
async rate(
|
||||
@CurrentUser() user: User,
|
||||
@Param('tripId') tripId: string,
|
||||
@Param('id') id: string,
|
||||
@@ -424,8 +424,8 @@ export class PlacesController {
|
||||
) {
|
||||
// Deliberately no place_edit check: every trip member may cast their own
|
||||
// vote (#1435), even when an admin restricts place editing.
|
||||
this.requireTrip(tripId, user);
|
||||
const place = this.places.rate(tripId, id, user.id, body.rating);
|
||||
await this.requireTrip(tripId, user);
|
||||
const place = await this.places.rate(tripId, id, user.id, body.rating);
|
||||
if (!place) {
|
||||
throw new HttpException({ error: 'Place not found' }, 404);
|
||||
}
|
||||
@@ -435,8 +435,8 @@ export class PlacesController {
|
||||
|
||||
@Delete(':id/rating')
|
||||
@UseGuards(TripAccessGuard)
|
||||
unrate(@CurrentUser() user: User, @Param('tripId') tripId: string, @Param('id') id: string, @Headers('x-socket-id') socketId?: string) {
|
||||
const place = this.places.rate(tripId, id, user.id, null);
|
||||
async unrate(@CurrentUser() user: User, @Param('tripId') tripId: string, @Param('id') id: string, @Headers('x-socket-id') socketId?: string) {
|
||||
const place = await this.places.rate(tripId, id, user.id, null);
|
||||
if (!place) {
|
||||
throw new HttpException({ error: 'Place not found' }, 404);
|
||||
}
|
||||
@@ -469,7 +469,7 @@ export class PlacesController {
|
||||
@Headers('x-socket-id') socketId?: string,
|
||||
@Headers('x-base-updated-at') ifMatch?: string,
|
||||
) {
|
||||
const trip = this.requireTrip(tripId, user);
|
||||
const trip = await this.requireTrip(tripId, user);
|
||||
validateLengths(body);
|
||||
validateRouteColor(body);
|
||||
validateUrlFields(body);
|
||||
@@ -495,11 +495,11 @@ export class PlacesController {
|
||||
async remove(@CurrentUser() user: User, @Param('tripId') tripId: string, @Param('id') id: string, @Headers('x-socket-id') socketId?: string) {
|
||||
// Scope the id to the trip before the hook (see bulkDelete), then sync the
|
||||
// journey ahead of the actual delete.
|
||||
if (!this.places.get(tripId, id)) {
|
||||
if (!(await this.places.get(tripId, id))) {
|
||||
throw new HttpException({ error: 'Place not found' }, 404);
|
||||
}
|
||||
await this.places.onDeleted(Number(id));
|
||||
const expenseIds = this.places.linkedExpenseIds(tripId, [id]);
|
||||
const expenseIds = await this.places.linkedExpenseIds(tripId, [id]);
|
||||
const { deleted, cancelled } = await this.places.remove(tripId, id);
|
||||
if (!deleted) {
|
||||
throw new HttpException({ error: 'Place not found' }, 404);
|
||||
|
||||
@@ -99,7 +99,7 @@ export class PlacesMcp {
|
||||
if (await this.auth.isDemoUser(ctx.userId)) return demoDenied();
|
||||
if (!this.db.canAccessTrip(tripId, ctx.userId)) return noAccess();
|
||||
if (!(await this.guards.hasTripPermission('place_edit', tripId, ctx.userId))) return permissionDenied();
|
||||
const place = this.places.create(String(tripId), { name, description, lat, lng, address, category_id, google_place_id, google_ftid, osm_id, amap_poi_id, notes, website, phone, image_url, price, currency, stop_type });
|
||||
const place = await this.places.create(String(tripId), { name, description, lat, lng, address, category_id, google_place_id, google_ftid, osm_id, amap_poi_id, notes, website, phone, image_url, price, currency, stop_type });
|
||||
this.guards.safeBroadcast(tripId, 'place:created', { place });
|
||||
return ok({ place });
|
||||
}
|
||||
@@ -144,10 +144,10 @@ export class PlacesMcp {
|
||||
if (await this.auth.isDemoUser(ctx.userId)) return demoDenied();
|
||||
if (!this.db.canAccessTrip(tripId, ctx.userId)) return noAccess();
|
||||
if (!(await this.guards.hasTripPermission('place_edit', tripId, ctx.userId))) return permissionDenied();
|
||||
if (!this.assignments.dayExists(dayId, tripId)) return { content: [{ type: 'text' as const, text: 'Day not found.' }], isError: true };
|
||||
if (!(await this.assignments.dayExists(dayId, tripId))) return { content: [{ type: 'text' as const, text: 'Day not found.' }], isError: true };
|
||||
try {
|
||||
const result = await this.uow.transactional(async () => {
|
||||
const place = this.places.create(String(tripId), { name, description, lat, lng, address, category_id, google_place_id, google_ftid, osm_id, amap_poi_id, notes: place_notes, website, phone, image_url, price, currency, stop_type });
|
||||
const place = await this.places.create(String(tripId), { name, description, lat, lng, address, category_id, google_place_id, google_ftid, osm_id, amap_poi_id, notes: place_notes, website, phone, image_url, price, currency, stop_type });
|
||||
const assignment = await this.assignments.createAssignment(dayId, place.id, assignment_notes ?? null);
|
||||
return { place, assignment };
|
||||
});
|
||||
@@ -231,7 +231,7 @@ export class PlacesMcp {
|
||||
if (await this.auth.isDemoUser(ctx.userId)) return demoDenied();
|
||||
// Rating is a personal vote — any trip member may cast one, place_edit not required.
|
||||
if (!this.db.canAccessTrip(tripId, ctx.userId)) return noAccess();
|
||||
const place = this.places.rate(String(tripId), String(placeId), ctx.userId, rating ?? null);
|
||||
const place = await this.places.rate(String(tripId), String(placeId), ctx.userId, rating ?? null);
|
||||
if (!place) return { content: [{ type: 'text' as const, text: 'Place not found.' }], isError: true };
|
||||
this.guards.safeBroadcast(tripId, 'place:updated', { place });
|
||||
return ok({ place });
|
||||
@@ -254,12 +254,12 @@ export class PlacesMcp {
|
||||
// Scope the id to the trip before the hook: onPlaceDeleted keys on the place
|
||||
// id alone, so a foreign id would detach that trip's journey entries even
|
||||
// though the delete below refuses it.
|
||||
if (!this.places.get(String(tripId), String(placeId))) {
|
||||
if (!(await this.places.get(String(tripId), String(placeId)))) {
|
||||
return { content: [{ type: 'text' as const, text: 'Place not found.' }], isError: true };
|
||||
}
|
||||
try { await this.journey.onPlaceDeleted(placeId); } catch { /* non-fatal */ } // sync journeys before the row is gone
|
||||
// The link is gone once the place is, so read it first (#1298).
|
||||
const expenseIds = this.places.linkedExpenseIds(tripId, [placeId]);
|
||||
const expenseIds = await this.places.linkedExpenseIds(tripId, [placeId]);
|
||||
const { deleted, cancelled } = await this.places.remove(String(tripId), String(placeId));
|
||||
if (!deleted) return { content: [{ type: 'text' as const, text: 'Place not found.' }], isError: true };
|
||||
this.guards.safeBroadcast(tripId, 'place:deleted', { placeId });
|
||||
@@ -378,7 +378,7 @@ export class PlacesMcp {
|
||||
if (!(await this.guards.hasTripPermission('place_edit', input.tripId, ctx.userId))) return permissionDenied();
|
||||
if (!input.importWaypoints && !input.importRoutes && !input.importTracks) return errorResult('No import types selected.');
|
||||
try {
|
||||
const imported = this.places.importGpx(String(input.tripId), Buffer.from(input.gpx, 'utf8'), { importWaypoints: input.importWaypoints, importRoutes: input.importRoutes, importTracks: input.importTracks, defaultName: input.name });
|
||||
const imported = await this.places.importGpx(String(input.tripId), Buffer.from(input.gpx, 'utf8'), { importWaypoints: input.importWaypoints, importRoutes: input.importRoutes, importTracks: input.importTracks, defaultName: input.name });
|
||||
if (!imported) return errorResult('No matching places found in GPX.');
|
||||
for (const place of imported.places) this.guards.safeBroadcast(input.tripId, 'place:created', { place });
|
||||
return ok(imported);
|
||||
@@ -408,7 +408,7 @@ export class PlacesMcp {
|
||||
// A read, like the REST route: seeing the trip is enough, no place_edit.
|
||||
if (!this.db.canAccessTrip(tripId, ctx.userId)) return noAccess();
|
||||
if (!waypoints && !tracks && !dayRoutes) return errorResult('No export types selected.');
|
||||
const result = this.places.exportGpx(String(tripId), { waypoints, tracks, dayRoutes });
|
||||
const result = await this.places.exportGpx(String(tripId), { waypoints, tracks, dayRoutes });
|
||||
if (!result) return errorResult('Nothing to export.');
|
||||
return ok({ gpx: result.gpx, filename: result.filename });
|
||||
}
|
||||
@@ -431,12 +431,12 @@ export class PlacesMcp {
|
||||
// Trip-scoped, and ahead of the DELETE: journey_entries.source_place_id is
|
||||
// ON DELETE SET NULL, so a hook that ran afterwards found nothing left to
|
||||
// detach and left the entries as orphans.
|
||||
const scoped = this.places.scopedIds(String(tripId), placeIds);
|
||||
const scoped = await this.places.scopedIds(String(tripId), placeIds);
|
||||
for (const id of scoped) {
|
||||
try { await this.journey.onPlaceDeleted(id); } catch { /* non-fatal */ }
|
||||
}
|
||||
// The link is gone once the places are, so read it first (#1298).
|
||||
const expenseIds = this.places.linkedExpenseIds(tripId, scoped);
|
||||
const expenseIds = await this.places.linkedExpenseIds(tripId, scoped);
|
||||
const { deleted, cancelled } = await this.places.removeMany(String(tripId), placeIds);
|
||||
for (const id of deleted) this.guards.safeBroadcast(tripId, 'place:deleted', { placeId: id });
|
||||
// A night booked at this place went with it, and took its partner booking and
|
||||
|
||||
@@ -60,7 +60,7 @@ export class PlacesRpc {
|
||||
this.guards.capStrings(parsed.data as Record<string, unknown>, PLACE_STR_LIMITS);
|
||||
capUrls(parsed.data as Record<string, unknown>);
|
||||
await this.guards.requireTripEdit(tripId, actor, PLACE_EDIT_ACTION);
|
||||
const place = this.places.create(String(tripId), parsed.data as unknown as PlaceCreateInput);
|
||||
const place = await this.places.create(String(tripId), parsed.data as unknown as PlaceCreateInput);
|
||||
this.realtime.broadcast(tripId, 'place:created', { place });
|
||||
await this.mirrorJourneys(() => this.journey.onPlaceCreated(tripId, place.id));
|
||||
return place;
|
||||
@@ -95,7 +95,7 @@ export class PlacesRpc {
|
||||
// Scope the id to the trip before anything else: onPlaceDeleted keys on the place
|
||||
// alone, so an id belonging to a foreign trip would detach THAT trip's journey
|
||||
// entries even though the delete below refuses it.
|
||||
if (!this.places.get(String(tripId), String(placeId))) {
|
||||
if (!(await this.places.get(String(tripId), String(placeId)))) {
|
||||
throw new ForbiddenResource(`no place ${placeId} on trip ${tripId}`);
|
||||
}
|
||||
// Ahead of the DELETE, like the REST route and the MCP tool:
|
||||
@@ -103,7 +103,7 @@ export class PlacesRpc {
|
||||
// finds nothing left to detach and the entries linger as orphans.
|
||||
await this.mirrorJourneys(() => this.journey.onPlaceDeleted(placeId));
|
||||
// The link is gone once the place is, so read it first (#1298).
|
||||
const expenseIds = this.places.linkedExpenseIds(tripId, [placeId]);
|
||||
const expenseIds = await this.places.linkedExpenseIds(tripId, [placeId]);
|
||||
// remove is async (it deletes the place's storage object): await it so the
|
||||
// refusal check sees the resolved answer, not an always-truthy Promise.
|
||||
const { deleted, cancelled } = await this.places.remove(String(tripId), String(placeId));
|
||||
|
||||
@@ -137,7 +137,7 @@ export class PlacesService {
|
||||
private readonly uow: UnitOfWork,
|
||||
) {}
|
||||
|
||||
verifyTripAccess(tripId: string, userId: number) {
|
||||
async verifyTripAccess(tripId: string, userId: number) {
|
||||
return this.dbs.canAccessTrip(Number(tripId), userId);
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ export class PlacesService {
|
||||
* arrive from an older client that had no business knowing it existed, and the
|
||||
* read-back join hands `tags.user_id` straight to the caller.
|
||||
*/
|
||||
private tagsOnTrip(tripId: string | number, tagIds: number[]): number[] {
|
||||
private async tagsOnTrip(tripId: string | number, tagIds: number[]): Promise<number[]> {
|
||||
const unique = [...new Set(tagIds)];
|
||||
if (unique.length === 0) return [];
|
||||
const roster = this.dbs.rosterUserIds(tripId);
|
||||
@@ -240,7 +240,7 @@ export class PlacesService {
|
||||
// Create place
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
create(tripId: string, body: PlaceCreateInput) {
|
||||
async create(tripId: string, body: PlaceCreateInput) {
|
||||
const {
|
||||
name, description, lat, lng, address, category_id, price, currency,
|
||||
place_time, end_time,
|
||||
@@ -278,7 +278,7 @@ export class PlacesService {
|
||||
|
||||
if (tags && tags.length > 0) {
|
||||
const insertTag = this.dbs.prepare('INSERT OR IGNORE INTO place_tags (place_id, tag_id) VALUES (?, ?)');
|
||||
for (const tagId of this.tagsOnTrip(tripId, tags)) {
|
||||
for (const tagId of await this.tagsOnTrip(tripId, tags)) {
|
||||
insertTag.run(placeId, tagId);
|
||||
}
|
||||
}
|
||||
@@ -290,7 +290,7 @@ export class PlacesService {
|
||||
// Get single place
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
get(tripId: string, placeId: string) {
|
||||
async get(tripId: string, placeId: string) {
|
||||
const placeCheck = this.dbs.get('SELECT id FROM places WHERE id = ? AND trip_id = ?', placeId, tripId);
|
||||
if (!placeCheck) return null;
|
||||
return this.dbs.getPlaceWithTags(placeId);
|
||||
@@ -306,22 +306,22 @@ export class PlacesService {
|
||||
body: PlaceUpdateInput,
|
||||
ifMatch?: string,
|
||||
): Promise<PlaceWithTags | UpdateConflict | null> {
|
||||
const { result, reclaim } = this.applyUpdate(tripId, placeId, body, ifMatch);
|
||||
const { result, reclaim } = await this.applyUpdate(tripId, placeId, body, ifMatch);
|
||||
if (reclaim !== undefined) await reclaimPlaceImage(this.storage, reclaim);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The synchronous DB half of update(). Split out so updateMany() can run it
|
||||
* inside a better-sqlite3 transaction (which cannot await) and settle the
|
||||
* storage reclaims after the transaction commits.
|
||||
* The DB half of update(). Split out so updateMany() can run it inside the
|
||||
* same UnitOfWork transaction and settle the storage reclaims after the
|
||||
* transaction commits.
|
||||
*/
|
||||
private applyUpdate(
|
||||
private async applyUpdate(
|
||||
tripId: string,
|
||||
placeId: string,
|
||||
body: PlaceUpdateInput,
|
||||
ifMatch?: string,
|
||||
): { result: PlaceWithTags | UpdateConflict | null; reclaim?: string | null } {
|
||||
): Promise<{ result: PlaceWithTags | UpdateConflict | null; reclaim?: string | null }> {
|
||||
const existingPlace = this.dbs.get<Place>('SELECT * FROM places WHERE id = ? AND trip_id = ?', placeId, tripId);
|
||||
if (!existingPlace) return { result: null };
|
||||
|
||||
@@ -405,7 +405,7 @@ export class PlacesService {
|
||||
this.dbs.run('DELETE FROM place_tags WHERE place_id = ?', placeId);
|
||||
if (tags.length > 0) {
|
||||
const insertTag = this.dbs.prepare('INSERT OR IGNORE INTO place_tags (place_id, tag_id) VALUES (?, ?)');
|
||||
for (const tagId of this.tagsOnTrip(tripId, tags)) {
|
||||
for (const tagId of await this.tagsOnTrip(tripId, tags)) {
|
||||
insertTag.run(placeId, tagId);
|
||||
}
|
||||
}
|
||||
@@ -430,7 +430,7 @@ export class PlacesService {
|
||||
* budget:deleted events for the rows remove()/removeMany() are about to take
|
||||
* with them. Read it BEFORE deleting — afterwards the link is gone.
|
||||
*/
|
||||
linkedExpenseIds(tripId: string | number, placeIds: Array<string | number>): number[] {
|
||||
async linkedExpenseIds(tripId: string | number, placeIds: Array<string | number>): Promise<number[]> {
|
||||
if (placeIds.length === 0) return [];
|
||||
const rows = this.dbs.all<{ id: number }>(
|
||||
`SELECT id FROM budget_items WHERE trip_id = ? AND place_id IN (${placeIds.map(() => '?').join(',')})`,
|
||||
@@ -518,7 +518,7 @@ export class PlacesService {
|
||||
* those key on the place id alone, so an id from another trip would detach
|
||||
* that trip's journey entries even though the delete itself refuses it.
|
||||
*/
|
||||
scopedIds(tripId: string, ids: number[]): number[] {
|
||||
async scopedIds(tripId: string, ids: number[]): Promise<number[]> {
|
||||
if (ids.length === 0) return [];
|
||||
const placeholders = ids.map(() => '?').join(',');
|
||||
const rows = this.dbs.all<{ id: number }>(
|
||||
@@ -542,11 +542,11 @@ export class PlacesService {
|
||||
if (ids.length === 0) return [];
|
||||
const updated: PlaceWithTags[] = [];
|
||||
const reclaims: (string | null)[] = [];
|
||||
this.dbs.transaction(() => {
|
||||
await this.uow.transactional(async () => {
|
||||
for (const id of ids) {
|
||||
// Bulk update sends no If-Match, so applyUpdate() never returns a
|
||||
// conflict here; the guard keeps the types honest.
|
||||
const { result: place, reclaim } = this.applyUpdate(tripId, String(id), body);
|
||||
const { result: place, reclaim } = await this.applyUpdate(tripId, String(id), body);
|
||||
if (place && !isUpdateConflict(place)) updated.push(place);
|
||||
if (reclaim !== undefined) reclaims.push(reclaim);
|
||||
}
|
||||
@@ -562,7 +562,7 @@ export class PlacesService {
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** Build a lookup of names/coords for places already in a trip. */
|
||||
private buildDedupSet(tripId: string): DedupSet {
|
||||
private async buildDedupSet(tripId: string): Promise<DedupSet> {
|
||||
const rows = this.dbs.all<{
|
||||
name: string | null; lat: number | null; lng: number | null;
|
||||
google_place_id: string | null; google_ftid: string | null; osm_id: string | null; amap_poi_id: string | null;
|
||||
@@ -595,8 +595,8 @@ export class PlacesService {
|
||||
* `google_ftid` for the bulk importer's backfill, which is a detail of that
|
||||
* caller and not part of the question "which place is this?".
|
||||
*/
|
||||
findMatchingPlaceId(tripId: string, candidate: PlaceMatchCandidate): number | null {
|
||||
return this.findDuplicatePlace(tripId, candidate)?.id ?? null;
|
||||
async findMatchingPlaceId(tripId: string, candidate: PlaceMatchCandidate): Promise<number | null> {
|
||||
return (await this.findDuplicatePlace(tripId, candidate))?.id ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -623,10 +623,10 @@ export class PlacesService {
|
||||
* `findMatchingPlaceId` wants — a booking with no place name should link to
|
||||
* the hotel that has one — so it is stated rather than removed.
|
||||
*/
|
||||
private findDuplicatePlace(
|
||||
private async findDuplicatePlace(
|
||||
tripId: string,
|
||||
place: PlaceMatchCandidate,
|
||||
): { id: number; google_ftid: string | null } | null {
|
||||
): Promise<{ id: number; google_ftid: string | null } | null> {
|
||||
for (const strategy of placeMatchStrategies(place)) {
|
||||
let hit: { id: number; google_ftid: string | null } | undefined;
|
||||
if (strategy.by === 'externalId') {
|
||||
@@ -663,9 +663,9 @@ export class PlacesService {
|
||||
// Import GPX
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
importGpx(tripId: string, fileBuffer: Buffer, opts: GpxImportOptions = {}): GpxImportResult | null {
|
||||
const result = this.importGpxRows(tripId, fileBuffer, opts);
|
||||
this.colorizeImportedTracks(tripId, result);
|
||||
async importGpx(tripId: string, fileBuffer: Buffer, opts: GpxImportOptions = {}): Promise<GpxImportResult | null> {
|
||||
const result = await this.importGpxRows(tripId, fileBuffer, opts);
|
||||
await this.colorizeImportedTracks(tripId, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -678,7 +678,7 @@ export class PlacesService {
|
||||
* Returns null when the selection yields nothing, so the caller answers 404 rather
|
||||
* than handing over a file that imports as nothing on the other end.
|
||||
*/
|
||||
exportGpx(tripId: string, opts: GpxExportOptions = {}): { gpx: string; filename: string } | null {
|
||||
async exportGpx(tripId: string, opts: GpxExportOptions = {}): Promise<{ gpx: string; filename: string } | null> {
|
||||
const trip = this.dbs.get<{ title: string }>('SELECT title FROM trips WHERE id = ?', tripId);
|
||||
if (!trip) return null;
|
||||
|
||||
@@ -717,7 +717,7 @@ export class PlacesService {
|
||||
return gpx ? { gpx, filename: gpxFilename(trip.title) } : null;
|
||||
}
|
||||
|
||||
private importGpxRows(tripId: string, fileBuffer: Buffer, opts: GpxImportOptions = {}): GpxImportResult | null {
|
||||
private async importGpxRows(tripId: string, fileBuffer: Buffer, opts: GpxImportOptions = {}): Promise<GpxImportResult | null> {
|
||||
const { importWaypoints = true, importRoutes = true, importTracks = true, defaultName } = opts;
|
||||
|
||||
const parsed = gpxParser.parse(fileBuffer.toString('utf-8'));
|
||||
@@ -789,14 +789,14 @@ export class PlacesService {
|
||||
|
||||
if (waypoints.length === 0) return null;
|
||||
|
||||
const dedup = this.buildDedupSet(tripId);
|
||||
const dedup = await this.buildDedupSet(tripId);
|
||||
const insertStmt = this.dbs.prepare(`
|
||||
INSERT INTO places (trip_id, name, description, lat, lng, transport_mode, route_geometry)
|
||||
VALUES (?, ?, ?, ?, ?, 'walking', ?)
|
||||
`);
|
||||
const created: PlaceWithTags[] = [];
|
||||
let skipped = 0;
|
||||
this.dbs.transaction(() => {
|
||||
await this.uow.transactional(async () => {
|
||||
for (const wp of waypoints) {
|
||||
if (isPlaceDuplicate({ name: wp.name, lat: wp.lat, lng: wp.lng }, dedup)) {
|
||||
skipped++;
|
||||
@@ -818,7 +818,7 @@ export class PlacesService {
|
||||
|
||||
async importMapFile(tripId: string, fileBuffer: Buffer, filename: string, opts: KmlImportOptions = {}): Promise<PlaceImportResult> {
|
||||
const result = await this.importMapFileRows(tripId, fileBuffer, filename, opts);
|
||||
this.colorizeImportedTracks(tripId, result);
|
||||
await this.colorizeImportedTracks(tripId, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -834,7 +834,7 @@ export class PlacesService {
|
||||
return this.importKmlPlaces(tripId, kmlBuffer, opts);
|
||||
}
|
||||
|
||||
importKmlPlaces(tripId: string, fileBuffer: Buffer, opts: KmlImportOptions = {}): PlaceImportResult {
|
||||
async importKmlPlaces(tripId: string, fileBuffer: Buffer, opts: KmlImportOptions = {}): Promise<PlaceImportResult> {
|
||||
const { importPoints = true, importPaths = true } = opts;
|
||||
const decoded = decodeUtf8WithWarning(fileBuffer);
|
||||
|
||||
@@ -859,7 +859,7 @@ export class PlacesService {
|
||||
|
||||
const categories = this.dbs.all<{ id: number; name: string }>('SELECT id, name FROM categories');
|
||||
const categoryLookup = buildCategoryNameLookup(categories);
|
||||
const dedup = this.buildDedupSet(tripId);
|
||||
const dedup = await this.buildDedupSet(tripId);
|
||||
const created: PlaceWithTags[] = [];
|
||||
let dupCount = 0;
|
||||
|
||||
@@ -868,7 +868,7 @@ export class PlacesService {
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'walking', ?)
|
||||
`);
|
||||
|
||||
this.dbs.transaction(() => {
|
||||
await this.uow.transactional(async () => {
|
||||
let fallbackIndex = 1;
|
||||
for (const node of placemarkNodes) {
|
||||
const parsedPlacemark = parsePlacemarkNode(node);
|
||||
@@ -955,25 +955,24 @@ export class PlacesService {
|
||||
* it used to hand the same object back, which read like a transformation and
|
||||
* was none.
|
||||
*/
|
||||
private colorizeImportedTracks(tripId: string, result: { places: ImportedPlace[] } | null): void {
|
||||
private async colorizeImportedTracks(tripId: string, result: { places: ImportedPlace[] } | null): Promise<void> {
|
||||
const tracks = result?.places?.filter((p) => p.route_geometry && !p.route_color) ?? [];
|
||||
if (tracks.length === 0) return;
|
||||
|
||||
// Read and write in one transaction so two concurrent imports cannot both
|
||||
// read the same set of free colours.
|
||||
this.dbs.transaction((conn) => {
|
||||
await this.uow.transactional(async () => {
|
||||
const taken = new Set(
|
||||
(conn
|
||||
.prepare('SELECT DISTINCT route_color AS c FROM places WHERE trip_id = ? AND route_color IS NOT NULL')
|
||||
.all(tripId) as { c: string }[]).map((r) => r.c),
|
||||
this.dbs.all<{ c: string }>(
|
||||
'SELECT DISTINCT route_color AS c FROM places WHERE trip_id = ? AND route_color IS NOT NULL', tripId,
|
||||
).map((r) => r.c),
|
||||
);
|
||||
const free = TRACK_COLORS.filter((c) => !taken.has(c));
|
||||
const stmt = conn.prepare('UPDATE places SET route_color = ? WHERE id = ?');
|
||||
tracks.forEach((track, i) => {
|
||||
// Free ones first, then wrap through the whole palette — never reuse a
|
||||
// free colour twice within the same import.
|
||||
const color = i < free.length ? free[i] : TRACK_COLORS[(i - free.length) % TRACK_COLORS.length];
|
||||
stmt.run(color, track.id);
|
||||
this.dbs.run('UPDATE places SET route_color = ? WHERE id = ?', color, track.id);
|
||||
track.route_color = color;
|
||||
});
|
||||
});
|
||||
@@ -1103,7 +1102,7 @@ export class PlacesService {
|
||||
return { error: 'No places with coordinates found in list', status: 400 };
|
||||
}
|
||||
|
||||
const { created, skipped } = this.storeGooglePlaces(tripId, places);
|
||||
const { created, skipped } = await this.storeGooglePlaces(tripId, places);
|
||||
|
||||
if (created.length) {
|
||||
void this.enrichImportedList(tripId, created as EnrichablePlace[], opts);
|
||||
@@ -1119,11 +1118,11 @@ export class PlacesService {
|
||||
* the same rules — a place already on the trip is skipped rather than doubled, and a
|
||||
* row that matches but carries no provider id is given the one this import knows.
|
||||
*/
|
||||
private storeGooglePlaces(
|
||||
private async storeGooglePlaces(
|
||||
tripId: string,
|
||||
places: { name: string; lat: number; lng: number; notes: string | null; googleFtid: string | null }[],
|
||||
): { created: PlaceWithTags[]; skipped: number } {
|
||||
const dedup = this.buildDedupSet(tripId);
|
||||
): Promise<{ created: PlaceWithTags[]; skipped: number }> {
|
||||
const dedup = await this.buildDedupSet(tripId);
|
||||
const insertStmt = this.dbs.prepare(`
|
||||
INSERT INTO places (trip_id, name, lat, lng, notes, google_ftid, transport_mode)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'walking')
|
||||
@@ -1131,7 +1130,7 @@ export class PlacesService {
|
||||
const updateGoogleFtidStmt = this.dbs.prepare('UPDATE places SET google_ftid = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?');
|
||||
const created: PlaceWithTags[] = [];
|
||||
let skipped = 0;
|
||||
this.dbs.transaction(() => {
|
||||
await this.uow.transactional(async () => {
|
||||
for (const p of places) {
|
||||
// One candidate for both halves. Passing the raw parser object to the SQL
|
||||
// half used to mean its provider id never arrived — the field is
|
||||
@@ -1140,7 +1139,7 @@ export class PlacesService {
|
||||
// this candidate's ftid on the backfill below.
|
||||
const candidate = { name: p.name, lat: p.lat, lng: p.lng, google_ftid: p.googleFtid };
|
||||
if (isPlaceDuplicate(candidate, dedup)) {
|
||||
const duplicate = this.findDuplicatePlace(tripId, candidate);
|
||||
const duplicate = await this.findDuplicatePlace(tripId, candidate);
|
||||
if (duplicate && !duplicate.google_ftid && p.googleFtid) {
|
||||
updateGoogleFtidStmt.run(p.googleFtid, duplicate.id);
|
||||
}
|
||||
@@ -1246,7 +1245,7 @@ export class PlacesService {
|
||||
return { error: 'None of the stops in that link could be placed on the map.', status: 400 };
|
||||
}
|
||||
|
||||
const { created, skipped } = this.storeGooglePlaces(tripId, places);
|
||||
const { created, skipped } = await this.storeGooglePlaces(tripId, places);
|
||||
if (created.length) {
|
||||
void this.enrichImportedList(tripId, created as EnrichablePlace[], opts);
|
||||
}
|
||||
@@ -1374,14 +1373,14 @@ export class PlacesService {
|
||||
return { error: 'No places with coordinates found in list', status: 400 };
|
||||
}
|
||||
|
||||
const dedup = this.buildDedupSet(tripId);
|
||||
const dedup = await this.buildDedupSet(tripId);
|
||||
const insertStmt = this.dbs.prepare(`
|
||||
INSERT INTO places (trip_id, name, lat, lng, address, notes, transport_mode)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'walking')
|
||||
`);
|
||||
const created: PlaceWithTags[] = [];
|
||||
let skipped = 0;
|
||||
this.dbs.transaction(() => {
|
||||
await this.uow.transactional(async () => {
|
||||
for (const p of places) {
|
||||
if (isPlaceDuplicate({ name: p.name, lat: p.lat, lng: p.lng }, dedup)) {
|
||||
skipped++;
|
||||
@@ -1579,7 +1578,7 @@ export class PlacesService {
|
||||
* a vote must not 409 another member's offline edit. Returns the refreshed
|
||||
* place (with the new aggregate) or null when the place isn't in the trip.
|
||||
*/
|
||||
rate(tripId: string, placeId: string, userId: number, rating: number | null): PlaceWithTags | null {
|
||||
async rate(tripId: string, placeId: string, userId: number, rating: number | null): Promise<PlaceWithTags | null> {
|
||||
const place = this.dbs.get('SELECT id FROM places WHERE id = ? AND trip_id = ?', placeId, tripId);
|
||||
if (!place) return null;
|
||||
if (rating === null) {
|
||||
|
||||
@@ -66,10 +66,10 @@ export class PublicApiController {
|
||||
* this is the one list in TREK it can answer questions about.
|
||||
*/
|
||||
@Get('bucket-list')
|
||||
listBucketList(@Req() req: Request): PublicApiBucketList {
|
||||
async listBucketList(@Req() req: Request): Promise<PublicApiBucketList> {
|
||||
this.limit(req);
|
||||
requireScope(req, 'bucket-list');
|
||||
return { items: this.api.listBucketList(requireUserId(req)) };
|
||||
return { items: await this.api.listBucketList(requireUserId(req)) };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,11 +80,11 @@ export class PublicApiController {
|
||||
* endpoint into a way to count someone else's trips.
|
||||
*/
|
||||
@Get('trips/:id')
|
||||
getTrip(
|
||||
async getTrip(
|
||||
@Req() req: Request,
|
||||
@Param('id') id: string,
|
||||
@Query('include') include?: string,
|
||||
): PublicApiTrip {
|
||||
): Promise<PublicApiTrip> {
|
||||
this.limit(req);
|
||||
requireScope(req, 'trips');
|
||||
const tripId = parseTripId(id);
|
||||
@@ -102,7 +102,7 @@ export class PublicApiController {
|
||||
// The grant goes in as well as the narrowed include: days are implied by any
|
||||
// section that hangs off them, and the implied block must still be measured
|
||||
// against what the key may read.
|
||||
const trip = this.api.getTrip(tripId, requireUserId(req), allowed, grantedScopes(req));
|
||||
const trip = await this.api.getTrip(tripId, requireUserId(req), allowed, grantedScopes(req));
|
||||
if (!trip) {
|
||||
throw new HttpException({ error: 'Trip not found' }, 404);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export class PublicApiService {
|
||||
* turns both into the same 404, so the endpoint cannot be used to probe which
|
||||
* trip ids exist.
|
||||
*/
|
||||
getTrip(tripId: number, userId: number, include: PublicApiInclude[], granted: readonly string[] = include): PublicApiTrip | null {
|
||||
async getTrip(tripId: number, userId: number, include: PublicApiInclude[], granted: readonly string[] = include): Promise<PublicApiTrip | null> {
|
||||
if (!this.db.canAccessTrip(tripId, userId)) return null;
|
||||
const row = this.db.get<TripRow>(
|
||||
`SELECT id, title, description, start_date, end_date, currency, is_archived, updated_at
|
||||
@@ -80,19 +80,19 @@ export class PublicApiService {
|
||||
// through the container those places arrive in. The shell it does get is the
|
||||
// join key the children are useless without.
|
||||
if (DAY_SCOPED.some((section) => include.includes(section))) {
|
||||
trip.days = this.buildDays(tripId, include, granted.includes('days'));
|
||||
trip.days = await this.buildDays(tripId, include, granted.includes('days'));
|
||||
}
|
||||
if (include.includes('places')) {
|
||||
trip.unplanned_places = this.buildUnplannedPlaces(tripId);
|
||||
trip.unplanned_places = await this.buildUnplannedPlaces(tripId);
|
||||
}
|
||||
if (include.includes('reservations')) {
|
||||
trip.unscheduled_reservations = this.buildUnscheduledReservations(tripId);
|
||||
trip.unscheduled_reservations = await this.buildUnscheduledReservations(tripId);
|
||||
}
|
||||
if (include.includes('accommodations')) {
|
||||
trip.accommodations = this.buildAccommodations(tripId);
|
||||
trip.accommodations = await this.buildAccommodations(tripId);
|
||||
}
|
||||
if (include.includes('travellers')) {
|
||||
trip.travellers = this.buildTravellers(tripId);
|
||||
trip.travellers = await this.buildTravellers(tripId);
|
||||
}
|
||||
return trip;
|
||||
}
|
||||
@@ -105,7 +105,7 @@ export class PublicApiService {
|
||||
* rather than queried per day — a two-week trip would otherwise cost 42 round
|
||||
* trips for the same rows.
|
||||
*/
|
||||
private buildDays(tripId: number, include: PublicApiInclude[], dayFields: boolean): PublicApiDay[] {
|
||||
private async buildDays(tripId: number, include: PublicApiInclude[], dayFields: boolean): Promise<PublicApiDay[]> {
|
||||
const days = this.db.all<DayRow>(
|
||||
`SELECT id, day_number, date, title, notes
|
||||
FROM days WHERE trip_id = ? ORDER BY day_number ASC`,
|
||||
@@ -113,10 +113,10 @@ export class PublicApiService {
|
||||
);
|
||||
if (days.length === 0) return [];
|
||||
|
||||
const placesByDay = include.includes('places') ? this.placesByDay(tripId) : new Map();
|
||||
const notesByDay = include.includes('notes') ? this.dayNotesByDay(tripId) : new Map();
|
||||
const placesByDay = include.includes('places') ? await this.placesByDay(tripId) : new Map();
|
||||
const notesByDay = include.includes('notes') ? await this.dayNotesByDay(tripId) : new Map();
|
||||
const reservationsByDay = include.includes('reservations')
|
||||
? this.reservationsByDay(tripId)
|
||||
? await this.reservationsByDay(tripId)
|
||||
: new Map();
|
||||
|
||||
return days.map((day) => ({
|
||||
@@ -143,7 +143,7 @@ export class PublicApiService {
|
||||
* listed here as well it would read as two different intentions. Same rule as
|
||||
* the shortlist below.
|
||||
*/
|
||||
private placesByDay(tripId: number): Map<number, PublicApiPlace[]> {
|
||||
private async placesByDay(tripId: number): Promise<Map<number, PublicApiPlace[]>> {
|
||||
const rows = this.db.all<PlaceRow>(
|
||||
`SELECT da.day_id,
|
||||
p.name, p.address, p.lat, p.lng, p.place_time, p.end_time,
|
||||
@@ -160,7 +160,7 @@ export class PublicApiService {
|
||||
return groupBy(rows, (r: PlaceRow) => r.day_id, toPlace);
|
||||
}
|
||||
|
||||
private dayNotesByDay(tripId: number): Map<number, PublicApiDayNote[]> {
|
||||
private async dayNotesByDay(tripId: number): Promise<Map<number, PublicApiDayNote[]>> {
|
||||
const rows = this.db.all<DayNoteRow>(
|
||||
`SELECT day_id, text, time
|
||||
FROM day_notes WHERE trip_id = ?
|
||||
@@ -180,7 +180,7 @@ export class PublicApiService {
|
||||
* repeated on each — a consumer that sees the same flight on three days has no
|
||||
* way to tell that from three flights.
|
||||
*/
|
||||
private reservationsByDay(tripId: number): Map<number, PublicApiReservation[]> {
|
||||
private async reservationsByDay(tripId: number): Promise<Map<number, PublicApiReservation[]>> {
|
||||
const rows = this.db.all<ReservationRow>(
|
||||
`SELECT day_id, type, title, location, reservation_time, reservation_end_time,
|
||||
status, notes
|
||||
@@ -198,7 +198,7 @@ export class PublicApiService {
|
||||
* Stored as day ids, reported as ISO dates: a consumer has no way to look up a
|
||||
* TREK day id, and the dates are what it actually needs to match its own nights.
|
||||
*/
|
||||
private buildAccommodations(tripId: number): PublicApiAccommodation[] {
|
||||
private async buildAccommodations(tripId: number): Promise<PublicApiAccommodation[]> {
|
||||
const rows = this.db.all<AccommodationRow>(
|
||||
`SELECT p.name, p.address, p.lat, p.lng,
|
||||
ds.date AS start_date, de.date AS end_date,
|
||||
@@ -240,7 +240,7 @@ export class PublicApiService {
|
||||
* but it is not a shortlist entry and it is already reported in full under
|
||||
* `accommodations` — listing it twice would read as two different intentions.
|
||||
*/
|
||||
private buildUnplannedPlaces(tripId: number): PublicApiPlace[] {
|
||||
private async buildUnplannedPlaces(tripId: number): Promise<PublicApiPlace[]> {
|
||||
const rows = this.db.all<Omit<PlaceRow, 'day_id'>>(
|
||||
`SELECT p.name, p.address, p.lat, p.lng, p.place_time, p.end_time,
|
||||
p.duration_minutes, p.notes, p.transport_mode,
|
||||
@@ -261,7 +261,7 @@ export class PublicApiService {
|
||||
* sets it null rather than cascading, so a flight can outlive the day it was
|
||||
* pinned to. Reporting only day-bound bookings would quietly lose those.
|
||||
*/
|
||||
private buildUnscheduledReservations(tripId: number): PublicApiReservation[] {
|
||||
private async buildUnscheduledReservations(tripId: number): Promise<PublicApiReservation[]> {
|
||||
const rows = this.db.all<Omit<ReservationRow, 'day_id'>>(
|
||||
`SELECT type, title, location, reservation_time, reservation_end_time,
|
||||
status, notes
|
||||
@@ -285,7 +285,7 @@ export class PublicApiService {
|
||||
* whether TREK shows the feature, not whether the rows exist, and a key whose
|
||||
* answers change when an unrelated toggle moves is a key nobody can build on.
|
||||
*/
|
||||
listBucketList(userId: number): PublicApiBucketListItem[] {
|
||||
async listBucketList(userId: number): Promise<PublicApiBucketListItem[]> {
|
||||
const rows = this.db.all<BucketListRow>(
|
||||
`SELECT name, lat, lng, country_code, notes, target_date
|
||||
FROM bucket_list WHERE user_id = ?
|
||||
@@ -310,7 +310,7 @@ export class PublicApiService {
|
||||
* itinerary, not for enumerating the people around them. What it returns is
|
||||
* exactly what those people already see on the trip in TREK.
|
||||
*/
|
||||
private buildTravellers(tripId: number): PublicApiTraveller[] {
|
||||
private async buildTravellers(tripId: number): Promise<PublicApiTraveller[]> {
|
||||
const rows = this.db.all<TravellerRow>(
|
||||
`SELECT u.username, 1 AS is_owner
|
||||
FROM trips t JOIN users u ON u.id = t.user_id
|
||||
|
||||
@@ -68,7 +68,7 @@ export class ReservationsController {
|
||||
@Headers('x-socket-id') socketId?: string,
|
||||
) {
|
||||
const body = rawBody as ReservationBody & { title: string };
|
||||
this.rejectForeignReferences(tripId, body);
|
||||
await this.rejectForeignReferences(tripId, body);
|
||||
const { reservation, accommodationCreated } = await this.reservations.create(tripId, body as never);
|
||||
if (accommodationCreated) {
|
||||
this.reservations.broadcast(tripId, 'accommodation:created', {}, socketId);
|
||||
@@ -108,7 +108,7 @@ export class ReservationsController {
|
||||
if (!current) {
|
||||
throw new HttpException({ error: 'Reservation not found' }, 404);
|
||||
}
|
||||
this.rejectForeignReferences(tripId, body);
|
||||
await this.rejectForeignReferences(tripId, body);
|
||||
const { reservation, accommodationChanged } = await this.reservations.update(id, tripId, body as never, current as never);
|
||||
if (accommodationChanged) {
|
||||
this.reservations.broadcast(tripId, 'accommodation:updated', {}, socketId);
|
||||
@@ -180,12 +180,12 @@ export class ReservationsController {
|
||||
* trip" about an id that is part of nothing would send the caller looking in
|
||||
* the wrong place.
|
||||
*/
|
||||
private rejectForeignReferences(tripId: string, body: ReservationBody): void {
|
||||
private async rejectForeignReferences(tripId: string, body: ReservationBody): Promise<void> {
|
||||
const offenders = this.reservations.referencesOutsideTrip(tripId, body as never);
|
||||
if (offenders.length > 0) {
|
||||
throw new HttpException({ error: `Not part of this trip: ${offenders.join(', ')}` }, 400);
|
||||
}
|
||||
const unknown = this.reservations.unresolvedReferences(tripId, body as never);
|
||||
const unknown = await this.reservations.unresolvedReferences(tripId, body as never);
|
||||
if (unknown.length > 0) {
|
||||
throw new HttpException({ error: `Unknown reference: ${unknown.join(', ')}` }, 400);
|
||||
}
|
||||
|
||||
@@ -368,13 +368,13 @@ export class ReservationsMcp {
|
||||
// Validate that all referenced IDs belong to this trip
|
||||
if (day_id && !(await this.days.getDay(day_id, tripId)))
|
||||
return errorResult('day_id does not belong to this trip.');
|
||||
if (place_id && !this.assignments.placeExists(place_id, tripId))
|
||||
if (place_id && !(await this.assignments.placeExists(place_id, tripId)))
|
||||
return errorResult('place_id does not belong to this trip.');
|
||||
if (start_day_id && !(await this.days.getDay(start_day_id, tripId)))
|
||||
return errorResult('start_day_id does not belong to this trip.');
|
||||
if (end_day_id && !(await this.days.getDay(end_day_id, tripId)))
|
||||
return errorResult('end_day_id does not belong to this trip.');
|
||||
if (assignment_id && !this.assignments.getAssignmentForTrip(assignment_id, tripId))
|
||||
if (assignment_id && !(await this.assignments.getAssignmentForTrip(assignment_id, tripId)))
|
||||
return errorResult('assignment_id does not belong to this trip.');
|
||||
|
||||
const createAccommodation = (type === 'hotel' && place_id && start_day_id && end_day_id)
|
||||
@@ -443,9 +443,9 @@ export class ReservationsMcp {
|
||||
const existing = this.reservations.getReservation(reservationId, tripId);
|
||||
if (!existing) return errorResult('Reservation not found.');
|
||||
|
||||
if (place_id != null && !this.assignments.placeExists(place_id, tripId))
|
||||
if (place_id != null && !(await this.assignments.placeExists(place_id, tripId)))
|
||||
return errorResult('place_id does not belong to this trip.');
|
||||
if (assignment_id != null && !this.assignments.getAssignmentForTrip(assignment_id, tripId))
|
||||
if (assignment_id != null && !(await this.assignments.getAssignmentForTrip(assignment_id, tripId)))
|
||||
return errorResult('assignment_id does not belong to this trip.');
|
||||
|
||||
const { reservation } = await this.reservations.update(reservationId, tripId, {
|
||||
@@ -577,7 +577,7 @@ export class ReservationsMcp {
|
||||
if (!current) return errorResult('Reservation not found.');
|
||||
if (current.type !== 'hotel') return errorResult('Reservation is not of type hotel.');
|
||||
|
||||
if (!this.assignments.placeExists(place_id, tripId))
|
||||
if (!(await this.assignments.placeExists(place_id, tripId)))
|
||||
return errorResult('place_id does not belong to this trip.');
|
||||
if (!(await this.days.getDay(start_day_id, tripId)))
|
||||
return errorResult('start_day_id does not belong to this trip.');
|
||||
|
||||
@@ -38,7 +38,7 @@ export class ReservationsRpc {
|
||||
const input = parsed.data as Record<string, unknown>;
|
||||
this.requireValidEndpoints(input.endpoints);
|
||||
await this.guards.requireTripEdit(tripId, actor, RESERVATION_EDIT_ACTION);
|
||||
this.requireOwnReferences(tripId, input);
|
||||
await this.requireOwnReferences(tripId, input);
|
||||
const { reservation, accommodationCreated } = await this.reservations.create(String(tripId), input as never);
|
||||
if (accommodationCreated) this.realtime.broadcast(tripId, 'accommodation:created', {}, undefined);
|
||||
const i = input as { title?: string; type?: string; create_budget_entry?: unknown };
|
||||
@@ -60,7 +60,7 @@ export class ReservationsRpc {
|
||||
await this.guards.requireTripEdit(tripId, actor, RESERVATION_EDIT_ACTION);
|
||||
const current = this.reservations.getReservation(String(reservationId), String(tripId));
|
||||
if (!current) throw new ForbiddenResource(`no reservation ${reservationId} on trip ${tripId}`);
|
||||
this.requireOwnReferences(tripId, input);
|
||||
await this.requireOwnReferences(tripId, input);
|
||||
const { reservation, accommodationChanged } = await this.reservations.update(String(reservationId), String(tripId), input as never, current as never);
|
||||
if (accommodationChanged) this.realtime.broadcast(tripId, 'accommodation:updated', {}, undefined);
|
||||
const cur = current as { title: string; type?: string };
|
||||
@@ -109,10 +109,10 @@ export class ReservationsRpc {
|
||||
* resolves to nothing is a foreign-key error the plugin reads as a crash (#2355),
|
||||
* so it is named here as well, after the ownership check.
|
||||
*/
|
||||
private requireOwnReferences(tripId: number, input: Record<string, unknown>): void {
|
||||
private async requireOwnReferences(tripId: number, input: Record<string, unknown>): Promise<void> {
|
||||
const offenders = this.reservations.referencesOutsideTrip(String(tripId), input as never);
|
||||
if (offenders.length > 0) throw new ForbiddenResource(`not part of trip ${tripId}: ${offenders.join(', ')}`);
|
||||
const unknown = this.reservations.unresolvedReferences(String(tripId), input as never);
|
||||
const unknown = await this.reservations.unresolvedReferences(String(tripId), input as never);
|
||||
if (unknown.length > 0) throw new BadParams(`unknown reference: ${unknown.join(', ')}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -587,7 +587,7 @@ export class ReservationsService {
|
||||
* that belongs to another trip is named here too — the REST controller asks
|
||||
* the older guard first, so that case keeps its own answer.
|
||||
*/
|
||||
unresolvedReferences(tripId: string | number, data: CreateReservationData | UpdateReservationData): string[] {
|
||||
async unresolvedReferences(tripId: string | number, data: CreateReservationData | UpdateReservationData): Promise<string[]> {
|
||||
const offenders: string[] = [];
|
||||
const onTrip = (table: 'days' | 'places', id: unknown) =>
|
||||
!!this.db.get(`SELECT id FROM ${table} WHERE id = ? AND trip_id = ?`, id, tripId);
|
||||
@@ -610,7 +610,7 @@ export class ReservationsService {
|
||||
// on a write that currently succeeds is not what this guard is for.
|
||||
if (data.create_accommodation && data.type === 'hotel') {
|
||||
const acc = data.create_accommodation;
|
||||
const errors = this.accommodations.validateAccommodationRefs(
|
||||
const errors = await this.accommodations.validateAccommodationRefs(
|
||||
tripId, acc.place_id || undefined, acc.start_day_id || undefined, acc.end_day_id || undefined,
|
||||
);
|
||||
for (const { field } of errors) offenders.push(`create_accommodation.${field}`);
|
||||
|
||||
@@ -65,13 +65,13 @@ export class GoogleRouteService {
|
||||
if (!(await this.permissions.checkPermission(action, role, access.user_id, userId, access.user_id !== userId)))
|
||||
throw new HttpException({ error: 'Permission denied' }, 403);
|
||||
}
|
||||
if (!this.assignments.dayExists(String(input.dayId), String(tripId))) throw new HttpException({ error: 'Day not found' }, 404);
|
||||
if (!(await this.assignments.dayExists(String(input.dayId), String(tripId)))) throw new HttpException({ error: 'Day not found' }, 404);
|
||||
// `map` cannot await the now-async assignment write, so the same per-stop
|
||||
// sequence runs as an explicit loop inside the transaction.
|
||||
const imported = await this.uow.transactional(async () => {
|
||||
const rows: { place: ReturnType<PlacesService['create']>; assignment: Awaited<ReturnType<AssignmentsService['createAssignment']>> }[] = [];
|
||||
const rows: { place: Awaited<ReturnType<PlacesService['create']>>; assignment: Awaited<ReturnType<AssignmentsService['createAssignment']>> }[] = [];
|
||||
for (const stop of input.stops) {
|
||||
const place = this.places.create(String(tripId), { ...stop, transport_mode: 'car', duration_minutes: 0 });
|
||||
const place = await this.places.create(String(tripId), { ...stop, transport_mode: 'car', duration_minutes: 0 });
|
||||
const assignment = await this.assignments.createAssignment(input.dayId, place.id);
|
||||
rows.push({ place, assignment });
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export class TripReadModelService {
|
||||
const { days: rawDays } = await this.days.list(tripId);
|
||||
const days = rawDays.map(({ notes_items, ...day }) => ({ ...day, notes: notes_items }));
|
||||
|
||||
const accommodations = this.accommodations.list(tripId);
|
||||
const accommodations = await this.accommodations.list(tripId);
|
||||
|
||||
const budgetItems = await this.budget.listBudgetItems(tripId);
|
||||
const budget = {
|
||||
@@ -111,7 +111,7 @@ export class TripReadModelService {
|
||||
budgetItems: await this.budget.listBudgetItems(tripId),
|
||||
reservations: this.reservations.list(tripId),
|
||||
files: await this.files.listFiles(tripId, false),
|
||||
accommodations: this.accommodations.list(tripId),
|
||||
accommodations: await this.accommodations.list(tripId),
|
||||
members: [owner, ...(members || [])].filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export class TripsRpc {
|
||||
|
||||
@PluginMethod('trips.getAccommodations', { permission: 'db:read:trips' })
|
||||
getAccommodations(params: Record<string, unknown>, ctx: PluginRpcContext): unknown {
|
||||
return this.guards.tripRead(params, ctx, () => this.accommodations.list(num(params.tripId, 'tripId')) as unknown[]);
|
||||
return this.guards.tripRead(params, ctx, async () => (await this.accommodations.list(num(params.tripId, 'tripId'))) as unknown[]);
|
||||
}
|
||||
|
||||
@PluginMethod('trips.listMine', { permission: 'db:read:trips' })
|
||||
|
||||
Reference in New Issue
Block a user