iridium
Version:
A custom lightweight ORM for MongoDB designed for power-users
244 lines (198 loc) • 8.81 kB
text/typescript
import _ = require('lodash');
import MongoDB = require('mongodb');
export class Omnom {
constructor(public options: {
atomicNumbers?: boolean;
} = {}) {
this._changes = {};
}
private _changes: {
$set?: any;
$unset?: any;
$inc?: any;
$push?: any;
$pull?: any;
$pullAll?: any;
};
get changes(): {
$set?: any;
$unset?: any;
$inc?: any;
$push?: any;
$pull?: any;
$pullAll?: any;
} {
return this._changes;
}
diff(original: number, modified: number): Omnom;
diff(original: [any], modified: any[]): Omnom;
diff(original: MongoDB.ObjectID, modified: MongoDB.ObjectID): Omnom;
diff(original: Object, modified: Object): Omnom;
diff(original: any, modified: any): Omnom {
this.onObject(original, modified);
return this;
}
static diff(original: number, modified: number, options?: {
atomicNumbers?: boolean;
});
static diff(original: [any], modified: any[], options?: {
atomicNumbers?: boolean;
});
static diff(original: MongoDB.ObjectID, modified: MongoDB.ObjectID, options?: {
atomicNumbers?: boolean;
});
static diff(original: Object, modified: Object, options?: {
atomicNumbers?: boolean;
});
static diff(original: any, modified: any, options?: {
atomicNumbers?: boolean;
}) {
return new Omnom(options).diff(original, modified).changes;
}
private onObject(original: number, modified: number, changePath?: string);
private onObject(original: [any], modified: any[], changePath?: string);
private onObject(original: MongoDB.ObjectID, modified: MongoDB.ObjectID, changePath?: string);
private onObject(original: Object, modified: Object, changePath?: string);
private onObject(original: any, modified: any, changePath?: string) {
if (original === undefined || original === null)
return (original !== modified) && this.set(changePath, modified);
if (typeof original == 'number' && typeof modified == 'number' && original !== modified) {
if (this.options.atomicNumbers) return this.inc(changePath, modified - original);
return this.set(changePath, modified);
}
if (Array.isArray(original) && Array.isArray(modified))
return this.onArray(original, modified, changePath);
if (original instanceof MongoDB.ObjectID && modified instanceof MongoDB.ObjectID)
return !original.equals(modified) && this.set(changePath, modified);
if (!_.isPlainObject(original) || !_.isPlainObject(modified))
return !_.isEqual(original, modified) && this.set(changePath, modified);
_.each(modified, function (value, key) {
// Handle array diffs in their own special way
if (Array.isArray(value) && Array.isArray(original[key])) this.onArray(original[key], value, this.resolve(changePath, key));
// Otherwise, just keep going
else this.onObject(original[key], value, this.resolve(changePath, key));
}, this);
// Unset removed properties
_.each(original, function (value, key) {
if (modified[key] === undefined) return this.unset(this.resolve(changePath, key));
}, this);
}
private onArray(original: [any], modified: [any], changePath: string) {
var i, j;
// Check if we can get from original => modified using just pulls
if (original.length > modified.length) {
var pulls = [];
for (i = 0, j = 0; i < original.length && j < modified.length; i++) {
if (this.almostEqual(original[i], modified[j])) j++;
else pulls.push(original[i]);
}
for (; i < original.length; i++)
pulls.push(original[i]);
if (j === modified.length) {
if (pulls.length === 1) return this.pull(changePath, pulls[0]);
// We can complete using just pulls
return pulls.forEach((pull) => this.pull(changePath, pull));
}
// If we have a smaller target array than our source, we will need to re-create it
// regardless (if we want to do so in a single operation anyway)
else return this.set(changePath, modified);
}
// Check if we can get from original => modified using just pushes
if (original.length < modified.length) {
var canPush = true;
for (i = 0; i < original.length; i++)
if (this.almostEqual(original[i], modified[i]) < 1) {
canPush = false;
break;
}
if (canPush) {
for (i = original.length; i < modified.length; i++)
this.push(changePath, modified[i]);
return;
}
}
// Otherwise, we need to use $set to generate the new array
// Check how many manipulations would need to be performed, if it's more than half the array size
// then rather re-create the array
var sets = [];
var partials = [];
for (i = 0; i < modified.length; i++) {
var equality = this.almostEqual(original[i], modified[i]);
if (equality === 0) sets.push(i);
else if (equality < 1) partials.push(i);
}
if (sets.length > modified.length / 2)
return this.set(changePath, modified);
for (i = 0; i < sets.length; i++)
this.set(this.resolve(changePath, sets[i].toString()), modified[sets[i]]);
for (i = 0; i < partials.length; i++)
this.onObject(original[partials[i]], modified[partials[i]], this.resolve(changePath, partials[i].toString()));
}
private set(path: string, value: any) {
if (!this.changes.$set)
this.changes.$set = {};
this.changes.$set[path] = value;
}
private unset(path: string) {
if (!this.changes.$unset)
this.changes.$unset = {};
this.changes.$unset[path] = 1;
}
private inc(path: string, value: number) {
if (!this.changes.$inc)
this.changes.$inc = {};
this.changes.$inc[path] = value;
}
private push(path: string, value: any) {
if (!this.changes.$push)
this.changes.$push = {};
if (this.changes.$push[path]) {
if (this.changes.$push[path].$each)
this.changes.$push[path].$each.push(value);
else
this.changes.$push[path] = { $each: [this.changes.$push[path], value] };
} else this.changes.$push[path] = value;
}
private pull(path: string, value: any) {
if (!this.changes.$pull)
this.changes.$pull = {};
if (this.changes.$pullAll && this.changes.$pullAll[path]) {
return this.changes.$pullAll[path].push(value);
}
if (this.changes.$pull[path]) {
this.pullAll(path, [this.changes.$pull[path], value]);
delete this.changes.$pull[path];
if (_.keys(this.changes.$pull).length === 0)
delete this.changes.$pull;
return;
}
this.changes.$pull[path] = value;
}
private pullAll(path: string, values: any[]) {
if (!this.changes.$pullAll)
this.changes.$pullAll = {};
this.changes.$pullAll[path] = values;
}
private resolve(...args) {
var validArguments = [];
args.forEach(function (arg) {
if (arg) validArguments.push(arg);
});
return validArguments.join('.');
}
private almostEqual(o1: Object, o2: Object);
private almostEqual(o1: any, o2: any) {
if (!_.isPlainObject(o1) || !_.isPlainObject(o2)) return o1 == o2 ? 1 : 0;
var o1i, o1k = Object.keys(o1);
var o2k = Object.keys(o2);
var commonKeys = [];
for (o1i = 0; o1i < o1k.length; o1i++)
if (~o2k.indexOf(o1k[o1i])) commonKeys.push(o1k[o1i]);
var totalKeys = o1k.length + o2k.length - commonKeys.length;
var keysDifference = totalKeys - commonKeys.length;
var requiredChanges = 0;
for (var i = 0; i < commonKeys.length; i++)
if (this.almostEqual(o1[commonKeys[i]], o2[commonKeys[i]]) < 1) requiredChanges++;
return 1 - (keysDifference / totalKeys) - (requiredChanges / commonKeys.length);
}
}