Skip to content

Commit e6c58f6

Browse files
authored
Implemented Tag functions and APIs (no permission checking yet) (#350)
* Implement tag functions * Tag API implementation
1 parent 50e4446 commit e6c58f6

File tree

18 files changed

+883
-86
lines changed

18 files changed

+883
-86
lines changed

server/supabase/functions/_dev/setup.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,10 @@ export async function courseTestSeedSetup(
199199
}
200200
return coursesCreated;
201201
}
202+
203+
// emptyDatabase().then(() => {
204+
// console.log("Database emptied");
205+
// setupDevSeedData().then(() => {
206+
// console.log("Database setup complete");
207+
// });
208+
// });
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Tag, NestedTag } from "@shared/schema/tag.ts";
2+
import { supa } from "../db.ts";
3+
import { NotFoundError } from "../errors.ts";
4+
5+
export async function getTag(tag_id: string): Promise<Tag> {
6+
const { data: tagData, error: tagError } = await supa.from("tags").select("*").eq("id", tag_id);
7+
8+
if (tagError) {
9+
throw new Error(`Failed to get tag ${tag_id}: ${tagError.message}`);
10+
}
11+
12+
if (!tagData || tagData.length !== 1) {
13+
throw new NotFoundError(`Tag ${tag_id} was not found`);
14+
}
15+
16+
return tagData[0] as Tag;
17+
}
18+
19+
export async function getTagNested(tag_id: string): Promise<NestedTag> {
20+
const { data: tagdata, error: tagError } = await supa.rpc('get_tag_with_nested_parents', {tag_id: tag_id});
21+
22+
if (tagError) {
23+
throw new Error(`Failed to retrieve the nested tag ${tag_id}: ${tagError.message}`);
24+
}
25+
26+
return tagdata as NestedTag;
27+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { supa } from "../../_shared/db.ts";
2+
3+
import { NewTag, Tag } from "@shared/mod.ts";
4+
import { NotFoundError } from "../../_shared/errors.ts";
5+
6+
export async function createTag(newTagData: NewTag, course_id: string): Promise<Tag> {
7+
const { data: course, error: courseError } = await supa.from("courses")
8+
.select("id")
9+
.eq("id", course_id);
10+
11+
if (courseError) {
12+
throw new Error(`Failed to fetch course ${course_id}: ${courseError.message}`);
13+
}
14+
15+
if (!course || course.length !== 1) {
16+
throw new NotFoundError(`Course ${course_id} not found`);
17+
}
18+
19+
const { data: tag, error: tagError } = await supa.from("tags")
20+
.insert({
21+
...newTagData,
22+
course_id: course_id
23+
})
24+
.select()
25+
.single();
26+
27+
if (tagError) {
28+
throw new Error(`Failed to create tag: ${tagError.message}`);
29+
}
30+
31+
return tag as Tag;
32+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { supa } from "../../_shared/db.ts";
2+
3+
export async function deleteTag(tag_id: string, course_id: string) {
4+
const { error } = await supa.from("tags").delete().eq("id", tag_id).eq("course_id", course_id);
5+
6+
if (error) {
7+
throw new Error(`Failed to delete tag ${tag_id}: ${error.message}`);
8+
}
9+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Tag } from "@shared/schema/tag.ts";
2+
import { supa } from "../../_shared/db.ts";
3+
4+
export async function getAllTags(course_id: string): Promise<Tag[]> {
5+
const { data: tagData, error } = await supa.from("tags")
6+
.select("*")
7+
.eq("course_id", course_id);
8+
9+
if (error) {
10+
throw new Error(`Failed to get tags of course ${course_id}: ${error.message}`);
11+
}
12+
13+
return tagData as Tag[];
14+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Tag, UpdateTag } from "@shared/schema/tag.ts";
2+
import { supa } from "../../_shared/db.ts";
3+
import { NotFoundError } from "../../_shared/errors.ts";
4+
5+
export async function updateTag(updateTagData: UpdateTag, tag_id: string) {
6+
const tag = await getTag(tag_id);
7+
const { id, ...updatedTag } = { ...tag, ...updateTagData };
8+
const { error } = await supa.from("tags").update(updatedTag).eq("id", tag_id);
9+
10+
if (error) {
11+
throw new Error(`Failed to update tag ${tag_id}: ${error.message}`);
12+
}
13+
}
14+
15+
async function getTag(tag_id: string): Promise<Tag> {
16+
const { data: tagData, error } = await supa.from("tags").select("*").eq("id", tag_id);
17+
18+
if (error) {
19+
throw new Error(`Failed to get tag ${tag_id}: ${error.message}`);
20+
}
21+
22+
if (!tagData || tagData.length !== 1) {
23+
throw new NotFoundError(`Tag with id ${tag_id} not found`);
24+
}
25+
26+
return tagData[0] as Tag;
27+
}

server/supabase/functions/courses/index.ts

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Context, Hono } from "jsr:@hono/hono";
22
import { cors } from 'jsr:@hono/hono/cors';
3-
import { AccountStatusValue, ACCOUNT_STATUS_VALUES } from "@shared/mod.ts";
3+
import { AccountStatusValue, ACCOUNT_STATUS_VALUES, baseTagSchema } from "@shared/mod.ts";
44
import { createCourse } from "./controller/create_course_activity.ts";
55
import { getCourse } from "./controller/get_course_activity.ts";
66
import { addUserToCourse } from "./controller/add_course_member_activity.ts";
@@ -26,6 +26,12 @@ import {
2626
DefaultRoles,
2727
Permissions
2828
} from '../../../../shared/schema/course.ts';
29+
import { getAllTags } from "./controller/get_all_tags_activity.ts";
30+
import { NestedTag, Tag, tagPermissionsSchema, updateTagSchema } from "@shared/schema/tag.ts";
31+
import { getTag, getTagNested } from "../_shared/utils/tag_helper.ts";
32+
import { createTag } from "./controller/create_tag_activity.ts";
33+
import { updateTag } from "./controller/update_tag_activity.ts";
34+
import { deleteTag } from "./controller/delete_tag_activity.ts";
2935

3036
const functionName = "courses";
3137
const app = new Hono().basePath(`/${functionName}`);
@@ -148,7 +154,7 @@ app.put("/:course_id", async (c: Context<{ Variables: UserVariables}>) => {
148154
// Add user to course
149155
app.post("/:course_id/members/:user_id", async (c: Context<{ Variables: UserVariables}>) => {
150156
try {
151-
const course_id= c.req.param("course_id");
157+
const course_id = c.req.param("course_id");
152158
const user_id = c.req.param("user_id");
153159
const caller = c.var.user;
154160
let role = (await c.req.json()).role;
@@ -233,5 +239,137 @@ app.get("/:course_id/members", async (c: Context<{ Variables: UserVariables}>) =
233239
}
234240
});
235241

242+
// Get all tags of course
243+
app.get("/:course_id/tags", async (c: Context<{ Variables: UserVariables }>) => {
244+
try {
245+
const course_id = c.req.param("course_id");
246+
const user = c.var.user;
247+
248+
if (!(await isUserMemberOfCourse(user.id, course_id))) {
249+
return c.json({ error: "Unauthorized" }, 401);
250+
}
251+
252+
const tags = await getAllTags(course_id);
253+
return c.json({ tags: tags }, 200);
254+
} catch (error) {
255+
console.error(error);
256+
return c.json({ error: "Internal server error" }, 500);
257+
}
258+
});
259+
260+
// Get tag
261+
app.get("/:course_id/tags/:tag_id", async (c: Context<{ Variables: UserVariables }>) => {
262+
try {
263+
const { course_id, tag_id } = c.req.param();
264+
const nested = c.req.query("nested");
265+
const user = c.var.user;
266+
267+
if (!(await isUserMemberOfCourse(user.id, course_id))) {
268+
return c.json({ error: "Unauthorized" }, 401);
269+
}
270+
271+
const tag: Tag | NestedTag = nested ? await getTagNested(tag_id) : await getTag(tag_id);
272+
return c.json({ tag: tag }, 200);
273+
} catch (error) {
274+
console.error(error);
275+
if (error instanceof NotFoundError) {
276+
return c.json({ error: error.message }, 404);
277+
}
278+
return c.json({ error: "Internal server error" }, 500);
279+
}
280+
});
281+
282+
// Create a tag
283+
app.post("/:course_id/tags", async (c: Context<{ Variables: UserVariables }>) => {
284+
try {
285+
const course_id = c.req.param("course_id");
286+
const user = c.var.user;
287+
const newTagData = await c.req.json();
288+
289+
if (!newTagData.permissions) {
290+
newTagData.permissions = tagPermissionsSchema.parse({});
291+
}
292+
293+
const validationResult = baseTagSchema.safeParse(newTagData);
294+
if (!validationResult.success) {
295+
return c.json({
296+
error: "Validation failed",
297+
details: validationResult.error.errors,
298+
}, 400);
299+
}
300+
301+
const completeNewTagData = validationResult.data;
302+
303+
const permissions = await getUserPermissionsInCourse(user.id, course_id);
304+
if (!permissions.can_create_tags) {
305+
return c.json({ error: "Unauthorized" }, 401);
306+
}
307+
308+
const tag = await createTag(completeNewTagData, course_id);
309+
return c.json({ tag: tag }, 200);
310+
} catch (error) {
311+
console.error(error);
312+
if (error instanceof NotFoundError) {
313+
return c.json({ error: error.message }, 404);
314+
}
315+
return c.json({ error: "Internal server error"}, 500);
316+
}
317+
});
318+
319+
// Update tag
320+
// Note that if user wants to update permission or can_use_tag, they must pass the whole permission/can_use_tag object
321+
app.put("/:course_id/tags/:tag_id", async (c: Context<{ Variables: UserVariables }>) => {
322+
try {
323+
const course_id = c.req.param("course_id");
324+
const tag_id = c.req.param("tag_id");
325+
const user = c.var.user;
326+
const updateTagData = await c.req.json();
327+
328+
const validationResult = updateTagSchema.safeParse(updateTagData);
329+
if (!validationResult.success) {
330+
return c.json({
331+
error: "Validation failed",
332+
details: validationResult.error.errors,
333+
}, 400);
334+
}
335+
336+
const completeUpdateTagData = validationResult.data;
337+
338+
const permissions = await getUserPermissionsInCourse(user.id, course_id);
339+
if (!permissions.can_edit_tags) {
340+
return c.json({ error: "Unauthorized" }, 401);
341+
}
342+
343+
await updateTag(completeUpdateTagData, tag_id);
344+
return c.json(200);
345+
} catch (error) {
346+
console.error(error);
347+
if (error instanceof NotFoundError) {
348+
return c.json({ error: error.message }, 404);
349+
}
350+
return c.json({ error: "Internal server error"}, 500);
351+
}
352+
});
353+
354+
// Delete tag
355+
app.delete("/:course_id/tags/:tag_id", async (c: Context<{ Variables: UserVariables }>) => {
356+
try {
357+
const course_id = c.req.param("course_id");
358+
const tag_id = c.req.param("tag_id");
359+
const user = c.var.user;
360+
361+
const permissions = await getUserPermissionsInCourse(user.id, course_id);
362+
if (!permissions.can_delete_tags) {
363+
return c.json({ error: "Unauthorized" }, 401);
364+
}
365+
366+
await deleteTag(tag_id, course_id);
367+
return c.json(200);
368+
} catch (error) {
369+
console.error(error);
370+
return c.json({ error: "Internal server error" }, 500);
371+
}
372+
});
373+
236374
export { app };
237375
Deno.serve(app.fetch);

0 commit comments

Comments
 (0)