because I got bored of customising my CV for every job
1import { createHash } from "node:crypto";
2import type { User as DomainUser } from "@cv/auth";
3import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth";
4import { validateFile } from "@cv/file-upload";
5import { BadRequestException, UseGuards } from "@nestjs/common";
6import { Args, Mutation, Resolver } from "@nestjs/graphql";
7import { CurrentUser } from "@/modules/current-user/current-user.decorator";
8import { UserFileType } from "@/modules/data-import/graphql/user-file.type";
9import { ImportService } from "@/modules/data-import/import.service";
10import { FileImportSource } from "@/modules/data-import/sources/file-import-source";
11import { ProfileService } from "@/modules/profile/profile.service";
12import { UploadFileInput } from "./upload-file.input";
13
14const MAX_DECODED_SIZE = 10 * 1024 * 1024; // 10MB
15
16@Resolver()
17@UseGuards(JwtAuthGuard, VerifiedScopeGuard)
18export class UploadFileResolver {
19 constructor(
20 private readonly importService: ImportService,
21 private readonly fileImportSource: FileImportSource,
22 private readonly profileService: ProfileService,
23 ) {}
24
25 @Mutation(() => UserFileType)
26 async uploadFile(
27 @CurrentUser() user: DomainUser,
28 @Args("input") input: UploadFileInput,
29 ): Promise<UserFileType> {
30 const buffer = Buffer.from(input.content, "base64");
31
32 if (buffer.byteLength > MAX_DECODED_SIZE) {
33 throw new BadRequestException(
34 `File exceeds maximum size of ${MAX_DECODED_SIZE / (1024 * 1024)}MB`,
35 );
36 }
37
38 const validation = validateFile({
39 buffer,
40 mimeType: input.mimeType,
41 originalName: input.fileName,
42 sizeBytes: buffer.byteLength,
43 });
44
45 if (!validation.valid) {
46 throw new BadRequestException(validation.error);
47 }
48
49 const fingerprint = createHash("sha256").update(buffer).digest("hex");
50
51 const duplicate = await this.importService.findDuplicateForUser(
52 user.id,
53 fingerprint,
54 );
55
56 if (duplicate) {
57 return UserFileType.fromDomain(duplicate, { isDuplicate: true });
58 }
59
60 const profile = input.profileId
61 ? await this.profileService.findByIdAndUserOrFail(input.profileId, user.id)
62 : await this.profileService.getOrCreateDefaultProfile(user.id);
63
64 const userFile = await this.importService.createImport(
65 user,
66 profile.id,
67 this.fileImportSource,
68 {
69 fileName: input.fileName,
70 mimeType: input.mimeType,
71 sizeBytes: buffer.byteLength,
72 fingerprint,
73 },
74 { buffer, mimeType: input.mimeType },
75 );
76
77 return UserFileType.fromDomain(userFile);
78 }
79}