mongoloid

0.0.0 • Public • Published

THIS IS CURRENTLY A REQUEST-FOR-COMMENTS WHITE-PAPER

THIS PROJECT IS CURRENTLY NON-FUNCTIONAL - PLEASE DO NOT INSTALL IT

Mongoloid

A nicer way to work with Mongo.

Mongoloid attempts to provide nicer, chainable functionality on top of base Mongo while making some of the more C centric designs of Mongo more Node like.

Features

Proper chainability

While libraries like Mongoose provide some chainable methods they don't quite work as expected. For example

users
	.find()
	.populate('widgets')
	.where('widgets.enabled', true)
	.exec(handler);

Will not work in Mongo or Mongoose due to the way that populate is a late binding. Mongoloid however will see that widgets needs populating THEN the where needs to be executed later on and actually make all this work.

Deep population

With Mongo / Mongoose at present you are limited to the first-layer-only of population.

For example assuming you had three collections: foo, bar and baz each having a foreign key linking them all together. The temptation is to do something like this during a retrieval:

foo
	.find()
	.populate('bar')
	.populate('bar.baz');
	.exec(handler)

However that will not work. Only the first level of population (in this case foo.bar) will be executed. The second level (foo.bar.baz) will not be actioned. Worse than that - it will also fail silently.

Mongoloid supports multi-level population automatically. The final handler (exec()) will only be invoked when all requested populations are executed first.

JavaScript style casing

As Mongo is C based some of the methods, properties or functions using first-letter-caps rather than the Node style camelCasing.

Mongoloid rewrites the cases to work in a more Node-y way.

Here are some examples:

Mongoose Mongoloid
mongoose.Schema.ObjectId mongoloid.types.id
mongoose.Schema mongoloid.schema or mongoloid.define
mongoose.Schema.Types.Mixed mongoloid.types.mixed

Hooks that actually work

Mongo / Mongoose provide very rudimentary hooks like model.pre('save', callback) which work some of the time (in the example this hook wont trigger if the document is being saved on an insert for example). Mongoloid extends the hook capability by using the Node EventEmitter to provide much-needed event support.

Hook Supported by Mongo Description
save as .pre('save', cb) but only for document creation Trigger callback(row, next) for a document save - on modify or creation
postsave as .post('save', cb) but without next handling Trigger callback(row, next) after a document save - on modify or creation
create as .pre('save', cb) Trigger callback(row, next) on the creation of a new document
postcreate as .post('save', cb) but without next handling Trigger callback(row, next) on after the creation of a new document
modify No Trigger callback(row, next) on the modification of an existing document
postmodify No Trigger callback(row, next) after the modification of an existing document
change No Trigger callback(field, row, next) on a individual field change
change-schema No Trigger callback(row, next) when any field changes in the specified schema
change-schema-field No Trigger callback(field, next) when the specified field changes in the specified schema

In addition to simple on() support Mogol supports all the usual EventEmitter functionality such as once(), listener support etc.

Hooks that fire on field changes

As with the above hooks for general schema manipulation it is now also possible to detect changes to individual changes.

Previously in Mongo / Mongoose this functionality had to be accomplished by wrapping an entire .pre('save', cb) handler, now we can listen for specific field changes using .on('change', cb).

See the above section for details on the change event.

Virtuals that work asynchronously

Virtuals are a great feature but the lack of async support in Node makes writing non-blocking scripts difficult.

Mongoloid patches this functionality by making all Virtuals async - both getters and setters.

userSchema
	.virtual('password')
		.get(function(next) {
			// Do something async
			next(null, 'valueOfPassword');
		})
		.set(function(value, next) {
			// Do something async
			next();
		});

In the above the field password is a virtual which can now operate async on the retrieved / set value before resuming the operation.

Tidier schema creation

Mongo requires that the schema be created before the object is created but the syntax for this operation is pretty messy.

One way is to create the schema using the new define() functionality:

mongrol.define('user', {
	id: mongoose.Schema.ObjectId,
	email: {type: String, required: true, index: {unique: true}},
	passhash: {type: String},
	passsalt: {type: String},
	name: {type: String},
	contact: {
		phone: {type: String},
		mobile: {type: String},
	},
	status: {type: String, enum: ['active', 'deleted'], default: 'active'},
	role: {type: String, enum: ['user', 'admin', 'root'], default: 'user'},
	created: {type: Date, default: Date.now},
});

Or fields can be defined individually in a chain using define() + field():

mongoloid.define('user')
	.field('id', mongrol.types.id)
	.field('email', mongoloid.types.string)
	.field('passhash', mongoloid.types.string)
	.field('passsalt', mongoloid.types.string)
	.field('name', mongoloid.types.string)
	.field('contact', mongoloid.types.object, {
		phone: {type: mongoloid.types.string},
		mobile: {type: mongoloid.types.string},
	})
	.field('status', mongoloid.types.string, {enum: ['active', 'deleted'], default: 'active'})
	.field('role', mongoloid.types.string, {enum: ['user', 'admin', 'root'], default: 'user'})
	.field('created', mongoloid.types.date, {default: Date.now});

Or by using Mongoloids pluggable type system:

mongoloid.define('user')
	.id('id')
	.string('email')
	.string('passhash')
	.string('passsalt')
	.string('name')
	.object('contact', {
		phone: {type: mongoloid.types.string},
		mobile: {type: mongoloid.types.string},
	})
	.string('status', {enum: ['active', 'deleted'], default: 'active'})
	.string('role', {enum: ['user', 'admin', 'root'], default: 'user'})
	.date('created', {default: Date.now});

Field transforms

Assuming you wanted to rewrite a value before it hits the database Mongoloid can asynchronously rewrite the incoming value on a per field basis without using hooks.

For example:

mongoloid.define('widgets')
	.string('status', {
		transform: function(value, next) {
			// Do something complicated
			next(null, 'newValue');
		}
	});

Multiple field functions

Instead of just one validation or transform function multiple functions can be defined per field.

For example:

mongoloid.define('widgets')
	.string('status', {
		validate: [
			function(value, next) {
				// Do something complicated
				next(); // Assume no issues - use next('My error') if validation fails
			},
			function(value, next) {
				// Do something else complicated
				next(); // No issues here either
			},
		]
	});

Late binding of field functions

Hooks and field functions can also be attached after the schema definition stage.

This is accomplished using the same definition syntax. If a schema is already defined with the given name Mongoloid will attach the new functionality to the existing schema.

For example:

// Assume that 'users' has already been defined elsewhere

mongoloid.define('users')
	.field('password', {
		changed: function(value, next) {
			// Do something when user password changes
		}
	});

Alternatively the event system can also be used via EventEmitter:

mongoloid.on('change-users-password', function(next) {
	// Password has changed
	next();
});

Functional operators

There are certain database operations which are so common they should be provided at the driver level.

Below are some query functions which can be used directly within Mongoloid without plugins.

Function Description
max(field, function) Return the minimum value of the selected field
min(field, function) Return the maximum value of the selected field
sum(field, function) Return the sum value of the selected field

DWIM selection syntax

There are certain places in Mongo / Mongoose where selectors do not seem to have the desired results.

For example:

db.users.find({
	auth: {
		tokens: {
			token: 'abc124'
		}
	}
}, callback);

Does not work. The reason is that Mongo expects all selectors to be written in dotted notation as follows:

db.users.find({
	'auth.tokens.token': 'abc124'
}, callback);

Mongoloid builds on the DWIM philosophy where certain functionality is handled by Mongoloid to get the expected results.

Mongoloid supports both of these formats allowing you to be as explicit as you require in your selection functions. Mongoloid will rewrite complex array selects (see first example) into dotted notation before passing the aggregate query onto the Mongo driver.

Readme

Keywords

Package Sidebar

Install

npm i mongoloid

Weekly Downloads

0

Version

0.0.0

License

MIT

Last publish

Collaborators

  • hash-bang