You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Build a real course-category taxonomy: a Category model with slugs and metadata, admin-managed CRUD endpoints, public category listing/detail endpoints with aggregated stats (course count, enrollment count, price range), and seed data for the core Islamic disciplines. The frontend is getting category landing pages (e.g. /categories/tafsir), and today there is nothing to power them — categories are free-text strings with no listing endpoint, no slugs, no counts, and no way to know which categories even exist without scanning every course.
Current state
Course.category is a required free-text String (src/models/Course.js) and Book.category is an optional free-text String (src/models/Book.js). There is no Category collection, no enum, no slug, and no referential integrity — "Qur'an", "Quran" and "quran" are three different categories.
createCourse and updateCourse (src/controllers/courses/courseController.js) accept whatever string the client sends; the only check is presence (if (!title || !description || !category)).
Recommendations depend on exact string equality: fetchRecommendedCourses runs Course.find({ category: { $in: interests } }) against User.interests, which is also a free-text [{ type: String }] (src/models/User.js) — so any spelling drift silently breaks recommendations.
There is no endpoint to list categories or filter courses by category: GET /api/courses (getCourses) returns every course with no query filtering, and src/routes/courses/courseRoutes.js has no category routes. src/controllers/searchController.js returns category on book results but cannot browse by it.
Seed data already encodes an implicit taxonomy — data/courses.js uses "Qur'an", "History", "Aqeedah", etc. — but it is dead weight: package.json defines "seed": "node src/scripts/seedDatabase.js" and src/scripts/ does not exist.
Model (src/models/Category.js): name (unique, trimmed), slug (unique, lowercase, indexed, generated from name), description, icon/image URL, parent (optional ObjectId ref Category for one level of subcategories, e.g. Qur'an → Tajweed), order (for curated sorting), isActive. Add a compound index on { parent: 1, order: 1 }.
Course/Book linkage without breaking existing data: add categoryRef (ObjectId ref Category) alongside the existing category string on Course (and optionally Book). Write a one-shot migration script (src/scripts/migrateCategories.js) that upserts a Category per distinct existing string (case/diacritic-insensitive matching) and backfills categoryRef. Keep the legacy category string populated (denormalized from the ref on save) so current frontend reads keep working.
Public API (src/routes/categoryRoutes.js, mounted at /api/categories in app.js):
GET /api/categories — active categories with stats per category: courseCount, total enrollmentCount (size of enrolledUsers across courses), freeCount/paidCount, and minPrice/maxPrice. Use a single aggregation pipeline ($lookup + $group), not N+1 queries.
GET /api/categories/:slug — category detail plus a paginated, sortable list of its courses (?page=&limit=&sort=newest|popular|price), populating createdBy with name avatar only.
Wire GET /api/courses?category=<slug> filtering into getCourses so existing course lists can filter too.
Admin CRUD (same router, protected): POST /api/categories, PATCH /api/categories/:id, DELETE /api/categories/:id (soft-delete via isActive: false when courses reference it; hard delete only when empty). Gate these behind protect plus a role check designed to align with issue [Enhancement] Introduce role-based authorization and fix registration privilege escalation #20 (a small requireRole(...) middleware is acceptable here; do not widen tutor powers).
Validation: reject createCourse/updateCourse submissions whose category does not resolve to an active Category (accept either slug or id), with a clear 400 listing valid slugs.
Seed data: create src/scripts/seedCategories.js (and fix the broken npm run seed entry or add npm run seed:categories) seeding the core Islamic disciplines — Qur'an (with Tajweed and Tafsir as children), Hadith, Aqeedah, Fiqh, Seerah/History, Arabic Language, Islamic Finance, Spirituality/Tazkiyah — with slugs, descriptions, and ordering. Seeding must be idempotent (upsert by slug).
Category model with unique slug generation (handles duplicates, Arabic transliteration characters like the apostrophe in "Qur'an") and one level of parent/child nesting.
GET /api/categories returns stats computed in a single aggregation (verify no per-category query loop) and hides isActive: false categories.
GET /api/categories/:slug returns 404 for unknown slugs and paginates courses with a total count; GET /api/courses?category=<slug> filters correctly.
Admin CRUD endpoints reject unauthenticated and non-privileged users (401/403), enforce unique names/slugs (409 or 400 on duplicates), and soft-delete categories that still have courses.
createCourse/updateCourse reject unknown or inactive categories with a 400 naming the valid slugs; existing courses with legacy string categories still load and are backfilled by the migration script.
Migration and seed scripts are idempotent (safe to run twice) and documented in the README or QUICK_START.
Jest + supertest coverage: stats aggregation correctness (seeded fixture with known counts), slug collision handling, category filter on /api/courses, and authorization of admin routes.
Gotchas: enrolledUsers is an array on Course, so enrollment counts come from $size inside the aggregation, not a separate collection. Jest runs via node --experimental-vm-modules node_modules/jest/bin/jest.js (see package.json); CI (.github/workflows/ci.yml) boots against a real Mongo service container, so aggregation tests can run for real. PRs target dev.
Difficulty
Medium — no protocol work, but it requires a careful non-breaking migration of free-text data, a correct single-pass stats aggregation, idempotent seeding, and authorization design that doesn't collide with the in-flight RBAC issue.
🏆 GrantFox OSS — Official Campaign | FWC26. Apply for this issue through the GrantFox campaign page. The maintainer assigns one contributor before work starts; unassigned PRs may not be reviewed. PRs target the dev branch. Quality bar: CI must stay green.
💬 Questions or need help? Reach the maintainers and other contributors on the DeenBridge Telegram: https://t.me/+nst9lXNj1wc4ZDE0
Summary
Build a real course-category taxonomy: a
Categorymodel with slugs and metadata, admin-managed CRUD endpoints, public category listing/detail endpoints with aggregated stats (course count, enrollment count, price range), and seed data for the core Islamic disciplines. The frontend is getting category landing pages (e.g./categories/tafsir), and today there is nothing to power them — categories are free-text strings with no listing endpoint, no slugs, no counts, and no way to know which categories even exist without scanning every course.Current state
Course.categoryis a required free-textString(src/models/Course.js) andBook.categoryis an optional free-textString(src/models/Book.js). There is noCategorycollection, no enum, no slug, and no referential integrity —"Qur'an","Quran"and"quran"are three different categories.createCourseandupdateCourse(src/controllers/courses/courseController.js) accept whatever string the client sends; the only check is presence (if (!title || !description || !category)).fetchRecommendedCoursesrunsCourse.find({ category: { $in: interests } })againstUser.interests, which is also a free-text[{ type: String }](src/models/User.js) — so any spelling drift silently breaks recommendations.GET /api/courses(getCourses) returns every course with no query filtering, andsrc/routes/courses/courseRoutes.jshas no category routes.src/controllers/searchController.jsreturnscategoryon book results but cannot browse by it.data/courses.jsuses"Qur'an","History","Aqeedah", etc. — but it is dead weight:package.jsondefines"seed": "node src/scripts/seedDatabase.js"andsrc/scripts/does not exist.User.roleisenum: ["student", "tutor"](src/models/User.js). Category management endpoints need a privilege gate; issue [Enhancement] Introduce role-based authorization and fix registration privilege escalation #20 (role-based authorization) is introducing the broader RBAC story.What to build
src/models/Category.js):name(unique, trimmed),slug(unique, lowercase, indexed, generated from name),description,icon/imageURL,parent(optionalObjectIdrefCategoryfor one level of subcategories, e.g. Qur'an → Tajweed),order(for curated sorting),isActive. Add a compound index on{ parent: 1, order: 1 }.categoryRef(ObjectIdrefCategory) alongside the existingcategorystring onCourse(and optionallyBook). Write a one-shot migration script (src/scripts/migrateCategories.js) that upserts aCategoryper distinct existing string (case/diacritic-insensitive matching) and backfillscategoryRef. Keep the legacycategorystring populated (denormalized from the ref on save) so current frontend reads keep working.src/routes/categoryRoutes.js, mounted at/api/categoriesinapp.js):GET /api/categories— active categories with stats per category:courseCount, totalenrollmentCount(size ofenrolledUsersacross courses),freeCount/paidCount, andminPrice/maxPrice. Use a single aggregation pipeline ($lookup+$group), not N+1 queries.GET /api/categories/:slug— category detail plus a paginated, sortable list of its courses (?page=&limit=&sort=newest|popular|price), populatingcreatedBywithname avataronly.GET /api/courses?category=<slug>filtering intogetCoursesso existing course lists can filter too.POST /api/categories,PATCH /api/categories/:id,DELETE /api/categories/:id(soft-delete viaisActive: falsewhen courses reference it; hard delete only when empty). Gate these behindprotectplus a role check designed to align with issue [Enhancement] Introduce role-based authorization and fix registration privilege escalation #20 (a smallrequireRole(...)middleware is acceptable here; do not widentutorpowers).createCourse/updateCoursesubmissions whose category does not resolve to an activeCategory(accept either slug or id), with a clear 400 listing valid slugs.src/scripts/seedCategories.js(and fix the brokennpm run seedentry or addnpm run seed:categories) seeding the core Islamic disciplines — Qur'an (with Tajweed and Tafsir as children), Hadith, Aqeedah, Fiqh, Seerah/History, Arabic Language, Islamic Finance, Spirituality/Tazkiyah — with slugs, descriptions, and ordering. Seeding must be idempotent (upsert by slug).src/utils/cache.js/src/middlewares/cache.jswith invalidation on category/course writes (coordinate with, but do not depend on, issue [Enhancement] Wire up the unused Redis cache layer on read endpoints with invalidation #19).Acceptance criteria
Categorymodel with unique slug generation (handles duplicates, Arabic transliteration characters like the apostrophe in "Qur'an") and one level of parent/child nesting.GET /api/categoriesreturns stats computed in a single aggregation (verify no per-category query loop) and hidesisActive: falsecategories.GET /api/categories/:slugreturns 404 for unknown slugs and paginates courses with a total count;GET /api/courses?category=<slug>filters correctly.createCourse/updateCoursereject unknown or inactive categories with a 400 naming the valid slugs; existing courses with legacy string categories still load and are backfilled by the migration script./api/courses, and authorization of admin routes.Pointers
src/models/Course.js,src/models/Book.js,src/models/User.js(free-textcategory/interests,roleenum),src/controllers/courses/courseController.js(createCourse,getCourses,fetchRecommendedCourses),src/routes/courses/courseRoutes.js,app.js(route mounting),data/courses.js(existing implicit taxonomy),package.json(brokenseedscript),src/utils/cache.js.enrolledUsersis an array onCourse, so enrollment counts come from$sizeinside the aggregation, not a separate collection. Jest runs vianode --experimental-vm-modules node_modules/jest/bin/jest.js(seepackage.json); CI (.github/workflows/ci.yml) boots against a real Mongo service container, so aggregation tests can run for real. PRs targetdev.Difficulty
Medium — no protocol work, but it requires a careful non-breaking migration of free-text data, a correct single-pass stats aggregation, idempotent seeding, and authorization design that doesn't collide with the in-flight RBAC issue.
🏆 GrantFox OSS — Official Campaign | FWC26. Apply for this issue through the GrantFox campaign page. The maintainer assigns one contributor before work starts; unassigned PRs may not be reviewed. PRs target the
devbranch. Quality bar: CI must stay green.💬 Questions or need help? Reach the maintainers and other contributors on the DeenBridge Telegram: https://t.me/+nst9lXNj1wc4ZDE0