Skip to content

[Enhancement] Course category taxonomy: Category model, slugs, admin CRUD, per-category stats, and Islamic-discipline seed data #42

Description

@zeemscript

Summary

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.
  • There is no admin role: User.role is enum: ["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

  1. 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 }.
  2. 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.
  3. 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.
  4. 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).
  5. 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.
  6. 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).
  7. Caching: category list/stat responses are read-heavy and change rarely — cache them with the existing helpers in src/utils/cache.js / src/middlewares/cache.js with 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

  • 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.

Pointers

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

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions