mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-14 08:42:08 +10:00
fix(i18n): delete local translations
This commit is contained in:
@ -14,6 +14,7 @@ import { MailModule } from "./mail/mail.module";
|
||||
import { PrinterModule } from "./printer/printer.module";
|
||||
import { ResumeModule } from "./resume/resume.module";
|
||||
import { StorageModule } from "./storage/storage.module";
|
||||
import { TranslationModule } from "./translation/translation.module";
|
||||
import { UserModule } from "./user/user.module";
|
||||
import { UtilsModule } from "./utils/utils.module";
|
||||
|
||||
@ -34,6 +35,7 @@ import { UtilsModule } from "./utils/utils.module";
|
||||
ResumeModule,
|
||||
StorageModule,
|
||||
PrinterModule,
|
||||
TranslationModule,
|
||||
|
||||
// Static Assets
|
||||
ServeStaticModule.forRoot({
|
||||
|
||||
23
apps/server/src/cache/cache.module.ts
vendored
23
apps/server/src/cache/cache.module.ts
vendored
@ -1,8 +1,6 @@
|
||||
import { CacheModule as NestCacheModule } from "@nestjs/cache-manager";
|
||||
import { Logger, Module } from "@nestjs/common";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { RedisModule } from "@songkeys/nestjs-redis";
|
||||
import { redisStore } from "cache-manager-redis-yet";
|
||||
|
||||
import { Config } from "../config/schema";
|
||||
|
||||
@ -14,25 +12,6 @@ import { Config } from "../config/schema";
|
||||
config: { url: configService.getOrThrow("REDIS_URL") },
|
||||
}),
|
||||
}),
|
||||
NestCacheModule.registerAsync({
|
||||
isGlobal: true,
|
||||
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService<Config>) => {
|
||||
const url = configService.get("REDIS_URL");
|
||||
|
||||
if (!url) {
|
||||
Logger.warn(
|
||||
"`REDIS_URL` was not set, using in-memory cache instead. This is not suitable for production.",
|
||||
"CacheModule",
|
||||
);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
return { store: await redisStore({ url }) };
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class CacheModule {}
|
||||
|
||||
@ -42,6 +42,9 @@ export const configSchema = z.object({
|
||||
// Sentry
|
||||
SENTRY_DSN: z.string().url().startsWith("https://").optional(),
|
||||
|
||||
// Crowdin (Optional)
|
||||
CROWDIN_DISTRIBUTION_HASH: z.string().optional(),
|
||||
|
||||
// GitHub (OAuth)
|
||||
GITHUB_CLIENT_ID: z.string().optional(),
|
||||
GITHUB_CLIENT_SECRET: z.string().optional(),
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { CacheInterceptor, CacheKey, CacheTTL } from "@nestjs/cache-manager";
|
||||
import { Controller, Get, NotFoundException, UseInterceptors } from "@nestjs/common";
|
||||
import { Controller, Get, NotFoundException } from "@nestjs/common";
|
||||
import { ApiTags } from "@nestjs/swagger";
|
||||
import { HealthCheck, HealthCheckService } from "@nestjs/terminus";
|
||||
import { RedisService } from "@songkeys/nestjs-redis";
|
||||
import { RedisHealthIndicator } from "@songkeys/nestjs-redis-health";
|
||||
|
||||
import { configSchema } from "../config/schema";
|
||||
import { UtilsService } from "../utils/utils.service";
|
||||
import { BrowserHealthIndicator } from "./browser.health";
|
||||
import { DatabaseHealthIndicator } from "./database.health";
|
||||
import { StorageHealthIndicator } from "./storage.health";
|
||||
@ -20,14 +20,10 @@ export class HealthController {
|
||||
private readonly storage: StorageHealthIndicator,
|
||||
private readonly redisService: RedisService,
|
||||
private readonly redis: RedisHealthIndicator,
|
||||
private readonly utils: UtilsService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@HealthCheck()
|
||||
@UseInterceptors(CacheInterceptor)
|
||||
@CacheKey("health:check")
|
||||
@CacheTTL(30000) // 30 seconds
|
||||
check() {
|
||||
private run() {
|
||||
return this.health.check([
|
||||
() => this.database.isHealthy(),
|
||||
() => this.storage.isHealthy(),
|
||||
@ -42,6 +38,12 @@ export class HealthController {
|
||||
]);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@HealthCheck()
|
||||
check() {
|
||||
return this.utils.getCachedOrSet(`health:check`, () => this.run(), 1000 * 30); // 30 seconds
|
||||
}
|
||||
|
||||
@Get("environment")
|
||||
environment() {
|
||||
if (process.env.NODE_ENV === "production") throw new NotFoundException();
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { CacheInterceptor, CacheKey } from "@nestjs/cache-manager";
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
@ -11,7 +10,6 @@ import {
|
||||
Patch,
|
||||
Post,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from "@nestjs/common";
|
||||
import { ApiTags } from "@nestjs/swagger";
|
||||
import { User as UserEntity } from "@prisma/client";
|
||||
@ -25,6 +23,7 @@ import { User } from "@/server/user/decorators/user.decorator";
|
||||
import { OptionalGuard } from "../auth/guards/optional.guard";
|
||||
import { TwoFactorGuard } from "../auth/guards/two-factor.guard";
|
||||
import { ErrorMessage } from "../constants/error-message";
|
||||
import { UtilsService } from "../utils/utils.service";
|
||||
import { Resume } from "./decorators/resume.decorator";
|
||||
import { ResumeGuard } from "./guards/resume.guard";
|
||||
import { ResumeService } from "./resume.service";
|
||||
@ -32,13 +31,18 @@ import { ResumeService } from "./resume.service";
|
||||
@ApiTags("Resume")
|
||||
@Controller("resume")
|
||||
export class ResumeController {
|
||||
constructor(private readonly resumeService: ResumeService) {}
|
||||
constructor(
|
||||
private readonly resumeService: ResumeService,
|
||||
private readonly utils: UtilsService,
|
||||
) {}
|
||||
|
||||
@Get("schema")
|
||||
@UseInterceptors(CacheInterceptor)
|
||||
@CacheKey("resume:schema")
|
||||
async getSchema() {
|
||||
return zodToJsonSchema(resumeDataSchema);
|
||||
getSchema() {
|
||||
return this.utils.getCachedOrSet(
|
||||
`resume:schema`,
|
||||
() => zodToJsonSchema(resumeDataSchema),
|
||||
1000 * 60 * 60 * 24, // 24 hours
|
||||
);
|
||||
}
|
||||
|
||||
@Post()
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
import { CACHE_MANAGER } from "@nestjs/cache-manager";
|
||||
import { BadRequestException, Inject, Injectable, Logger } from "@nestjs/common";
|
||||
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { CreateResumeDto, ImportResumeDto, ResumeDto, UpdateResumeDto } from "@reactive-resume/dto";
|
||||
import { defaultResumeData, ResumeData } from "@reactive-resume/schema";
|
||||
import type { DeepPartial } from "@reactive-resume/utils";
|
||||
import { generateRandomName, kebabCase } from "@reactive-resume/utils";
|
||||
import { RedisService } from "@songkeys/nestjs-redis";
|
||||
import { Cache } from "cache-manager";
|
||||
import deepmerge from "deepmerge";
|
||||
import Redis from "ioredis";
|
||||
import { PrismaService } from "nestjs-prisma";
|
||||
@ -28,7 +26,6 @@ export class ResumeService {
|
||||
private readonly storageService: StorageService,
|
||||
private readonly redisService: RedisService,
|
||||
private readonly utils: UtilsService,
|
||||
@Inject(CACHE_MANAGER) private readonly cache: Cache,
|
||||
) {
|
||||
this.redis = this.redisService.getClient();
|
||||
}
|
||||
@ -54,8 +51,8 @@ export class ResumeService {
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
this.cache.del(`user:${userId}:resumes`),
|
||||
this.cache.set(`user:${userId}:resume:${resume.id}`, resume),
|
||||
this.redis.del(`user:${userId}:resumes`),
|
||||
this.redis.set(`user:${userId}:resume:${resume.id}`, JSON.stringify(resume)),
|
||||
]);
|
||||
|
||||
return resume;
|
||||
@ -75,8 +72,8 @@ export class ResumeService {
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
this.cache.del(`user:${userId}:resumes`),
|
||||
this.cache.set(`user:${userId}:resume:${resume.id}`, resume),
|
||||
this.redis.del(`user:${userId}:resumes`),
|
||||
this.redis.set(`user:${userId}:resume:${resume.id}`, JSON.stringify(resume)),
|
||||
]);
|
||||
|
||||
return resume;
|
||||
@ -142,10 +139,10 @@ export class ResumeService {
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
this.cache.set(`user:${userId}:resume:${id}`, resume),
|
||||
this.cache.del(`user:${userId}:resumes`),
|
||||
this.cache.del(`user:${userId}:storage:resumes:${id}`),
|
||||
this.cache.del(`user:${userId}:storage:previews:${id}`),
|
||||
this.redis.set(`user:${userId}:resume:${id}`, JSON.stringify(resume)),
|
||||
this.redis.del(`user:${userId}:resumes`),
|
||||
this.redis.del(`user:${userId}:storage:resumes:${id}`),
|
||||
this.redis.del(`user:${userId}:storage:previews:${id}`),
|
||||
]);
|
||||
|
||||
return resume;
|
||||
@ -163,8 +160,8 @@ export class ResumeService {
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
this.cache.set(`user:${userId}:resume:${id}`, resume),
|
||||
this.cache.del(`user:${userId}:resumes`),
|
||||
this.redis.set(`user:${userId}:resume:${id}`, JSON.stringify(resume)),
|
||||
this.redis.del(`user:${userId}:resumes`),
|
||||
]);
|
||||
|
||||
return resume;
|
||||
@ -173,8 +170,8 @@ export class ResumeService {
|
||||
async remove(userId: string, id: string) {
|
||||
await Promise.all([
|
||||
// Remove cached keys
|
||||
this.cache.del(`user:${userId}:resumes`),
|
||||
this.cache.del(`user:${userId}:resume:${id}`),
|
||||
this.redis.del(`user:${userId}:resumes`),
|
||||
this.redis.del(`user:${userId}:resume:${id}`),
|
||||
|
||||
// Remove files in storage, and their cached keys
|
||||
this.storageService.deleteObject(userId, "resumes", id),
|
||||
|
||||
@ -1,14 +1,8 @@
|
||||
import { CACHE_MANAGER } from "@nestjs/cache-manager";
|
||||
import {
|
||||
Inject,
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
OnModuleInit,
|
||||
} from "@nestjs/common";
|
||||
import { Injectable, InternalServerErrorException, Logger, OnModuleInit } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { Cache } from "cache-manager";
|
||||
import { RedisService } from "@songkeys/nestjs-redis";
|
||||
import { Redis } from "ioredis";
|
||||
import { Client } from "minio";
|
||||
import { MinioService } from "nestjs-minio-client";
|
||||
import sharp from "sharp";
|
||||
@ -44,6 +38,7 @@ const PUBLIC_ACCESS_POLICY = {
|
||||
|
||||
@Injectable()
|
||||
export class StorageService implements OnModuleInit {
|
||||
private readonly redis: Redis;
|
||||
private readonly logger = new Logger(StorageService.name);
|
||||
|
||||
private client: Client;
|
||||
@ -52,8 +47,10 @@ export class StorageService implements OnModuleInit {
|
||||
constructor(
|
||||
private readonly configService: ConfigService<Config>,
|
||||
private readonly minioService: MinioService,
|
||||
@Inject(CACHE_MANAGER) private readonly cache: Cache,
|
||||
) {}
|
||||
private readonly redisService: RedisService,
|
||||
) {
|
||||
this.redis = this.redisService.getClient();
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
this.client = this.minioService.client;
|
||||
@ -125,7 +122,7 @@ export class StorageService implements OnModuleInit {
|
||||
|
||||
await Promise.all([
|
||||
this.client.putObject(this.bucketName, filepath, buffer, metadata),
|
||||
this.cache.set(`user:${userId}:storage:${type}:${filename}`, url),
|
||||
this.redis.set(`user:${userId}:storage:${type}:${filename}`, url),
|
||||
]);
|
||||
|
||||
return url;
|
||||
@ -140,7 +137,7 @@ export class StorageService implements OnModuleInit {
|
||||
|
||||
try {
|
||||
return Promise.all([
|
||||
this.cache.del(`user:${userId}:storage:${type}:${filename}`),
|
||||
this.redis.del(`user:${userId}:storage:${type}:${filename}`),
|
||||
this.client.removeObject(this.bucketName, path),
|
||||
]);
|
||||
} catch (error) {
|
||||
|
||||
35
apps/server/src/translation/translation.controller.ts
Normal file
35
apps/server/src/translation/translation.controller.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { HttpService } from "@nestjs/axios";
|
||||
import { Controller, Get, Header, Param } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
import { Config } from "../config/schema";
|
||||
import { UtilsService } from "../utils/utils.service";
|
||||
|
||||
@Controller("translation")
|
||||
export class TranslationController {
|
||||
constructor(
|
||||
private readonly httpService: HttpService,
|
||||
private readonly configService: ConfigService<Config>,
|
||||
private readonly utils: UtilsService,
|
||||
) {}
|
||||
|
||||
private async fetchTranslations(locale: string) {
|
||||
const distributionHash = this.configService.get("CROWDIN_DISTRIBUTION_HASH");
|
||||
const response = await this.httpService.axiosRef.get(
|
||||
`https://distributions.crowdin.net/${distributionHash}/content/${locale}/messages.json`,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@Get("/:locale")
|
||||
@Header("Content-Type", "application/octet-stream")
|
||||
@Header("Content-Disposition", 'attachment; filename="messages.po"')
|
||||
async getTranslation(@Param("locale") locale: string) {
|
||||
return this.utils.getCachedOrSet(
|
||||
`translation:${locale}`,
|
||||
async () => this.fetchTranslations(locale),
|
||||
1000 * 60 * 60 * 24, // 24 hours
|
||||
);
|
||||
}
|
||||
}
|
||||
10
apps/server/src/translation/translation.module.ts
Normal file
10
apps/server/src/translation/translation.module.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { HttpModule } from "@nestjs/axios";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { TranslationController } from "./translation.controller";
|
||||
|
||||
@Module({
|
||||
imports: [HttpModule],
|
||||
controllers: [TranslationController],
|
||||
})
|
||||
export class TranslationModule {}
|
||||
@ -1,7 +1,7 @@
|
||||
import { CACHE_MANAGER } from "@nestjs/cache-manager";
|
||||
import { Inject, Injectable, InternalServerErrorException } from "@nestjs/common";
|
||||
import { Injectable, InternalServerErrorException } from "@nestjs/common";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { Cache } from "cache-manager";
|
||||
import { RedisService } from "@songkeys/nestjs-redis";
|
||||
import Redis from "ioredis";
|
||||
import { PrismaService } from "nestjs-prisma";
|
||||
|
||||
import { ErrorMessage } from "../constants/error-message";
|
||||
@ -9,11 +9,15 @@ import { StorageService } from "../storage/storage.service";
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
private readonly redis: Redis;
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly storageService: StorageService,
|
||||
@Inject(CACHE_MANAGER) private readonly cache: Cache,
|
||||
) {}
|
||||
private readonly redisService: RedisService,
|
||||
) {
|
||||
this.redis = this.redisService.getClient();
|
||||
}
|
||||
|
||||
async findOneById(id: string) {
|
||||
const user = await this.prisma.user.findUniqueOrThrow({
|
||||
@ -67,10 +71,7 @@ export class UserService {
|
||||
}
|
||||
|
||||
async deleteOneById(id: string) {
|
||||
await Promise.all([
|
||||
...(await this.cache.store.keys(`user:${id}:*`)).map((key) => this.cache.del(key)),
|
||||
this.storageService.deleteFolder(id),
|
||||
]);
|
||||
await Promise.all([this.redis.del(`user:${id}:*`), this.storageService.deleteFolder(id)]);
|
||||
|
||||
return this.prisma.user.delete({ where: { id } });
|
||||
}
|
||||
|
||||
@ -1,18 +1,21 @@
|
||||
import { CACHE_MANAGER } from "@nestjs/cache-manager";
|
||||
import { Inject, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||
import { Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { Cache } from "cache-manager";
|
||||
import { RedisService } from "@songkeys/nestjs-redis";
|
||||
import Redis from "ioredis";
|
||||
|
||||
import { Config } from "../config/schema";
|
||||
|
||||
@Injectable()
|
||||
export class UtilsService {
|
||||
private readonly redis: Redis;
|
||||
logger = new Logger(UtilsService.name);
|
||||
|
||||
constructor(
|
||||
private readonly redisService: RedisService,
|
||||
private readonly configService: ConfigService<Config>,
|
||||
@Inject(CACHE_MANAGER) private readonly cache: Cache,
|
||||
) {}
|
||||
) {
|
||||
this.redis = this.redisService.getClient();
|
||||
}
|
||||
|
||||
getUrl(): string {
|
||||
const url =
|
||||
@ -27,28 +30,34 @@ export class UtilsService {
|
||||
return url;
|
||||
}
|
||||
|
||||
async getCachedOrSet<T>(key: string, callback: () => Promise<T>, ttl?: number): Promise<T> {
|
||||
async getCachedOrSet<T>(
|
||||
key: string,
|
||||
callback: () => Promise<T> | T,
|
||||
ttl: number = 1000 * 60 * 60 * 24, // 24 hours
|
||||
type: "json" | "string" = "json",
|
||||
): Promise<T> {
|
||||
// Try to get the value from the cache
|
||||
const start = performance.now();
|
||||
const cachedValue = await this.cache.get<T>(key);
|
||||
const cachedValue = await this.redis.get(key);
|
||||
const duration = Number(performance.now() - start).toFixed(0);
|
||||
|
||||
if (cachedValue === undefined) {
|
||||
if (!cachedValue) {
|
||||
this.logger.debug(`Cache Key "${key}": miss`);
|
||||
} else {
|
||||
this.logger.debug(`Cache Key "${key}": hit - ${duration}ms`);
|
||||
}
|
||||
|
||||
// If the value is in the cache, return it
|
||||
if (cachedValue !== undefined) {
|
||||
return cachedValue;
|
||||
if (cachedValue) {
|
||||
return (type === "string" ? cachedValue : JSON.parse(cachedValue)) as T;
|
||||
}
|
||||
|
||||
// If the value is not in the cache, run the callback
|
||||
const value = await callback();
|
||||
const valueToCache = (type === "string" ? value : JSON.stringify(value)) as string;
|
||||
|
||||
// Store the value in the cache
|
||||
await this.cache.set(key, value, ttl);
|
||||
await this.redis.set(key, valueToCache, "PX", ttl);
|
||||
|
||||
// Return the value
|
||||
return value;
|
||||
|
||||
Reference in New Issue
Block a user