Skip to content

Latest commit

 

History

History
288 lines (210 loc) · 11.8 KB

IDEAS.md

File metadata and controls

288 lines (210 loc) · 11.8 KB

Ideas

This document is designed to outline some of the differences between Monoxide and Mongoose.

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. Monoxide 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.

Monoxide 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.

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

Here are some examples:

Mongoose Monoxide
mongoose.Schema.ObjectId monoxide.types.id
mongoose.Schema monoxide.schema or monoxide.define
mongoose.Schema.Types.Mixed monoxide.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). Monoxide 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.

Monoxide 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:

monoxide.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():

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

Or by using Monoxides pluggable type system:

monoxide.define('user')
	.id('id')
	.string('email')
	.string('passhash')
	.string('passsalt')
	.string('name')
	.object('contact', {
		phone: {type: monoxide.types.string},
		mobile: {type: monoxide.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 Monoxide can asynchronously rewrite the incoming value on a per field basis without using hooks.

For example:

monoxide.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:

monoxide.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 Monoxide will attach the new functionality to the existing schema.

For example:

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

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

Alternatively the event system can also be used via EventEmitter:

monoxide.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 Monoxide 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);

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

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

Document rebuild on schema change

Documents in Mongoose are saved against the schema at the time of creation and not on each load.

If for example yo uhad the following schema:

mongoose.define('user', {
	id: mongoose.Schema.ObjectId,
	email: {type: String, required: true, index: {unique: true}},
	passhash: {type: String},
	passsalt: {type: String},
	name: {type: String},
});

... and then at some later point added the role field with a suitable default:

mongoose.define('user', {
	id: mongoose.Schema.ObjectId,
	email: {type: String, required: true, index: {unique: true}},
	passhash: {type: String},
	passsalt: {type: String},
	name: {type: String},
	role: {type: String, enum: ['user', 'admin'], default: 'user', index: true},
});

Mongoose will only apply the new specification for role after the change has been made. Any existing user documents will not get the role field applied, nor the default value.

IMPLEMENTED: Monoxide fixes this issue by applying schemas on each query. If your schema changes the retrieved document has the defaults or additional fields applied as needed. Deleted fields are not removed.

Other ideas

  • ForEach ability on returned record. Combines .exec() + found.forEach(). Possibly uses a generator or something so we dont have to hold the entire found result set in memory