release: v4.1.0

This commit is contained in:
Amruth Pillai
2024-05-05 14:55:06 +02:00
parent 68252c35fc
commit e87b05a93a
282 changed files with 11461 additions and 10713 deletions
@@ -1,10 +1,11 @@
import { ExecutionContext } from "@nestjs/common";
import { createParamDecorator } from "@nestjs/common";
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
import { ResumeDto } from "@reactive-resume/dto";
export const Resume = createParamDecorator((data: keyof ResumeDto, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const resume = request.payload?.resume as ResumeDto;
export const Resume = createParamDecorator(
(data: keyof ResumeDto | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const resume = request.payload?.resume as ResumeDto;
return data ? resume?.[data] : resume;
});
return data ? resume[data] : resume;
},
);
@@ -27,7 +27,7 @@ export class ResumeGuard implements CanActivate {
// If the resume is private and the user is authenticated and is the owner of the resume, attach the resume to the request payload.
// Else, if either the user is not authenticated or is not the owner of the resume, throw a 404 error.
if (resume.visibility === "private") {
if (user && user && user.id === resume.userId) {
if (user && user.id === resume.userId) {
request.payload = { resume };
} else {
throw new NotFoundException(ErrorMessage.ResumeNotFound);
@@ -35,7 +35,7 @@ export class ResumeGuard implements CanActivate {
}
return true;
} catch (error) {
} catch {
throw new NotFoundException(ErrorMessage.ResumeNotFound);
}
}
+4 -12
View File
@@ -23,7 +23,6 @@ import { User } from "@/server/user/decorators/user.decorator";
import { OptionalGuard } from "../auth/guards/optional.guard";
import { TwoFactorGuard } from "../auth/guards/two-factor.guard";
import { UtilsService } from "../utils/utils.service";
import { Resume } from "./decorators/resume.decorator";
import { ResumeGuard } from "./guards/resume.guard";
import { ResumeService } from "./resume.service";
@@ -31,18 +30,11 @@ import { ResumeService } from "./resume.service";
@ApiTags("Resume")
@Controller("resume")
export class ResumeController {
constructor(
private readonly resumeService: ResumeService,
private readonly utils: UtilsService,
) {}
constructor(private readonly resumeService: ResumeService) {}
@Get("schema")
getSchema() {
return this.utils.getCachedOrSet(
`resume:schema`,
() => zodToJsonSchema(resumeDataSchema),
1000 * 60 * 60 * 24, // 24 hours
);
return zodToJsonSchema(resumeDataSchema);
}
@Post()
@@ -89,8 +81,8 @@ export class ResumeController {
@Get(":id/statistics")
@UseGuards(TwoFactorGuard)
findOneStatistics(@User("id") userId: string, @Param("id") id: string) {
return this.resumeService.findOneStatistics(userId, id);
findOneStatistics(@Param("id") id: string) {
return this.resumeService.findOneStatistics(id);
}
@Get("/public/:username/:slug")
+31 -62
View File
@@ -8,31 +8,21 @@ 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 { ErrorMessage } from "@reactive-resume/utils";
import { RedisService } from "@songkeys/nestjs-redis";
import { ErrorMessage, generateRandomName, kebabCase } from "@reactive-resume/utils";
import deepmerge from "deepmerge";
import Redis from "ioredis";
import { PrismaService } from "nestjs-prisma";
import { PrinterService } from "@/server/printer/printer.service";
import { StorageService } from "../storage/storage.service";
import { UtilsService } from "../utils/utils.service";
@Injectable()
export class ResumeService {
private readonly redis: Redis;
constructor(
private readonly prisma: PrismaService,
private readonly printerService: PrinterService,
private readonly storageService: StorageService,
private readonly redisService: RedisService,
private readonly utils: UtilsService,
) {
this.redis = this.redisService.getClient();
}
) {}
async create(userId: string, createResumeDto: CreateResumeDto) {
const { name, email, picture } = await this.prisma.user.findUniqueOrThrow({
@@ -44,7 +34,7 @@ export class ResumeService {
basics: { name, email, picture: { url: picture ?? "" } },
} satisfies DeepPartial<ResumeData>);
const resume = await this.prisma.resume.create({
return this.prisma.resume.create({
data: {
data,
userId,
@@ -53,34 +43,20 @@ export class ResumeService {
slug: createResumeDto.slug ?? kebabCase(createResumeDto.title),
},
});
// await Promise.all([
// this.redis.del(`user:${userId}:resumes`),
// this.redis.set(`user:${userId}:resume:${resume.id}`, JSON.stringify(resume)),
// ]);
return resume;
}
async import(userId: string, importResumeDto: ImportResumeDto) {
const randomTitle = generateRandomName();
const resume = await this.prisma.resume.create({
return this.prisma.resume.create({
data: {
userId,
visibility: "private",
data: importResumeDto.data,
title: importResumeDto.title || randomTitle,
slug: importResumeDto.slug || kebabCase(randomTitle),
title: importResumeDto.title ?? randomTitle,
slug: importResumeDto.slug ?? kebabCase(randomTitle),
},
});
// await Promise.all([
// this.redis.del(`user:${userId}:resumes`),
// this.redis.set(`user:${userId}:resume:${resume.id}`, JSON.stringify(resume)),
// ]);
return resume;
}
findAll(userId: string) {
@@ -95,15 +71,16 @@ export class ResumeService {
return this.prisma.resume.findUniqueOrThrow({ where: { id } });
}
async findOneStatistics(userId: string, id: string) {
const result = await Promise.all([
this.redis.get(`user:${userId}:resume:${id}:views`),
this.redis.get(`user:${userId}:resume:${id}:downloads`),
]);
async findOneStatistics(id: string) {
const result = await this.prisma.statistics.findFirst({
select: { views: true, downloads: true },
where: { resumeId: id },
});
const [views, downloads] = result.map((value) => Number(value) || 0);
return { views, downloads };
return {
views: result?.views ?? 0,
downloads: result?.downloads ?? 0,
};
}
async findOneByUsernameSlug(username: string, slug: string, userId?: string) {
@@ -112,7 +89,13 @@ export class ResumeService {
});
// Update statistics: increment the number of views by 1
if (!userId) await this.redis.incr(`user:${resume.userId}:resume:${resume.id}:views`);
if (!userId) {
await this.prisma.statistics.upsert({
where: { resumeId: resume.id },
create: { views: 1, downloads: 0, resumeId: resume.id },
update: { views: { increment: 1 } },
});
}
return resume;
}
@@ -126,7 +109,7 @@ export class ResumeService {
if (locked) throw new BadRequestException(ErrorMessage.ResumeLocked);
const resume = await this.prisma.resume.update({
return await this.prisma.resume.update({
data: {
title: updateResumeDto.title,
slug: updateResumeDto.slug,
@@ -135,15 +118,6 @@ export class ResumeService {
},
where: { userId_id: { userId, id } },
});
// await Promise.all([
// 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;
} catch (error) {
if (error.code === "P2025") {
Logger.error(error);
@@ -153,25 +127,14 @@ export class ResumeService {
}
async lock(userId: string, id: string, set: boolean) {
const resume = await this.prisma.resume.update({
return this.prisma.resume.update({
data: { locked: set },
where: { userId_id: { userId, id } },
});
// await Promise.all([
// this.redis.set(`user:${userId}:resume:${id}`, JSON.stringify(resume)),
// this.redis.del(`user:${userId}:resumes`),
// ]);
return resume;
}
async remove(userId: string, id: string) {
await Promise.all([
// Remove cached keys
// 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),
this.storageService.deleteObject(userId, "previews", id),
@@ -184,7 +147,13 @@ export class ResumeService {
const url = await this.printerService.printResume(resume);
// Update statistics: increment the number of downloads by 1
if (!userId) await this.redis.incr(`user:${resume.userId}:resume:${resume.id}:downloads`);
if (!userId) {
await this.prisma.statistics.upsert({
where: { resumeId: resume.id },
create: { views: 0, downloads: 1, resumeId: resume.id },
update: { downloads: { increment: 1 } },
});
}
return url;
}