At this point, the logic for CRUD operations and routing is contained in the routes.js file as shown in figure 8.6.
We have the server accepting calls from the client, validating the data, and saving it to the database. The only way to validate and save the data is through calling the routes with an HTTP call. If this were all we needed for the application, then it’d prob- ably makes sense to stop here with no further abstraction. But our SPA will need to cre- ate and modify objects through web socket connections as well. Therefore, we’ll create a CRUD module that has all of the logic for validating and managing our docu- ments in the database. The router will then use the CRUD module for any required CRUD operations.
Before we create the CRUD module, we want to emphasize why we waited until now to create it. We like to keep our code as direct and simple as possible, but not simpler.
If we have to do an operation only once in the code, we prefer to usually have it inline or at least as a local function. But when we find that we need to perform an operation two or more times, we want to abstract it. Though this may not save initial coding time, it almost always saves maintenance time, as we centralize the logic into a single routine and avoid subtle errors that can result in variances in implementation. Of course, it takes good judgment to determine how far to take this philosophy. For example, we feel abstracting all for loops is generally not a good idea even though it’s thoroughly possible with JavaScript.
After we move the MongoDB connection and validations over to a separate CRUD module, our router will no longer be concerned with the implementation of data stor- age and will act more like a controller: it dispatches requests to other modules instead of performing actions itself, as shown in figure 8.7.
Client Routes.js MongoDB
Validate object type Validate object Insert to MongoDB
Store in JSON
Send JSON response
Send JSON response
Figure 8.6 Path through code
291 Create a separate CRUD module
Our first step in creating the CRUD module will be to prepare the file structure.
8.5.1 Prepare the file structure
Our file structure has remained consistent since the beginning of this chapter. Now that we need to add an additional module, we need to rethink it a bit. Our current structure is shown in listing 8.19:
chapter_8
`-- webapp
|-- app.js
|-- node_modules/
|-- package.json
|-- public
| |-- css/
| |-- js/
| `-- spa.html
|-- user.json
`-- routes.js
We’d rather keep our modules in a separate directory called lib. This will tidy up the webapp directory and keep our modules out of the node_modules directory. The node_modules directory should only contain external modules added through npm install so that it can be deleted and recreated without interfering with our modules.
Listing 8.20 shows how we want to structure our files. Changes are shown in bold:
chapter_8
`-- webapp
|-- app.js
|-- lib
| |-- crud.js
| |-- routes.js
| `-- user.json
|-- node_modules/
|-- package.json
Listing 8.19 Our current file structure
Listing 8.20 A new and enlightened file structure
Client Routes.js CRUD.js MongoDB
Call CRUD
Store in JSON
Send JSON response
Validate object type Validate object Insert to MongoDB
Send JSON response
Send JSON response Figure 8.7 Code path on the server
|-- public
|-- css/
|-- js/
`-- spa.html
Our first step toward file enlightenment is to move our routes file into webapp/lib.
Once we’ve done that, we need to update our server application to point to the new path, as shown in listing 8.21. Changes are shown in bold:
/*
* app.js - Express server with routing
*/
...
// --- BEGIN MODULE SCOPE VARIABLES --- 'use strict';
var
http = require( 'http' ), express = require( 'express' ), routes = require( './lib/routes' ), app = express(),
server = http.createServer( app );
// --- END MODULE SCOPE VARIABLES --- ...
Our next step is to include the CRUD module in our routes module, as shown in list- ing 8.22. Changes are shown in bold:
/*
* routes.js - module to provide routing
*/
...
// --- BEGIN MODULE SCOPE VARIABLES --- 'use strict';
var
loadSchema, checkSchema, configRoutes, mongodb = require( 'mongodb' ), fsHandle = require( 'fs' ), JSV = require( 'JSV' ).JSV, crud = require( './crud' ),
...
We can create our CRUD module and sketch its API. We’ll use module.exports to share the CRUD methods, as shown in listing 8.23.
/*
* crud.js - module to provide CRUD db capabilities
*/
Listing 8.21 Revise app.js to require moved routes.js—webapp/app.js
Listing 8.22 Adjust the routes module to require CRUD—webapp/lib/routes.js
Listing 8.23 Create the CRUD module—webapp/lib/crud.js
293 Create a separate CRUD module
/*jslint node : true, continue : true, devel : true, indent : 2, maxerr : 50, newcap : true, nomen : true, plusplus : true, regexp : true, sloppy : true, vars : false, white : true
*/
/*global */
// --- BEGIN MODULE SCOPE VARIABLES --- 'use strict';
var
checkType, constructObj, readObj, updateObj, destroyObj;
// --- END MODULE SCOPE VARIABLES --- // --- BEGIN PUBLIC METHODS --- checkType = function () {};
constructObj = function () {};
readObj = function () {};
updateObj = function () {};
destroyObj = function () {};
module.exports = { makeMongoId : null, checkType : checkType, construct : constructObj, read : readObj, update : updateObj, destroy : destroyObj };
// --- END PUBLIC METHODS --- // --- BEGIN MODULE INITIALIZATION --- console.log( '** CRUD module loaded **' );
// --- END MODULE INITIALIZATION ---
When we start the server with node app.js it should run without any errors:
** CRUD module loaded **
Express server listening on port 3000 in development mode
** Connected to MongoDB **
Note that we’ve added two public methods beyond basic CRUD operations. The first is makeMongoId, which provides the capability to make a MongoDBID object. The sec- ond is checkType, which we intend to use to check allowable object types. Now that we have our files in place, we can move our CRUD logic into the proper module.
8.5.2 Move CRUD into its own module
We can complete the CRUD module by copying our methods from the routes mod- ule and then replacing the HTTP-specific parameters with general ones. We won’t go into the minutia as we feel the conversion is obvious. The completed module is shown in listing 8.24. Please pay attention to the annotations as they provide some additional insight:
Use construct as our method name because create is a root method on the JavaScript Object prototype.
Use destroy as our method name because delete is a reserved word in JavaScript.
/*
* crud.js - module to provide CRUD db capabilities
*/
/*jslint node : true, continue : true, devel : true, indent : 2, maxerr : 50, newcap : true, nomen : true, plusplus : true, regexp : true, sloppy : true, vars : false, white : true
*/
/*global */
// --- BEGIN MODULE SCOPE VARIABLES --- 'use strict';
var
loadSchema, checkSchema, clearIsOnline, checkType, constructObj, readObj, updateObj, destroyObj,
mongodb = require( 'mongodb' ), fsHandle = require( 'fs' ), JSV = require( 'JSV' ).JSV, mongoServer = new mongodb.Server(
'localhost',
mongodb.Connection.DEFAULT_PORT ),
dbHandle = new mongodb.Db(
'spa', mongoServer, { safe : true } ),
validator = JSV.createEnvironment(), objTypeMap = { 'user' : {} };
// --- END MODULE SCOPE VARIABLES --- // --- BEGIN UTILITY METHODS --- loadSchema = function ( schema_name, schema_path ) {
fsHandle.readFile( schema_path, 'utf8', function ( err, data ) { objTypeMap[ schema_name ] = JSON.parse( data );
});
};
checkSchema = function ( obj_type, obj_map, callback ) { var
schema_map = objTypeMap[ obj_type ],
report_map = validator.validate( obj_map, schema_map );
callback( report_map.errors );
};
clearIsOnline = function () { updateObj(
'user',
{ is_online : true }, { is_online : false }, function ( response_map ) {
Listing 8.24 Move logic to our CRUD module—webapp/lib/crud.js
Include libraries required for CRUD per webapp/
lib/routes.js. Create the database connection variables (mongodb and dbHandle) and the JSON schema validator per webapp/lib/
routes.js.
Declare the allowable object types map (objTypeMap) per webapp/lib/routes.js. Add schema loading
and checking utilities per webapp/lib/
routes.js.
Create a clearIsOnline method to be executed once MongoDB is connected.
It ensures all users are marked as offline when the server is started.
295 Create a separate CRUD module
console.log( 'All users set to offline', response_map );
} );
};
// --- END UTILITY METHODS --- // --- BEGIN PUBLIC METHODS --- checkType = function ( obj_type ) {
if ( ! objTypeMap[ obj_type ] ) {
return ({ error_msg : 'Object type "' + obj_type + '" is not supported.'
});
}
return null;
};
constructObj = function ( obj_type, obj_map, callback ) { var type_check_map = checkType( obj_type );
if ( type_check_map ) { callback( type_check_map );
return;
}
checkSchema(
obj_type, obj_map,
function ( error_list ) {
if ( error_list.length === 0 ) { dbHandle.collection(
obj_type,
function ( outer_error, collection ) { var options_map = { safe: true };
collection.insert(
obj_map, options_map,
function ( inner_error, result_map ) { callback( result_map );
} );
} );
} else { callback({
error_msg : 'Input document not valid', error_list : error_list
});
} } );
};
readObj = function ( obj_type, find_map, fields_map, callback ) { var type_check_map = checkType( obj_type );
if ( type_check_map ) { callback( type_check_map );
return;
Create a method to check if an object type, for example, user or horse, is supported by this module. At present user is the only supported object type.
Move logic to create (construct) an object from webapp/
lib/routes.js to this module. Use the same logic but adjust to be more general.
This facilitates calls from the routes module
and other modules. Add logic to ensure the
requested object type is supported. If not, return a JSON error document.
Create the read method per webapp/lib/routes.js. Adjust the logic to be more general.
Ensure the requested object type is supported. If not, return a JSON error document.
}
dbHandle.collection(
obj_type,
function ( outer_error, collection ) {
collection.find( find_map, fields_map ).toArray(
function ( inner_error, map_list ) { callback( map_list );
} );
} );
};
updateObj = function ( obj_type, find_map, set_map, callback ) { var type_check_map = checkType( obj_type );
if ( type_check_map ) { callback( type_check_map );
return;
}
checkSchema(
obj_type, set_map,
function ( error_list ) {
if ( error_list.length === 0 ) { dbHandle.collection(
obj_type,
function ( outer_error, collection ) { collection.update(
find_map,
{ $set : set_map },
{ safe : true, multi : true, upsert : false }, function ( inner_error, update_count ) { callback({ update_count : update_count });
} );
} );
} else { callback({
error_msg : 'Input document not valid', error_list : error_list
});
} } );
};
destroyObj = function ( obj_type, find_map, callback ) { var type_check_map = checkType( obj_type );
if ( type_check_map ) { callback( type_check_map );
return;
}
dbHandle.collection(
Create the update method per webapp/lib/routes.js. Adjust the logic to be more general.
Ensure the requested object type is supported.
If not, return a JSON error document.
Create the delete (destroy) method per webapp/lib/
routes.js. Adjust the logic to be more general.
Ensure the requested object type is supported.
If not, return a JSON error document.
297 Create a separate CRUD module
obj_type,
function ( outer_error, collection ) {
var options_map = { safe: true, single: true };
collection.remove( find_map, options_map, function ( inner_error, delete_count ) { callback({ delete_count: delete_count });
} );
} );
};
module.exports = {
makeMongoId : mongodb.ObjectID, checkType : checkType, construct : constructObj, read : readObj, update : updateObj, destroy : destroyObj };
// --- END PUBLIC METHODS --- // --- BEGIN MODULE INITIALIZATION --- dbHandle.open( function () {
console.log( '** Connected to MongoDB **' );
clearIsOnline();
});
// load schemas into memory (objTypeMap) (function () {
var schema_name, schema_path;
for ( schema_name in objTypeMap ) {
if ( objTypeMap.hasOwnProperty( schema_name ) ) {
schema_path = __dirname + '/' + schema_name + '.json';
loadSchema( schema_name, schema_path );
} } }());
// --- END MODULE INITIALIZATION ---
The routes module now becomes much simpler, as most logic and many dependen- cies have been moved to the CRUD module. A revised routes file should look like list- ing 8.25. Changes are shown in bold:
/*
* routes.js - module to provide routing
*/
/*jslint node : true, continue : true, devel : true, indent : 2, maxerr : 50, newcap : true, nomen : true, plusplus : true, regexp : true, sloppy : true, vars : false, white : true
Listing 8.25 Our modified routes module—webapp/lib/routes.js
Neatly export all our public methods.
Call the clearIsOnline method once MongoDB is connected.
Initialize the in- memory schema storage per webapp/lib/
routes.js.
*/
/*global */
// --- BEGIN MODULE SCOPE VARIABLES --- 'use strict';
var
configRoutes,
crud = require( './crud' ), makeMongoId = crud.makeMongoId;
// --- END MODULE SCOPE VARIABLES --- // --- BEGIN PUBLIC METHODS --- configRoutes = function ( app, server ) {
app.get( '/', function ( request, response ) { response.redirect( '/spa.html' );
});
app.all( '/:obj_type/*?', function ( request, response, next ) { response.contentType( 'json' );
next();
});
app.get( '/:obj_type/list', function ( request, response ) { crud.read(
request.params.obj_type, {}, {},
function ( map_list ) { response.send( map_list ); } );
});
app.post( '/:obj_type/create', function ( request, response ) { crud.construct(
request.params.obj_type, request.body,
function ( result_map ) { response.send( result_map ); } );
});
app.get( '/:obj_type/read/:id', function ( request, response ) { crud.read(
request.params.obj_type,
{ _id: makeMongoId( request.params.id ) }, {},
function ( map_list ) { response.send( map_list ); } );
});
app.post( '/:obj_type/update/:id', function ( request, response ) { crud.update(
request.params.obj_type,
{ _id: makeMongoId( request.params.id ) }, request.body,
function ( result_map ) { response.send( result_map ); } );
});
app.get( '/:obj_type/delete/:id', function ( request, response ) { crud.destroy(
Most variable declarations are moved to the CRUD module and removed here.
Remove the utilities section.
Remove the object- type check here, as this is now handled by the CRUD module. It’s much safer to rely on the CRUD module to do this check.
Use the CRUD module read method to get an object list. The response from the CRUD module can be data or an error. In either case, return the results unaltered.
Use the CRUD module construct method to create a user. Return the results unaltered.
Use the CRUD module read method to get a single object.
Return the results unaltered. Note that this is different than our prior read method, as a successful response returns a single object inside an array.
Use the CRUD module destroy method to remove a single object. Return the results unaltered.
Use the CRUD module update method to update a single object. Return the results unaltered.
299 Build the Chat module
request.params.obj_type,
{ _id: makeMongoId( request.params.id ) },
function ( result_map ) { response.send( result_map ); } );
});
};
module.exports = { configRoutes : configRoutes };
// --- END PUBLIC METHODS ---
Now our routes module is much smaller and uses the CRUD module to service routes.
And, perhaps more importantly, our CRUD module is ready to be used by the chat module we’ll build in the next section.