Identify and assert differences betwen objects
obj_diff is for examining changes between Javascript and JSON objects. Use it to see how data has changed and to decide whether that change is good or bad. Thus obj_diff is useful for security and validation.
obj_diff comes from an internal Iris Couch application used in production for two years. It works in the browser, in CouchDB, and as an NPM module.
npm install obj_diff
Is it any good?
Yes.
Usage
Diff two objects. Then use helper functions to see what's changed.
var obj_diff = ; var original = hello:"world" note: "nice":"shoes" ;var modified = hello:"underworld" note: "nice":"hat" ; var diff = ; // Mandatory changesifdiff // true console; // Approved changesifdiff // false (.hello.note.nice also changed) console; ifdiff console;
Design
To work well with databases, obj_diff has these design goals:
- Declarative. Data validation is crucial. It must be correct. Validation rules must be easy to express clearly and easy to reason about.
- JSON compatible. Diffs and validation rules (containing regexes, functions, etc.) can be encoded and decoded as JSON, without losing functionality. You can store changes and validation policies as plain JSON.
Mandatory vs. Approved changes
There is a symbiotic relationship between atleast and atmost:
- atleast() returns
true
only if every rule matches a change. - atmost() returns
true
only if every change matches a rule.
// Give a key name, an expected old value, and expected new value.diff; // Specify multiple rules simultaneously.diff; diff; // Or as an assertion, with an extra "reason" argument.try diff; catch er if!erdiff throw er; // Unknown error, not a policy failure, e.g. bad parameters, or a predicate error. console; // e.g. Hey! options.log.level must upgrade to info try diff; catch er if!erdiff throw er; // Unknown error // .reason, .key, .from, .to are available. console; // detailed { return weapon != processenvsharp_weapon;}
A useful trick with atmost() is to assert no changes.
try diff2; // No rules given, i.e. "zero changes, at most" diff2; // Same as atmost() but more readable. catch er console;
CouchDB validation
obj_diff excels (and was designed for) Apache CouchDB validate_doc_update()
functions. Combine atleast() and atmost() to make a sieve and sift out good and bad changes. obj_diff cannot replace all validation code, but it augments it well.
- atleast() confirms required changes.
- atmost() confirms allowed changes.
First of all, CouchDB changes document metadata under the hood, and you don't want that triggering false alarms. So the first thing is to set obj_diff's defaults for CouchDB mode, which modifies atmost() to allow normal document changes:
null
is treated as an empty object,{}
. This always works:doc_diff(oldDoc, newDoc)
- atmost() allows normal changes:
_id
for document creation_rev
may change appropriately._revisions.ids
and_revisions.start
may change appropriately.
- assert_atleast() and assert_atmost() throw
{"forbidden": <reason>}
objects that Couch likes.
Thus, this is your typical validate_doc_update
function:
{ var doc_diff = // Relaxed diff. ANY = doc_diffANY GONE = doc_diffGONE ; var diff = ; // Start validating!}
Valid data vs. valid changes
obj_diff validates changes, not data. What happens if you GET a document and PUT it back unmodified? There will be zero changes in the diff. Any atleast() checks will necessarily fail. Therefore, the best practice is to check the data and then apply certain policies based on that.
Of course, sometimes you want changes in every update, such as timestamp validation:
if!oldDoc // Creation, require the timestamp fields. diff;else // Update, exact() will reject changes to .created_at (and all other fields) diff;
Example: User Documents
TODO
JSON Support
obj_diff supports regular expressions and function callbacks in its rules. Yet it can be nice to store them as JSON, and to load them later. For example, you could store a few rules in a CouchDB _security
object, and do database-specific data validation with an identical validate_doc_update()
function.
Both Diff and Rule obejcts behave the same after a JSON round-trip. They have a .toJSON
function to handle things, so just JSON.stringify()
them and store them in a file or database. Later, JSON.parse()
them and pass the object to the constructors.
var obj_diff = ; { return guygood || guyawesome } var diffs = ; var rules = "some_key" "old_value" "new_value" "log.level" obj_diffANY /^$/ "guy" good_guy obj_diffANY ; console;console;
Note, functions are stored using their source code, so be careful about global or closed variables they depend on.
Development
obj_diff uses node-tap unit tests. Install it globally (npm -g install node-tap
) and run tap t
. Or for a more robust local install:
$ npm install --dev
tap@0.0.10 ./node_modules/tap
└── tap-runner@0.0.7
$ ./node_modules/.bin/tap t
ok api.js ......................... 82/82
ok diffs.js ....................... 60/60
ok policy.js .................... 123/123
ok rules.js ..................... 774/774
total ......................... 1043/1043
ok
Finally, you can use the diff object yourself. Here's what it looks like:
> obj_diff({x:"hi"}, {x:"bye"})
{ x: { from: 'hi', to: 'bye' } }
> obj_diff({name:"Joe", word:"hi"},
... {name:"Joe", word:"bye"})
{ word: { from: 'hi', to: 'bye' } }
> obj_diff({name:"Joe", contact: {email:"doe@example.com"}},
... {name:"Joe", contact: {email:"doe@example.com", cell:"555-1212"}})
{ contact: { cell: { from: ['gone'], to: '555-1212' } } }
> obj_diff({name:"Joe", contact: {email:"doe@example.com", cell:null }},
... {name:"Joe", contact: {email:"doe@example.com", cell:"555-1212"}})
{ contact: { cell: { from: null, to: '555-1212' } } }
License
obj_diff is licensed under the Apache License, version 2.0