mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
fix(ci): the second round of the quick-win checks on the server
oidc_auto_redirect is managed-locked in the registry too, costs_mode is read as the mode it is, the days of a dated trip are read through the factories, and the dashboard's country order moves out of the atlas service into a module of its own, so the service keeps its size.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
"src/db/repositories/Reservations.repository.ts": 1559,
|
||||
"src/db/repositories/Trips.repository.ts": 1120,
|
||||
"src/db/repositories/Users.repository.ts": 1445,
|
||||
"src/nest/atlas/atlas.service.ts": 1177,
|
||||
"src/nest/atlas/atlas.service.ts": 1174,
|
||||
"src/nest/auth/auth.service.ts": 1143,
|
||||
"src/nest/budget/budget.service.ts": 1607,
|
||||
"src/nest/collections/collections.service.ts": 1414,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* The visited countries in the order the dashboard shows them (#2648): most
|
||||
* recently visited first, by the last day of the latest started trip that
|
||||
* reached each one, so the few flags the tile has room for are the ones of the
|
||||
* last journeys rather than the start of the alphabet. A country only marked by
|
||||
* hand carries no date and follows, in the order it was marked.
|
||||
*/
|
||||
export class CountryVisits {
|
||||
private readonly lastVisit = new Map<string, string>();
|
||||
|
||||
/** Keeps the latest trip day a country was reached on. */
|
||||
note(code: string | null | undefined, tripLastDate: string | null): void {
|
||||
if (!code) return;
|
||||
const key = code.toUpperCase();
|
||||
const when = tripLastDate ?? '';
|
||||
const known = this.lastVisit.get(key);
|
||||
if (known === undefined || when > known) this.lastVisit.set(key, when);
|
||||
}
|
||||
|
||||
/** The reached countries, latest first, then the hand-marked ones not among them. */
|
||||
ordered(manual: Iterable<string>): string[] {
|
||||
// Array.prototype.sort is stable, so countries of the same trip keep the order they were found in.
|
||||
const dated = [...this.lastVisit].sort(([, a], [, b]) => (a < b ? 1 : a > b ? -1 : 0)).map(([code]) => code);
|
||||
return [...new Set([...dated, ...manual])];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One row per coordinate, the one of the latest trip through it: the country
|
||||
* lookup is a point-in-polygon scan and runs once per coordinate.
|
||||
*/
|
||||
export function latestPerCoordinate<T extends { lat: number; lng: number; trip_last_date: string | null }>(
|
||||
rows: T[],
|
||||
): T[] {
|
||||
const byCoord = new Map<string, T>();
|
||||
for (const row of rows) {
|
||||
const key = `${row.lat},${row.lng}`;
|
||||
const known = byCoord.get(key);
|
||||
if (!known || (row.trip_last_date ?? '') > (known.trip_last_date ?? '')) byCoord.set(key, row);
|
||||
}
|
||||
return [...byCoord.values()];
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import { transferEndpointIds } from './transfer-endpoints';
|
||||
import { countryVisitDates } from './visit-dates';
|
||||
import { placeCountryResolver } from './place-countries';
|
||||
import { haversineKm } from '../common/geo';
|
||||
import { CountryVisits, latestPerCoordinate } from './atlas-visit-order';
|
||||
|
||||
/** The part of a place that its cached region is derived from. */
|
||||
type LocatedPlace = Pick<PlaceRow, 'id' | 'lat' | 'lng' | 'address'>;
|
||||
@@ -70,19 +71,6 @@ function daysBetween(today: string, isoDate: string): number {
|
||||
*/
|
||||
type EndpointRow = TravelerOwnedEndpointRow;
|
||||
|
||||
/**
|
||||
* The visited countries in the order the dashboard shows them (#2648): most
|
||||
* recently visited first, by the last day of the latest started trip that
|
||||
* reached each one, so the few flags the tile has room for are the ones of the
|
||||
* last journeys rather than the start of the alphabet. A country only marked
|
||||
* by hand carries no date and follows, in the order it was marked.
|
||||
*/
|
||||
export function countriesByLastVisit(lastVisit: ReadonlyMap<string, string>, manual: Iterable<string>): string[] {
|
||||
// Array.prototype.sort is stable, so countries of the same trip keep the order they were found in.
|
||||
const dated = [...lastVisit].sort(([, a], [, b]) => (a < b ? 1 : a > b ? -1 : 0)).map(([code]) => code);
|
||||
return [...new Set([...dated, ...manual])];
|
||||
}
|
||||
|
||||
export type CreateBucketData = {
|
||||
name: string;
|
||||
lat?: number | null;
|
||||
@@ -1087,21 +1075,14 @@ export class AtlasService {
|
||||
const manualCountries = (await this.visitedCountries.listCodesForUser(userId)) // AT43
|
||||
.filter(Boolean)
|
||||
.map(code => code.toUpperCase());
|
||||
const lastVisit = new Map<string, string>();
|
||||
const noteVisit = (code: string | null | undefined, tripLastDate: string | null) => {
|
||||
if (!code) return;
|
||||
const key = code.toUpperCase();
|
||||
const when = tripLastDate ?? '';
|
||||
const known = lastVisit.get(key);
|
||||
if (known === undefined || when > known) lastVisit.set(key, when);
|
||||
};
|
||||
const visits = new CountryVisits();
|
||||
|
||||
// Only trips that have already started count as visited — a country you have merely
|
||||
// booked a trip to isn't stamped in the passport yet, and one you jotted down without
|
||||
// any dates even less so (#1048). `today` is UTC, matching tripVisitStatus.
|
||||
const countryOf = placeCountryResolver();
|
||||
for (const place of await this.placeRegions.listStartedTripPlaceCountriesForUser(userId, today)) { // AT44
|
||||
noteVisit(countryOf(place, place.country_code), place.trip_last_date);
|
||||
visits.note(countryOf(place, place.country_code), place.trip_last_date);
|
||||
}
|
||||
|
||||
// Transport bookings don't create a place row, so their geocoded endpoints never
|
||||
@@ -1120,17 +1101,8 @@ export class AtlasService {
|
||||
// in the projection, so collapse here instead: getCountryFromCoords is a
|
||||
// point-in-polygon scan and must stay at one run per coordinate. The latest
|
||||
// trip through a coordinate is the one its country is dated by.
|
||||
const endpointCoords = new Map<string, { lat: number; lng: number; tripLastDate: string | null }>();
|
||||
for (const e of endpointRows) {
|
||||
if (transfers.has(e.id)) continue;
|
||||
const key = `${e.lat},${e.lng}`;
|
||||
const known = endpointCoords.get(key);
|
||||
if (!known || (e.trip_last_date ?? '') > (known.tripLastDate ?? '')) {
|
||||
endpointCoords.set(key, { lat: e.lat, lng: e.lng, tripLastDate: e.trip_last_date });
|
||||
}
|
||||
}
|
||||
for (const e of endpointCoords.values()) {
|
||||
noteVisit(getCountryFromCoords(e.lat, e.lng), e.tripLastDate);
|
||||
for (const e of latestPerCoordinate(endpointRows.filter(e => !transfers.has(e.id)))) {
|
||||
visits.note(getCountryFromCoords(e.lat, e.lng), e.trip_last_date);
|
||||
}
|
||||
|
||||
// Countries the user removed in Atlas stay removed on the dashboard too, so the
|
||||
@@ -1138,7 +1110,7 @@ export class AtlasService {
|
||||
const hidden = new Set([...(await this.getHiddenCountries(userId))].map(code => code.toUpperCase()));
|
||||
|
||||
return {
|
||||
countries: countriesByLastVisit(lastVisit, manualCountries).filter(code => !hidden.has(code)),
|
||||
countries: visits.ordered(manualCountries).filter(code => !hidden.has(code)),
|
||||
cities: [...cities],
|
||||
coords,
|
||||
totalTrips: tripStats.trips || 0,
|
||||
|
||||
@@ -308,8 +308,7 @@ export class AuthService {
|
||||
const userCount = await this.usersRepo.countNonGuest();
|
||||
const isDemo = readEnv().demo.enabled;
|
||||
const toggles = await this.resolveAuthToggles();
|
||||
// One directory deeper than the legacy src/services location — the extra
|
||||
// '../' keeps resolving to the workspace package.json.
|
||||
// One directory deeper than the old src/services, so the extra '../' still reaches the package.json.
|
||||
const version: string = readEnv().app.appVersion ?? require('../../../package.json').version;
|
||||
// Asked through the same resolver the search itself uses, so the client can
|
||||
// never show Google features that rest on a key this caller does not get —
|
||||
@@ -359,9 +358,7 @@ export class AuthService {
|
||||
password_registration: isDemo ? false : toggles.password_registration,
|
||||
oidc_login: toggles.oidc_login,
|
||||
oidc_registration: isDemo ? false : toggles.oidc_registration,
|
||||
// Read by the login page only where password login is off: anything but
|
||||
// 'false' keeps the redirect every such instance had before (#2925).
|
||||
oidc_auto_redirect: settings.get('oidc_auto_redirect') !== 'false',
|
||||
oidc_auto_redirect: settings.get('oidc_auto_redirect') !== 'false', // read only without password login (#2925)
|
||||
// Passkey login: the instance toggle + whether a usable RP ID resolves for
|
||||
// this deployment. The login page shows the passkey button only when both
|
||||
// are true. `passkey_configured` stays a pure boolean — it never leaks the
|
||||
|
||||
@@ -86,7 +86,7 @@ export const APP_SETTINGS = {
|
||||
oidc_registration: { schema: flag, default: 'true', adminForm: true, managedLocked: true },
|
||||
// Whether an instance without password login sends a visitor straight to the
|
||||
// identity provider. Off shows the login page with its SSO button first (#2925).
|
||||
oidc_auto_redirect: { schema: flag, default: 'true', adminForm: true, publicConfig: true },
|
||||
oidc_auto_redirect: { schema: flag, default: 'true', adminForm: true, publicConfig: true, managedLocked: true },
|
||||
passkey_login: { schema: flag, default: 'false', adminForm: true },
|
||||
webauthn_rp_id: { schema: text, default: null, adminForm: true, managedLocked: true },
|
||||
webauthn_origins: { schema: text, default: null, adminForm: true, managedLocked: true },
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
type DayGridRemoval,
|
||||
type TrekWsPayload,
|
||||
type TrekWsTripEventName,
|
||||
type TripCostsMode,
|
||||
type TripUpdateRequest,
|
||||
} from '@trek/shared';
|
||||
import { RealtimeService } from '../realtime/realtime.service';
|
||||
@@ -473,7 +474,8 @@ export class TripsService {
|
||||
const newCurrency = currency || trip.currency;
|
||||
const newArchived = is_archived !== undefined ? (is_archived ? 1 : 0) : trip.is_archived;
|
||||
const newCover = cover_image !== undefined ? cover_image : trip.cover_image;
|
||||
const newCostsMode = costs_mode ?? trip.costs_mode;
|
||||
// The column's CHECK holds a stored trip to the two modes.
|
||||
const newCostsMode = costs_mode ?? (trip.costs_mode as TripCostsMode);
|
||||
const oldReminder = (trip as any).reminder_days ?? 3;
|
||||
const newReminder = reminder_days !== undefined
|
||||
? (Number(reminder_days) >= 0 && Number(reminder_days) <= 30 ? Number(reminder_days) : oldReminder)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
import { db as testDb } from '../../../src/db/database';
|
||||
import { BudgetItems } from '../../../src/db/entities/BudgetItems.entity';
|
||||
import { Days } from '../../../src/db/entities/Days.entity';
|
||||
import { ShareTokens } from '../../../src/db/entities/ShareTokens.entity';
|
||||
import { TripMembers } from '../../../src/db/entities/TripMembers.entity';
|
||||
import { Trips } from '../../../src/db/entities/Trips.entity';
|
||||
@@ -15,7 +16,7 @@ import { NotificationsService } from '../../../src/nest/notifications/notificati
|
||||
import { invalidatePermissionsCache } from '../../../src/nest/permissions/permissions-cache';
|
||||
import { createUser, createAdmin, createTrip, addTripMember, createDayNote } from '../../helpers/factories';
|
||||
import { addBudgetItemMember, addBudgetItemPayer, makeBudgetItem } from '../../helpers/factories/budget';
|
||||
import { countRows, findRow, updateRows } from '../../helpers/factories/rows';
|
||||
import { countRows, findRow, findRows, updateRows } from '../../helpers/factories/rows';
|
||||
import { setAppSetting } from '../../helpers/factories/settings';
|
||||
import { makeShareToken } from '../../helpers/factories/trips';
|
||||
import { FakeRealtimeService } from '../../helpers/fake-realtime';
|
||||
@@ -496,9 +497,7 @@ describe('Tool: export_trip_ics', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Paris Trip', start_date: '2025-06-01', end_date: '2025-06-05' });
|
||||
// A dated trip comes with its days; the first one is the 1st of June.
|
||||
const day = testDb.prepare('SELECT id FROM days WHERE trip_id = ? AND day_number = 1').get(trip.id) as {
|
||||
id: number;
|
||||
};
|
||||
const [day] = await findRows(orm, Days, { trip: trip.id }, { id: 'asc' });
|
||||
createDayNote(testDb, day.id, trip.id, { text: 'Pick up the museum pass' });
|
||||
await withHarness(user.id, async (h) => {
|
||||
const call = async (args: Record<string, unknown>) =>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// The dashboard's order of visited countries (#2648).
|
||||
import { CountryVisits, latestPerCoordinate } from '../../../src/nest/atlas/atlas-visit-order';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('CountryVisits', () => {
|
||||
it('ATLAS-ORDER-001: puts the latest reached country first and the hand-marked ones after', () => {
|
||||
const visits = new CountryVisits();
|
||||
visits.note('pt', '2025-04-10');
|
||||
visits.note('JP', '2026-05-02');
|
||||
visits.note('pt', '2026-01-20');
|
||||
visits.note(null, '2026-09-01');
|
||||
expect(visits.ordered(['IS', 'JP'])).toEqual(['JP', 'PT', 'IS']);
|
||||
});
|
||||
|
||||
it('ATLAS-ORDER-002: keeps the order a country of one trip was found in', () => {
|
||||
const visits = new CountryVisits();
|
||||
visits.note('FR', '2026-03-01');
|
||||
visits.note('ES', '2026-03-01');
|
||||
visits.note('IT', null);
|
||||
expect(visits.ordered([])).toEqual(['FR', 'ES', 'IT']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('latestPerCoordinate', () => {
|
||||
it('ATLAS-ORDER-003: keeps one row per coordinate, the one of the latest trip', () => {
|
||||
const rows = [
|
||||
{ id: 1, lat: 1, lng: 2, trip_last_date: '2025-01-01' },
|
||||
{ id: 2, lat: 1, lng: 2, trip_last_date: '2026-01-01' },
|
||||
{ id: 3, lat: 3, lng: 4, trip_last_date: null },
|
||||
];
|
||||
expect(latestPerCoordinate(rows).map((r) => r.id)).toEqual([2, 3]);
|
||||
});
|
||||
});
|
||||
@@ -1687,7 +1687,7 @@ describe('per-day summaries (#2602)', () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Lisbon', start_date: '2025-06-02', end_date: '2025-06-03' });
|
||||
// A dated trip comes with its days; the first one is the 2nd of June.
|
||||
const day = testDb.prepare('SELECT id FROM days WHERE trip_id = ? AND day_number = 1').get(trip.id) as { id: number };
|
||||
const [day] = await findRows(orm, Days, { trip: trip.id }, { id: 'asc' });
|
||||
const untimed = createPlace(testDb, trip.id, { name: 'Untimed Spot' });
|
||||
createDayAssignment(testDb, day.id, untimed.id);
|
||||
createDayNote(testDb, day.id, trip.id, { text: 'Buy tram tickets' });
|
||||
|
||||
Reference in New Issue
Block a user