because I got bored of customising my CV for every job
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

wip(CVG-13): cv variants data model and customisation pass

+346 -205
+2 -1
apps/server/package.json
··· 94 94 "@nestjs/testing": "^10.4.7", 95 95 "@swc/core": "^1.15.13", 96 96 "@types/bcryptjs": "^2.4.6", 97 - "@types/express": "^5.0.0", 97 + "@types/express": "^4.17.21", 98 + "@types/express-serve-static-core": "^4.19.8", 98 99 "@types/jest": "^29.5.12", 99 100 "@types/node": "^22.7.5", 100 101 "@types/passport-jwt": "^4.0.1",
+98
apps/server/prisma/models/cv-variant.prisma
··· 1 + model ProfileTitle { 2 + id String @id @default(cuid()) 3 + profileId String 4 + title String 5 + priority Int @default(0) 6 + createdAt DateTime @default(now()) 7 + updatedAt DateTime @updatedAt 8 + 9 + profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) 10 + variantTitles CVVariantTitle[] 11 + 12 + @@map("profile_titles") 13 + } 14 + 15 + model CVVariant { 16 + id String @id @default(cuid()) 17 + cvId String 18 + templateId String? 19 + name String 20 + introduction String? 21 + createdAt DateTime @default(now()) 22 + updatedAt DateTime @updatedAt 23 + 24 + cv CV @relation(fields: [cvId], references: [id], onDelete: Cascade) 25 + template CVTemplate? @relation(fields: [templateId], references: [id], onDelete: Restrict) 26 + titles CVVariantTitle[] 27 + locations CVVariantLocation[] 28 + experiences CVVariantExperience[] 29 + educations CVVariantEducation[] 30 + skills CVVariantSkill[] 31 + applications Application[] 32 + 33 + @@map("cv_variants") 34 + } 35 + 36 + model CVVariantTitle { 37 + variantId String 38 + titleId String 39 + order Int 40 + 41 + variant CVVariant @relation(fields: [variantId], references: [id], onDelete: Cascade) 42 + title ProfileTitle @relation(fields: [titleId], references: [id], onDelete: Cascade) 43 + 44 + @@id([variantId, titleId]) 45 + @@map("cv_variant_titles") 46 + } 47 + 48 + model CVVariantLocation { 49 + variantId String 50 + name String? 51 + cityId String? 52 + locationTypeId String 53 + label String? 54 + order Int 55 + 56 + variant CVVariant @relation(fields: [variantId], references: [id], onDelete: Cascade) 57 + city City? @relation(fields: [cityId], references: [id], onDelete: SetNull) 58 + locationType LocationType @relation(fields: [locationTypeId], references: [id], onDelete: Restrict) 59 + 60 + @@id([variantId, order]) 61 + @@map("cv_variant_locations") 62 + } 63 + 64 + model CVVariantExperience { 65 + variantId String 66 + experienceId String 67 + order Int 68 + 69 + variant CVVariant @relation(fields: [variantId], references: [id], onDelete: Cascade) 70 + experience UserJobExperience @relation(fields: [experienceId], references: [id], onDelete: Cascade) 71 + 72 + @@id([variantId, experienceId]) 73 + @@map("cv_variant_experiences") 74 + } 75 + 76 + model CVVariantEducation { 77 + variantId String 78 + educationId String 79 + order Int 80 + 81 + variant CVVariant @relation(fields: [variantId], references: [id], onDelete: Cascade) 82 + education Education @relation(fields: [educationId], references: [id], onDelete: Cascade) 83 + 84 + @@id([variantId, educationId]) 85 + @@map("cv_variant_educations") 86 + } 87 + 88 + model CVVariantSkill { 89 + variantId String 90 + skillId String 91 + order Int 92 + 93 + variant CVVariant @relation(fields: [variantId], references: [id], onDelete: Cascade) 94 + skill Skill @relation(fields: [skillId], references: [id], onDelete: Cascade) 95 + 96 + @@id([variantId, skillId]) 97 + @@map("cv_variant_skills") 98 + }
+11 -11
apps/server/prisma/models/cv.prisma
··· 8 8 createdAt DateTime @default(now()) 9 9 updatedAt DateTime @updatedAt 10 10 11 - // CVs using this template 12 - cvs CV[] 11 + cvs CV[] 12 + variants CVVariant[] 13 13 14 14 @@map("cv_templates") 15 15 } 16 16 17 17 model CV { 18 - id String @id @default(cuid()) 19 - profileId String 20 - templateId String 21 - title String 18 + id String @id @default(cuid()) 19 + profileId String 20 + templateId String 21 + title String 22 22 introduction String? 23 - createdAt DateTime @default(now()) 24 - updatedAt DateTime @updatedAt 23 + createdAt DateTime @default(now()) 24 + updatedAt DateTime @updatedAt 25 25 26 - // Relations 27 - profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) 28 - template CVTemplate @relation(fields: [templateId], references: [id], onDelete: Restrict) 26 + profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) 27 + template CVTemplate @relation(fields: [templateId], references: [id], onDelete: Restrict) 28 + variants CVVariant[] 29 29 applications Application[] 30 30 31 31 @@map("cvs")
+5 -5
apps/server/prisma/models/data-import.prisma
··· 20 20 } 21 21 22 22 model ImportJob { 23 - id String @id @default(cuid()) 23 + id String @id @default(cuid()) 24 24 userFileId String 25 25 source String 26 - status String @default("pending") 27 - statusMessage String @default("") 26 + status String @default("pending") 27 + statusMessage String @default("") 28 28 error String? 29 29 startedAt DateTime? 30 30 completedAt DateTime? 31 - createdAt DateTime @default(now()) 32 - updatedAt DateTime @updatedAt 31 + createdAt DateTime @default(now()) 32 + updatedAt DateTime @updatedAt 33 33 34 34 userFile UserFile @relation(fields: [userFileId], references: [id], onDelete: Cascade) 35 35
+5 -9
apps/server/prisma/models/job-experience.prisma
··· 5 5 createdAt DateTime @default(now()) 6 6 updatedAt DateTime @updatedAt 7 7 8 - // Job experiences that use this skill 9 8 jobExperiences UserJobExperience[] 10 - 11 - // Vacancies requiring this skill 12 - vacancies Vacancy[] 13 - 14 - // Education entries that use this skill 15 - educations Education[] 9 + vacancies Vacancy[] 10 + educations Education[] 11 + variantSkills CVVariantSkill[] 16 12 17 13 @@map("skills") 18 14 } ··· 84 80 role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) 85 81 level Level @relation(fields: [levelId], references: [id], onDelete: Cascade) 86 82 87 - // Skills used in this job experience 88 - skills Skill[] 83 + skills Skill[] 84 + variantExperiences CVVariantExperience[] 89 85 90 86 @@map("user_job_experiences") 91 87 }
+67
apps/server/prisma/models/location.prisma
··· 1 + model Country { 2 + id String @id @default(cuid()) 3 + name String @unique 4 + alpha2 String @unique 5 + alpha3 String @unique 6 + createdAt DateTime @default(now()) 7 + updatedAt DateTime @updatedAt 8 + 9 + cities City[] 10 + 11 + @@map("countries") 12 + } 13 + 14 + model City { 15 + id String @id @default(cuid()) 16 + name String 17 + countryId String 18 + createdAt DateTime @default(now()) 19 + updatedAt DateTime @updatedAt 20 + 21 + country Country @relation(fields: [countryId], references: [id], onDelete: Cascade) 22 + profileLocations ProfileLocation[] 23 + variantLocations CVVariantLocation[] 24 + 25 + @@unique([name, countryId]) 26 + @@map("cities") 27 + } 28 + 29 + enum LocationTypeCode { 30 + OFFICE 31 + HOME 32 + REMOTE 33 + HYBRID 34 + COWORKING 35 + OTHER 36 + } 37 + 38 + model LocationType { 39 + id String @id @default(cuid()) 40 + code LocationTypeCode @unique 41 + name String @unique 42 + description String? 43 + createdAt DateTime @default(now()) 44 + updatedAt DateTime @updatedAt 45 + 46 + profileLocations ProfileLocation[] 47 + variantLocations CVVariantLocation[] 48 + 49 + @@map("location_types") 50 + } 51 + 52 + model ProfileLocation { 53 + id String @id @default(cuid()) 54 + profileId String 55 + cityId String? 56 + locationTypeId String 57 + label String? 58 + priority Int @default(0) 59 + createdAt DateTime @default(now()) 60 + updatedAt DateTime @updatedAt 61 + 62 + profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) 63 + city City? @relation(fields: [cityId], references: [id], onDelete: SetNull) 64 + locationType LocationType @relation(fields: [locationTypeId], references: [id], onDelete: Restrict) 65 + 66 + @@map("profile_locations") 67 + }
+11 -11
apps/server/prisma/models/organization.prisma
··· 4 4 description String? 5 5 createdAt DateTime @default(now()) 6 6 updatedAt DateTime @updatedAt 7 - 7 + 8 8 // Users in this organization 9 9 memberships Membership[] 10 10 ··· 18 18 color String @default("#6366f1") 19 19 createdAt DateTime @default(now()) 20 20 updatedAt DateTime @updatedAt 21 - 21 + 22 22 // Memberships with this role 23 23 memberships Membership[] 24 24 ··· 26 26 } 27 27 28 28 model Membership { 29 - id String @id @default(cuid()) 30 - userId String 31 - organizationId String 29 + id String @id @default(cuid()) 30 + userId String 31 + organizationId String 32 32 organizationRoleId String 33 - createdAt DateTime @default(now()) 34 - updatedAt DateTime @updatedAt 35 - 33 + createdAt DateTime @default(now()) 34 + updatedAt DateTime @updatedAt 35 + 36 36 // Relations 37 - user User @relation(fields: [userId], references: [id], onDelete: Cascade) 38 - organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) 37 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) 38 + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) 39 39 role OrganizationRole @relation(fields: [organizationRoleId], references: [id], onDelete: Cascade) 40 - 40 + 41 41 @@unique([userId, organizationId]) 42 42 @@map("memberships") 43 43 }
+8 -8
apps/server/prisma/models/profile.prisma
··· 7 7 encryptedPhone String? 8 8 encryptedAddress String? 9 9 encryptedPostalCode String? 10 - city String? 11 - country String? 12 10 website String? 13 11 linkedInUrl String? 14 12 summary String? 15 13 createdAt DateTime @default(now()) 16 14 updatedAt DateTime @updatedAt 17 15 18 - user User @relation(fields: [userId], references: [id], onDelete: Cascade) 19 - educations Education[] 20 - jobExperiences UserJobExperience[] 21 - cvs CV[] 22 - applications Application[] 23 - userFiles UserFile[] 16 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) 17 + educations Education[] 18 + jobExperiences UserJobExperience[] 19 + cvs CV[] 20 + applications Application[] 21 + userFiles UserFile[] 22 + locations ProfileLocation[] 23 + titles ProfileTitle[] 24 24 25 25 @@map("profiles") 26 26 }
+4 -5
apps/server/prisma/models/refresh-token.prisma
··· 1 1 model RefreshToken { 2 - id String @id @default(cuid()) 3 - token String @unique 2 + id String @id @default(cuid()) 3 + token String @unique 4 4 encryptedToken String 5 5 userId String 6 6 userAgent String? ··· 11 11 city String? 12 12 usedAt DateTime? 13 13 expiresAt DateTime 14 - createdAt DateTime @default(now()) 15 - updatedAt DateTime @updatedAt 14 + createdAt DateTime @default(now()) 15 + updatedAt DateTime @updatedAt 16 16 17 17 // Relation to User 18 18 user User @relation(fields: [userId], references: [id], onDelete: Cascade) ··· 22 22 @@index([expiresAt]) 23 23 @@map("refresh_tokens") 24 24 } 25 -
+17 -16
apps/server/prisma/models/user.prisma
··· 39 39 } 40 40 41 41 model Credentials { 42 - id String @id @default(cuid()) 43 - userId String @unique 44 - email String @unique 45 - password String 46 - emailVerifiedAt DateTime? 47 - emailVerificationToken String? 42 + id String @id @default(cuid()) 43 + userId String @unique 44 + email String @unique 45 + password String 46 + emailVerifiedAt DateTime? 47 + emailVerificationToken String? 48 48 emailVerificationTokenExpiresAt DateTime? 49 - passwordResetToken String? 50 - passwordResetTokenExpiresAt DateTime? 51 - createdAt DateTime @default(now()) 52 - updatedAt DateTime @updatedAt 49 + passwordResetToken String? 50 + passwordResetTokenExpiresAt DateTime? 51 + createdAt DateTime @default(now()) 52 + updatedAt DateTime @updatedAt 53 53 54 54 // Relation to User 55 55 user User @relation(fields: [userId], references: [id], onDelete: Cascade) ··· 71 71 } 72 72 73 73 model Education { 74 - id String @id @default(cuid()) 74 + id String @id @default(cuid()) 75 75 profileId String 76 76 institutionId String 77 77 degree String ··· 79 79 startDate DateTime 80 80 endDate DateTime? 81 81 description String? 82 - createdAt DateTime @default(now()) 83 - updatedAt DateTime @updatedAt 82 + createdAt DateTime @default(now()) 83 + updatedAt DateTime @updatedAt 84 84 85 85 // Relations 86 - profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) 87 - institution Institution @relation(fields: [institutionId], references: [id], onDelete: Cascade) 88 - skills Skill[] 86 + profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) 87 + institution Institution @relation(fields: [institutionId], references: [id], onDelete: Cascade) 88 + skills Skill[] 89 + variantEducations CVVariantEducation[] 89 90 90 91 @@map("educations") 91 92 }
+40 -39
apps/server/prisma/models/vacancy.prisma
··· 25 25 } 26 26 27 27 model Vacancy { 28 - id String @id @default(cuid()) 29 - ownerId String 30 - title String 31 - companyId String 32 - roleId String 33 - levelId String? 34 - jobTypeId String? 35 - description String? 36 - requirements String? 37 - location String? 38 - minSalary Int? 39 - maxSalary Int? 28 + id String @id @default(cuid()) 29 + ownerId String 30 + title String 31 + companyId String 32 + roleId String 33 + levelId String? 34 + jobTypeId String? 35 + description String? 36 + requirements String? 37 + location String? 38 + minSalary Int? 39 + maxSalary Int? 40 40 applicationUrl String? 41 - deadline DateTime? 42 - isActive Boolean @default(true) 43 - isPublic Boolean @default(false) 44 - createdAt DateTime @default(now()) 45 - updatedAt DateTime @updatedAt 41 + deadline DateTime? 42 + isActive Boolean @default(true) 43 + isPublic Boolean @default(false) 44 + createdAt DateTime @default(now()) 45 + updatedAt DateTime @updatedAt 46 46 47 47 // Relations 48 - owner User @relation("VacancyOwner", fields: [ownerId], references: [id], onDelete: Cascade) 49 - company Company @relation(fields: [companyId], references: [id], onDelete: Cascade) 50 - role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) 51 - level Level? @relation(fields: [levelId], references: [id], onDelete: Cascade) 52 - jobType JobType? @relation(fields: [jobTypeId], references: [id], onDelete: Cascade) 53 - skills Skill[] 48 + owner User @relation("VacancyOwner", fields: [ownerId], references: [id], onDelete: Cascade) 49 + company Company @relation(fields: [companyId], references: [id], onDelete: Cascade) 50 + role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) 51 + level Level? @relation(fields: [levelId], references: [id], onDelete: Cascade) 52 + jobType JobType? @relation(fields: [jobTypeId], references: [id], onDelete: Cascade) 53 + skills Skill[] 54 54 applications Application[] 55 55 56 56 @@map("vacancies") 57 57 } 58 58 59 59 model Application { 60 - id String @id @default(cuid()) 61 - userId String 62 - profileId String 63 - vacancyId String 64 - cvId String? 65 - coverLetter String? 66 - statusId String 67 - appliedAt DateTime @default(now()) 68 - createdAt DateTime @default(now()) 69 - updatedAt DateTime @updatedAt 60 + id String @id @default(cuid()) 61 + userId String 62 + profileId String 63 + vacancyId String 64 + cvId String? 65 + cvVariantId String? 66 + coverLetter String? 67 + statusId String 68 + appliedAt DateTime @default(now()) 69 + createdAt DateTime @default(now()) 70 + updatedAt DateTime @updatedAt 70 71 71 - // Relations 72 - user User @relation(fields: [userId], references: [id], onDelete: Cascade) 73 - profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) 74 - vacancy Vacancy @relation(fields: [vacancyId], references: [id], onDelete: Cascade) 75 - cv CV? @relation(fields: [cvId], references: [id], onDelete: SetNull) 76 - status ApplicationStatus @relation(fields: [statusId], references: [id], onDelete: Restrict) 72 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) 73 + profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) 74 + vacancy Vacancy @relation(fields: [vacancyId], references: [id], onDelete: Cascade) 75 + cv CV? @relation(fields: [cvId], references: [id], onDelete: SetNull) 76 + cvVariant CVVariant? @relation(fields: [cvVariantId], references: [id], onDelete: SetNull) 77 + status ApplicationStatus @relation(fields: [statusId], references: [id], onDelete: Restrict) 77 78 78 79 @@unique([userId, vacancyId]) 79 80 @@map("applications")
+2 -2
apps/server/src/modules/application/application.service.ts
··· 1 1 import { notFound } from "@cv/auth"; 2 2 import { type EntityService, PrismaService } from "@cv/system"; 3 3 import { Injectable } from "@nestjs/common"; 4 - import { Prisma } from "@prisma/client"; 4 + import { PrismaClientKnownRequestError } from "@prisma/client/runtime/client"; 5 5 import { Application } from "./application.entity"; 6 6 import { DuplicateApplicationError } from "./application.error"; 7 7 import { ApplicationMapper } from "./application.mapper"; ··· 119 119 return domain; 120 120 } catch (error: unknown) { 121 121 if ( 122 - error instanceof Prisma.PrismaClientKnownRequestError && 122 + error instanceof PrismaClientKnownRequestError && 123 123 error.code === "P2002" && 124 124 error.meta && 125 125 typeof error.meta === "object" &&
+12 -5
apps/server/src/modules/cv-template/cv-data-assembler.service.ts
··· 8 8 name: string; 9 9 headline: string | null; 10 10 phone: string | null; 11 - city: string | null; 12 - country: string | null; 11 + location: string | null; 13 12 website: string | null; 14 13 linkedInUrl: string | null; 15 14 summary: string | null; ··· 73 72 async assemble(cvId: string): Promise<CVRenderContext> { 74 73 const cv = await this.prisma.cV.findUniqueOrThrow({ where: { id: cvId } }); 75 74 76 - const [profile, experiences, educations] = await Promise.all([ 75 + const [profile, experiences, educations, primaryLocation] = await Promise.all([ 77 76 this.profileService.findByIdOrFail(cv.profileId), 78 77 this.prisma.userJobExperience.findMany({ 79 78 where: { profileId: cv.profileId }, ··· 85 84 orderBy: { startDate: "desc" }, 86 85 include: { institution: true, skills: true }, 87 86 }), 87 + this.prisma.profileLocation.findFirst({ 88 + where: { profileId: cv.profileId }, 89 + orderBy: { priority: "asc" }, 90 + include: { city: { include: { country: true } } }, 91 + }), 88 92 ]); 89 93 90 94 const profileRecord = await this.prisma.profile.findUniqueOrThrow({ ··· 131 135 name: profile.fullName ?? profile.name, 132 136 headline: profile.headline ?? null, 133 137 phone: profile.phone ?? null, 134 - city: profile.city ?? null, 135 - country: profile.country ?? null, 138 + location: primaryLocation 139 + ? [primaryLocation.city?.name, primaryLocation.city?.country?.name] 140 + .filter(Boolean) 141 + .join(", ") || primaryLocation.label || null 142 + : null, 136 143 website: profile.website ?? null, 137 144 linkedInUrl: profile.linkedInUrl ?? null, 138 145 summary: profile.summary ?? null,
+1 -1
apps/server/src/modules/cv-template/seed/templates/classic-executive.hbs
··· 5 5 <div class="contact-row"> 6 6 {{#if profile.email}}<span>{{profile.email}}</span>{{/if}} 7 7 {{#if profile.phone}}<span>{{profile.phone}}</span>{{/if}} 8 - {{#if profile.city}}<span>{{profile.city}}{{#if profile.country}}, {{profile.country}}{{/if}}</span>{{/if}} 8 + {{#if profile.location}}<span>{{profile.location}}</span>{{/if}} 9 9 {{#if profile.website}}<span>{{profile.website}}</span>{{/if}} 10 10 {{#if profile.linkedInUrl}}<span>{{profile.linkedInUrl}}</span>{{/if}} 11 11 </div>
+1 -1
apps/server/src/modules/cv-template/seed/templates/creative-portfolio.hbs
··· 9 9 <h3>Contact</h3> 10 10 {{#if profile.email}}<p>{{profile.email}}</p>{{/if}} 11 11 {{#if profile.phone}}<p>{{profile.phone}}</p>{{/if}} 12 - {{#if profile.city}}<p>{{profile.city}}{{#if profile.country}}, {{profile.country}}{{/if}}</p>{{/if}} 12 + {{#if profile.location}}<p>{{profile.location}}</p>{{/if}} 13 13 {{#if profile.website}}<p><a href="{{profile.website}}">{{profile.website}}</a></p>{{/if}} 14 14 {{#if profile.linkedInUrl}}<p><a href="{{profile.linkedInUrl}}">LinkedIn</a></p>{{/if}} 15 15 </div>
+1 -1
apps/server/src/modules/cv-template/seed/templates/modern-professional.hbs
··· 5 5 <div class="contact-row"> 6 6 {{#if profile.email}}<span>{{profile.email}}</span>{{/if}} 7 7 {{#if profile.phone}}<span>{{profile.phone}}</span>{{/if}} 8 - {{#if profile.city}}<span>{{profile.city}}{{#if profile.country}}, {{profile.country}}{{/if}}</span>{{/if}} 8 + {{#if profile.location}}<span>{{profile.location}}</span>{{/if}} 9 9 {{#if profile.website}}<span><a href="{{profile.website}}">{{profile.website}}</a></span>{{/if}} 10 10 {{#if profile.linkedInUrl}}<span><a href="{{profile.linkedInUrl}}">LinkedIn</a></span>{{/if}} 11 11 </div>
-2
apps/server/src/modules/data-import/listeners/import-job.listener.ts
··· 160 160 fullName: info.name, 161 161 headline: info.headline, 162 162 summary: info.introduction, 163 - city: info.city, 164 - country: info.country, 165 163 phone: info.phone, 166 164 website: info.website, 167 165 linkedInUrl: info.linkedInUrl,
+1 -3
apps/server/src/modules/data-import/sources/file-import-source.ts
··· 17 17 * Build ExistingUserContext from domain models for AI prompt enrichment. 18 18 */ 19 19 const buildExistingUserContext = ( 20 - profile: { fullName?: string | null; headline?: string | null; city?: string | null; country?: string | null } | null, 20 + profile: { fullName?: string | null; headline?: string | null } | null, 21 21 jobs: Array<{ company: { name: string }; role: { name: string }; startDate: Date; endDate?: Date }>, 22 22 educations: Array<{ institution: { name: string }; degree: string; startDate: Date; endDate: Date | null }>, 23 23 ): ExistingUserContext | undefined => { ··· 25 25 26 26 if (profile?.fullName) context.name = profile.fullName; 27 27 if (profile?.headline) context.headline = profile.headline; 28 - if (profile?.city) context.city = profile.city; 29 - if (profile?.country) context.country = profile.country; 30 28 31 29 if (jobs.length > 0) { 32 30 context.jobs = jobs.map((j) => {
-18
apps/server/src/modules/profile/graphql/profile.type.ts
··· 24 24 postalCode!: string | null; 25 25 26 26 @Field(() => String, { nullable: true }) 27 - city!: string | null; 28 - 29 - @Field(() => String, { nullable: true }) 30 - country!: string | null; 31 - 32 - @Field(() => String, { nullable: true }) 33 27 website!: string | null; 34 28 35 29 @Field(() => String, { nullable: true }) ··· 66 60 postalCode?: string; 67 61 68 62 @Field(() => String, { nullable: true }) 69 - city?: string; 70 - 71 - @Field(() => String, { nullable: true }) 72 - country?: string; 73 - 74 - @Field(() => String, { nullable: true }) 75 63 website?: string; 76 64 77 65 @Field(() => String, { nullable: true }) ··· 100 88 101 89 @Field(() => String, { nullable: true }) 102 90 postalCode?: string; 103 - 104 - @Field(() => String, { nullable: true }) 105 - city?: string; 106 - 107 - @Field(() => String, { nullable: true }) 108 - country?: string; 109 91 110 92 @Field(() => String, { nullable: true }) 111 93 website?: string;
+2 -2
apps/server/src/modules/profile/onboarding/profile.step.ts
··· 19 19 async computeStatus(user: User): Promise<OnboardingStepStatus> { 20 20 const profile = await this.prisma.profile.findFirst({ 21 21 where: { userId: user.id }, 22 - select: { headline: true, city: true }, 22 + select: { headline: true, locations: { take: 1 } }, 23 23 }); 24 24 25 25 if (!profile) return OnboardingStepStatus.NOT_STARTED; 26 26 27 - return profile.headline && profile.city 27 + return profile.headline && profile.locations.length > 0 28 28 ? OnboardingStepStatus.COMPLETE 29 29 : OnboardingStepStatus.IN_PROGRESS; 30 30 }
+2 -14
apps/server/src/modules/profile/profile.service.ts
··· 10 10 phone?: string; 11 11 address?: string; 12 12 postalCode?: string; 13 - city?: string; 14 - country?: string; 15 13 website?: string; 16 14 linkedInUrl?: string; 17 15 summary?: string; ··· 24 22 phone?: string; 25 23 address?: string; 26 24 postalCode?: string; 27 - city?: string; 28 - country?: string; 29 25 website?: string; 30 26 linkedInUrl?: string; 31 27 summary?: string; ··· 40 36 phone: string | null; 41 37 address: string | null; 42 38 postalCode: string | null; 43 - city: string | null; 44 - country: string | null; 45 39 website: string | null; 46 40 linkedInUrl: string | null; 47 41 summary: string | null; ··· 144 138 async hasKeyFields(userId: string): Promise<boolean> { 145 139 const profile = await this.prisma.profile.findFirst({ 146 140 where: { userId }, 147 - select: { headline: true, city: true }, 141 + select: { headline: true, locations: { take: 1 } }, 148 142 }); 149 143 150 - return profile !== null && profile.headline !== null && profile.city !== null; 144 + return profile !== null && profile.headline !== null && profile.locations.length > 0; 151 145 } 152 146 153 147 private decrypt(profile: { ··· 159 153 encryptedPhone: string | null; 160 154 encryptedAddress: string | null; 161 155 encryptedPostalCode: string | null; 162 - city: string | null; 163 - country: string | null; 164 156 website: string | null; 165 157 linkedInUrl: string | null; 166 158 summary: string | null; ··· 182 174 postalCode: profile.encryptedPostalCode 183 175 ? this.encryption.decrypt(profile.encryptedPostalCode) 184 176 : null, 185 - city: profile.city, 186 - country: profile.country, 187 177 website: profile.website, 188 178 linkedInUrl: profile.linkedInUrl, 189 179 summary: profile.summary, ··· 199 189 200 190 if (data.fullName !== undefined) result["fullName"] = data.fullName || null; 201 191 if (data.headline !== undefined) result["headline"] = data.headline || null; 202 - if (data.city !== undefined) result["city"] = data.city || null; 203 - if (data.country !== undefined) result["country"] = data.country || null; 204 192 if (data.website !== undefined) result["website"] = data.website || null; 205 193 if (data.linkedInUrl !== undefined) 206 194 result["linkedInUrl"] = data.linkedInUrl || null;
+2 -1
package.json
··· 38 38 "tesseract.js" 39 39 ], 40 40 "overrides": { 41 - "pdfjs-dist>canvas": "^3.1.0" 41 + "pdfjs-dist>canvas": "^3.1.0", 42 + "@types/jsonwebtoken": "9.0.5" 42 43 } 43 44 } 44 45 }
+6 -2
packages/auth/package.json
··· 26 26 "@nestjs/core": "^10.4.7", 27 27 "@nestjs/event-emitter": "^3.0.1", 28 28 "@nestjs/graphql": "^12.2.2", 29 - "@nestjs/jwt": "^10.2.0", 29 + "@nestjs/jwt": "^11.0.2", 30 30 "bcryptjs": "^2.4.3", 31 31 "graphql": "^16.12.0", 32 + "ms": "^2.1.3", 32 33 "reflect-metadata": "^0.2.2", 33 34 "zod": "^4.3.6" 34 35 }, ··· 37 38 "@cv/biome-config": "*", 38 39 "@cv/tsconfig": "*", 39 40 "@types/bcryptjs": "^2.4.0", 40 - "@types/express": "^5.0.0", 41 + "@types/express": "^4.17.21", 42 + "@types/express-serve-static-core": "^4.19.8", 43 + "@types/jsonwebtoken": "9.0.5", 44 + "@types/ms": "^2.1.0", 41 45 "typescript": "^5.6.3" 42 46 }, 43 47 "peerDependencies": {
+48 -48
pnpm-lock.yaml
··· 6 6 7 7 overrides: 8 8 pdfjs-dist>canvas: ^3.1.0 9 + '@types/jsonwebtoken': 9.0.5 9 10 10 11 importers: 11 12 ··· 295 296 version: link:../../../../riotbyte/project-q/packages/transport/prisma 296 297 '@types/cookie-parser': 297 298 specifier: ^1.4.10 298 - version: 1.4.10(@types/express@5.0.6) 299 + version: 1.4.10(@types/express@4.17.25) 299 300 '@types/handlebars': 300 301 specifier: ^4.0.40 301 302 version: 4.1.0 ··· 409 410 specifier: ^2.4.6 410 411 version: 2.4.6 411 412 '@types/express': 412 - specifier: ^5.0.0 413 - version: 5.0.6 413 + specifier: ^4.17.21 414 + version: 4.17.25 415 + '@types/express-serve-static-core': 416 + specifier: ^4.19.8 417 + version: 4.19.8 414 418 '@types/jest': 415 419 specifier: ^29.5.12 416 420 version: 29.5.14 ··· 622 626 specifier: ^12.2.2 623 627 version: 12.2.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.2.2) 624 628 '@nestjs/jwt': 625 - specifier: ^10.2.0 626 - version: 10.2.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)) 629 + specifier: ^11.0.2 630 + version: 11.0.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)) 627 631 bcryptjs: 628 632 specifier: ^2.4.3 629 633 version: 2.4.3 630 634 graphql: 631 635 specifier: ^16.12.0 632 636 version: 16.12.0 637 + ms: 638 + specifier: ^2.1.3 639 + version: 2.1.3 633 640 reflect-metadata: 634 641 specifier: ^0.2.2 635 642 version: 0.2.2 ··· 650 657 specifier: ^2.4.0 651 658 version: 2.4.6 652 659 '@types/express': 653 - specifier: ^5.0.0 654 - version: 5.0.6 660 + specifier: ^4.17.21 661 + version: 4.17.25 662 + '@types/express-serve-static-core': 663 + specifier: ^4.19.8 664 + version: 4.19.8 665 + '@types/jsonwebtoken': 666 + specifier: 9.0.5 667 + version: 9.0.5 668 + '@types/ms': 669 + specifier: ^2.1.0 670 + version: 2.1.0 655 671 typescript: 656 672 specifier: ^5.6.3 657 673 version: 5.9.3 ··· 2563 2579 peerDependencies: 2564 2580 '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 2565 2581 2582 + '@nestjs/jwt@11.0.2': 2583 + resolution: {integrity: sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==} 2584 + peerDependencies: 2585 + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 2586 + 2566 2587 '@nestjs/mapped-types@2.0.6': 2567 2588 resolution: {integrity: sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==} 2568 2589 peerDependencies: ··· 3484 3505 '@types/express-serve-static-core@4.17.31': 3485 3506 resolution: {integrity: sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==} 3486 3507 3487 - '@types/express-serve-static-core@4.19.7': 3488 - resolution: {integrity: sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==} 3489 - 3490 - '@types/express-serve-static-core@5.1.0': 3491 - resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==} 3508 + '@types/express-serve-static-core@4.19.8': 3509 + resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} 3492 3510 3493 3511 '@types/express@4.17.14': 3494 3512 resolution: {integrity: sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==} 3495 3513 3496 3514 '@types/express@4.17.25': 3497 3515 resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} 3498 - 3499 - '@types/express@5.0.6': 3500 - resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} 3501 3516 3502 3517 '@types/graceful-fs@4.1.9': 3503 3518 resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} ··· 3529 3544 3530 3545 '@types/jest@29.5.14': 3531 3546 resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} 3532 - 3533 - '@types/jsonwebtoken@9.0.10': 3534 - resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} 3535 3547 3536 3548 '@types/jsonwebtoken@9.0.5': 3537 3549 resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} ··· 8621 8633 '@apollo/utils.withrequired': 2.0.1 8622 8634 '@graphql-tools/schema': 9.0.19(graphql@16.12.0) 8623 8635 '@types/express': 4.17.25 8624 - '@types/express-serve-static-core': 4.19.7 8636 + '@types/express-serve-static-core': 4.19.8 8625 8637 '@types/node-fetch': 2.6.13 8626 8638 async-retry: 1.3.3 8627 8639 cors: 2.8.5 ··· 10681 10693 '@types/jsonwebtoken': 9.0.5 10682 10694 jsonwebtoken: 9.0.2 10683 10695 10696 + '@nestjs/jwt@11.0.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))': 10697 + dependencies: 10698 + '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) 10699 + '@types/jsonwebtoken': 9.0.5 10700 + jsonwebtoken: 9.0.3 10701 + 10684 10702 '@nestjs/mapped-types@2.0.6(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': 10685 10703 dependencies: 10686 10704 '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) ··· 11548 11566 dependencies: 11549 11567 '@types/node': 22.19.3 11550 11568 11551 - '@types/cookie-parser@1.4.10(@types/express@5.0.6)': 11569 + '@types/cookie-parser@1.4.10(@types/express@4.17.25)': 11552 11570 dependencies: 11553 - '@types/express': 5.0.6 11571 + '@types/express': 4.17.25 11554 11572 11555 11573 '@types/cors@2.8.12': {} 11556 11574 ··· 11572 11590 '@types/qs': 6.14.0 11573 11591 '@types/range-parser': 1.2.7 11574 11592 11575 - '@types/express-serve-static-core@4.19.7': 11576 - dependencies: 11577 - '@types/node': 22.19.3 11578 - '@types/qs': 6.14.0 11579 - '@types/range-parser': 1.2.7 11580 - '@types/send': 1.2.1 11581 - 11582 - '@types/express-serve-static-core@5.1.0': 11593 + '@types/express-serve-static-core@4.19.8': 11583 11594 dependencies: 11584 11595 '@types/node': 22.19.3 11585 11596 '@types/qs': 6.14.0 ··· 11588 11599 11589 11600 '@types/express@4.17.14': 11590 11601 dependencies: 11591 - '@types/body-parser': 1.19.2 11592 - '@types/express-serve-static-core': 4.17.31 11602 + '@types/body-parser': 1.19.6 11603 + '@types/express-serve-static-core': 4.19.8 11593 11604 '@types/qs': 6.14.0 11594 11605 '@types/serve-static': 2.2.0 11595 11606 11596 11607 '@types/express@4.17.25': 11597 11608 dependencies: 11598 11609 '@types/body-parser': 1.19.6 11599 - '@types/express-serve-static-core': 4.19.7 11610 + '@types/express-serve-static-core': 4.19.8 11600 11611 '@types/qs': 6.14.0 11601 11612 '@types/serve-static': 1.15.10 11602 11613 11603 - '@types/express@5.0.6': 11604 - dependencies: 11605 - '@types/body-parser': 1.19.6 11606 - '@types/express-serve-static-core': 5.1.0 11607 - '@types/serve-static': 2.2.0 11608 - 11609 11614 '@types/graceful-fs@4.1.9': 11610 11615 dependencies: 11611 11616 '@types/node': 22.19.3 ··· 11641 11646 dependencies: 11642 11647 expect: 29.7.0 11643 11648 pretty-format: 29.7.0 11644 - 11645 - '@types/jsonwebtoken@9.0.10': 11646 - dependencies: 11647 - '@types/ms': 2.1.0 11648 - '@types/node': 22.19.3 11649 11649 11650 11650 '@types/jsonwebtoken@9.0.5': 11651 11651 dependencies: ··· 11669 11669 11670 11670 '@types/multer@2.0.0': 11671 11671 dependencies: 11672 - '@types/express': 5.0.6 11672 + '@types/express': 4.17.25 11673 11673 11674 11674 '@types/node-fetch@2.6.13': 11675 11675 dependencies: ··· 11686 11686 11687 11687 '@types/passport-jwt@4.0.1': 11688 11688 dependencies: 11689 - '@types/jsonwebtoken': 9.0.10 11689 + '@types/jsonwebtoken': 9.0.5 11690 11690 '@types/passport-strategy': 0.2.38 11691 11691 11692 11692 '@types/passport-local@1.0.38': 11693 11693 dependencies: 11694 - '@types/express': 5.0.6 11694 + '@types/express': 4.17.25 11695 11695 '@types/passport': 1.0.17 11696 11696 '@types/passport-strategy': 0.2.38 11697 11697 11698 11698 '@types/passport-strategy@0.2.38': 11699 11699 dependencies: 11700 - '@types/express': 5.0.6 11700 + '@types/express': 4.17.25 11701 11701 '@types/passport': 1.0.17 11702 11702 11703 11703 '@types/passport@1.0.17': 11704 11704 dependencies: 11705 - '@types/express': 5.0.6 11705 + '@types/express': 4.17.25 11706 11706 11707 11707 '@types/pdf-parse@1.1.5': 11708 11708 dependencies: