A REST Service in Node.js with a MariaDB Backend

The code given below is intended to implement a simple demonstration of a client-server application that incorporates a JavaScript client running in a web browser as the user interface, a JavaScript service running via Node.js as the middle tier, and a MariaDB database on the backend.

The client will, once per second, pass in a RESTful URL to the server, which parses the name of a entity type (“MessageBoard”) and the ID of the desired entity (“1”) out of the URL, reads the row corresponding to that entity ID from the database, and returns the result as an XML string.

Other threads running on the client will also, at random intervals, send URLs to the server that will cause values to be WRITTEN to the persistent entity in the database. Thus, when the next read occurs, the value being displayed once per second will change based on whatever was last written to the database.

DrinkPepsiDrinkCoke

1. If you have not already done so, install Node.js, MariaDB, and the nodejs-mariadb binding. Details are given in a previous tutorial.

2. In any convenient location, create a new directory called “RestTest”.

3. In the newly created RestTest directory, create a new text file named “Client.html”, containing the following text.

<html>
<body>

<script type='text/javascript' src="Common.js"></script>
<script type='text/javascript' src="Configuration.js"></script>
<script type='text/javascript' src="Entities.js"></script>

<script type='text/javascript'>

function ClientTest()
{
	this.main = function()
	{
		ClientTest.ServerURL = 
			"http://"
			+ Configuration.ServerHostName
			+ ":"
			+ Configuration.ServerPortNumber;		

		ClientTest.changeMessageTextToDrinkCoke();
		ClientTest.changeMessageTextToDrinkPepsi();

		setInterval(this.displayCurrentMessageText, 1000);
	}

	ClientTest.changeMessageTextToDrinkCoke = function()
	{
		ClientTest.changeMessageTextAndWait
		(
			"Drink Coke!", 
			ClientTest.changeMessageTextToDrinkCoke
		);
	}

	ClientTest.changeMessageTextToDrinkPepsi = function()
	{
		ClientTest.changeMessageTextAndWait
		(
			"Drink Pepsi!", 
			ClientTest.changeMessageTextToDrinkPepsi
		);
	}

	ClientTest.changeMessageTextAndWait = function(messageText, functionToCallAfterWaiting)
	{
		NetworkHelper.httpGet
		(
			ClientTest.ServerURL 
				+ "/MessageBoard/1/?text=" 
				+ encodeURIComponent(messageText)
		);

		var millisecondsUntilNextChange = Math.floor
		(
			3000 + Math.random() * 7000 
		);

		setTimeout
		(
			functionToCallAfterWaiting, 
			millisecondsUntilNextChange
		);
	}

	this.displayCurrentMessageText = function()
	{
		var response = NetworkHelper.httpGet
		(
			ClientTest.ServerURL + "/MessageBoard/1"
		);		

		var responseAsXMLElement = XMLElement.parseFromString
		(
			response
		);

		var responseAsMessageBoard = MessageBoard.fromXMLElement
		(
			responseAsXMLElement
		);		

		document.write(responseAsMessageBoard.text + "<br />");
	}
}

new ClientTest().main();

</script>
</body>
</html>

4. Still in the RestTest directory, create a new text file named “Common.js”, containing the following text.

function Database(hostName, databaseName, username, password)
{
	var MariaSQLClient = require('mariasql');

	this.systemDatabase = new MariaSQLClient();
	this.systemDatabase.connect
	(
		{
	  		host: hostName,
			db: databaseName,
	  		user: username,
	  		password: password
		}
	);

	this.systemDatabase.on
	(
		"connect", 
		function() { }
		).on
	(
		"error", 
		function(err) { console.log('Client error: ' + err); }
	).on
	(
		"close", 
		function(hadError) {  }
	);
}
{	
	Database.prototype.query = function
	(
		queryText, 
		entityToReadInto, 
		objectForEvents,
		eventLoaded,
		eventFinished
	)
	{
		this.systemDatabase.query(queryText).on
		(
			"result", 
			function(result) 
			{
				result.on
				(
					"row",
					function(row)
					{					
						entityToReadInto.readFromDatabase_BuildFromRow.call
						(
							entityToReadInto,
							row,
							objectForEvents,
							eventLoaded
						);
					}
				).on
				(
					"error", 
					function(err) 
					{ 
						console.log("Database error!"); 
					}
				).on
				(
					"end", 
					function(info) 
					{							
						if (eventFinished != null)
						{
							eventFinished.call(objectForEvents);
						}
					}
				);
		 	}
		).on
		(
			'end', 
			function() { }
		);
	}

	Database.prototype.dispose = function()
	{
		this.systemDatabase.end();
	}
}

function HTMLHelper()
{}
{
	HTMLHelper.escape = function(stringToEscape)
	{
		var returnValue = stringToEscape.replace
		(
			"&", "&amp;"
		).replace
		(
			"<", "&lt;"
		).replace
		(
			">", "&gt;"
		);

		return returnValue;
	}

	HTMLHelper.unescape = function(stringToUnescape)
	{
		var returnValue = stringToEscape.replace
		(
			"&amp;", "&"
		).replace
		(
			"&lt;", "<"
		).replace
		(
			"&gt;", ">"
		);

		return returnValue;
	}
}

function NetworkHelper()
{}
{
	NetworkHelper.httpGet = function(urlToGetFrom)
	{
		var request = new XMLHttpRequest();
		request.open("GET", urlToGetFrom, false);
		request.send(null);
		return request.response;
	}

	NetworkHelper.httpPost = function(urlToPostTo, valueToPost)
	{
		var request = new XMLHttpRequest();
		request.open("POST", urlToPostTo, false);
		request.send(valueToPost);
		return request.response;
	}
}

function PersistenceStatus(name)
{
	this.name = name;
}
{
	PersistenceStatus.Instances = new PersistenceStatus_Instances();

	function PersistenceStatus_Instances()
	{
		this.Created = new PersistenceStatus("Created");
		this.Modified = new PersistenceStatus("Modified");
		this.Saved = new PersistenceStatus("Saved");
		this.Unknown = new PersistenceStatus("Unknown");

		this._All = 
		[
			this.Created,
			this.Modified,
			this.Saved,
			this.Unknown,
		];
	}
}

function XMLAttribute(name, value)
{
	this.name = name;
	this.value = value;
}

function XMLElement(parent, tagName, attributes, children)
{
	this.parent = parent;
	this.tagName = tagName;
	this.attributes = attributes;
	this.children = children;
}
{
	XMLElement.parseFromString = function(stringToParse)
	{
		var tagsAsStrings = stringToParse.split("<");

		var elementRoot = new XMLElement
		(
			null, // parent
			null, // tagName
			[], // attributes
			[] // children
		);

		var elementCurrent = elementRoot;

		for (var t = 0; t < tagsAsStrings.length; t++)
		{
			var tagAsString = tagsAsStrings[t];

			if (tagAsString.length == 0)
			{
				// do nothing
			}
			else if (tagAsString.indexOf("/") == 0)
			{
				elementCurrent = elementCurrent.parent;
			}
			else
			{				
				if (tagAsString.lastIndexOf("/>") == tagAsString.length - 2)
				{
					tagAsString = tagAsString.substring(0, tagAsString.length - 2) + " />";
				}
				else if (tagAsString.lastIndexOf(">") == tagAsString.length - 1)
				{
					tagAsString = tagAsString.substring(0, tagAsString.length - 1) + " >";
				}				

				var tokensFromTag = tagAsString.split(" ");

				var tagName = tokensFromTag[0];

				var attributes = [];

				for (var a = 1; a <= tokensFromTag.length - 1; a++)
				{
					var attributeAsString = tokensFromTag[a];

					var indexOfEqualsSign = attributeAsString.indexOf("=");
					if (indexOfEqualsSign >= 0)
					{	
						var attributeName = attributeAsString.substring(0, indexOfEqualsSign);
						var attributeValue = attributeAsString.substring(indexOfEqualsSign + 1);

						if (attributeValue.indexOf("'") == 0)
						{
							while (attributeValue.lastIndexOf("'") != attributeValue.length - 1)
							{
								a++;
								attributeValue += " " + tokensFromTag[a];
							}

							attributeValue = attributeValue.substring
							(
								1, attributeValue.length - 1
							);
						}

						var attribute = new XMLAttribute
						(
							attributeName,
							attributeValue
						);

						attributes.push(attribute);
						attributes[attribute.name] = attribute;
					}
				}

				var elementNew = new XMLElement
				(
					elementCurrent,
					tagName,
					attributes,
					[] // children
				);

				elementCurrent.children.push(elementNew);

				elementCurrent = elementNew;

				if (tagAsString.indexOf("/>") == (tagAsString.length - 2))
				{
					elementCurrent = elementCurrent.parent;
				}
			}
		}

		var returnValue = elementRoot.children[0];

		return returnValue;
	}

	// instance methods

	XMLElement.prototype.toString = function()
	{
		var returnValue = "<" + this.tagName;

		for (var i = 0; i < this.attributes.length; i++)
		{
			var attribute = this.attributes[i];

			returnValue += 
				" " + attribute.name 
				+ "='" + attribute.value + "'"
		}

		if (this.children.length == 0)
		{
			returnValue += "/";	
		}

		returnValue += ">";

		for (var i = 0; i < this.children.length; i++)
		{
			var child = this.children[i];

			returnValue += child.toString();
		}

		return returnValue;
	}
}

5. Still in the RestTest directory, create a new text file named “Entities.js”, containing the following text.

function MessageBoard(id, text)
{
	this.id = id;
	this.text = text;
}
{
	// instance methods

	// database

	MessageBoard.prototype.readFromDatabase = function
	(
		database, 
		id, 
		objectForEventLoaded, 
		eventLoaded
	)
	{
		var queryText = 
			"select * from " 
			+ this.entityName();

		database.query
		(
			queryText,
			this,
			objectForEventLoaded,
			eventLoaded,
			null // eventFinished
		);
	}

	MessageBoard.prototype.readFromDatabase_BuildFromRow = function
	(
		row,
		objectForEventLoaded, 
		eventLoaded
	)
	{
		this.id = row.ID;
		this.text = row.Text;

		eventLoaded.call(objectForEventLoaded, this);
	}

	MessageBoard.prototype.writeToDatabase = function
	(
		database, 
		objectForEvents, 
		eventFinished
	)
	{
		var queryText;

		if (this.persistenceStatus == PersistenceStatus.Instances.Saved)
		{
			// do nothing
		}
		else if (this.persistenceStatus == PersistenceStatus.Instances.Created)
		{
			queryText = 
				"insert into "
				+ this.entityName()
				+ " select " 
				+ this.id + ","
				+ "'" + this.text + "'";			

			database.query
			(
				queryText,
				this,
				objectForEvents,
				null, //eventLoaded
				eventFinished
			);
		}
		else if (this.persistenceStatus == PersistenceStatus.Instances.Modified)
		{
			queryText = 
				"update "
				+ this.entityName()
				+ " set " 
				+ " text = '" + this.text + "'";

			database.query
			(
				queryText,
				this,
				objectForEvents,
				null, // eventLoaded
				eventFinished
			);
		}
	}

	// entity

	MessageBoard.EntityName = "MessageBoard";
	MessageBoard.prototype.entityName = function() { return MessageBoard.EntityName; }

	// XML

	MessageBoard.fromXMLElement = function(xmlElement)
	{
		var returnValue = new MessageBoard
		(			
			xmlElement.attributes["ID"].value,
			xmlElement.attributes["Text"].value
		);

		return returnValue;
	}

	MessageBoard.prototype.toXMLElement = function()
	{
		var returnValue = new XMLElement
		(
			null, // parent
			this.entityName(),
			[ 
				new XMLAttribute("ID", this.id),
				new XMLAttribute("Text", this.text), 
			],
			[] // children
		);

		return returnValue;	
	}
}

6. Still in the RestTest directory, create a new text file named “Configuration.js”, containing the following text.

function Configuration()
{}
{
	Configuration.DatabaseHostName = "localhost";
	Configuration.DatabaseName = "MessageBoards";
	Configuration.DatabaseUsername = "root";
	Configuration.DatabasePassword = "Password42";

	Configuration.ServerHostName = "localhost"
	Configuration.ServerPortNumber = "1337"; 
}

7. Still in the RestTest directory, create a new text file named “DatabasePopulate.bat”, containing the following text.

set mariaDBPath="C:\Program Files\MariaDB 5.5\bin\"
set username=root
set password=Password42
set batchFileToExecutePath=DatabasePopulate.sql

%mariaDBPath%\mysql.exe --user=%username% --password=%password% < %batchFileToExecutePath%
pause

8. Still in the RestTest directory, create a new text file named “DatabasePopulate.sql”, containing the following text.

--drop database MessageBoards;
create database MessageBoards;
use MessageBoards;
create table MessageBoard (ID int primary key, Text varchar(128));
insert into MessageBoard select 1, '[blank]';
exit

9. Still in the RestTest directory, create a new text file named “Server.js”, containing the following text.

var fs = require('fs');

// includes
eval(fs.readFileSync('./Common.js').toString());
eval(fs.readFileSync('./Configuration.js').toString());
eval(fs.readFileSync('./Entities.js').toString());

var http = require('http');

function Server()
{}
{
	Server.prototype.run = function()
	{ 	
		this.entitiesKnown = 
		[
			MessageBoard,
		];

		for (var i = 0; i < this.entitiesKnown.length; i++)
		{
			var entity = this.entitiesKnown[i];
			this.entitiesKnown[entity.EntityName] = entity;
		}

		this.systemServer = http.createServer
		(
			this.handleRequest
		);

		this.systemServer.parent = this;

		this.systemServer.listen
		(	
			Configuration.ServerPortNumber, 
			Configuration.ServerHostName
		);

		this.database = new Database
		(
			Configuration.DatabaseHostName,
			Configuration.DatabaseName,
			Configuration.DatabaseUsername,
			Configuration.DatabasePassword
		);

		console.log
		(
			"Server running at " 
			+ Configuration.ServerHostName
			+ ":"
			+ Configuration.ServerPortNumber
		);
	}

	Server.prototype.handleRequest = function(request, response) 
	{	
		var that = this.parent;
		that.response = response;

		console.log("Server.handleRequest(): url is '" + request.url + "'");

		var urlParts = request.url.split("/");

		if (urlParts.length < 2)
		{
			response.end("ERROR: The URL of the request was not valid.");
		}

		// urlParts[0] is blank
		var entityName = urlParts[1];
		var entityID = urlParts[2];		

		var entityConstructor = that.entitiesKnown[entityName];

		var entity = new entityConstructor();		

		if (urlParts.length < 4)
		{
			entity.readFromDatabase
			(
				that.database, 
				entityID,
				that,
				that.handleRequest_TableRowRead
			);
		}
		else
		{
			var queryString = urlParts[3];

			var queryStringErrorText = 
				"ERROR: The URL's queryString (" 
				+ queryString 
				+ ") is not valid.";

			if (queryString[0] != "?")
			{
				response.end(queryStringErrorText);
			}
			else
			{
				queryString = queryString.substring(1);

				var queryStringVariables = [];
				var queryStringVariablesAsStrings = queryString.split("&");

				for (var v = 0; v < queryStringVariablesAsStrings.length; v++)
				{
					var queryStringVariableAsString = queryStringVariablesAsStrings[v];

					var variableNameAndValue = queryStringVariableAsString.split("=");
					var variableName = decodeURIComponent(variableNameAndValue[0]);
					var variableValue = decodeURIComponent(variableNameAndValue[1]);

					entity[variableName] = variableValue;
				}
			}

			entity.id = entityID;				
			entity.persistenceStatus = PersistenceStatus.Instances.Modified; // hack

			entity.writeToDatabase
			(
				that.database, 
				that,
				that.handleRequest_TableRowWritten
			);
		}
	}

	Server.prototype.handleRequest_TableRowRead = function(entityRead)
	{		
		entityRead.persistenceStatus = PersistenceStatus.Instances.Saved;

		var entityReadAsXmlElement = entityRead.toXMLElement();	

		var responseText = entityReadAsXmlElement.toString();		

		this.response.writeHead
		(
			200, // OK
			{'Content-Type': 'text/plain'}
		);

		this.response.end(responseText);
	}

	Server.prototype.handleRequest_TableRowWritten = function()
	{
		this.response.writeHead
		(
			200, // OK
			{'Content-Type': 'text/plain'}
		);

		this.response.end();
	}

}

new Server().run();

10. Still in the RestTest directory, create a new text file named “ServerRun.bat”, containing the following text.

node Server.js
pause

11. Run DatabasePopulate.bat. The database will be populated with the necessary data.

12. Run ServerRun.bat. The Node.js server will begin running.

13. Open Client.html in a web browser. The client will begin polling the REST service for the current value of the Text column of the only row in the MessageBoard table, and display it in the web browser window. Simultaneously, two separate threads will begin changing that column’s value at random intervals of three to ten seconds. The web browser will thus display a message that changes according to whichever of those threads last ran.

Notes

  • If you’re not accessing this through a web server, you may need to disable some security settings on your browser. For example, I used Chrome browser, which I had to run with the command-line option –disable-web-security.
  • Speaking of disabling security, I haven’t looked at this code in a while, but I suspect it’s probably incredibly vulnerable to SQL injection attacks.  Sorry to set a bad example.  Do as I say, not as I do.
Advertisements
This entry was posted in Uncategorized. Bookmark the permalink.

One Response to A REST Service in Node.js with a MariaDB Backend

  1. Pingback: A Simple Web Server in Node.js with a MariaDB Backend | This Could Be Better

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s