Skip to content

Commit a4970e8

Browse files
committed
Ajout de nouvelles dépendances @nestjs/axios et @nestjs/schedule, mise à jour de la configuration Axios, et amélioration de la gestion des mises à jour des projets avec des méthodes asynchrones et des tâches cron.
1 parent 5d326b0 commit a4970e8

File tree

6 files changed

+262
-83
lines changed

6 files changed

+262
-83
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"dependencies": {
4949
"@nestjs-modules/ioredis": "^2.0.2",
5050
"@nestjs-modules/mailer": "^2.0.2",
51+
"@nestjs/axios": "^4.0.1",
5152
"@nestjs/bullmq": "^10.1.1",
5253
"@nestjs/common": "^10.4.16",
5354
"@nestjs/config": "^3.3.0",
@@ -57,6 +58,7 @@
5758
"@nestjs/mongoose": "^10.1.0",
5859
"@nestjs/passport": "^10.0.3",
5960
"@nestjs/platform-express": "^10.4.8",
61+
"@nestjs/schedule": "^6.0.0",
6062
"@nestjs/swagger": "^8.0.7",
6163
"@the-software-compagny/nestjs_module_factorydrive": "^1.1.5",
6264
"@the-software-compagny/nestjs_module_factorydrive-s3": "^1.0.1",
@@ -67,6 +69,7 @@
6769
"ajv-i18n": "^4.2.0",
6870
"argon2": "^0.41.1",
6971
"awesome-phonenumber": "^6.10.0",
72+
"axios": "^1.10.0",
7073
"bullmq": "^5.8.2",
7174
"class-transformer": "^0.5.1",
7275
"class-validator": "^0.14.1",

src/app.controller.ts

Lines changed: 19 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,9 @@
11
import { BadRequestException, Controller, Get, Param, Query, Res } from '@nestjs/common';
2-
import { AppService } from './app.service';
2+
import { AppService, ProjectsList } from './app.service';
33
import { Response } from 'express';
44
import { AbstractController } from '~/_common/abstracts/abstract.controller';
55
import { Public } from './_common/decorators/public.decorator';
66
import { ApiBearerAuth, ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger';
7-
import { LRUCache } from 'lru-cache';
8-
9-
interface GithubAuthor {
10-
login: string;
11-
id: number;
12-
node_id: string;
13-
avatar_url: string;
14-
gravatar_id: string;
15-
url: string;
16-
html_url: string;
17-
followers_url: string;
18-
following_url: string;
19-
gists_url: string;
20-
starred_url: string;
21-
subscriptions_url: string;
22-
organizations_url: string;
23-
repos_url: string;
24-
events_url: string;
25-
received_events_url: string;
26-
type: string;
27-
user_view_type: string;
28-
site_admin: boolean;
29-
}
30-
31-
interface GithubAsset {
32-
[key: string]: any;
33-
}
34-
35-
interface GithubUpdate {
36-
url: string;
37-
assets_url: string;
38-
upload_url: string;
39-
html_url: string;
40-
id: number;
41-
author: GithubAuthor;
42-
node_id: string;
43-
tag_name: string;
44-
target_commitish: string;
45-
name: string;
46-
draft: boolean;
47-
prerelease: boolean;
48-
created_at: string;
49-
published_at: string;
50-
assets: GithubAsset[];
51-
tarball_url: string;
52-
zipball_url: string;
53-
body: string;
54-
}
55-
56-
const storage = new LRUCache({
57-
ttl: 1000 * 60 * 60,
58-
ttlAutopurge: true,
59-
});
607

618
@Public()
629
@Controller()
@@ -80,43 +27,35 @@ export class AppController extends AbstractController {
8027
@Get('/get-update/:project(sesame-orchestrator|sesame-daemon|sesame-app-manager)')
8128
public async update(
8229
@Res() res: Response,
83-
@Param('project') project?: string,
30+
@Param('project') project?: ProjectsList,
8431
@Query('current') current?: string,
8532
): Promise<Response> {
86-
const validProjects = ['sesame-orchestrator', 'sesame-daemon', 'sesame-app-manager'];
87-
if (!validProjects.includes(project)) {
88-
throw new BadRequestException(`Invalid project: ${project}`);
89-
}
90-
91-
let data = <GithubUpdate>{};
92-
if (storage.has(project)) {
93-
this.logger.log(`Fetching ${project} tags from cache`);
94-
data = storage.get(project) as GithubUpdate;
95-
} else {
96-
this.logger.log(`Fetching ${project} tags`);
97-
const update = await fetch(`https://api.github.com/repos/Libertech-FR/${project}/releases/latest`, {
98-
signal: AbortSignal.timeout(1000),
99-
});
100-
data = await update.json();
101-
console.log('update', data)
102-
storage.set(project, data);
103-
}
104-
// if (!Array.isArray(data)) {
105-
// throw new BadRequestException(`Invalid data from Github <${JSON.stringify(data)}>`);
106-
// }
107-
const lastVersion = data.tag_name.replace(/^v/, '');
10833
const pkgInfo = this.appService.getInfo();
10934
const currentVersion = current || pkgInfo.version;
35+
const [lastMajor, lastMinor, lastPatch] = currentVersion.split('.').map(Number);
11036

37+
/**
38+
* If the project is not the same as the package name or if a current version is provided,
39+
* we validate the current version format.
40+
* If the current version is not in the format X.Y.Z, we throw a BadRequestException.
41+
*
42+
* This ensures that the current version is always in a valid format before proceeding with the comparison.
43+
*/
11144
if (project !== pkgInfo.name || current) {
11245
if (!/[0-9]+\.[0-9]+\.[0-9]+/.test(current)) {
11346
throw new BadRequestException('Invalid version for current parameter');
11447
}
11548
}
11649

117-
const [currentMajor, currentMinor, currentPatch] = lastVersion.split('.').map(Number);
118-
const [lastMajor, lastMinor, lastPatch] = currentVersion.split('.').map(Number);
119-
const updateAvailable = currentMajor > lastMajor || currentMinor > lastMinor || currentPatch > lastPatch;
50+
let lastVersion = '0.0.0';
51+
let updateAvailable = false;
52+
let data = await this.appService.getProjectUpdate(project);
53+
54+
if (data) {
55+
lastVersion = data.tag_name.replace(/^v/, '');
56+
const [currentMajor, currentMinor, currentPatch] = lastVersion.split('.').map(Number);
57+
updateAvailable = currentMajor > lastMajor || currentMinor > lastMinor || currentPatch > lastPatch;
58+
}
12059

12160
return res.json({
12261
data: {

src/app.module.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { FactorydriveModule } from '@the-software-compagny/nestjs_module_factory
2323
import { MigrationsModule } from './migrations/migrations.module';
2424
import { EventEmitterModule } from '@nestjs/event-emitter';
2525
import { ShutdownObserver } from './_common/observers/shutdown.observer';
26+
import { ScheduleModule } from '@nestjs/schedule';
27+
import { HttpModule } from '@nestjs/axios';
2628

2729
@Module({
2830
imports: [
@@ -118,7 +120,15 @@ import { ShutdownObserver } from './_common/observers/shutdown.observer';
118120
...config.get('factorydrive.options'),
119121
}),
120122
}),
123+
HttpModule.registerAsync({
124+
imports: [ConfigModule],
125+
inject: [ConfigService],
126+
useFactory: async (config: ConfigService) => ({
127+
...config.get('axios.options'),
128+
}),
129+
}),
121130
RequestContextModule,
131+
ScheduleModule.forRoot(),
122132
CoreModule.register(),
123133
ManagementModule.register(),
124134
SettingsModule.register(),

src/app.service.ts

Lines changed: 176 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,193 @@
1-
import { Injectable } from '@nestjs/common';
1+
import { BadRequestException, Injectable, OnApplicationBootstrap } from '@nestjs/common';
22
import { PackageJson } from 'types-package-json';
33
import { ModuleRef } from '@nestjs/core';
44
import { readFileSync } from 'fs';
55
import { AbstractService } from '~/_common/abstracts/abstract.service';
66
import { pick } from 'radash';
7+
import { HttpService } from '@nestjs/axios';
8+
import { LRUCache } from 'lru-cache';
9+
import { catchError, firstValueFrom } from 'rxjs';
10+
import { Cron, CronExpression } from '@nestjs/schedule';
11+
12+
export enum ProjectsList {
13+
SESAME_ORCHESTRATOR = 'sesame-orchestrator',
14+
SESAME_DAEMON = 'sesame-daemon',
15+
SESAME_APP_MANAGER = 'sesame-app-manager',
16+
}
17+
18+
export interface GithubAuthor {
19+
login: string;
20+
id: number;
21+
node_id: string;
22+
avatar_url: string;
23+
gravatar_id: string;
24+
url: string;
25+
html_url: string;
26+
followers_url: string;
27+
following_url: string;
28+
gists_url: string;
29+
starred_url: string;
30+
subscriptions_url: string;
31+
organizations_url: string;
32+
repos_url: string;
33+
events_url: string;
34+
received_events_url: string;
35+
type: string;
36+
user_view_type: string;
37+
site_admin: boolean;
38+
}
39+
40+
export interface GithubAsset {
41+
[key: string]: any;
42+
}
43+
44+
export interface GithubUpdate {
45+
url: string;
46+
assets_url: string;
47+
upload_url: string;
48+
html_url: string;
49+
id: number;
50+
author: GithubAuthor;
51+
node_id: string;
52+
tag_name: string;
53+
target_commitish: string;
54+
name: string;
55+
draft: boolean;
56+
prerelease: boolean;
57+
created_at: string;
58+
published_at: string;
59+
assets: GithubAsset[];
60+
tarball_url: string;
61+
zipball_url: string;
62+
body: string;
63+
}
764

865
@Injectable()
9-
export class AppService extends AbstractService {
66+
export class AppService extends AbstractService implements OnApplicationBootstrap {
67+
protected storage = new LRUCache({
68+
ttl: 1_000 * 60 * 60 * 6, // 6 hours
69+
ttlAutopurge: true,
70+
});
71+
1072
protected package: Partial<PackageJson>;
1173

12-
public constructor(protected moduleRef: ModuleRef) {
74+
public constructor(
75+
protected moduleRef: ModuleRef,
76+
private readonly httpService: HttpService,
77+
) {
1378
super({ moduleRef });
1479
this.package = JSON.parse(readFileSync('package.json', 'utf-8'));
1580
}
1681

82+
/**
83+
* On application bootstrap, this method is called to initialize the service.
84+
* It logs the start of the bootstrap process and fetches the latest releases for each project
85+
* in the ProjectsList enum.
86+
* It uses the fetchGithubRelease method to get the latest releases from GitHub.
87+
*
88+
* @returns {Promise<void>} A promise that resolves when the bootstrap process is complete.
89+
*/
90+
public async onApplicationBootstrap(): Promise<void> {
91+
this.logger.debug('Application service bootstrap starting...');
92+
93+
for (const project of Object.values(ProjectsList)) {
94+
this.logger.verbose(`Checking for updates for project: ${project}`);
95+
96+
await this.fetchGithubRelease(project);
97+
}
98+
99+
this.logger.log('Application service bootstrap completed.');
100+
}
101+
102+
/**
103+
* Cron job to fetch the latest releases of projects every 6 hours.
104+
* This method logs the start and end of the job, and fetches updates for each project in the ProjectsList.
105+
* It uses the fetchGithubRelease method to get the latest releases from GitHub.
106+
* The job is scheduled using the CronExpression.EVERY_6_HOURS expression.
107+
*
108+
* @Cron(CronExpression.EVERY_6_HOURS)
109+
* @returns {Promise<void>}
110+
* @memberof AppService
111+
*/
112+
@Cron(CronExpression.EVERY_6_HOURS)
113+
public async handleCron(): Promise<void> {
114+
this.logger.debug('Cron job started.');
115+
116+
for (const project of Object.values(ProjectsList)) {
117+
this.logger.verbose(`Checking for updates for project: ${project}`);
118+
119+
await this.fetchGithubRelease(project);
120+
}
121+
122+
this.logger.debug('Cron job completed.');
123+
}
124+
125+
/**
126+
* Returns basic information about the application, such as name and version.
127+
*
128+
* @returns {Partial<PackageJson>} Returns basic information about the application, such as name and version.
129+
*/
17130
public getInfo(): Partial<PackageJson> {
18131
return pick(this.package, ['name', 'version']);
19132
}
133+
134+
/**
135+
* Fetches the latest release information for a specified project from GitHub.
136+
*
137+
* @param project The project name to fetch updates for.
138+
* @returns {Promise<GithubUpdate>} A promise that resolves to the latest release information for the specified project.
139+
*/
140+
public getProjectUpdate(project: ProjectsList): GithubUpdate {
141+
if (!Object.values(ProjectsList).includes(project)) {
142+
throw new BadRequestException(`Invalid project: ${project}`);
143+
}
144+
145+
if (this.storage.has(project)) {
146+
this.logger.debug(`Fetching ${project} tags from cache`);
147+
148+
return this.storage.get(project) as GithubUpdate;
149+
}
150+
151+
return null; // Return null if the project is not cached
152+
}
153+
154+
/**
155+
* Fetches the latest release information for a specified project from GitHub.
156+
*
157+
* @param project The project name to fetch updates for.
158+
* @returns {Promise<GithubUpdate>} A promise that resolves to the latest release information for the specified project.
159+
*/
160+
private async fetchGithubRelease(project: ProjectsList, retry = 0): Promise<any> {
161+
if (this.storage.has(project)) {
162+
this.logger.debug(`Fetching ${project} tags from cache`);
163+
164+
return this.storage.get(project) as GithubUpdate;
165+
}
166+
167+
this.logger.debug(`Fetching ${project} tags from GitHub API`);
168+
169+
try {
170+
const { data } = await firstValueFrom(
171+
this.httpService.get<GithubUpdate>(`https://api.github.com/repos/Libertech-FR/${project}/releasesppp/latest`).pipe(
172+
catchError((error) => {
173+
this.logger.error(`Error fetching release for ${project}: ${error.message}`);
174+
throw error;
175+
})
176+
)
177+
);
178+
this.storage.set(project, data);
179+
return data;
180+
} catch (error) {
181+
if (retry >= 3) {
182+
this.logger.fatal(`Failed to fetch release for ${project} after multiple retries: ${error.message}`);
183+
184+
return null; // Return null or handle as needed
185+
}
186+
187+
setTimeout(() => {
188+
this.logger.verbose(`Retrying to fetch ${project} release after error: ${error.message}`);
189+
return this.fetchGithubRelease(project, retry + 1);
190+
}, 1_000 * 60); // Retry after 60 seconds
191+
}
192+
}
20193
}

0 commit comments

Comments
 (0)