Systems Hub
YouTube-এর মত nested comment
তুমি কীভাবে বানাবে?
রহিমের comment-এ করিম reply করল,
করিমের reply-তে জাবের reply করল —
এই ৩ level কীভাবে database-এ রাখব?
Self-Referential Table Recursive Fetch Admin Dashboard Soft Delete 3-Layer Check
Production Comment System বানাব
SLIDE 01

সবাই যে ভুলগুলো করে

Simple comment system কেন fail করে
ভুল ১: Separate reply table — reply_of_reply table আলাদা করলে ৫ level হলে ৫টা table লাগবে!
ভুল ২: Hard delete — comment delete করলে সব reply মুছে যায়। রহিমের thread ধ্বংস।
ভুল ৩: No permission check — যেকোনো user যেকোনো course-এ comment করতে পারে?
ভুল ৪: N+1 query — প্রতিটা comment-এর জন্য আলাদা query → ১০০ comment = ১০০ DB query
সমাধান ১: Self-Referential table — একটাই table, parentId দিয়ে hierarchy
সমাধান ২: Soft delete — isDeleted flag, data থাকে কিন্তু দেখায় না
সমাধান ৩: 3-layer permission check — enrollment → host → role
সমাধান ৪: nestedSelectFields() — একটা query-তে সব level fetch
SLIDE 02

Self-Referential Table Design

একটাই table, অসীম level
-- Comment Table CREATE TABLE Comment ( id UUID PRIMARY KEY, content TEXT NOT NULL, authorId UUID REFERENCES User(id), lessonId UUID REFERENCES Lesson(id), parentId UUID REFERENCES Comment(id), -- নিজেকেই reference করছে! isDeleted BOOLEAN DEFAULT false, createdAt TIMESTAMP DEFAULT NOW() ); -- রহিমের মূল comment: parentId = NULL -- করিমের reply: parentId = রহিমের comment id -- জাবেরের reply: parentId = করিমের reply id
💬 রহিমের comment (parentId: null)
↳ 💬 করিমের reply (parentId: রহিম.id)
↳ 💬 জাবেরের reply (parentId: করিম.id)
↳ 💬 হাচানের reply (parentId: করিম.id)
↳ 💬 অন্যর reply (parentId: রহিম.id)

Prisma Schema

model Comment { id String @id content String author User @relation(...) lesson Lesson @relation(...) // Self-reference — জাদু এখানে parentId String? parent Comment? @relation( "Replies", fields:[parentId], references:[id]) replies Comment[] @relation("Replies") isDeleted Boolean @default(false) createdAt DateTime @default(now()) }
💡
একটাই Comment table — ৩ level হোক, ১০ level হোক, সবই এখানে। parentId = null মানে মূল comment।
SLIDE 03

Recursive Reply Fetch — nestedSelectFields()

একটা query-তে সব level নামিয়ে আনা
// Nested select তৈরি করো function nestedSelectFields(depth = 3) { if (depth === 0) return {}; return { id: true, content: true, isDeleted: true, createdAt: true, author: { select: { id: true, name: true } }, replies: { where: { isDeleted: false }, orderBy: { createdAt: 'asc' }, select: { ...nestedSelectFields(depth - 1), // Recursion! নিজেকে call করছে } } }; } // Use করো const comments = await prisma.comment.findMany({ where: { lessonId, parentId: null }, select: nestedSelectFields(3), orderBy: { createdAt: 'desc' } });

Output Structure

[ { id: "c1", content: "রহিমের comment", replies: [ { id: "c2", content: "করিমের reply", replies: [ { id: "c3", content: "জাবেরের reply", replies: [] // depth শেষ } ] } ] } ]
depth=3 মানে ৩ level deep fetch। একটাই Prisma query! ৩টা আলাদা query নয়।
SLIDE 04

Admin Dashboard — ৫টা Key Query

Admin কী দেখতে পাবে?

Total Comments — lesson-এ মোট কতটা comment আছে (isDeleted: false)

Flagged Comments — report করা comment list — Admin review করবে

Top Commenters — কোন user সবচেয়ে বেশি active → engagement দেখা

Deleted Comments — soft delete হওয়া comment — audit trail

Comment Analytics — দিনে কতটা comment হচ্ছে → growth track

// Admin dashboard data async function getAdminStats(lessonId) { const [total, flagged, topUsers] = await Promise.all([ // Query 1: total prisma.comment.count({ where: { lessonId, isDeleted: false } }), // Query 2: flagged prisma.comment.findMany({ where: { lessonId, isFlagged: true }, include: { author: true } }), // Query 3: top commenters prisma.comment.groupBy({ by: ['authorId'], where: { lessonId }, _count: { id: true }, orderBy: { _count: { id: 'desc' } }, take: 10 }) ]); return { total, flagged, topUsers }; }
SLIDE 05

Host-Based + Role-Based Filtering

কোন platform-এ কে কী দেখতে পাবে?

Host-Based Filtering

  • Platform A-এর comment Platform B দেখবে না
  • host field দিয়ে isolate করা হয়
  • Multi-tenant system-এ জরুরি

Role-Based Filtering

  • Student: শুধু নিজের course-এর comment
  • Teacher: নিজের course-এর সব comment
  • Admin: সব course-এর সব comment
  • SuperAdmin: সব platform-এর সব comment
function buildCommentFilter(user, host) { const base = { host, // Host isolation isDeleted: false }; switch (user.role) { case 'student': return { ...base, lesson: { course: { enrollments: { some: { studentId: user.id } } } } }; case 'teacher': return { ...base, lesson: { course: { teacherId: user.id } } }; case 'admin': return { ...base }; case 'superadmin': return { isDeleted: false }; // no host filter } }
SLIDE 06

Soft Delete & Push Notification

Delete মানে মোছা নয় — লুকানো
Hard Delete সমস্যা: রহিমের comment delete করলে করিম ও জাবেরের reply orphan হয়ে যাবে। Thread নষ্ট।
// Soft Delete — data রাখো, hide করো async function deleteComment(commentId, userId) { const comment = await prisma.comment .findUnique({ where: { id: commentId } }); // শুধু নিজের comment delete করতে পারবে // (অথবা admin) if (comment.authorId !== userId) { throw new AppError('Permission নেই', 403); } await prisma.comment.update({ where: { id: commentId }, data: { isDeleted: true, content: '[এই comment মুছে ফেলা হয়েছে]' } }); }
Soft delete করলে রহিমের comment "[মুছে ফেলা হয়েছে]" দেখাবে কিন্তু করিমের reply ঠিকই দেখা যাবে। Thread অক্ষুণ্ণ।

Push Notification

  • করিম reply করলে → রহিম notification পাবে
  • Admin delete করলে → author-কে জানাও
  • Comment flag হলে → Admin alert
// Reply করলে parent-কে notify করো if (comment.parentId) { const parent = await prisma.comment.findUnique( { where: { id: comment.parentId }, include: { author: true } } ); await sendPushNotification( parent.author.id, `${user.name} তোমার comment-এ reply করেছে` ); }
SLIDE 07

Comment করার আগে ৩ Layer Check

যে কেউ comment করতে পারবে না

Authentication — Login করা আছে? JWT valid? না থাকলে ৪০১।

Enrollment Check — রহিম কি এই course-এ enrolled? কিনেছে? না হলে ৪০৩।

Content Validation — Comment ৫০০ char-এর বেশি? Empty? Spam keyword আছে? তাহলে ৪০০।

সব check পাস করলে তবেই DB-তে insert। কোনো check fail করলে error response — comment সেভ হয় না।
async function createComment(req, res) { const { content, lessonId, parentId } = req.body; // Layer 1: authenticate middleware handled it // Layer 2: enrollment check const enrolled = await checkEnrollment( req.user.id, lessonId ); if (!enrolled) throw new AppError( 'এই course-এ enrolled নও', 403 ); // Layer 3: content validation (Zod schema-তে) // content: z.string().min(1).max(500) const comment = await prisma.comment.create({ data: { content, lessonId, parentId, authorId: req.user.id } }); // notify parent author... }
SLIDE 08

এই Comment System কেন Best?

Production-ready, scalable, safe

Flexible

  • Self-referential → unlimited nesting
  • nestedSelectFields() → recursive fetch
  • একটা table সব কাজ করে

Safe

  • 3-layer check → unauthorized comment নেই
  • Soft delete → data হারায় না
  • Host isolation → cross-platform leak নেই

Smart

  • Role-based filtering → সবাই সঠিক data দেখে
  • Admin dashboard → ৫ key query
  • Push notification → engagement বাড়ে
🎯
রহিম comment করবে, করিম reply করবে, জাবের reply-তে reply করবে — সব কিছু একটা table-এ, একটা query-তে, তিনটা check-এ। এটাই professional comment system।
সব একসাথে ধাপে ধাপে