Skip to content

a mongoose plugin that enables git like change tracking

License

Notifications You must be signed in to change notification settings

hollandjake/git-goose

Repository files navigation

npm package documentation licence

git-goose

A mongoose plugin that enables git like change tracking with CommonJS, ESM, and TypeScript support

Installation

npm install git-goose

Import into your script

Supports both CommonJS and ESM

const git = require("git-goose");

or

import git from "git-goose";
// or import { git } from "git-goose";

Usage

With JavaScript

import { mongoose } from "mongoose";
import git from "git-goose";

const YourSchema = new mongoose.Schema({
  firstName: String,
  lastName: String
});
// Register the plugin
YourSchema.plugin(git);

// Create your model
const YourModel = mongoose.model("Test", YourSchema, "tests");

/* Then use your model however you would normally */

With Typescript

import mongoose from 'mongoose';
import git, { committable } from 'git-goose';

const YourSchema = new mongoose.Schema({
  firstName: String,
  lastName: String
});
// Register the plugin
YourSchema.plugin(git);

// Create your model
// Use committable to inject all the typings for you
const YourModel = committable(mongoose.model("Test", YourSchema, "tests"));

/* Then use your model however you would normally */

API

Config

YourSchema.plugin(git, conf?: ContextualGitConfig);
Optional ContextualGitConfig argument

opts.connection?: Mongoose.Connection

Override the connection used to store the model history. By default, we use the connection that is bound to the model, this is done on a per-model basis. So all models are handled as you would expect

opts.collectionName?: string

By default, we generate a collection per model using the logic ${model.name}${opts.collectionSuffix}, this means each models history is stored in a separate collection (effectively treating a model as a repository).

You can override this collectionName forcing all histories to be saved into a singular collection

opts.collectionSuffix: string

Override the suffix used to generate collection names. By default, this is is .git

If you want to override the entire collection name, please see opts.collectionName

opts.patcher: string | Patcher

Override the default patcher to use for generating patches.

By default, we use mini-json-patch which is a minified version of RFC6902. We also have support for json-patch which is the full size version of RFC6902

You can also provide a Custom Patcher

[!NOTE] This does not break any existing patches, it just changes how we store new patches and compute diff(X, Y)

opts.snapshotWindow: number

Override the default snapshot window, used as a performance optimisation to stop having to trawl back through thousands of commits to build the current state.

To disable snapshotting, set this to -1

By default, we use 100

Supports

Whenever an instance is created or updated it will save the changes to a new mongo collection containing the commit log. So from a normal users perspective they can keep doing what they would normally do!

By default, a new collection is created per collection the plugin is loaded on, however this can be configured if you wish to have all the logs for all collections in a single collection

Creation

Through Document.save()

const instance = new YourModel({ firstName: 'hello', lastName: 'world' });
await instance.save();

Through Model.create()

const instance = await YourModel.create({ firstName: "hello", lastName: "world" });

Updating

Through a document update

instance.firstName = 'world';
instance.lastName = 'hello';
await instance.save();

Through any of the Model level mutators

await YourModel.updateOne({ firstName: 'hello' }, { firstName: 'world', lastName: 'hello' });
await YourModel.updateMany({ firstName: "hello" }, { firstName: 'world', lastName: 'hello' });
await YourModel.findOneAndUpdate({ firstName: "hello" }, { firstName: 'world', lastName: 'hello' });
await YourModel.findOneAndReplace({ firstName: "hello" }, { firstName: 'world', lastName: 'hello' });

Return the changes since last commit

Similar to git, it will return all the changes since last commit

const instance = new YourModel({ firstName: 'hello', lastName: 'world' });
const status = await instance.$git.status();
/*
{
  type: 'json-patch',
  ops: [
    {
      op: 'replace',
      path: '',
      value: {
        firstName: 'hello',
        lastName: 'world',
        _id: new ObjectId('66be1b5ed47739c9e7a52a0f')
      }
    }
  ]
}
*/

View commit history

By default, the logs are ordered by descending date so the latest commit is in index 0

You can provide custom filters, projections and options as its arguments for custom sorting etc.

const log = await instance.$git.log()
/*
[
  {
    _id: new ObjectId('66be1b5ed47739c9e7a52a17'),
    patch: {
      type: 'json-patch',
      ops: [
        { op: 'replace', path: '/firstName', value: 'world' },
        { op: 'replace', path: '/lastName', value: 'hello' }
      ],
      _id: new ObjectId('66be1b5ed47739c9e7a52a18')
    },
    date: 2024-08-15T15:41:52.892Z,
    id: '66be1b5ed47739c9e7a52a17'
  },
  {
    _id: new ObjectId('66be1b5ed47739c9e7a52a12'),
    patch: {
      type: 'json-patch',
      ops: [
        {
          op: 'replace',
          path: '',
          value: {
            firstName: 'hello',
            lastName: 'world',
            _id: new ObjectId('66be1b5ed47739c9e7a52a0f')
          }
        }
      ],
      _id: new ObjectId('66be1b5ed47739c9e7a52a13')
    },
    date: 2024-08-15T15:39:49.436Z,
    id: '66be1b5ed47739c9e7a52a12'
  }
]
*/

Checkout a previous commit

You are able to restore to a previous commit using checkout, this will reproduce the instance as it was at that point in time. The response will be a fully hydrated object so you can use all the bells and whistles that mongoose provides like population

const snapshot = await instance.$git.checkout(1)
/*
{
  firstName: 'hello',
  lastName: 'world',
  _id: new ObjectId('66be1b5ed47739c9e7a52a0f')
}
*/

or if you prefer a more git like syntax HEAD, HEAD^, HEAD^N, and its corresponding @ versions are all supported

or you can use a date string or Date object, this will find the newest commit that meets this timestamp, so remember that JS dates default to midnight if no time is provided.

const snapshot = await instance.$git.checkout("2024-08-15T15:39:49.436Z")

Computing the difference between two commits

As with checkout, all arguments support all types of commit references.

Compare against HEAD

const diff = await instance.$git.diff(1)
/*
{
  type: 'json-patch',
  ops: [
    { op: 'replace', path: '/firstName', value: 'world' },
    { op: 'replace', path: '/lastName', value: 'hello' }
  ]
}
*/

Compare two other commits

const instance = await YourModel.create({ firstName: 'hello', lastName: 'world' });
instance.firstName = 'wow';
await instance.save();
instance.firstName = 'amazing';
await instance.save();
instance.firstName = 'cool';
await instance.save();

const diff = await instance.$git.diff(3, 1);
/*
{
  type: 'json-patch',
  ops: [ { op: 'replace', path: '/firstName', value: 'amazing' } ]
}
*/

Custom Patcher

If you want to define your own patcher you can define one as such

import {Patchers} from "git-goose";

Patchers["custom"] = <Patcher<TPatchType, DocType>>{
  create(committed: Nullable<DocType>, active: Nullable<DocType>): TPatchType | Promise<TPatchType> {},
  apply(target: Nullable<DocType>, patch: TPatchType): Nullable<DocType> {},
}

you can then use this custom patcher in your config

YourSchema.plugin(git, {patcher: "custom"});

or globally

import { GitGlobalConfig } from "./config";

GitGlobalConfig["patcher"] = "custom"

Homepage

You can find more about this on GitHub.

Contributing

Contributions, issues and feature requests are welcome!

Feel free to check issues page.

Authors

See also the list of contributors who participated in this project.

License

This project is MIT licensed.