feat(courses): add content versioning with immutable revision history#271
Merged
Merged
Conversation
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.
|
@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! 🚀 |
Contributor
|
@dunnidev |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 (coursesandcourse_revisions) via TypeORM repositories instead of holding state in in-memoryMaps.What changed
CourseEntity(BackendAcademy/src/courses/course.entity.ts)Persists the canonical, mutable course record.
iduuid(PK)crypto.randomUUID()-generated by servicetitlevarchar(200)descriptiontextlevelenumCourseLevelorderintlearning_path_iduuidindexedidx_courses_learning_path_idprerequisites,skillstext[]{}xp_rewardint0is_activebooleantrueversionint1latest_revision_iduuidnullablecourse_revisions.idcreated_at/updated_attimestamptz@CreateDateColumn/@UpdateDateColumnCourseRevisionEntity(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.iduuid(PK)course_iduuidindexedidx_course_revisions_course_idversionintcourse_idsnapshotjsonbchange_notetextnullablerevision_authorvarchar(120)reasonvarchar(32)create/update/restoreprevious_versionintnullablereference_revision_iduuidnullablerestorereverted tocreated_attimestamptzThe relation to
CourseEntityis 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 parentCourseEntityrow is removed.CourseService(BackendAcademy/src/courses/course.service.ts)Repository<CourseEntity>andRepository<CourseRevisionEntity>via@InjectRepository.Maps.create/update/restoreappends an immutable revision through a singleappendRevisionhelper that keepslatest_revision_idin sync.restorerevision whosereference_revision_idpoints at the source revision — keeping the audit trail strictly append-only.GET /revisions,/revisions/latest,/revisions/count,/revisions/:version, andPOST /revisions/:version/restore.CourseModule(BackendAcademy/src/courses/course.module.ts)Registers
CourseEntityandCourseRevisionEntitywithTypeOrmModule.forFeature([...])so the injected repositories resolve to real Postgres tables. The parentDatabaseModule(autoLoadEntities: true) automatically picks them up.Tests (
BackendAcademy/src/courses/course.service.spec.ts)CourseServicewith a minimal in-memoryRepositorymock that mirrors the TypeORM surface (create/save/find/findOne/count/remove) and invokes the entity constructor so default values still apply.Validation
Closes
Closes #218