+75
apps/server/prisma/migrations/20251026204100_add_application_and_education_entities/migration.sql
+75
apps/server/prisma/migrations/20251026204100_add_application_and_education_entities/migration.sql
···
1
+
/*
2
+
Warnings:
3
+
4
+
- You are about to drop the column `education` on the `cvs` table. All the data in the column will be lost.
5
+
6
+
*/
7
+
-- AlterTable
8
+
ALTER TABLE "cvs" DROP COLUMN "education";
9
+
10
+
-- CreateTable
11
+
CREATE TABLE "applications" (
12
+
"id" TEXT NOT NULL,
13
+
"userId" TEXT NOT NULL,
14
+
"vacancyId" TEXT NOT NULL,
15
+
"cvId" TEXT,
16
+
"coverLetter" TEXT,
17
+
"status" TEXT NOT NULL DEFAULT 'pending',
18
+
"appliedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
19
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
20
+
"updatedAt" TIMESTAMP(3) NOT NULL,
21
+
22
+
CONSTRAINT "applications_pkey" PRIMARY KEY ("id")
23
+
);
24
+
25
+
-- CreateTable
26
+
CREATE TABLE "educations" (
27
+
"id" TEXT NOT NULL,
28
+
"name" TEXT NOT NULL,
29
+
"description" TEXT,
30
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
31
+
"updatedAt" TIMESTAMP(3) NOT NULL,
32
+
33
+
CONSTRAINT "educations_pkey" PRIMARY KEY ("id")
34
+
);
35
+
36
+
-- CreateTable
37
+
CREATE TABLE "user_educations" (
38
+
"id" TEXT NOT NULL,
39
+
"userId" TEXT NOT NULL,
40
+
"cvId" TEXT,
41
+
"institutionId" TEXT NOT NULL,
42
+
"degree" TEXT NOT NULL,
43
+
"fieldOfStudy" TEXT,
44
+
"startDate" TIMESTAMP(3) NOT NULL,
45
+
"endDate" TIMESTAMP(3),
46
+
"description" TEXT,
47
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
48
+
"updatedAt" TIMESTAMP(3) NOT NULL,
49
+
50
+
CONSTRAINT "user_educations_pkey" PRIMARY KEY ("id")
51
+
);
52
+
53
+
-- CreateIndex
54
+
CREATE UNIQUE INDEX "applications_userId_vacancyId_key" ON "applications"("userId", "vacancyId");
55
+
56
+
-- CreateIndex
57
+
CREATE UNIQUE INDEX "educations_name_key" ON "educations"("name");
58
+
59
+
-- AddForeignKey
60
+
ALTER TABLE "applications" ADD CONSTRAINT "applications_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
61
+
62
+
-- AddForeignKey
63
+
ALTER TABLE "applications" ADD CONSTRAINT "applications_vacancyId_fkey" FOREIGN KEY ("vacancyId") REFERENCES "vacancies"("id") ON DELETE CASCADE ON UPDATE CASCADE;
64
+
65
+
-- AddForeignKey
66
+
ALTER TABLE "applications" ADD CONSTRAINT "applications_cvId_fkey" FOREIGN KEY ("cvId") REFERENCES "cvs"("id") ON DELETE SET NULL ON UPDATE CASCADE;
67
+
68
+
-- AddForeignKey
69
+
ALTER TABLE "user_educations" ADD CONSTRAINT "user_educations_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
70
+
71
+
-- AddForeignKey
72
+
ALTER TABLE "user_educations" ADD CONSTRAINT "user_educations_cvId_fkey" FOREIGN KEY ("cvId") REFERENCES "cvs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
73
+
74
+
-- AddForeignKey
75
+
ALTER TABLE "user_educations" ADD CONSTRAINT "user_educations_institutionId_fkey" FOREIGN KEY ("institutionId") REFERENCES "educations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+58
apps/server/prisma/migrations/20251026204243_rename_education_to_institution/migration.sql
+58
apps/server/prisma/migrations/20251026204243_rename_education_to_institution/migration.sql
···
1
+
/*
2
+
Warnings:
3
+
4
+
- You are about to drop the column `name` on the `educations` table. All the data in the column will be lost.
5
+
- You are about to drop the `user_educations` table. If the table is not empty, all the data it contains will be lost.
6
+
- Added the required column `degree` to the `educations` table without a default value. This is not possible if the table is not empty.
7
+
- Added the required column `institutionId` to the `educations` table without a default value. This is not possible if the table is not empty.
8
+
- Added the required column `startDate` to the `educations` table without a default value. This is not possible if the table is not empty.
9
+
- Added the required column `userId` to the `educations` table without a default value. This is not possible if the table is not empty.
10
+
11
+
*/
12
+
-- DropForeignKey
13
+
ALTER TABLE "public"."user_educations" DROP CONSTRAINT "user_educations_cvId_fkey";
14
+
15
+
-- DropForeignKey
16
+
ALTER TABLE "public"."user_educations" DROP CONSTRAINT "user_educations_institutionId_fkey";
17
+
18
+
-- DropForeignKey
19
+
ALTER TABLE "public"."user_educations" DROP CONSTRAINT "user_educations_userId_fkey";
20
+
21
+
-- DropIndex
22
+
DROP INDEX "public"."educations_name_key";
23
+
24
+
-- AlterTable
25
+
ALTER TABLE "educations" DROP COLUMN "name",
26
+
ADD COLUMN "cvId" TEXT,
27
+
ADD COLUMN "degree" TEXT NOT NULL,
28
+
ADD COLUMN "endDate" TIMESTAMP(3),
29
+
ADD COLUMN "fieldOfStudy" TEXT,
30
+
ADD COLUMN "institutionId" TEXT NOT NULL,
31
+
ADD COLUMN "startDate" TIMESTAMP(3) NOT NULL,
32
+
ADD COLUMN "userId" TEXT NOT NULL;
33
+
34
+
-- DropTable
35
+
DROP TABLE "public"."user_educations";
36
+
37
+
-- CreateTable
38
+
CREATE TABLE "institutions" (
39
+
"id" TEXT NOT NULL,
40
+
"name" TEXT NOT NULL,
41
+
"description" TEXT,
42
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
43
+
"updatedAt" TIMESTAMP(3) NOT NULL,
44
+
45
+
CONSTRAINT "institutions_pkey" PRIMARY KEY ("id")
46
+
);
47
+
48
+
-- CreateIndex
49
+
CREATE UNIQUE INDEX "institutions_name_key" ON "institutions"("name");
50
+
51
+
-- AddForeignKey
52
+
ALTER TABLE "educations" ADD CONSTRAINT "educations_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
53
+
54
+
-- AddForeignKey
55
+
ALTER TABLE "educations" ADD CONSTRAINT "educations_cvId_fkey" FOREIGN KEY ("cvId") REFERENCES "cvs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
56
+
57
+
-- AddForeignKey
58
+
ALTER TABLE "educations" ADD CONSTRAINT "educations_institutionId_fkey" FOREIGN KEY ("institutionId") REFERENCES "institutions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+11
apps/server/prisma/migrations/20251026204502_remove_education_from_cv/migration.sql
+11
apps/server/prisma/migrations/20251026204502_remove_education_from_cv/migration.sql
···
1
+
/*
2
+
Warnings:
3
+
4
+
- You are about to drop the column `cvId` on the `educations` table. All the data in the column will be lost.
5
+
6
+
*/
7
+
-- DropForeignKey
8
+
ALTER TABLE "public"."educations" DROP CONSTRAINT "educations_cvId_fkey";
9
+
10
+
-- AlterTable
11
+
ALTER TABLE "educations" DROP COLUMN "cvId";
+16
apps/server/prisma/migrations/20251103203729_add_skills_to_education/migration.sql
+16
apps/server/prisma/migrations/20251103203729_add_skills_to_education/migration.sql
···
1
+
-- CreateTable
2
+
CREATE TABLE "_EducationToSkill" (
3
+
"A" TEXT NOT NULL,
4
+
"B" TEXT NOT NULL,
5
+
6
+
CONSTRAINT "_EducationToSkill_AB_pkey" PRIMARY KEY ("A","B")
7
+
);
8
+
9
+
-- CreateIndex
10
+
CREATE INDEX "_EducationToSkill_B_index" ON "_EducationToSkill"("B");
11
+
12
+
-- AddForeignKey
13
+
ALTER TABLE "_EducationToSkill" ADD CONSTRAINT "_EducationToSkill_A_fkey" FOREIGN KEY ("A") REFERENCES "educations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
14
+
15
+
-- AddForeignKey
16
+
ALTER TABLE "_EducationToSkill" ADD CONSTRAINT "_EducationToSkill_B_fkey" FOREIGN KEY ("B") REFERENCES "skills"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+27
apps/server/src/modules/education/education.entity.ts
+27
apps/server/src/modules/education/education.entity.ts
···
1
+
import { BaseEntity } from "@/domain/base.entity";
2
+
import type { Skill } from "@/modules/job-experience/skill/skill.entity";
3
+
import { User } from "@/modules/user/user.entity";
4
+
import { Institution } from "./institution.entity";
5
+
6
+
export class Education extends BaseEntity {
7
+
constructor(
8
+
id: string,
9
+
public readonly userId: string,
10
+
public readonly user: User | null,
11
+
public readonly institutionId: string,
12
+
public readonly institution: Institution | null,
13
+
public readonly degree: string,
14
+
public readonly fieldOfStudy: string | null,
15
+
public readonly startDate: Date,
16
+
public readonly endDate: Date | null,
17
+
public readonly description: string | null,
18
+
createdAt: Date,
19
+
updatedAt: Date,
20
+
public readonly skills?: Skill[],
21
+
) {
22
+
super(id, createdAt, updatedAt);
23
+
if (skills !== undefined) {
24
+
this.skills = skills;
25
+
}
26
+
}
27
+
}
+61
apps/server/src/modules/education/education.mapper.ts
+61
apps/server/src/modules/education/education.mapper.ts
···
1
+
import { Injectable } from "@nestjs/common";
2
+
import type { Prisma } from "@prisma/client";
3
+
import type { BaseMapper } from "@/modules/base/mapper.interface";
4
+
import { SkillMapper } from "@/modules/job-experience/skill/skill.mapper";
5
+
import { UserMapper } from "@/modules/user/user.mapper";
6
+
import { Education } from "./education.entity";
7
+
import { InstitutionMapper } from "./institution.mapper";
8
+
9
+
type PrismaEducation = Prisma.EducationGetPayload<{
10
+
include: {
11
+
institution: true;
12
+
user: true;
13
+
skills: true;
14
+
};
15
+
}>;
16
+
17
+
@Injectable()
18
+
export class EducationMapper implements BaseMapper<PrismaEducation, Education> {
19
+
constructor(
20
+
private readonly institutionMapper: InstitutionMapper,
21
+
private readonly userMapper: UserMapper,
22
+
private readonly skillMapper: SkillMapper,
23
+
) {}
24
+
25
+
/**
26
+
* Maps a Prisma Education entity to a domain Education entity
27
+
* Uses overloads to return the correct type based on input
28
+
*/
29
+
toDomain(prismaEducation: null): null;
30
+
toDomain(prismaEducation: PrismaEducation): Education;
31
+
toDomain(prismaEducation: PrismaEducation | null): Education | null;
32
+
toDomain(prismaEducation: PrismaEducation | null): Education | null {
33
+
if (!prismaEducation) {
34
+
return null;
35
+
}
36
+
37
+
return new Education(
38
+
prismaEducation.id,
39
+
prismaEducation.userId,
40
+
this.userMapper.toDomain(prismaEducation.user),
41
+
prismaEducation.institutionId,
42
+
this.institutionMapper.toDomain(prismaEducation.institution),
43
+
prismaEducation.degree,
44
+
prismaEducation.fieldOfStudy,
45
+
prismaEducation.startDate,
46
+
prismaEducation.endDate,
47
+
prismaEducation.description,
48
+
prismaEducation.createdAt,
49
+
prismaEducation.updatedAt,
50
+
prismaEducation.skills
51
+
? this.skillMapper.mapToDomain(prismaEducation.skills)
52
+
: undefined,
53
+
);
54
+
}
55
+
56
+
mapToDomain(prismaEducations: PrismaEducation[]): Education[] {
57
+
return prismaEducations
58
+
.map((education) => this.toDomain(education))
59
+
.filter((education): education is Education => education !== null);
60
+
}
61
+
}
+28
apps/server/src/modules/education/education.module.ts
+28
apps/server/src/modules/education/education.module.ts
···
1
+
import { Module } from "@nestjs/common";
2
+
import { AuthModule } from "@/modules/auth/auth.module";
3
+
import { BaseModule } from "@/modules/base/base.module";
4
+
import { DatabaseModule } from "@/modules/database/database.module";
5
+
import { SkillModule } from "@/modules/job-experience/skill/skill.module";
6
+
import { UserModule } from "@/modules/user/user.module";
7
+
import { EducationMapper } from "./education.mapper";
8
+
import { EducationService } from "./education.service";
9
+
import { EducationResolver } from "./graphql/education.resolver";
10
+
import { InstitutionResolver } from "./graphql/institution.resolver";
11
+
import { EducationUserFieldResolver } from "./graphql/user-field.resolver";
12
+
import { InstitutionMapper } from "./institution.mapper";
13
+
import { InstitutionService } from "./institution.service";
14
+
15
+
@Module({
16
+
imports: [DatabaseModule, BaseModule, AuthModule, UserModule, SkillModule],
17
+
providers: [
18
+
EducationService,
19
+
EducationMapper,
20
+
EducationResolver,
21
+
InstitutionService,
22
+
InstitutionMapper,
23
+
InstitutionResolver,
24
+
EducationUserFieldResolver,
25
+
],
26
+
exports: [EducationService, EducationMapper],
27
+
})
28
+
export class EducationModule {}
+137
apps/server/src/modules/education/education.service.ts
+137
apps/server/src/modules/education/education.service.ts
···
1
+
import { Injectable } from "@nestjs/common";
2
+
import type { Prisma } from "@prisma/client";
3
+
import { notFound } from "@/modules/base/not-found.util";
4
+
import { PaginationService } from "@/modules/base/pagination.service";
5
+
import type {
6
+
PaginationOptions,
7
+
PaginationResult,
8
+
} from "@/modules/base/pagination.types";
9
+
import { PrismaService } from "@/modules/database/prisma.service";
10
+
import { Education } from "./education.entity";
11
+
import { EducationMapper } from "./education.mapper";
12
+
13
+
@Injectable()
14
+
export class EducationService {
15
+
constructor(
16
+
private readonly prisma: PrismaService,
17
+
private readonly educationMapper: EducationMapper,
18
+
private readonly paginationService: PaginationService,
19
+
) {}
20
+
21
+
async findMany(
22
+
userId?: string,
23
+
options: PaginationOptions = {},
24
+
): Promise<PaginationResult<Education>> {
25
+
const where: Prisma.EducationWhereInput = userId ? { userId } : {};
26
+
const queryOptions = this.paginationService.buildQueryOptions(
27
+
where,
28
+
{ startDate: "desc" },
29
+
options,
30
+
);
31
+
32
+
const [items, totalCount] = await Promise.all([
33
+
this.prisma["education"].findMany({
34
+
...queryOptions,
35
+
include: {
36
+
institution: true,
37
+
user: true,
38
+
skills: true,
39
+
},
40
+
}),
41
+
this.count(userId),
42
+
]);
43
+
44
+
const domainEducations = this.educationMapper.mapToDomain(items);
45
+
return this.paginationService.buildPaginationResult(
46
+
domainEducations,
47
+
totalCount,
48
+
options,
49
+
);
50
+
}
51
+
52
+
async findManyForUser(
53
+
userId: string,
54
+
options: PaginationOptions = {},
55
+
): Promise<PaginationResult<Education>> {
56
+
return this.findMany(userId, options);
57
+
}
58
+
59
+
async findById(id: string): Promise<Education | null> {
60
+
const education = await this.prisma["education"].findUnique({
61
+
where: { id },
62
+
include: {
63
+
institution: true,
64
+
user: true,
65
+
skills: true,
66
+
},
67
+
});
68
+
return this.educationMapper.toDomain(education);
69
+
}
70
+
71
+
async count(userId?: string): Promise<number> {
72
+
const where: Prisma.EducationWhereInput = userId ? { userId } : {};
73
+
return this.prisma["education"].count({
74
+
where,
75
+
});
76
+
}
77
+
78
+
async create(
79
+
userId: string,
80
+
data: {
81
+
institutionId: string;
82
+
degree: string;
83
+
fieldOfStudy?: string | null;
84
+
startDate: Date;
85
+
endDate?: Date | null;
86
+
description?: string | null;
87
+
skillIds?: string[];
88
+
},
89
+
): Promise<Education> {
90
+
const createData: Prisma.EducationCreateInput = {
91
+
user: { connect: { id: userId } },
92
+
institution: { connect: { id: data.institutionId } },
93
+
degree: data.degree,
94
+
fieldOfStudy: data.fieldOfStudy ?? null,
95
+
startDate: data.startDate,
96
+
endDate: data.endDate ?? null,
97
+
description: data.description ?? null,
98
+
};
99
+
100
+
if (data.skillIds && data.skillIds.length > 0) {
101
+
createData.skills = {
102
+
connect: data.skillIds.map((id) => ({ id })),
103
+
};
104
+
}
105
+
106
+
const education = await this.prisma["education"].create({
107
+
data: createData,
108
+
include: {
109
+
institution: true,
110
+
user: true,
111
+
skills: true,
112
+
},
113
+
});
114
+
const domainEducation = this.educationMapper.toDomain(education);
115
+
if (!domainEducation) {
116
+
throw new Error("Failed to create education");
117
+
}
118
+
return domainEducation;
119
+
}
120
+
121
+
async findByIdOrFail(id: string): Promise<Education> {
122
+
const education = await this.findById(id);
123
+
return education ?? notFound("Education", "id", id);
124
+
}
125
+
126
+
async delete(id: string, userId: string): Promise<void> {
127
+
// Verify the education belongs to the user
128
+
const education = await this.findById(id);
129
+
if (!education || education.userId !== userId) {
130
+
throw new Error("Education not found or does not belong to user");
131
+
}
132
+
133
+
await this.prisma["education"].delete({
134
+
where: { id },
135
+
});
136
+
}
137
+
}
+38
apps/server/src/modules/education/graphql/education-connection.type.ts
+38
apps/server/src/modules/education/graphql/education-connection.type.ts
···
1
+
import { Field, Int, ObjectType } from "@nestjs/graphql";
2
+
import { PageInfo, PaginationResult } from "@/modules/base/pagination.types";
3
+
import type { Education as EducationEntity } from "../education.entity";
4
+
import { Education } from "./education.type";
5
+
import { EducationEdge } from "./education-edge.type";
6
+
7
+
@ObjectType()
8
+
export class EducationConnection {
9
+
@Field(() => [EducationEdge])
10
+
edges: EducationEdge[];
11
+
12
+
@Field(() => PageInfo)
13
+
pageInfo: PageInfo;
14
+
15
+
@Field(() => Int)
16
+
totalCount: number;
17
+
18
+
constructor(edges: EducationEdge[], pageInfo: PageInfo, totalCount: number) {
19
+
this.edges = edges;
20
+
this.pageInfo = pageInfo;
21
+
this.totalCount = totalCount;
22
+
}
23
+
24
+
/**
25
+
* Static factory method to create a connection from pagination result
26
+
*/
27
+
static fromPaginationResult(
28
+
result: PaginationResult<EducationEntity>,
29
+
): EducationConnection {
30
+
const edges = result.edges.map((edge) =>
31
+
EducationEdge.fromPaginationEdge({
32
+
cursor: edge.cursor,
33
+
node: Education.fromDomain(edge.node),
34
+
}),
35
+
);
36
+
return new EducationConnection(edges, result.pageInfo, result.totalCount);
37
+
}
38
+
}
+13
apps/server/src/modules/education/graphql/education-edge.type.ts
+13
apps/server/src/modules/education/graphql/education-edge.type.ts
···
1
+
import { Field, ObjectType } from "@nestjs/graphql";
2
+
import { GraphQLString } from "graphql";
3
+
import { BaseEdge } from "@/modules/base/connection.types";
4
+
import { Education } from "./education.type";
5
+
6
+
@ObjectType()
7
+
export class EducationEdge extends BaseEdge<Education> {
8
+
@Field(() => GraphQLString)
9
+
declare cursor: string;
10
+
11
+
@Field(() => Education)
12
+
declare node: Education;
13
+
}
+54
apps/server/src/modules/education/graphql/education.resolver.ts
+54
apps/server/src/modules/education/graphql/education.resolver.ts
···
1
+
import { UseGuards } from "@nestjs/common";
2
+
import { Args, Mutation, Query, Resolver } from "@nestjs/graphql";
3
+
import { CurrentUser } from "@/modules/auth/current-user.decorator";
4
+
import { JwtAuthGuard } from "@/modules/auth/jwt-auth.guard";
5
+
import { User } from "@/modules/user/user.type";
6
+
import { EducationService } from "../education.service";
7
+
import { Education } from "./education.type";
8
+
9
+
@Resolver(() => Education)
10
+
@UseGuards(JwtAuthGuard)
11
+
export class EducationResolver {
12
+
constructor(private readonly educationService: EducationService) {}
13
+
14
+
@Query(() => [Education])
15
+
async myEducationHistory(@CurrentUser() user: User): Promise<Education[]> {
16
+
const result = await this.educationService.findManyForUser(user.id);
17
+
return result.edges
18
+
.map((edge) => edge.node)
19
+
.map((education) => Education.fromDomain(education));
20
+
}
21
+
22
+
@Mutation(() => Education)
23
+
async createEducation(
24
+
@CurrentUser() user: User,
25
+
@Args("institutionId") institutionId: string,
26
+
@Args("degree") degree: string,
27
+
@Args("startDate") startDate: Date,
28
+
@Args("fieldOfStudy", { nullable: true }) fieldOfStudy?: string,
29
+
@Args("endDate", { nullable: true }) endDate?: Date,
30
+
@Args("description", { nullable: true }) description?: string,
31
+
@Args("skillIds", { type: () => [String], nullable: true })
32
+
skillIds?: string[],
33
+
): Promise<Education> {
34
+
const education = await this.educationService.create(user.id, {
35
+
institutionId,
36
+
degree,
37
+
fieldOfStudy: fieldOfStudy ?? null,
38
+
startDate,
39
+
endDate: endDate ?? null,
40
+
description: description ?? null,
41
+
...(skillIds !== undefined && skillIds.length > 0 ? { skillIds } : {}),
42
+
});
43
+
return Education.fromDomain(education);
44
+
}
45
+
46
+
@Mutation(() => Boolean)
47
+
async deleteEducation(
48
+
@CurrentUser() user: User,
49
+
@Args("id") id: string,
50
+
): Promise<boolean> {
51
+
await this.educationService.delete(id, user.id);
52
+
return true;
53
+
}
54
+
}
+92
apps/server/src/modules/education/graphql/education.type.ts
+92
apps/server/src/modules/education/graphql/education.type.ts
···
1
+
import { Field, ObjectType } from "@nestjs/graphql";
2
+
import { Skill } from "@/modules/job-experience/skill/graphql/skill.type";
3
+
import { Education as EducationEntity } from "../education.entity";
4
+
import { Institution } from "./institution.type";
5
+
6
+
@ObjectType()
7
+
export class Education {
8
+
@Field(() => String)
9
+
id: string;
10
+
11
+
@Field(() => String)
12
+
userId: string;
13
+
14
+
@Field(() => String)
15
+
institutionId: string;
16
+
17
+
@Field(() => Institution, { nullable: true })
18
+
institution: Institution | null;
19
+
20
+
@Field(() => String)
21
+
degree: string;
22
+
23
+
@Field(() => String, { nullable: true })
24
+
fieldOfStudy: string | null;
25
+
26
+
@Field(() => Date)
27
+
startDate: Date;
28
+
29
+
@Field(() => Date, { nullable: true })
30
+
endDate: Date | null;
31
+
32
+
@Field(() => String, { nullable: true })
33
+
description: string | null;
34
+
35
+
@Field(() => [Skill], { nullable: true })
36
+
skills: Skill[] | null;
37
+
38
+
@Field(() => Date)
39
+
createdAt: Date;
40
+
41
+
@Field(() => Date)
42
+
updatedAt: Date;
43
+
44
+
constructor(data: {
45
+
id: string;
46
+
userId: string;
47
+
institutionId: string;
48
+
institution: Institution | null;
49
+
degree: string;
50
+
fieldOfStudy: string | null;
51
+
startDate: Date;
52
+
endDate: Date | null;
53
+
description: string | null;
54
+
skills: Skill[] | null;
55
+
createdAt: Date;
56
+
updatedAt: Date;
57
+
}) {
58
+
this.id = data.id;
59
+
this.userId = data.userId;
60
+
this.institutionId = data.institutionId;
61
+
this.institution = data.institution;
62
+
this.degree = data.degree;
63
+
this.fieldOfStudy = data.fieldOfStudy;
64
+
this.startDate = data.startDate;
65
+
this.endDate = data.endDate;
66
+
this.description = data.description;
67
+
this.skills = data.skills;
68
+
this.createdAt = data.createdAt;
69
+
this.updatedAt = data.updatedAt;
70
+
}
71
+
72
+
static fromDomain(education: EducationEntity): Education {
73
+
return new Education({
74
+
id: education.id,
75
+
userId: education.userId,
76
+
institutionId: education.institutionId,
77
+
institution: education.institution
78
+
? Institution.fromDomain(education.institution)
79
+
: null,
80
+
degree: education.degree,
81
+
fieldOfStudy: education.fieldOfStudy,
82
+
startDate: education.startDate,
83
+
endDate: education.endDate,
84
+
description: education.description,
85
+
skills: education.skills
86
+
? education.skills.map((skill) => Skill.fromDomain(skill))
87
+
: null,
88
+
createdAt: education.createdAt,
89
+
updatedAt: education.updatedAt,
90
+
});
91
+
}
92
+
}
+31
apps/server/src/modules/education/graphql/institution.resolver.ts
+31
apps/server/src/modules/education/graphql/institution.resolver.ts
···
1
+
import { UseGuards } from "@nestjs/common";
2
+
import { Args, Mutation, Query, Resolver } from "@nestjs/graphql";
3
+
import { JwtAuthGuard } from "@/modules/auth/jwt-auth.guard";
4
+
import { InstitutionService } from "../institution.service";
5
+
import { Institution } from "./institution.type";
6
+
7
+
@Resolver(() => Institution)
8
+
@UseGuards(JwtAuthGuard)
9
+
export class InstitutionResolver {
10
+
constructor(private readonly institutionService: InstitutionService) {}
11
+
12
+
@Query(() => [Institution])
13
+
async institutions(): Promise<Institution[]> {
14
+
const institutions = await this.institutionService.findMany();
15
+
return institutions.map((institution) =>
16
+
Institution.fromDomain(institution),
17
+
);
18
+
}
19
+
20
+
@Mutation(() => Institution)
21
+
async createInstitution(
22
+
@Args("name") name: string,
23
+
@Args("description", { nullable: true }) description?: string,
24
+
): Promise<Institution> {
25
+
const institution = await this.institutionService.create({
26
+
name,
27
+
description: description ?? null,
28
+
});
29
+
return Institution.fromDomain(institution);
30
+
}
31
+
}
+44
apps/server/src/modules/education/graphql/institution.type.ts
+44
apps/server/src/modules/education/graphql/institution.type.ts
···
1
+
import { Field, ObjectType } from "@nestjs/graphql";
2
+
import { Institution as InstitutionEntity } from "../institution.entity";
3
+
4
+
@ObjectType()
5
+
export class Institution {
6
+
@Field(() => String)
7
+
id: string;
8
+
9
+
@Field(() => String)
10
+
name: string;
11
+
12
+
@Field(() => String, { nullable: true })
13
+
description: string | null;
14
+
15
+
@Field(() => Date)
16
+
createdAt: Date;
17
+
18
+
@Field(() => Date)
19
+
updatedAt: Date;
20
+
21
+
constructor(data: {
22
+
id: string;
23
+
name: string;
24
+
description: string | null;
25
+
createdAt: Date;
26
+
updatedAt: Date;
27
+
}) {
28
+
this.id = data.id;
29
+
this.name = data.name;
30
+
this.description = data.description;
31
+
this.createdAt = data.createdAt;
32
+
this.updatedAt = data.updatedAt;
33
+
}
34
+
35
+
static fromDomain(institution: InstitutionEntity): Institution {
36
+
return new Institution({
37
+
id: institution.id,
38
+
name: institution.name,
39
+
description: institution.description,
40
+
createdAt: institution.createdAt,
41
+
updatedAt: institution.updatedAt,
42
+
});
43
+
}
44
+
}
+30
apps/server/src/modules/education/graphql/user-field.resolver.ts
+30
apps/server/src/modules/education/graphql/user-field.resolver.ts
···
1
+
import { UseGuards } from "@nestjs/common";
2
+
import { Args, Parent, ResolveField, Resolver } from "@nestjs/graphql";
3
+
import { JwtAuthGuard } from "@/modules/auth/jwt-auth.guard";
4
+
import { PaginationService } from "@/modules/base/pagination.service";
5
+
import { PaginationArgs } from "@/modules/base/pagination.types";
6
+
import { User } from "@/modules/user/user.type";
7
+
import { EducationService } from "../education.service";
8
+
import { EducationConnection } from "./education-connection.type";
9
+
10
+
@Resolver(() => User)
11
+
@UseGuards(JwtAuthGuard)
12
+
export class EducationUserFieldResolver {
13
+
constructor(
14
+
private readonly educationService: EducationService,
15
+
private readonly paginationService: PaginationService,
16
+
) {}
17
+
18
+
@ResolveField(() => EducationConnection, { nullable: true })
19
+
async educationHistory(
20
+
@Parent() user: User,
21
+
@Args() args: PaginationArgs = {},
22
+
): Promise<EducationConnection> {
23
+
const options = this.paginationService.parsePaginationArgs(args);
24
+
const result = await this.educationService.findManyForUser(
25
+
user.id,
26
+
options,
27
+
);
28
+
return EducationConnection.fromPaginationResult(result);
29
+
}
30
+
}
+13
apps/server/src/modules/education/institution.entity.ts
+13
apps/server/src/modules/education/institution.entity.ts
···
1
+
import { BaseEntity } from "@/domain/base.entity";
2
+
3
+
export class Institution extends BaseEntity {
4
+
constructor(
5
+
id: string,
6
+
public readonly name: string,
7
+
public readonly description: string | null,
8
+
createdAt: Date,
9
+
updatedAt: Date,
10
+
) {
11
+
super(id, createdAt, updatedAt);
12
+
}
13
+
}
+40
apps/server/src/modules/education/institution.mapper.ts
+40
apps/server/src/modules/education/institution.mapper.ts
···
1
+
import { Injectable } from "@nestjs/common";
2
+
import type { Prisma } from "@prisma/client";
3
+
import type { BaseMapper } from "@/modules/base/mapper.interface";
4
+
import { Institution } from "./institution.entity";
5
+
6
+
type PrismaInstitution = Prisma.InstitutionGetPayload<Record<string, never>>;
7
+
8
+
@Injectable()
9
+
export class InstitutionMapper
10
+
implements BaseMapper<PrismaInstitution, Institution>
11
+
{
12
+
/**
13
+
* Maps a Prisma Institution entity to a domain Institution entity
14
+
* Uses overloads to return the correct type based on input
15
+
*/
16
+
toDomain(prismaInstitution: null): null;
17
+
toDomain(prismaInstitution: PrismaInstitution): Institution;
18
+
toDomain(prismaInstitution: PrismaInstitution | null): Institution | null;
19
+
toDomain(prismaInstitution: PrismaInstitution | null): Institution | null {
20
+
if (!prismaInstitution) {
21
+
return null;
22
+
}
23
+
24
+
return new Institution(
25
+
prismaInstitution.id,
26
+
prismaInstitution.name,
27
+
prismaInstitution.description,
28
+
prismaInstitution.createdAt,
29
+
prismaInstitution.updatedAt,
30
+
);
31
+
}
32
+
33
+
mapToDomain(prismaInstitutions: PrismaInstitution[]): Institution[] {
34
+
return prismaInstitutions
35
+
.map((institution) => this.toDomain(institution))
36
+
.filter(
37
+
(institution): institution is Institution => institution !== null,
38
+
);
39
+
}
40
+
}
+49
apps/server/src/modules/education/institution.service.ts
+49
apps/server/src/modules/education/institution.service.ts
···
1
+
import { Injectable } from "@nestjs/common";
2
+
import { notFound } from "@/modules/base/not-found.util";
3
+
import { PrismaService } from "@/modules/database/prisma.service";
4
+
import { Institution } from "./institution.entity";
5
+
import { InstitutionMapper } from "./institution.mapper";
6
+
7
+
@Injectable()
8
+
export class InstitutionService {
9
+
constructor(
10
+
private readonly prisma: PrismaService,
11
+
private readonly institutionMapper: InstitutionMapper,
12
+
) {}
13
+
14
+
async findMany(): Promise<Institution[]> {
15
+
const institutions = await this.prisma["institution"].findMany({
16
+
orderBy: { name: "asc" },
17
+
});
18
+
return this.institutionMapper.mapToDomain(institutions);
19
+
}
20
+
21
+
async findById(id: string): Promise<Institution | null> {
22
+
const institution = await this.prisma["institution"].findUnique({
23
+
where: { id },
24
+
});
25
+
return this.institutionMapper.toDomain(institution);
26
+
}
27
+
28
+
async findByIdOrFail(id: string): Promise<Institution> {
29
+
const institution = await this.findById(id);
30
+
return institution ?? notFound("Institution", "id", id);
31
+
}
32
+
33
+
async create(data: {
34
+
name: string;
35
+
description?: string | null;
36
+
}): Promise<Institution> {
37
+
const institution = await this.prisma["institution"].create({
38
+
data: {
39
+
name: data.name,
40
+
description: data.description ?? null,
41
+
},
42
+
});
43
+
const domainInstitution = this.institutionMapper.toDomain(institution);
44
+
if (!domainInstitution) {
45
+
throw new Error("Failed to create institution");
46
+
}
47
+
return domainInstitution;
48
+
}
49
+
}