Skip to content

Commit 180417a

Browse files
authored
implement course-completion feature (#10)
Apply authentication middleware and server-side validation, and implement a course, topic, and track endpoints with course-completion feature
2 parents 1dd8f91 + 3597b22 commit 180417a

11 files changed

Lines changed: 351 additions & 0 deletions

File tree

apps/api/src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import express from "express";
33
import { auth } from "./lib/auth.js";
44
import { toNodeHandler } from "better-auth/node";
55
import { admin, adminRouter } from "./lib/admin.js";
6+
import api from "./routes/index.js";
67
import { notesRouter } from "./routes/notes.js";
78

89
const app = express();
@@ -13,5 +14,6 @@ app.use(admin.options.rootPath, adminRouter);
1314
console.log(`AdminJS is running under ${admin.options.rootPath}`);
1415
app.use(express.json());
1516
app.use(notesRouter);
17+
app.use("/api", api);
1618

1719
export default app;

apps/api/src/routes/courses.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import exprees from "express";
2+
const router = exprees.Router();
3+
import { requireAuth } from "../middlewares/auth.js";
4+
import { validate } from "../middlewares/validate.js";
5+
import {
6+
getCourseParamsSchema,
7+
getCourseQuerySchema,
8+
} from "../schemas/courses.js";
9+
import * as Service from "../services/courses.js";
10+
import * as topicService from "../services/topics.js";
11+
12+
router.get(
13+
"/:courseId",
14+
requireAuth,
15+
validate({ params: getCourseParamsSchema }),
16+
async (req, res) => {
17+
const course = await Service.getCourse(req.params.courseId);
18+
19+
const totalTopics = await Service.getToltalTopics(req.params.courseId);
20+
21+
const completedTopics = await Service.getCompletedTopics(
22+
req.user!.id,
23+
req.params.courseId,
24+
);
25+
const persentage = (completedTopics.length / totalTopics.length) * 100;
26+
27+
res.status(200).json({
28+
course,
29+
totalTopics,
30+
persentage,
31+
});
32+
},
33+
);
34+
router.get(
35+
"/:courseId/topics",
36+
requireAuth,
37+
validate({ params: getCourseParamsSchema, query: getCourseQuerySchema }),
38+
async (req, res) => {
39+
const { completed } = req.query;
40+
41+
const totalTopics = await topicService.getToltalTopics(req.params.courseId);
42+
43+
const completedTopics = await topicService.getCompletedTopics(
44+
req.user!.id,
45+
req.params.courseId,
46+
);
47+
48+
let filterdTopics;
49+
if (completed === "true") {
50+
filterdTopics = completedTopics;
51+
} else if (completed === "false") {
52+
const completedTopicsIds = new Set();
53+
for (const topic of completedTopics) {
54+
completedTopicsIds.add(topic.id);
55+
}
56+
const unCompletedTopics = totalTopics.filter(
57+
(ele) => !completedTopicsIds.has(ele.id),
58+
);
59+
filterdTopics = unCompletedTopics;
60+
} else {
61+
filterdTopics = totalTopics;
62+
}
63+
64+
res.status(200).json({
65+
filterdTopics,
66+
});
67+
},
68+
);
69+
70+
export { router as coursesRouter };

apps/api/src/routes/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import express from "express";
2+
const router = express.Router();
3+
import { tracksRouter } from "./tracks.js";
4+
import { coursesRouter } from "./courses.js";
5+
import { topicsRouter } from "./topics.js";
6+
7+
router.get("/", (req, res) => {
8+
res.status(200).json({
9+
message: "hello from api.",
10+
});
11+
});
12+
13+
router.use("/tracks", tracksRouter);
14+
router.use("/courses", coursesRouter);
15+
router.use("/topics", topicsRouter);
16+
17+
export default router;

apps/api/src/routes/topics.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import exprees from "express";
2+
const router = exprees.Router();
3+
import { validate } from "../middlewares/validate.js";
4+
import { getTopicParamsSchema } from "../schemas/topics.js";
5+
import * as Service from "../services/topics.js";
6+
import { requireAuth } from "../middlewares/auth.js";
7+
8+
router.get(
9+
"/:topicId",
10+
validate({ params: getTopicParamsSchema }),
11+
async (req, res) => {
12+
const topic = await Service.getTopic(req.params.topicId);
13+
14+
res.status(200).json({
15+
topic,
16+
});
17+
},
18+
);
19+
router.post(
20+
"/:topicId/completion",
21+
requireAuth,
22+
validate({ params: getTopicParamsSchema }),
23+
async (req, res) => {
24+
const topic = await Service.completeTopic(req.user!.id, req.params.topicId);
25+
res.status(201).json({
26+
topic,
27+
});
28+
},
29+
);
30+
router.delete(
31+
"/:topicId/completion",
32+
requireAuth,
33+
validate({ params: getTopicParamsSchema }),
34+
async (req, res) => {
35+
await Service.inCompleteTopic(req.user!.id, req.params.topicId);
36+
res.status(204).json({});
37+
},
38+
);
39+
40+
export { router as topicsRouter };

apps/api/src/routes/tracks.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import exprees from "express";
2+
const router = exprees.Router();
3+
import { Prisma } from "../generated/prisma/client.js";
4+
import { validate } from "../middlewares/validate.js";
5+
import {
6+
getTrackQuerySchema,
7+
getTrackParamsSchema,
8+
} from "../schemas/tracks.js";
9+
import * as Service from "../services/tracks.js";
10+
import { requireAuth } from "../middlewares/auth.js";
11+
import * as courseSrvice from "../services/courses.js";
12+
13+
router.get("/", async (req, res) => {
14+
const tracks = await Service.getAllTracks();
15+
16+
res.status(200).json({
17+
length: tracks.length,
18+
data: tracks,
19+
});
20+
});
21+
router.get(
22+
"/:trackId",
23+
requireAuth,
24+
validate({ params: getTrackParamsSchema, query: getTrackQuerySchema }),
25+
async (req, res) => {
26+
const { levelId } = req.query as { levelId: string };
27+
const track = await Service.getTrack(req.params.trackId);
28+
29+
const courses = await courseSrvice.getCourses(
30+
levelId,
31+
req.params.trackId,
32+
req.user!.id,
33+
);
34+
const progress = courses.map(
35+
(
36+
course: Prisma.CourseGetPayload<{
37+
include: { topics: { select: { userCompletions: true } } };
38+
}>,
39+
) => {
40+
const totalTopics = course.topics.length;
41+
const completedTopics = course.topics.filter(
42+
(topic) => topic.userCompletions.length > 0,
43+
).length;
44+
const percentage = (completedTopics / totalTopics) * 100;
45+
46+
return {
47+
id: course.id,
48+
title: course.title,
49+
percentage,
50+
};
51+
},
52+
);
53+
res.status(200).json({
54+
track,
55+
courses: progress,
56+
});
57+
},
58+
);
59+
60+
export { router as tracksRouter };

apps/api/src/schemas/courses.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import z from "zod";
2+
3+
export const getCourseParamsSchema = z.object({
4+
courseId: z.string().min(1, "courseId is required"),
5+
});
6+
7+
export const getCourseQuerySchema = z.object({
8+
completed: z.string().optional(),
9+
});

apps/api/src/schemas/topics.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import z from "zod";
2+
3+
export const getTopicParamsSchema = z.object({
4+
topicId: z.string().min(1, "topicId is required"),
5+
});

apps/api/src/schemas/tracks.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import z from "zod";
2+
3+
export const getTrackParamsSchema = z.object({
4+
trackId: z.string().min(1, "trackId is required"),
5+
});
6+
export const getTrackQuerySchema = z.object({
7+
levelId: z.string().optional(),
8+
});

apps/api/src/services/courses.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { prisma } from "../lib/prisma.js";
2+
3+
export const getCourse = async (courseId: string) => {
4+
return await prisma.course.findUnique({
5+
where: {
6+
id: courseId,
7+
},
8+
});
9+
};
10+
11+
export const getToltalTopics = async (courseId: string) => {
12+
return await prisma.topic.findMany({
13+
where: {
14+
courseId,
15+
},
16+
select: {
17+
title: true,
18+
},
19+
orderBy: {
20+
order: "asc",
21+
},
22+
});
23+
};
24+
25+
export const getCompletedTopics = async (userId: string, courseId: string) => {
26+
return await prisma.userCompletion.findMany({
27+
where: {
28+
userId,
29+
topic: {
30+
courseId,
31+
},
32+
},
33+
});
34+
};
35+
36+
export const getCourses = async (
37+
levelId: string,
38+
trackId: string,
39+
userId: string,
40+
) => {
41+
return await prisma.course.findMany({
42+
where: {
43+
trackId,
44+
...(levelId && { levelId }),
45+
},
46+
orderBy: {
47+
order: "asc",
48+
},
49+
include: {
50+
topics: {
51+
select: {
52+
userCompletions: {
53+
where: {
54+
userId,
55+
},
56+
},
57+
},
58+
},
59+
},
60+
});
61+
};

apps/api/src/services/topics.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { prisma } from "../lib/prisma.js";
2+
3+
export const getTopic = async (topicId: string) => {
4+
return prisma.topic.findUnique({
5+
where: {
6+
id: topicId,
7+
},
8+
select: {
9+
title: true,
10+
durationInMinutes: true,
11+
content: true,
12+
},
13+
});
14+
};
15+
16+
export const completeTopic = async (userId: string, topicId: string) => {
17+
return await prisma.userCompletion.create({
18+
data: {
19+
userId,
20+
topicId,
21+
},
22+
});
23+
};
24+
export const inCompleteTopic = async (userId: string, topicId: string) => {
25+
prisma.userCompletion.delete({
26+
where: {
27+
userId_topicId: { userId, topicId },
28+
},
29+
});
30+
};
31+
32+
export const getToltalTopics = async (courseId: string) => {
33+
return await prisma.topic.findMany({
34+
where: {
35+
courseId,
36+
},
37+
orderBy: {
38+
order: "asc",
39+
},
40+
});
41+
};
42+
export const getCompletedTopics = async (userId: string, courseId: string) => {
43+
return await prisma.userCompletion.findMany({
44+
where: {
45+
userId,
46+
topic: {
47+
courseId,
48+
},
49+
},
50+
include: {
51+
topic: true,
52+
},
53+
});
54+
};

0 commit comments

Comments
 (0)