Skip to content

Commit 2c43cd2

Browse files
More Course migration: Add/Remove/List course members, get all courses of a user, update course (#343)
* Created zod schemas for permissions and default roles * Refactoring of add_course_member and adding tests * get courses by user id * update courses * Finished refactoring add_course_member (modified error checking) and added tests * Implemented remove course and fixfixed issues in add course member and delete course * Fixed errors with get courses by useuserId and update course. Added Tests * Implemented get course members --------- Co-authored-by: Ray Zhou <rayzhou4@gmail.com>
1 parent 9e31eed commit 2c43cd2

16 files changed

+818
-100
lines changed

server/supabase/functions/_shared/errors.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,11 @@ export class UserStatusError extends Error {
3737
// constructor(message: string) {
3838
// super(message)
3939
// }
40-
// }
40+
// }
41+
42+
export class PermissionError extends Error {
43+
constructor(message: string) {
44+
super(message);
45+
this.name = "PermissionError";
46+
}
47+
}
Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,60 @@
11
import { supa } from "../../_shared/db.ts";
2+
import { Course, DefaultRoles, studentRole } from '../../../../../shared/schema/course.ts';
3+
import { NotFoundError, PermissionError, InputValidationError } from '../../_shared/errors.ts';
24

35
export async function addUserToCourse(
46
course_id: string,
57
user_id: string,
6-
role?: any // TODO: @victor: fix this
7-
): Promise<boolean> { // TODO: @victor: probably good for this function to use one of error handling or return false/true instead of a mix of both
8+
role?: DefaultRoles
9+
) {
10+
const course = await getCourse(course_id);
11+
12+
await checkIfUserExistsAndInCourse(user_id, course_id);
13+
14+
const roleData = await getRole(role);
15+
16+
const { error: joinError } = await supa
17+
.from("course_members")
18+
.insert([{
19+
course_id: course_id,
20+
user_id: user_id,
21+
role_id: roleData.id
22+
}]);
23+
24+
if (joinError) {
25+
throw new Error(`Failed to add user to course: ${joinError.message}`);
26+
}
27+
28+
return true;
29+
}
30+
31+
async function getCourse(course_id: string): Promise<Course> {
832
const { data: course, error: courseError } = await supa.from("courses")
9-
.select("*").eq("id", course_id).single();
33+
.select("*").eq("id", course_id);
34+
35+
if (courseError) {
36+
throw new Error(`Database error when retrieving course with id ${course_id}: ${courseError.message}`);
37+
}
1038

11-
if (courseError || !course) {
12-
throw new Error("Course not found");
39+
if (!course || course.length !== 1) {
40+
throw new NotFoundError(`Course with id ${course_id} not found`);
1341
}
1442

15-
if (course.access === "private") {
16-
throw new Error("Cannot join a private course");
43+
if (course[0].access === "private") {
44+
throw new PermissionError("Cannot join a private course");
1745
}
46+
47+
return course[0] as Course;
48+
}
1849

19-
const { data: user, error: userError } = await supa.from("profiles").select("*").eq("id", user_id).single();
50+
async function checkIfUserExistsAndInCourse(user_id: string, course_id: string) {
51+
const { data: user, error: userError } = await supa.from("profiles").select("*").eq("id", user_id);
2052

21-
if (userError || !user) {
22-
throw new Error("User not found");
53+
if (userError) {
54+
throw new Error(`Database error when retrieving user with id ${user_id}: ${userError.message}`);
55+
}
56+
if (!user || user.length !== 1) {
57+
throw new NotFoundError(`User ${user_id} not found`);
2358
}
2459

2560
const { data: existingUserCourse } = await supa
@@ -30,30 +65,26 @@ export async function addUserToCourse(
3065
.single();
3166

3267
if (existingUserCourse) {
33-
throw new Error("User already registered in course");
68+
throw new InputValidationError(`User ${user_id} already registered in course ${course_id}`);
3469
}
70+
}
3571

36-
// TODO: @victor: check shared/schema/course.ts for notes on how to get this is a more type safe way
72+
async function getRole(role: DefaultRoles | undefined ): Promise<{ id: string }> {
3773
if (!role) {
38-
role = "student"; // use student by default
74+
role = studentRole; // use student by default
3975
}
4076

4177
const { data: roleData, error: roleError } = await supa.from("account_roles")
4278
.select("id")
43-
.eq("name", role)
44-
.single();
79+
.eq("name", role);
4580

46-
if (roleError || !roleData) {
47-
throw new Error("Role not found: " + (roleError?.message || "No data returned"));
81+
if (roleError) {
82+
throw new Error("Database error ")
4883
}
4984

50-
const { error: joinError } = await supa
51-
.from("user_courses")
52-
.insert([{ course_id: course_id, user_id: user_id, role_id: roleData.id }]);
53-
54-
if (joinError) {
55-
throw new Error(`Failed to add user to course: ${joinError.message}`);
85+
if (!roleData || roleData.length !== 1) {
86+
throw new NotFoundError(`Role ${role} not found`);
5687
}
5788

58-
return true;
59-
}
89+
return roleData[0];
90+
}

server/supabase/functions/courses/controller/create_course_activity.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { supa } from "../../_shared/db.ts";
22

3-
import { Course, NewCourse } from "@shared/mod.ts";
3+
import { Course, NewCourse, DEFAULT_ROLES, instructorRole } from "@shared/mod.ts";
44
import { NotFoundError, InputValidationError } from '../../_shared/errors.ts';
55

66
export async function createCourse(
@@ -28,13 +28,10 @@ export async function createCourse(
2828
}
2929

3030
async function createCourseRoles(courseId: string) {
31-
// Assumes that the roles are already created
32-
const roleNames = ["instructor", "staff", "student"];
33-
3431
const { data: rolesData, error: rolesError } = await supa
3532
.from("account_roles")
3633
.select("id, name")
37-
.in("name", roleNames);
34+
.in("name", DEFAULT_ROLES);
3835

3936
if (rolesError || !rolesData || rolesData.length !== 3) {
4037
throw new Error(
@@ -65,7 +62,7 @@ async function addInstructorToCourse(courseId: string, userId: string) {
6562
const { data: instructorRoleData, error: instructorRoleError } = await supa
6663
.from("account_roles")
6764
.select("id")
68-
.eq("name", "instructor")
65+
.eq("name", instructorRole)
6966
.single();
7067

7168
if (instructorRoleError || !instructorRoleData) {
Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
import { supa } from "../../_shared/db.ts";
2+
import { Course } from '../../../../../shared/schema/course.ts';
23

34
export async function getAllCourses() {
45
const { data, error } = await supa.from("courses").select("*");
56
if (error) {
6-
throw new Error("Failed to get all courses: " + error.message);
7+
throw new Error(`Database error when retrieving courses: ${error.message}`);
78
}
89
return data;
10+
}
11+
12+
export async function getUserCourses(user_id: string): Promise<Course[]> {
13+
const { data: courses, error: coursesError } = await supa.from("course_members")
14+
.select("courses(*)").eq("user_id", user_id);
15+
16+
if(coursesError || !courses) {
17+
throw new Error(`Database error when retrieving courses for user with id ${user_id}: ${coursesError.message}`);
18+
}
19+
20+
return courses.flatMap(entry => entry.courses) as Course[];
921
}

server/supabase/functions/courses/controller/get_course_activity.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@ import {
44
} from "@shared/schema/course.ts";
55
import { NotFoundError } from "../../_shared/errors.ts";
66

7+
// get course by course ID
78
export async function getCourse(course_id: string): Promise<Course> {
8-
const { data: course, error: _ } = await supa.from("courses")
9-
.select("*").eq("id", course_id).single();
9+
const { data: course, error: courseError } = await supa.from("courses")
10+
.select("*").eq("id", course_id);
1011

11-
if (!course) {
12-
throw new NotFoundError("Course not found");
13-
}
14-
15-
return course as Course;
16-
}
12+
if (courseError) {
13+
throw new Error(`Database error when retrieving course with id ${course_id}: ${courseError.message}`);
14+
}
1715

16+
if (!course || course.length === 0) {
17+
throw new NotFoundError("Course not found");
18+
}
19+
20+
return course[0] as Course;
21+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { supa } from "../../_shared/db.ts";
2+
import { User } from '../../../../../shared/schema/users.ts';
3+
4+
export async function getCourseMembers(course_id: string): Promise<User[]> {
5+
const { data: courses, error: coursesError } = await supa.from("course_members")
6+
.select("profiles(*)").eq("course_id", course_id);
7+
8+
if(coursesError || !courses) {
9+
throw new Error(`Database error when retrieving members for course with id ${course_id}: ${coursesError.message}`);
10+
}
11+
12+
return courses.flatMap(entry => entry.profiles) as User[];
13+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { supa } from "../../_shared/db.ts";
2+
import { Course, DefaultRoles, studentRole } from '../../../../../shared/schema/course.ts';
3+
import { NotFoundError, PermissionError, InputValidationError } from '../../_shared/errors.ts';
4+
5+
export async function removeCourseMember(course_id: string, user_id: string) {
6+
await getCourse(course_id);
7+
await checkIfUserExists(user_id);
8+
await checkIfUserInCourse(user_id, course_id);
9+
10+
const { error: deleteError } = await supa
11+
.from("course_members")
12+
.delete()
13+
.eq("course_id", course_id)
14+
.eq("user_id", user_id);
15+
16+
if (deleteError) {
17+
throw new Error(`Failed to remove user from course: ${deleteError.message}`);
18+
}
19+
}
20+
21+
async function getCourse(course_id: string): Promise<Course> {
22+
const { data: course, error: courseError } = await supa.from("courses")
23+
.select("*").eq("id", course_id);
24+
25+
if (courseError) {
26+
throw new Error(`Database error when retrieving course with id ${course_id}: ${courseError.message}`);
27+
}
28+
29+
if (!course || course.length !== 1) {
30+
throw new NotFoundError(`Course with id ${course_id} not found`);
31+
}
32+
33+
return course[0] as Course;
34+
}
35+
36+
async function checkIfUserExists(user_id: string) {
37+
const { data: user, error: userError } = await supa.from("profiles").select("*").eq("id", user_id);
38+
39+
if (userError) {
40+
throw new Error(`Database error when retrieving user with id ${user_id}: ${userError.message}`);
41+
}
42+
if (!user || user.length !== 1) {
43+
throw new NotFoundError(`User ${user_id} not found`);
44+
}
45+
}
46+
47+
async function checkIfUserInCourse(user_id: string, course_id: string) {
48+
const { data: existingUserCourse } = await supa
49+
.from("course_members")
50+
.select("*")
51+
.eq("course_id", course_id)
52+
.eq("user_id", user_id)
53+
.single();
54+
55+
if (!existingUserCourse) {
56+
throw new InputValidationError(`User ${user_id} already registered in course ${course_id}`);
57+
}
58+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { supa } from "../../_shared/db.ts";
2+
import { Course } from "@shared/schema/course.ts";
3+
import { NotFoundError } from "../../_shared/errors.ts";
4+
import { UpdateCourseReq } from "@shared/schema/course.ts";
5+
6+
export async function updateCourse(createCourseReq: UpdateCourseReq, course_id: string) {
7+
let course = await getCourse(course_id);
8+
9+
const { id, ...updatedCourse } = { ...course, ...createCourseReq };
10+
11+
const { error: updateError } = await supa.from("courses").update(updatedCourse).eq("id", course_id);
12+
13+
if (updateError) {
14+
throw new Error(`Failed to update course: ${updateError.message}`);
15+
}
16+
}
17+
18+
async function getCourse(course_id: string): Promise<Course> {
19+
const { data: course, error } = await supa.from("courses").select('*').eq('id', course_id);
20+
21+
if (error) {
22+
throw new Error(`Failed to get course ${course_id}: ${error.message}`);
23+
}
24+
25+
if (!course || course.length === 0) {
26+
throw new NotFoundError(`Course with id ${course_id} not found`);
27+
}
28+
29+
return course[0] as Course;
30+
}

0 commit comments

Comments
 (0)