Feature/upgrade to NestJS 11 (#4270)

* Upgrade to NestJS 11

* Update changelog
This commit is contained in:
Haruka Kishida 2025-05-08 03:34:31 +09:00 committed by GitHub
parent 03e27dd233
commit 828bd5f172
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 2380 additions and 709 deletions

View File

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- Upgraded `nestjs` from version `10.4.15` to `11.0.12`
## 2.161.0 - 2025-05-06 ## 2.161.0 - 2025-05-06
### Added ### Added

View File

@ -21,37 +21,38 @@ export class ApiKeyStrategy extends PassportStrategy(
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly userService: UserService private readonly userService: UserService
) { ) {
super( super({ header: HEADER_KEY_TOKEN, prefix: 'Api-Key ' }, true);
{ header: HEADER_KEY_TOKEN, prefix: 'Api-Key ' }, }
true,
async (apiKey: string, done: (error: any, user?: any) => void) => {
try {
const user = await this.validateApiKey(apiKey);
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { public async validate(
if (hasRole(user, 'INACTIVE')) { apiKey: string,
throw new HttpException( done: (error: any, user?: any) => void
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS), ) {
StatusCodes.TOO_MANY_REQUESTS try {
); const user = await this.validateApiKey(apiKey);
}
await this.prismaService.analytics.upsert({ if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
create: { User: { connect: { id: user.id } } }, if (hasRole(user, 'INACTIVE')) {
update: { throw new HttpException(
activityCount: { increment: 1 }, getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
lastRequestAt: new Date() StatusCodes.TOO_MANY_REQUESTS
}, );
where: { userId: user.id }
});
}
done(null, user);
} catch (error) {
done(error, null);
} }
await this.prismaService.analytics.upsert({
create: { User: { connect: { id: user.id } } },
update: {
activityCount: { increment: 1 },
lastRequestAt: new Date()
},
where: { userId: user.id }
});
} }
);
done(null, user);
} catch (error) {
done(error, null);
}
} }
private async validateApiKey(apiKey: string) { private async validateApiKey(apiKey: string) {

View File

@ -1,17 +1,16 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { createKeyv } from '@keyv/redis';
import { CacheModule } from '@nestjs/cache-manager'; import { CacheModule } from '@nestjs/cache-manager';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { redisStore } from 'cache-manager-redis-yet';
import type { RedisClientOptions } from 'redis';
import { RedisCacheService } from './redis-cache.service'; import { RedisCacheService } from './redis-cache.service';
@Module({ @Module({
exports: [RedisCacheService], exports: [RedisCacheService],
imports: [ imports: [
CacheModule.registerAsync<RedisClientOptions>({ CacheModule.registerAsync({
imports: [ConfigurationModule], imports: [ConfigurationModule],
inject: [ConfigurationService], inject: [ConfigurationService],
useFactory: async (configurationService: ConfigurationService) => { useFactory: async (configurationService: ConfigurationService) => {
@ -20,10 +19,13 @@ import { RedisCacheService } from './redis-cache.service';
); );
return { return {
store: redisStore, stores: [
ttl: configurationService.get('CACHE_TTL'), createKeyv(
url: `redis://${redisPassword ? `:${redisPassword}` : ''}@${configurationService.get('REDIS_HOST')}:${configurationService.get('REDIS_PORT')}/${configurationService.get('REDIS_DB')}` `redis://${redisPassword ? `:${redisPassword}` : ''}@${configurationService.get('REDIS_HOST')}:${configurationService.get('REDIS_PORT')}/${configurationService.get('REDIS_DB')}`
} as RedisClientOptions; )
],
ttl: configurationService.get('CACHE_TTL')
};
} }
}), }),
ConfigurationModule ConfigurationModule

View File

@ -2,20 +2,18 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { AssetProfileIdentifier, Filter } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier, Filter } from '@ghostfolio/common/interfaces';
import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
import { Milliseconds } from 'cache-manager';
import { RedisCache } from 'cache-manager-redis-yet';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import ms from 'ms'; import ms from 'ms';
@Injectable() @Injectable()
export class RedisCacheService { export class RedisCacheService {
public constructor( public constructor(
@Inject(CACHE_MANAGER) private readonly cache: RedisCache, @Inject(CACHE_MANAGER) private readonly cache: Cache,
private readonly configurationService: ConfigurationService private readonly configurationService: ConfigurationService
) { ) {
const client = cache.store.client; const client = cache.stores[0];
client.on('error', (error) => { client.on('error', (error) => {
Logger.error(error, 'RedisCacheService'); Logger.error(error, 'RedisCacheService');
@ -27,13 +25,33 @@ export class RedisCacheService {
} }
public async getKeys(aPrefix?: string): Promise<string[]> { public async getKeys(aPrefix?: string): Promise<string[]> {
let prefix = aPrefix; const keys: string[] = [];
const prefix = aPrefix;
if (prefix) { this.cache.stores[0].deserialize = (value) => {
prefix = `${prefix}*`; try {
return JSON.parse(value);
} catch (error: any) {
if (error instanceof SyntaxError) {
Logger.debug(
`Failed to parse json, returning the value as String: ${value}`,
'RedisCacheService'
);
return value;
} else {
throw error;
}
}
};
for await (const [key] of this.cache.stores[0].iterator({})) {
if ((prefix && key.startsWith(prefix)) || !prefix) {
keys.push(key);
}
} }
return this.cache.store.keys(prefix); return keys;
} }
public getPortfolioSnapshotKey({ public getPortfolioSnapshotKey({
@ -62,10 +80,8 @@ export class RedisCacheService {
public async isHealthy() { public async isHealthy() {
try { try {
const client = this.cache.store.client;
const isHealthy = await Promise.race([ const isHealthy = await Promise.race([
client.ping(), this.getKeys(),
new Promise((_, reject) => new Promise((_, reject) =>
setTimeout( setTimeout(
() => reject(new Error('Redis health check timeout')), () => reject(new Error('Redis health check timeout')),
@ -93,16 +109,14 @@ export class RedisCacheService {
`${this.getPortfolioSnapshotKey({ userId })}` `${this.getPortfolioSnapshotKey({ userId })}`
); );
for (const key of keys) { return this.cache.mdel(keys);
await this.remove(key);
}
} }
public async reset() { public async reset() {
return this.cache.reset(); return this.cache.clear();
} }
public async set(key: string, value: string, ttl?: Milliseconds) { public async set(key: string, value: string, ttl?: number) {
return this.cache.set( return this.cache.set(
key, key,
value, value,

2934
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -76,17 +76,18 @@
"@dfinity/principal": "0.15.7", "@dfinity/principal": "0.15.7",
"@dinero.js/currencies": "2.0.0-alpha.8", "@dinero.js/currencies": "2.0.0-alpha.8",
"@internationalized/number": "3.6.0", "@internationalized/number": "3.6.0",
"@nestjs/bull": "10.2.3", "@keyv/redis": "4.3.4",
"@nestjs/cache-manager": "2.3.0", "@nestjs/bull": "11.0.2",
"@nestjs/common": "10.4.15", "@nestjs/cache-manager": "3.0.1",
"@nestjs/config": "3.3.0", "@nestjs/common": "11.1.0",
"@nestjs/core": "10.4.15", "@nestjs/config": "4.0.2",
"@nestjs/event-emitter": "2.1.1", "@nestjs/core": "11.1.0",
"@nestjs/jwt": "10.2.0", "@nestjs/event-emitter": "3.0.1",
"@nestjs/passport": "10.0.3", "@nestjs/jwt": "11.0.0",
"@nestjs/platform-express": "10.4.15", "@nestjs/passport": "11.0.5",
"@nestjs/schedule": "4.1.2", "@nestjs/platform-express": "11.1.0",
"@nestjs/serve-static": "4.0.2", "@nestjs/schedule": "6.0.0",
"@nestjs/serve-static": "5.0.3",
"@prisma/client": "6.7.0", "@prisma/client": "6.7.0",
"@simplewebauthn/browser": "13.1.0", "@simplewebauthn/browser": "13.1.0",
"@simplewebauthn/server": "13.1.1", "@simplewebauthn/server": "13.1.1",
@ -95,8 +96,6 @@
"big.js": "6.2.2", "big.js": "6.2.2",
"bootstrap": "4.6.2", "bootstrap": "4.6.2",
"bull": "4.16.5", "bull": "4.16.5",
"cache-manager": "5.7.6",
"cache-manager-redis-yet": "5.1.4",
"chart.js": "4.4.9", "chart.js": "4.4.9",
"chartjs-adapter-date-fns": "3.0.0", "chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-treemap": "3.1.0", "chartjs-chart-treemap": "3.1.0",
@ -153,8 +152,8 @@
"@angular/pwa": "19.2.1", "@angular/pwa": "19.2.1",
"@eslint/eslintrc": "3.3.1", "@eslint/eslintrc": "3.3.1",
"@eslint/js": "9.24.0", "@eslint/js": "9.24.0",
"@nestjs/schematics": "10.2.3", "@nestjs/schematics": "11.0.5",
"@nestjs/testing": "10.4.15", "@nestjs/testing": "11.1.0",
"@nx/angular": "20.8.1", "@nx/angular": "20.8.1",
"@nx/cypress": "20.8.1", "@nx/cypress": "20.8.1",
"@nx/eslint-plugin": "20.8.1", "@nx/eslint-plugin": "20.8.1",
@ -173,7 +172,6 @@
"@storybook/core-server": "8.6.12", "@storybook/core-server": "8.6.12",
"@trivago/prettier-plugin-sort-imports": "5.2.2", "@trivago/prettier-plugin-sort-imports": "5.2.2",
"@types/big.js": "6.2.2", "@types/big.js": "6.2.2",
"@types/cache-manager": "4.0.6",
"@types/google-spreadsheet": "3.1.5", "@types/google-spreadsheet": "3.1.5",
"@types/jest": "29.5.13", "@types/jest": "29.5.13",
"@types/lodash": "4.17.16", "@types/lodash": "4.17.16",