mongodb-track-changes-rollback-plugin
Version:
A plugin for tracking create, update, and delete operations with rollback support and snapshots for MongoDB/Mongoose
132 lines (112 loc) • 3.51 kB
text/typescript
import mongoose, { Schema, Document } from 'mongoose';
import { ChangeLog } from './models/ChangeLog';
import { ChangeSnapshot } from './models/ChangeSnapshot';
interface ChangeLoggerOptions {
excludeFields?: string[];
operations?: {
create?: boolean;
update?: boolean;
delete?: boolean;
};
enableSnapshots?: boolean;
snapshotInterval?: number;
}
interface DiffChange {
[key: string]: [any, any];
}
export function changeLoggerPlugin(
schema: Schema,
options: ChangeLoggerOptions = {}
) {
const {
excludeFields = [],
operations = { create: true, update: true, delete: true },
enableSnapshots = false,
snapshotInterval = 5,
} = options;
// Store original document state before save
schema.pre('save', async function (next: (err?: any) => void) {
const ModelClass = this.constructor as mongoose.Model<any>;
try {
if (this.isNew) {
(this as any)._original = {};
} else {
const original = await ModelClass.findById(this._id).lean();
(this as any)._original = original || {};
}
next();
} catch (err) {
next(err);
}
});
// Log create/update after save
schema.post('save', async function (doc: Document) {
const ModelClass = doc.constructor as mongoose.Model<any>;
const isNew = doc.isNew;
const excluded = new Set(excludeFields);
const logOps = operations;
if ((isNew && !logOps.create) || (!isNew && !logOps.update)) return;
const original = (doc as any)._original || {};
const current = doc.toObject({ depopulate: true });
const changes: DiffChange = {};
for (const key of Object.keys(current)) {
if (excluded.has(key) || key === '__v' || key === '_id') continue;
const oldVal = original[key];
const newVal = current[key];
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
changes[key] = [oldVal, newVal];
}
}
if (Object.keys(changes).length === 0 && !isNew) return;
try {
const collectionName = ModelClass.modelName;
await ChangeLog.create({
documentId: doc._id,
collectionName,
operation: isNew ? 'create' : 'update',
changes,
version: doc.__v,
createdAt: new Date(),
});
if (
enableSnapshots &&
doc.__v !== undefined &&
doc.__v % snapshotInterval === 0
) {
await ChangeSnapshot.create({
documentId: doc._id,
collectionName,
version: doc.__v,
snapshot: current,
createdAt: new Date(),
});
}
} catch (err) {
console.error('ChangeLog plugin error:', err);
}
});
// Log delete operations
if (operations.delete) {
schema.pre(
'deleteOne',
{ document: true, query: false },
async function (this: Document, next: (err?: any) => void) {
try {
const ModelClass = this.constructor as mongoose.Model<any>;
const collectionName = ModelClass.modelName;
await ChangeLog.create({
documentId: this._id,
collectionName,
operation: 'delete',
changes: {}, // no changes for delete
version: this.__v,
createdAt: new Date(),
});
next();
} catch (err) {
next(err);
}
}
);
}
}