mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 23:46:35 +00:00
refactor(server): inject the transit base URL and the kitinerary paths as config tokens
The ownership rule says a boot-stable value that only Nest providers read belongs to a registerAs token, yet TransitService froze TRANSIT_API_URL in a module-top readEnv() const and KitineraryExtractorService read KITINERARY_EXTRACTOR_PATH and PATH through readEnv() in its probe. Both now come from tokens: transitConfig and kitineraryConfig, derived in boot-derive.ts and dropped from deriveIntegrations, so readEnv() no longer reads those variables at all. TransitModule and KitineraryExtractorModule import AppConfigModule, which keeps them buildable on their own in tests. Behaviour is unchanged: the same defaults, the same trailing-slash strip and PATH split, now frozen per built app instead of at import. The app-config README names each boot-stable variable that stays on readEnv() together with the reader the container cannot inject into.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
export { AppConfigModule } from './app-config.module';
|
||||
export { RuntimeEnvService } from './runtime-env.service';
|
||||
export { DataPathsService } from './data-paths.service';
|
||||
export { httpConfig, storageConfig, BOOT_STABLE_TOKENS } from './tokens';
|
||||
export { httpConfig, storageConfig, transitConfig, kitineraryConfig, BOOT_STABLE_TOKENS } from './tokens';
|
||||
|
||||
@@ -27,5 +27,9 @@ import { BOOT_DERIVERS } from '../../app-config/boot-derive';
|
||||
export const httpConfig = registerAs('http', () => BOOT_DERIVERS.http(process.env));
|
||||
/** The storage registry's conditional place-photo backend (TREK_PLACE_PHOTO_DIR). */
|
||||
export const storageConfig = registerAs('storage', () => BOOT_DERIVERS.storage(process.env));
|
||||
/** TransitService's upstream (TRANSIT_API_URL). */
|
||||
export const transitConfig = registerAs('transit', () => BOOT_DERIVERS.transit(process.env));
|
||||
/** KitineraryExtractorService's binary probe (KITINERARY_EXTRACTOR_PATH, then PATH). */
|
||||
export const kitineraryConfig = registerAs('kitinerary', () => BOOT_DERIVERS.kitinerary(process.env));
|
||||
|
||||
export const BOOT_STABLE_TOKENS = [httpConfig, storageConfig];
|
||||
export const BOOT_STABLE_TOKENS = [httpConfig, storageConfig, transitConfig, kitineraryConfig];
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppConfigModule } from '../app-config/app-config.module';
|
||||
import { KitineraryExtractorService } from './kitinerary-extractor.service';
|
||||
|
||||
/** The kitinerary binary probe on its own. It has no dependencies, and the
|
||||
* feature-flag endpoint in health/ needs nothing else from booking-import —
|
||||
* importing the whole domain there would drag llm-parse, reservations, budget,
|
||||
* maps and places along for one `isAvailable()`. */
|
||||
/** The kitinerary binary probe on its own. Its only dependency is the
|
||||
* kitineraryConfig token (AppConfigModule), and the feature-flag endpoint in
|
||||
* health/ needs nothing else from booking-import — importing the whole domain
|
||||
* there would drag llm-parse, reservations, budget, maps and places along for
|
||||
* one `isAvailable()`. */
|
||||
@Module({
|
||||
imports: [AppConfigModule],
|
||||
providers: [KitineraryExtractorService],
|
||||
exports: [KitineraryExtractorService],
|
||||
})
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import type { ConfigType } from '@nestjs/config';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { existsSync, readdirSync, writeFileSync, unlinkSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, extname } from 'node:path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { readEnv } from '../../app-config';
|
||||
import { kitineraryConfig } from '../app-config/tokens';
|
||||
import { logDebug } from '../audit/audit-log.logger';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
@@ -29,8 +30,11 @@ export class KitineraryExtractorService implements OnModuleInit {
|
||||
* one failure that otherwise looks exactly like "not installed". */
|
||||
private configuredPath: string | null = null;
|
||||
|
||||
/** KITINERARY_EXTRACTOR_PATH and PATH, frozen per built app like the probe result itself. */
|
||||
constructor(@Inject(kitineraryConfig.KEY) private readonly kitineraryEnv: ConfigType<typeof kitineraryConfig>) {}
|
||||
|
||||
onModuleInit() {
|
||||
this.configuredPath = readEnv().integrations.kitineraryExtractorPath || null;
|
||||
this.configuredPath = this.kitineraryEnv.extractorPath || null;
|
||||
this.binaryPath = this.findBinary();
|
||||
if (this.binaryPath) {
|
||||
this.binaryVersion = this.probeVersion(this.binaryPath);
|
||||
@@ -131,7 +135,7 @@ export class KitineraryExtractorService implements OnModuleInit {
|
||||
}
|
||||
|
||||
private findBinary(): string | null {
|
||||
const envPath = readEnv().integrations.kitineraryExtractorPath;
|
||||
const envPath = this.kitineraryEnv.extractorPath;
|
||||
if (envPath) {
|
||||
if (existsSync(envPath)) return envPath;
|
||||
console.warn(`[KItinerary] KITINERARY_EXTRACTOR_PATH="${envPath}" not found`);
|
||||
@@ -153,7 +157,7 @@ export class KitineraryExtractorService implements OnModuleInit {
|
||||
// this branch is dead there) anyone who could write to a PATH directory could
|
||||
// have their binary run as the TREK user. Probing the concrete file with
|
||||
// execFileSync also drops the /bin/sh hop the old execSync string needed.
|
||||
for (const dir of readEnv().integrations.searchPath) {
|
||||
for (const dir of this.kitineraryEnv.searchPath) {
|
||||
const candidate = join(dir, BINARY_NAME);
|
||||
if (!existsSync(candidate)) continue;
|
||||
try {
|
||||
|
||||
@@ -13,12 +13,15 @@ import { AppSettings } from '../../db/entities/AppSettings.entity';
|
||||
import { Users } from '../../db/entities/Users.entity';
|
||||
import { Trips } from '../../db/entities/Trips.entity';
|
||||
import { GoogleQuotaModule } from '../google-quota/google-quota.module';
|
||||
import { AppConfigModule } from '../app-config/app-config.module';
|
||||
|
||||
/**
|
||||
* Transit domain (#1065) — the Transitous/MOTIS proxy, with the optional
|
||||
* Google backend (#1699) behind it. TransitMcp carries the
|
||||
* decorator-registered MCP tools; DaysModule/ReservationsModule feed
|
||||
* create_transit_journey. Exports TransitService for in-container consumers.
|
||||
* AppConfigModule provides the transitConfig token (TRANSIT_API_URL) that
|
||||
* TransitService injects.
|
||||
*
|
||||
* MikroOrmModule.forFeature([AppSettings, Users, Trips]): GoogleTransitProvider
|
||||
* passes its own AppSettingsRepository/UsersRepository to
|
||||
@@ -30,7 +33,7 @@ import { GoogleQuotaModule } from '../google-quota/google-quota.module';
|
||||
*/
|
||||
@Module({
|
||||
// DaysModule + ReservationsModule: TransitMcp's create_transit_journey injects both.
|
||||
imports: [McpSharedModule, RateLimitModule, DaysModule, ReservationsModule, AuthModule, MikroOrmModule.forFeature([AppSettings, Users, Trips]), GoogleQuotaModule],
|
||||
imports: [AppConfigModule, McpSharedModule, RateLimitModule, DaysModule, ReservationsModule, AuthModule, MikroOrmModule.forFeature([AppSettings, Users, Trips]), GoogleQuotaModule],
|
||||
controllers: [TransitController],
|
||||
providers: [TransitService, TransitMcp, GoogleTransitProvider],
|
||||
exports: [TransitService],
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { getAppUrl, readEnv } from '../../app-config';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { ConfigType } from '@nestjs/config';
|
||||
import { getAppUrl } from '../../app-config';
|
||||
import { transitConfig } from '../app-config/tokens';
|
||||
import { buildUserAgent } from '../maps/maps.helpers';
|
||||
import { GoogleTransitProvider } from './google-transit.provider';
|
||||
import type { TransitProvider } from '@trek/shared';
|
||||
@@ -37,15 +39,13 @@ import {
|
||||
* contact info, which we send once from the server), and responses are mapped
|
||||
* to a compact shape so the client isn't coupled to the MOTIS schema.
|
||||
*
|
||||
* Folded 1:1 from the legacy services/transitService.ts. The base URL, the
|
||||
* lazy User-Agent memo and the response cache stay module-scoped on purpose
|
||||
* (the permissions/exchange-rates precedent): the DI singleton and any future
|
||||
* non-Nest caller share one cache, and the base URL keeps its frozen-at-import
|
||||
* legacy timing.
|
||||
* Folded 1:1 from the legacy services/transitService.ts. The lazy User-Agent
|
||||
* memo and the response cache stay module-scoped on purpose (the
|
||||
* permissions/exchange-rates precedent): the DI singleton and any future
|
||||
* non-Nest caller share one cache. The base URL (TRANSIT_API_URL) is
|
||||
* boot-stable and comes from the transitConfig token, frozen per built app.
|
||||
*/
|
||||
|
||||
// Frozen at import on purpose (legacy timing).
|
||||
const TRANSIT_API_BASE = readEnv().integrations.transitApiBase;
|
||||
let userAgent: string | null = null;
|
||||
|
||||
function getUserAgent(): string {
|
||||
@@ -95,8 +95,8 @@ function isCoord(v: string): boolean {
|
||||
// megabyte; anything bigger is a misbehaving provider, not data we want to map.
|
||||
const MAX_RESPONSE_BYTES = 5_000_000;
|
||||
|
||||
async function upstream(path: string, params: URLSearchParams): Promise<unknown> {
|
||||
const url = `${TRANSIT_API_BASE}${path}?${params}`;
|
||||
async function upstream(base: string, path: string, params: URLSearchParams): Promise<unknown> {
|
||||
const url = `${base}${path}?${params}`;
|
||||
const res = await fetch(url, {
|
||||
headers: { 'User-Agent': getUserAgent(), Accept: 'application/json' },
|
||||
signal: AbortSignal.timeout(8000),
|
||||
@@ -154,7 +154,10 @@ function mapStop(p: MotisPlaceRaw | undefined, kind: 'departure' | 'arrival'): T
|
||||
|
||||
@Injectable()
|
||||
export class TransitService {
|
||||
constructor(private readonly google: GoogleTransitProvider) {}
|
||||
constructor(
|
||||
private readonly google: GoogleTransitProvider,
|
||||
@Inject(transitConfig.KEY) private readonly transitEnv: ConfigType<typeof transitConfig>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Station/place search for the from/to pickers. `near` biases results.
|
||||
@@ -192,7 +195,7 @@ export class TransitService {
|
||||
const cached = cacheGet(key);
|
||||
if (cached) return { ...(cached as { results: TransitPlace[] }), provider };
|
||||
|
||||
const raw = (await upstream('/api/v1/geocode', params)) as Array<{
|
||||
const raw = (await upstream(this.transitEnv.apiBase, '/api/v1/geocode', params)) as Array<{
|
||||
name?: string;
|
||||
lat?: number;
|
||||
lon?: number;
|
||||
@@ -261,7 +264,7 @@ export class TransitService {
|
||||
const cached = cacheGet(key);
|
||||
if (cached) return { ...(cached as { itineraries: TransitItinerary[] }), provider: 'transitous' };
|
||||
|
||||
const raw = (await upstream('/api/v6/plan', params)) as {
|
||||
const raw = (await upstream(this.transitEnv.apiBase, '/api/v6/plan', params)) as {
|
||||
itineraries?: Array<{
|
||||
duration?: number;
|
||||
startTime?: string;
|
||||
|
||||
Reference in New Issue
Block a user