Skip to content

Commit 9eb6b2d

Browse files
authored
Merge pull request #42 from GitAddRemote/feature/uex-locations-sync-jobs
feat: implement UEX locations sync jobs
2 parents 9864c0e + ce11bd5 commit 9eb6b2d

5 files changed

Lines changed: 1239 additions & 1 deletion

File tree

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import { Injectable, Logger } from '@nestjs/common';
2+
import { HttpService } from '@nestjs/axios';
3+
import { ConfigService } from '@nestjs/config';
4+
import { firstValueFrom } from 'rxjs';
5+
import {
6+
RateLimitException,
7+
UEXServerException,
8+
UEXClientException,
9+
} from '../exceptions/uex-exceptions';
10+
11+
export interface UEXStarSystemResponse {
12+
id: number;
13+
name: string;
14+
code: string;
15+
is_available?: boolean;
16+
date_added?: string;
17+
date_modified?: string;
18+
}
19+
20+
export interface UEXPlanetResponse {
21+
id: number;
22+
id_star_system: number;
23+
name: string;
24+
code?: string;
25+
is_available?: boolean;
26+
is_landable?: boolean;
27+
date_added?: string;
28+
date_modified?: string;
29+
}
30+
31+
export interface UEXMoonResponse {
32+
id: number;
33+
id_planet: number;
34+
id_star_system: number;
35+
name: string;
36+
code?: string;
37+
is_available?: boolean;
38+
is_landable?: boolean;
39+
date_added?: string;
40+
date_modified?: string;
41+
}
42+
43+
export interface UEXCityResponse {
44+
id: number;
45+
id_planet?: number;
46+
id_moon?: number;
47+
name: string;
48+
code?: string;
49+
is_available?: boolean;
50+
date_added?: string;
51+
date_modified?: string;
52+
}
53+
54+
export interface UEXSpaceStationResponse {
55+
id: number;
56+
id_orbit?: number;
57+
id_planet?: number;
58+
id_moon?: number;
59+
name: string;
60+
code?: string;
61+
is_available?: boolean;
62+
date_added?: string;
63+
date_modified?: string;
64+
}
65+
66+
export interface UEXOutpostResponse {
67+
id: number;
68+
id_planet?: number;
69+
id_moon?: number;
70+
name: string;
71+
is_available?: boolean;
72+
date_added?: string;
73+
date_modified?: string;
74+
}
75+
76+
export interface UEXPOIResponse {
77+
id: number;
78+
id_star_system?: number;
79+
id_planet?: number;
80+
id_moon?: number;
81+
id_orbit?: number;
82+
id_space_station?: number;
83+
id_city?: number;
84+
id_outpost?: number;
85+
name: string;
86+
type?: string;
87+
is_available?: boolean;
88+
date_added?: string;
89+
date_modified?: string;
90+
}
91+
92+
export interface UEXLocationFilters {
93+
date_modified?: Date;
94+
}
95+
96+
@Injectable()
97+
export class UEXLocationsClient {
98+
private readonly logger = new Logger(UEXLocationsClient.name);
99+
private readonly baseUrl: string;
100+
private readonly timeout: number;
101+
102+
private readonly endpoints = {
103+
star_systems: '/star_systems',
104+
planets: '/planets',
105+
moons: '/moons',
106+
cities: '/cities',
107+
space_stations: '/space_stations',
108+
outposts: '/outposts',
109+
poi: '/poi',
110+
};
111+
112+
constructor(
113+
private readonly httpService: HttpService,
114+
private readonly configService: ConfigService,
115+
) {
116+
this.baseUrl = this.configService.get<string>(
117+
'UEX_API_BASE_URL',
118+
'https://uexcorp.space/api/2.0',
119+
);
120+
this.timeout = this.configService.get<number>('UEX_TIMEOUT_MS', 30000);
121+
}
122+
123+
async fetchStarSystems(
124+
filters?: UEXLocationFilters,
125+
): Promise<UEXStarSystemResponse[]> {
126+
return this.fetchLocations<UEXStarSystemResponse>('star_systems', filters);
127+
}
128+
129+
async fetchPlanets(
130+
filters?: UEXLocationFilters,
131+
): Promise<UEXPlanetResponse[]> {
132+
return this.fetchLocations<UEXPlanetResponse>('planets', filters);
133+
}
134+
135+
async fetchMoons(filters?: UEXLocationFilters): Promise<UEXMoonResponse[]> {
136+
return this.fetchLocations<UEXMoonResponse>('moons', filters);
137+
}
138+
139+
async fetchCities(filters?: UEXLocationFilters): Promise<UEXCityResponse[]> {
140+
return this.fetchLocations<UEXCityResponse>('cities', filters);
141+
}
142+
143+
async fetchSpaceStations(
144+
filters?: UEXLocationFilters,
145+
): Promise<UEXSpaceStationResponse[]> {
146+
return this.fetchLocations<UEXSpaceStationResponse>(
147+
'space_stations',
148+
filters,
149+
);
150+
}
151+
152+
async fetchOutposts(
153+
filters?: UEXLocationFilters,
154+
): Promise<UEXOutpostResponse[]> {
155+
return this.fetchLocations<UEXOutpostResponse>('outposts', filters);
156+
}
157+
158+
async fetchPOI(filters?: UEXLocationFilters): Promise<UEXPOIResponse[]> {
159+
return this.fetchLocations<UEXPOIResponse>('poi', filters);
160+
}
161+
162+
private async fetchLocations<T>(
163+
endpoint: keyof typeof this.endpoints,
164+
filters?: UEXLocationFilters,
165+
): Promise<T[]> {
166+
const params: Record<string, string> = {};
167+
168+
if (filters?.date_modified) {
169+
params.date_modified = filters.date_modified.toISOString();
170+
}
171+
172+
this.logger.log(
173+
`Fetching ${endpoint} from UEX API with filters: ${JSON.stringify(params)}`,
174+
);
175+
176+
try {
177+
const response = await firstValueFrom(
178+
this.httpService.get(`${this.baseUrl}${this.endpoints[endpoint]}`, {
179+
params,
180+
timeout: this.timeout,
181+
headers: {
182+
'User-Agent': 'Station/1.0',
183+
},
184+
}),
185+
);
186+
187+
// Check for rate limit in response
188+
if (
189+
response.data.status === 'error' &&
190+
response.data.message?.includes('requests_limit_reached')
191+
) {
192+
throw new RateLimitException('UEX API rate limit exceeded');
193+
}
194+
195+
const locations = response.data.data || [];
196+
this.logger.log(`Fetched ${locations.length} ${endpoint} from UEX API`);
197+
198+
return locations;
199+
} catch (error: any) {
200+
if (error instanceof RateLimitException) {
201+
throw error;
202+
}
203+
204+
if (error.response?.status >= 500) {
205+
throw new UEXServerException(
206+
`UEX server error: ${error.message || 'Unknown error'}`,
207+
);
208+
}
209+
210+
throw new UEXClientException(
211+
`Failed to fetch ${endpoint}: ${error.message || 'Unknown error'}`,
212+
);
213+
}
214+
}
215+
}

backend/src/modules/uex-sync/schedulers/uex-sync.scheduler.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Cron, CronExpression } from '@nestjs/schedule';
33
import { ConfigService } from '@nestjs/config';
44
import { CategoriesSyncService } from '../services/categories-sync.service';
55
import { ItemsSyncService } from '../services/items-sync.service';
6+
import { LocationsSyncService } from '../services/locations-sync.service';
67

78
@Injectable()
89
export class UEXSyncScheduler {
@@ -11,6 +12,7 @@ export class UEXSyncScheduler {
1112
constructor(
1213
private readonly categoriesSync: CategoriesSyncService,
1314
private readonly itemsSync: ItemsSyncService,
15+
private readonly locationsSync: LocationsSyncService,
1416
private readonly configService: ConfigService,
1517
) {}
1618

@@ -89,4 +91,42 @@ export class UEXSyncScheduler {
8991
// Add alerting here if needed
9092
}
9193
}
94+
95+
// Runs daily at 4:00 AM UTC (after items sync)
96+
@Cron('0 4 * * *', {
97+
name: 'sync-uex-locations',
98+
})
99+
async scheduledLocationsSync(): Promise<void> {
100+
const syncEnabled = this.configService.get<boolean>(
101+
'UEX_SYNC_ENABLED',
102+
true,
103+
);
104+
const locationsSyncEnabled = this.configService.get<boolean>(
105+
'UEX_LOCATIONS_SYNC_ENABLED',
106+
true,
107+
);
108+
109+
if (!syncEnabled || !locationsSyncEnabled) {
110+
this.logger.log('Locations sync is disabled via configuration');
111+
return;
112+
}
113+
114+
this.logger.log('Starting scheduled locations sync');
115+
116+
try {
117+
const result = await this.locationsSync.syncAllLocations();
118+
this.logger.log(
119+
`Scheduled locations sync completed successfully: ` +
120+
`total created: ${result.totalCreated}, updated: ${result.totalUpdated}, ` +
121+
`deleted: ${result.totalDeleted}, duration: ${result.totalDurationMs}ms`,
122+
);
123+
} catch (error: any) {
124+
this.logger.error(
125+
`Scheduled locations sync failed: ${error.message}`,
126+
error.stack,
127+
);
128+
// Error already recorded in sync state by service
129+
// Add alerting here if needed
130+
}
131+
}
92132
}

0 commit comments

Comments
 (0)