Skip to content

How to track old versions of documents on a Mongo database using mongoose

What's the problem?

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

Implementation

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
            }
        ]
    }
]

GitHub repository

In this repository https://github.com/obravocedillo/VersionTrackingMongo you can find the code.