Some projects require to track old versions of documents on the database to generate metrics, show a history of changes or even as part of the main logic of the platform, on relation databases we have access to temporal tables which will maintain historical versions of data for us, on Mongo things are a little different because we have to implement this functionality ourselves. So I'm going to show you a way to do this
We are going to save the old versions of the documents inside of a new collection called documentVersions, and we will add a new value to the model of our documents called version, we will increment this value by 1 each time we save a new documnt on the documentVersions collection, we will also use a virtual field called versions to populate an array containing previous versions inside of the document
First we are going to create a model called songs
// ./models/songs.js
const mongoose = require("mongoose");
// Songs schema
const SongsSchema = new mongoose.Schema(
{
name: {
type: String,
},
duration: {
type: Number,
},
version: {
type: Number,
},
},
{
toJSON: { virtuals: true },
toObject: { virtuals: true },
},
);
// Create versions array containing previous version of documents
SongsSchema.virtual("versions", {
ref: "SongsVersions",
localField: "_id",
foreignField: "songId",
});
const Songs = mongoose.model("Songs", SongsSchema, "Songs");
module.exports = Songs;
Next we are going to create a model called songsVersions that will contain previous version of songs
// ./models/songsVersions.js
const mongoose = require("mongoose");
// Songs versions schema
const SongsVersionsSchema = new mongoose.Schema({
songId: { type: mongoose.Schema.Types.ObjectId, ref: "Songs" },
name: {
type: String,
},
duration: {
type: Number,
},
});
const SongsVersions = mongoose.model(
"SongsVersions",
SongsVersionsSchema,
"SongsVersions",
);
module.exports = SongsVersions;
Now we are going to create a controller and routes to save,update and save previous versions and get songs with previous version
// ./controllers/songs
const mongoose = require("mongoose");
const Songs = require("../models/songs");
const SongsVersions = require("../models/songsVersions");
/**
* @desc updates song and saves previous version
* @param songId _id of the song to update
* @param newValues new song values used on the update
*/
const getSongs = async () => {
const songs = await Songs.find({})
.populate({ path: "versions" })
.lean()
.exec();
return songs;
};
/**
* @desc save song
* @param song information used on new document
*/
const saveSong = async (song) => {
const newSong = new Songs({
...song,
_id: new mongoose.Types.ObjectId(),
version: 1,
});
await newSong.save();
return newSong;
};
/**
* @desc updates song and saves previous version
* @param songId _id of the song to update
* @param newValues new song values used on the update
*/
const updateSong = async (songName, newValues) => {
// When updating a song we also need to save old version and increment version number
const currentSong = await Songs.findOne({ name: songName }).lean().exec();
// Save current song on songs versions
const newSongVersion = new SongsVersions({
...currentSong,
_id: new mongoose.Types.ObjectId(),
songId: currentSong._id,
});
await newSongVersion.save();
// Update song with new values
await Songs.updateOne(
{
name: songName,
},
{
$set: {
...newValues,
},
$inc: {
version: 1,
},
},
);
// Get songs with previous versions dont forget to populate virtual field
const updatedSong = await Songs.findOne({ name: songName })
.populate({ path: "versions" })
.lean()
.exec();
return updatedSong;
};
module.exports = {
getSongs,
saveSong,
updateSong,
};
// ./routes/songs
const express = require("express");
const songsController = require("../controllers/songs");
// Express router
const router = express.Router();
/**
* @desc Will save a new song in the database
* @param {*} req express request
* @param {*} res espress response
*/
const saveSong = async (req, res) => {
try {
let song = req.body.song;
// If song is not present on the request body then the call is not valid
if (song) {
// Call controller logic
let ceateSongResponse = await songsController.saveSong(song);
// Return controller response
return res.status(200).send(ceateSongResponse);
}
// Return bad request error
res.status(400).send("Error data incomplete");
} catch (error) {
// Return error message
res.status(500).send(error);
}
};
/**
* @desc Will update a song in the database
* @param {*} req express request
* @param {*} res espress response
*/
const updateSong = async (req, res) => {
try {
let songName = req.body.songName;
let newValues = req.body.newValues;
// If songName or new values are not present on the request body then the call is not valid
if (songName && newValues) {
// Call controller logic
let updateSongResponse = await songsController.updateSong(
songName,
newValues,
);
// Return controller response
return res.status(200).send(updateSongResponse);
}
// Return bad request error
res.status(400).send("Error data incomplete");
} catch (error) {
// Return error message
res.status(500).send(error);
}
};
/**
* @desc Will get songs from the database
* @param {*} req express request
* @param {*} res espress response
*/
const getSongs = async (req, res) => {
try {
// Call controller logic
let getSongsResponse = await songsController.getSongs();
// Return controller response
return res.status(200).send(getSongsResponse);
} catch (error) {
// Return error message
res.status(500).send(error);
}
};
// Route that saves a new song
router.post("/", saveSong);
// Route that updates a song
router.put("/", updateSong);
// Route that gets songs
router.get("/", getSongs);
module.exports = router;
This is an example of the response obtained when getting songs on the database
[
{
"_id": "670586f0185244fa47b934d6",
"name": "Test song 2",
"duration": 180,
"version": 2,
"__v": 0,
"versions": [
{
"_id": "6705909e150effbb5d71f5bc",
"songId": "670586f0185244fa47b934d6",
"name": "Test song 1",
"duration": 180,
"__v": 0
}
]
}
]
In this repository https://github.com/obravocedillo/VersionTrackingMongo you can find the code.