fix(i18n): delete local translations

This commit is contained in:
Amruth Pillai
2023-11-10 13:14:44 +01:00
parent d8c605d047
commit 48727be809
65 changed files with 143 additions and 70442 deletions

View File

@ -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({

View File

@ -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 {}

View File

@ -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(),

View File

@ -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();

View File

@ -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()

View File

@ -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),

View File

@ -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) {

View 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
);
}
}

View 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 {}

View File

@ -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 } });
}

View File

@ -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;