Skip to content

Commit 6c2a1b4

Browse files
committed
feat: implement comments functionality with database integration and recursive retrieval
1 parent fe9c1a3 commit 6c2a1b4

File tree

8 files changed

+234
-19
lines changed

8 files changed

+234
-19
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"lint": "next lint",
1010
"db:generate": "npx drizzle-kit generate",
1111
"db:push": "npx drizzle-kit push",
12-
"db:studio": "npx drizzle-kit studio"
12+
"db:studio": "npx drizzle-kit studio",
13+
"play": "npx tsx ./src/backend/play.ts"
1314
},
1415
"dependencies": {
1516
"@cloudinary/react": "^1.14.1",

src/app/api/play/route.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import { getComments } from "@/backend/services/comment.action";
12
import { NextResponse } from "next/server";
2-
// import * as reactionActions from "@/backend/services/reaction.actions";
3-
import * as searchService from "@/backend/services/search.service";
43

54
export async function GET() {
6-
const response = await searchService.syncAllArticles();
5+
const comments = await getComments({
6+
resource_id: "05553726-5168-41f3-b2d1-5d27cf5c8beb",
7+
resource_type: "ARTICLE",
8+
});
79

8-
return NextResponse.json(response, {
10+
return NextResponse.json(comments, {
911
status: 200,
1012
headers: { "Content-Type": "application/json" },
1113
});

src/backend/persistence/persistence-contracts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export enum DatabaseTableName {
55
user_sessions = "user_sessions",
66
series = "series",
77
series_items = "series_items",
8+
comments = "comments",
89
bookmarks = "bookmarks",
910
reactions = "reactions",
1011
tags = "tags",

src/backend/persistence/persistence-repositories.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
Tag,
1010
User,
1111
UserSession,
12+
Comment,
1213
UserSocial,
1314
} from "../models/domain-models";
1415
import { pgClient } from "./clients";
@@ -73,12 +74,19 @@ const reactionRepository = new Repository<Reaction>(
7374
repositoryConfig
7475
);
7576

77+
const commentRepository = new Repository<Comment>(
78+
DatabaseTableName.comments,
79+
pgClient,
80+
repositoryConfig
81+
);
82+
7683
export const persistenceRepository = {
7784
user: userRepository,
7885
userSocial: userSocialRepository,
7986
userSession: userSessionRepository,
8087
article: articleRepository,
8188
bookmark: bookmarkRepository,
89+
comment: commentRepository,
8290
reaction: reactionRepository,
8391
articleTagPivot: articleTagRepository,
8492
tags: tagRepository,
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
CREATE OR REPLACE FUNCTION public.get_comments(
2+
p_resource_id uuid,
3+
p_resource_type character varying,
4+
p_parent_id uuid DEFAULT NULL::uuid,
5+
p_current_level integer DEFAULT 0
6+
)
7+
RETURNS json
8+
LANGUAGE plpgsql
9+
10+
AS $function$
11+
DECLARE
12+
result JSON;
13+
BEGIN
14+
-- Base case: get comments for the given parent
15+
IF p_parent_id IS NULL THEN
16+
-- Get root comments
17+
SELECT json_agg(
18+
json_build_object(
19+
'id', c.id,
20+
'body', c.body,
21+
'level', 0,
22+
'created_at', c.created_at,
23+
'parent_id', NULL,
24+
'author', json_build_object(
25+
'id', u.id,
26+
'name', u.name,
27+
'email', u.email,
28+
'username', u.username
29+
),
30+
'replies', get_comments(p_resource_id, p_resource_type, c.id, 1)
31+
) ORDER BY c.created_at
32+
) INTO result
33+
FROM comments c
34+
JOIN users u ON c.user_id = u.id
35+
WHERE c.resource_id = p_resource_id
36+
AND c.resource_type = p_resource_type;
37+
ELSE
38+
-- Get replies to a specific comment
39+
IF p_current_level >= 3 THEN
40+
-- Stop recursion at level 3
41+
RETURN '[]'::json;
42+
END IF;
43+
44+
SELECT json_agg(
45+
json_build_object(
46+
'id', c.id,
47+
'body', c.body,
48+
'level', p_current_level,
49+
'created_at', c.created_at,
50+
'parent_id', p_parent_id,
51+
'author', json_build_object(
52+
'id', u.id,
53+
'name', u.name,
54+
'email', u.email,
55+
'username', u.username -- Fixed: was 'useruser'
56+
),
57+
'replies', get_comments(p_resource_id, p_resource_type, c.id, p_current_level + 1)
58+
) ORDER BY c.created_at
59+
) INTO result
60+
FROM comments c
61+
JOIN users u ON c.user_id = u.id
62+
WHERE c.resource_id = p_parent_id
63+
AND c.resource_type = 'COMMENT';
64+
END IF;
65+
66+
RETURN COALESCE(result, '[]'::json);
67+
END;
68+
$function$

src/backend/play.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import * as commentActions from "./services/comment.action";
2+
3+
async function main() {
4+
// Comment on a resource
5+
const newComment = await commentActions.createMyComment({
6+
body: "This is a comment on a resource",
7+
resource_id: "14fced36-31a7-42b3-9811-8887fc1331db",
8+
resource_type: "ARTICLE",
9+
});
10+
11+
console.log(newComment);
12+
}
13+
14+
main();

src/backend/services/comment.action.ts

Lines changed: 131 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,146 @@
11
import z from "zod";
22
import { CommentActionInput } from "./inputs/comment.input";
3+
import { authID } from "./session.actions";
4+
import { ActionException } from "./RepositoryException";
5+
import { persistenceRepository } from "../persistence/persistence-repositories";
6+
import { eq } from "sqlkit";
7+
8+
const sql = String.raw;
39

410
export const getComments = async (
5-
resourceId: string,
6-
resourceType: "ARTICLE" | "COMMENT"
11+
_input: z.infer<typeof CommentActionInput.getComments>
712
) => {
8-
// Fetch comments from the database based on resourceId and resourceType
9-
// const comments = await db
10-
// .select()
11-
// .from(commentsTable)
12-
// .where(commentsTable.resource_id.eq(resourceId))
13-
// .and(commentsTable.resource_type.eq(resourceType))
14-
// .orderBy(commentsTable.created_at.desc());
15-
// return comments;
16-
return []; // Placeholder for actual database query
13+
const input = CommentActionInput.getComments.parse(_input);
14+
15+
const query = sql`
16+
WITH RECURSIVE comment_tree AS (
17+
-- Root comments
18+
SELECT
19+
id, body, user_id, created_at, resource_id, resource_type,
20+
0 as level,
21+
id as root_id
22+
FROM comments
23+
WHERE resource_id = $1
24+
AND resource_type = $2
25+
26+
UNION ALL
27+
28+
-- Nested replies (up to level 3)
29+
SELECT
30+
c.id, c.body, c.user_id, c.created_at, c.resource_id, c.resource_type,
31+
ct.level + 1,
32+
ct.root_id
33+
FROM comments c
34+
JOIN comment_tree ct ON c.resource_id = ct.id
35+
WHERE c.resource_type = 'COMMENT'
36+
AND ct.level < 3 -- This allows up to level 3 (0, 1, 2, 3)
37+
),
38+
-- Function to build replies recursively
39+
build_replies(parent_id, max_level) AS (
40+
SELECT
41+
ct.resource_id as parent_id,
42+
3 as max_level,
43+
json_agg(
44+
json_build_object(
45+
'id', ct.id,
46+
'body', ct.body,
47+
'level', ct.level,
48+
'created_at', ct.created_at,
49+
'parent_id', ct.resource_id,
50+
'author', json_build_object(
51+
'name', u.name,
52+
'email', u.email
53+
),
54+
'replies', CASE
55+
WHEN ct.level < 3 THEN
56+
COALESCE(
57+
(SELECT json_agg(child_reply ORDER BY (child_reply->>'created_at')::timestamp)
58+
FROM build_replies(ct.id, 3) br
59+
CROSS JOIN json_array_elements(br.replies) as child_reply),
60+
'[]'::json
61+
)
62+
ELSE '[]'::json
63+
END
64+
) ORDER BY ct.created_at
65+
) as replies
66+
FROM comment_tree ct
67+
JOIN users u ON ct.user_id = u.id
68+
WHERE ct.resource_id = parent_id AND ct.level > 0
69+
GROUP BY ct.resource_id
70+
)
71+
SELECT json_agg(
72+
json_build_object(
73+
'id', ct.id,
74+
'body', ct.body,
75+
'level', ct.level,
76+
'created_at', ct.created_at,
77+
'parent_id', null,
78+
'author', json_build_object(
79+
'name', u.name,
80+
'email', u.email
81+
),
82+
'replies', COALESCE(br.replies, '[]'::json)
83+
) ORDER BY ct.created_at
84+
) as comments
85+
FROM comment_tree ct
86+
JOIN users u ON ct.user_id = u.id
87+
LEFT JOIN build_replies(ct.id, 3) br ON true
88+
WHERE ct.level = 0;
89+
`;
90+
91+
const comments = await pgClient?.executeSQL(query, [
92+
input.resource_id,
93+
input.resource_type,
94+
]);
95+
return comments?.rows?.[0]; // Placeholder for actual database query
1796
};
1897

19-
export const createComment = async (
98+
export const createMyComment = async (
2099
input: z.infer<typeof CommentActionInput.create>
21100
) => {
101+
const sessionId = await authID();
102+
if (!sessionId) {
103+
throw new ActionException("Unauthorized: No session ID found");
104+
}
22105
const { resource_id, resource_type, body } = input;
23106

24-
// Create the comment in the database
107+
switch (resource_type) {
108+
case "ARTICLE":
109+
// Validate that the resource exists
110+
const [exists] = await persistenceRepository.article.find({
111+
where: eq("id", resource_id),
112+
limit: 1,
113+
columns: ["id"],
114+
});
115+
if (!exists) {
116+
throw new ActionException("Resource not found");
117+
}
118+
break;
119+
case "COMMENT":
120+
// Validate that the parent comment exists
121+
const [parentExists] = await persistenceRepository.comment.find({
122+
where: eq("id", resource_id),
123+
limit: 1,
124+
columns: ["id"],
125+
});
126+
if (!parentExists) {
127+
throw new ActionException("Parent comment not found");
128+
}
129+
break;
130+
default:
131+
throw new ActionException("Invalid resource type");
132+
}
133+
134+
const created = await persistenceRepository.comment.insert([
135+
{
136+
body,
137+
resource_id,
138+
resource_type,
139+
user_id: sessionId,
140+
},
141+
]);
25142

26-
// return newComment[0];
143+
return created?.rows?.[0];
27144
};
28145

29146
export const deleteComment = async (

src/backend/services/inputs/comment.input.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import z from "zod";
22

33
export const CommentActionInput = {
4+
getComments: z.object({
5+
resource_id: z.string().uuid(),
6+
resource_type: z.enum(["ARTICLE", "COMMENT"]),
7+
}),
48
create: z.object({
59
resource_id: z.string().uuid(),
610
resource_type: z.enum(["ARTICLE", "COMMENT"]),

0 commit comments

Comments
 (0)