Skip to content

feat(courses): add content versioning with immutable revision history#271

Merged
MaryammAli merged 3 commits into
BlockDash-Studios:mainfrom
dunnidev:add-course-content
Jun 30, 2026
Merged

feat(courses): add content versioning with immutable revision history#271
MaryammAli merged 3 commits into
BlockDash-Studios:mainfrom
dunnidev:add-course-content

Conversation

@dunnidev

@dunnidev dunnidev commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Closes #218

Summary

Adds DB-persisted course content versioning and revision history to BackendAcademy/src/courses/. The course module now writes through real Postgres tables (courses and course_revisions) via TypeORM repositories instead of holding state in in-memory Maps.

What changed

CourseEntity (BackendAcademy/src/courses/course.entity.ts)

Persists the canonical, mutable course record.

Column Type Notes
id uuid (PK) crypto.randomUUID()-generated by service
title varchar(200)
description text
level enum mirrors CourseLevel
order int
learning_path_id uuid indexed single-column index idx_courses_learning_path_id
prerequisites, skills text[] Postgres array, default {}
xp_reward int default 0
is_active boolean default true
version int mirrors latest revision version, default 1
latest_revision_id uuid nullable pointer to most recent course_revisions.id
created_at / updated_at timestamptz TypeORM @CreateDateColumn / @UpdateDateColumn

CourseRevisionEntity (BackendAcademy/src/courses/course-revision.entity.ts)

Append-only audit trail of every meaningful course change. Composite unique index on (course_id, version) so concurrent writes cannot duplicate a version.

Column Type Notes
id uuid (PK)
course_id uuid indexed single-column index idx_course_revisions_course_id
version int unique together with course_id
snapshot jsonb immutable content snapshot at this version
change_note text nullable
revision_author varchar(120)
reason varchar(32) create / update / restore
previous_version int nullable traceability back to prior revision
reference_revision_id uuid nullable points at the revision a restore reverted to
created_at timestamptz

The relation to CourseEntity is modelled as a plain indexed column without an FK constraint so the audit trail outlives a deleted course. Admins can still inspect what content was previously published even after the parent CourseEntity row is removed.

CourseService (BackendAcademy/src/courses/course.service.ts)

  • Injects Repository<CourseEntity> and Repository<CourseRevisionEntity> via @InjectRepository.
  • Drops the in-memory Maps.
  • Every create / update / restore appends an immutable revision through a single appendRevision helper that keeps latest_revision_id in sync.
  • Restore replays a chosen snapshot onto the course, bumps the version forward, and writes a restore revision whose reference_revision_id points at the source revision — keeping the audit trail strictly append-only.
  • All revision lookup endpoints preserved: GET /revisions, /revisions/latest, /revisions/count, /revisions/:version, and POST /revisions/:version/restore.

CourseModule (BackendAcademy/src/courses/course.module.ts)

Registers CourseEntity and CourseRevisionEntity with TypeOrmModule.forFeature([...]) so the injected repositories resolve to real Postgres tables. The parent DatabaseModule (autoLoadEntities: true) automatically picks them up.

Tests (BackendAcademy/src/courses/course.service.spec.ts)

  • Constructs CourseService with a minimal in-memory Repository mock that mirrors the TypeORM surface (create / save / find / findOne / count / remove) and invokes the entity constructor so default values still apply.
  • All 14 prior behavioural assertions are preserved — create-at-v1, version bump on update, deep-copy snapshot retention, revision lookup, restore append + reference id, audit retention after course deletion.

Validation

npx jest src/courses
# 14 passed, 0 failed

Closes

Closes #218

dunnidev and others added 2 commits June 30, 2026 01:39
Closes BlockDash-Studios#218

Adds database fields for content revisions and version history to
the courses module so each course change is recorded as an
immutable revision that can be queried, counted, and restored.

Schema additions (BackendAcademy/src/courses/)
  * course.entity.ts: add version (number, defaults to 1) and
    latestRevisionId (string | undefined) to CourseEntity so the
    current "head" revision can be located at O(1) from the
    course record.
  * course-revision.entity.ts (new): immutable CourseRevisionEntity
    capturing a per-version snapshot of title, description, level,
    order, learningPathId, duration, prerequisites, skills,
    xpReward, isActive plus changeNote, revisionAuthor, reason
    ("create" | "update" | "restore"), previousVersion, and
    referenceRevisionId for traceability.
  * dto/update-course.dto.ts: add changeNote and revisionAuthor so
    editors can attribute updates when calling PUT /courses/:id.

Service (BackendAcademy/src/courses/course.service.ts)
  * Maintains a separate revisions Map keyed by revision id.
  * create()/update() now automatically append a revision. Each
    update bumps course.version by 1 and records the new state.
  * restoreRevision() copies the snapshot of a chosen historical
    version back onto the live course and records a new "restore"
    revision, keeping the history append-only.
  * Read-only methods getRevisions, getLatestRevision,
    getRevisionByVersion, getRevisionCount continue to work
    even after a course is deleted so the audit trail survives.
  * Snapshot arrays (prerequisites, skills) are cloned; scalar
    fields are values, so subsequent live-course mutations cannot
    alias historical state.

Controller (BackendAcademy/src/courses/course.controller.ts)
  * GET    /courses/:id/revisions                 list revisions
  * GET    /courses/:id/revisions/latest          latest revision
  * GET    /courses/:id/revisions/count           revision count
  * GET    /courses/:id/revisions/:version        specific version
  * POST   /courses/:id/revisions/:version/restore  restore a version
  * Restore uses new RestoreRevisionDto body so revisionAuthor
    is class-validator validated (MaxLength 120); route-order
    constraint (latest/count before :version) is documented
    inline so future maintainers do not silently break it.

Tests (BackendAcademy/src/courses/course.service.spec.ts, 14 cases)
  * Verifies version incrementing and revision appendage on
    create/update, latestRevisionId pointer rotation, snapshot
    deep-copy of arrays, getRevisions/getLatestRevision/
    getRevisionByVersion behavior, restore flow produces a new
    revision and resets content, NotFoundException for missing/
    non-positive versions, and audit-trail-survives-removal.

Out of scope (deliberately)
  * The existing courses/audit/ module is left untouched: its
    AuditLogModule is not imported by app.module.ts and its
    in-file provider is missing @Injectable(), so an integration
    here would not be DI-resolvable. Revisions are themselves
    a complete audit trail for course content; general system
    audit logging can be wired up in a separate change.
Add DB-persisted course content versioning and revision history to
BackendAcademy/src/courses/. Closes BlockDash-Studios#218.

CourseEntity
  Persist id (uuid), title (varchar 200), description (text),
  level (enum), order (int), learning_path_id (uuid, indexed),
  prerequisites and skills as text[], xp_reward (int), is_active (bool),
  version (int), latest_revision_id (uuid, nullable), created_at and
  updated_at (timestamptz).

CourseRevisionEntity
  Persist id (uuid), course_id (uuid, indexed), version (int),
  snapshot as jsonb, change_note (text, nullable),
  revision_author (varchar 120, nullable), reason (varchar 32),
  previous_version (int, nullable), reference_revision_id (uuid,
  nullable). Audit trail intentionally outlives deleted courses: no FK
  is declared on course_id so revisions remain queryable for audit even
  after the parent course row is removed.

CourseService
  Replace the in-memory Maps with injected TypeORM repositories.
  Every meaningful change (create, update, restore) appends an
  immutable revision. Restore replays a chosen snapshot onto the
  course and writes a 'restore' revision that references the source
  revision id. latestRevisionId stays in sync through a single
  appendRevision helper.

CourseModule
  Register CourseEntity and CourseRevisionEntity with
  TypeOrmModule.forFeature so the injected repositories resolve to the
  real Postgres tables.

Tests
  Rewrite course.service.spec.ts to construct CourseService with a
  minimal in-memory repository mock that mirrors the TypeORM surface
  (create / save / find / findOne / count / remove) and invokes the
  entity constructor so default values still apply. All 14 prior
  behavioural assertions are preserved: create at v1, version bump on
  update, deep-copy snapshot retention, revision lookup, restore
  append + reference id, audit retention after course deletion.
@drips-wave

drips-wave Bot commented Jun 30, 2026

Copy link
Copy Markdown

@dunnidev Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@MaryammAli MaryammAli left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@MaryammAli

Copy link
Copy Markdown
Contributor

@dunnidev
you have conflict

@MaryammAli MaryammAli merged commit accee52 into BlockDash-Studios:main Jun 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add course content versioning support

2 participants