Recommended practice for application exception handling in AngularJS - exception

I am currently exploring possible methods to handle application-wide exceptions in AngularJS.
One of the things we really wanted to avoid was wrapping multiple parts of the application in nested try/catch blocks, but handle things cleanly - i.e throw an exception in response to a promise.
Has anyone covered this issue before and have any recommendations?
Any suggestions on how to pick up exceptions in services as well as controllers/directives. (See below - broadcast works ok, but only if you can attach a listener to a scope).
Progress so far
A few short design goals:
Allow exceptions from one part of the application to be handled elsewhere - or possibly multiple places (i.e. 'display error notification to user', 'disable widget').
Provide central management of common error conditions - i.e. log to server, display notification to user, redirect to login.
Allow exceptions to be thrown from controllers, directives, services etc.
Eventually allow localized messages.
The current leaning of my team is to write a service to handle exceptions, which would expose a range of simple calls:
exceptionService.warn('exception_token');
exceptionService.crit('another_exception_token');
This service would then format an 'exception' object and broadcast this from the rootscope. This would allow a default handler to watch for any broadcasts and apply default actions, as well as allow custom listeners to be set in others scopes, which could handle more specific conditions - i.e. disable a part of the UI.
var exception = {
token: 'exception_token',
severity': 'crit'
};
// broadcast exception
$rootScope.$broadcast(
'application_exception',
exception
);

I was thinking about the same recently, and it occurred to me that when it comes to a good error handling in javascript, it is irrelevant which framework you are using, Angular on something else. I wrote one such error handler recently for an AngularJS project, but I did it in a way it can be used in any framework.
Here's the complete code. You can either use it directly, or modify to your needs...
/*
Factory errorFact is to simplify error handling and reporting in other objects.
It supports detailed error output as a text string and into the browser's console.
Usage example:
A function that supports return of an error object would have the following declaration
as its very first line:
var e = errorFact.create("objectName.funcName", arguments);
- in this declaration we specify the full object + method name as the first string parameter,
- and as the second parameter we pass javascript's reserved variable called arguments, which
provides reference to all of the function's parameters for logging.
When an error occurs, the function would return:
return e.error("Error description text");
- this line will create and return a complete error context.
When a function that supports return of an error object makes a call into another
function that also supports the error context, then it can return the nested error
result by passing the embedded error to the current error object instead of the error
text.
Example:
var e = errorFact.create("objectName.funcName", arguments);
var data = callAnotherFunc(...); // calling a function that support an error object;
if(data.isError){ // If an error was triggered;
return e.error(data); // return that error from the current context;
}
The top-level code that calls an error-returning function would do verification
and if an error occurred, log all its details into console (typically).
Example:
var data = getData(...);
if(data.isError){
data.log(); // Output all the error details into the browser's console;
}
*/
"use strict";
app.factory("errorFact", function(){
return {
// creates a new error context;
create: function(method, args){
var result = {
// initiates and returns the error context;
error: function(msg){
this.info.isError = true;
if(msg.isError){
this.info.details.caller = msg;
}else{
this.info.details.msg = msg;
}
return this.info;
},
info:
{
isError: false,
details: {},
log: function(){
if(this.isError){
console.error(this.format());
}
},
// formats complete error details into a text string;
format: function(){
if(this.details.caller){
var txt = this.details.caller.format();
txt += "\nCALLER: " + this.details.method + "(" + this.formatArguments() + ")";
return txt;
}
if(this.details.method){
return "Error calling " + this.details.method + "(" + this.formatArguments() + "): " + this.details.msg;
}else{
return this.details.msg;
}
return "";
},
// formats function argument details into a text string;
formatArguments: function(){
if(!this.details.args){
return "";
}
var params = "";
for(var i = 0;i < this.details.args.length;i ++){
if(params.length > 0){
params += ",";
}
var p = this.details.args[i];
if(p === undefined){
params += "undefined";
}else{
if(p === null){
params += "null";
}else{
if(typeof(p) == "object"){
params += "Object";
}else{
params += p;
}
}
}
}
return params;
}
}
};
if(method){
result.info.details.method = method;
}
if(args){
result.info.details.args = args;
}
return result;
}
}
});
Below is a factory that shows how it is used:
"use strict";
app.factory('moduleFact', ['errorFact', function(errorFact){
return {
// Locates existing module and expands its key Id references
// into corresponding object references:
// - If 'hintGroupId' is present, property 'hints' is added from
// the corresponding hint group.
// - If 'repModules' is present, properties 'question' and 'refs'
// are added.
// On success, return the expanded module object.
// On failure, returns an error object.
//
// NOTE: Currently supports only the first value in repModules.
expandModule: function(moduleData, moduleId){
var e = errorFact.create("moduleFact.expandModule", arguments);
if(!moduleData || !moduleData.modules || !moduleId){
return e.error("Invalid parameters passed");
}
var mod = this.findModule(moduleData, moduleId);
if(mod.isError){
return e.error(mod);
}
var src = mod;
if(mod.repModules){
var repId = mod.repModules[0];
if(!repId){
return e.error("Invalid repModules encountered");
}
///////////////////////////////////////
// temporary check to throw a warning:
if(mod.repModules.length > 1){
console.warn("Multiple values in property repModules: " + JSON.stringify(mod.repModules) +
", which is not supported yet (only the first value is used)");
}
///////////////////////////////////////
src = this.findModule(moduleData, repId);
if(src.isError){
return e.error(src);
}
}
if(src.question){
mod.question = src.question;
}else{
return e.error("Question not specified");
}
if(src.refs){
mod.refs = src.refs;
}
if(src.hintGroupId){
var hg = this.findHintGroup(moduleData, src.hintGroupId);
if(hg.isError){
return e.error(hg);
}
mod.hints = hg.hints;
}
return mod; // needed extra: expand attribute repModules
},
// Expands all the modules and returns the data;
expandAllModules: function(moduleData){
var e = errorFact.create("moduleFact.expandAllModules", arguments);
if(!moduleData || !moduleData.modules){
return e.error("Invalid parameters passed");
}
for(var i = 0;i < moduleData.modules.length;i ++){
var result = this.expandModule(moduleData, moduleData.modules[i].id);
if(result.isError){
return e.error(result);
}
}
return moduleData;
},
// Locates and returns module by its Id;
findModule: function(moduleData, moduleId){
var e = errorFact.create("moduleFact.findModule", arguments);
if(!moduleData || !moduleData.modules || !moduleId){
return e.error("Invalid parameters passed");
}
for(var i = 0;i < moduleData.modules.length;i ++){
if(moduleData.modules[i].id == moduleId){
return moduleData.modules[i];
}
}
return e.error("Module with Id = " + moduleId + " not found");
},
// Locates and returns Hint Group by its Id;
findHintGroup: function(moduleData, hintGroupId){
var e = errorFact.create("moduleFact.findHintGroup", arguments);
if(!moduleData || !moduleData.hintGroups || !hintGroupId){
return e.error("Invalid parameters passed");
}
for(var i = 0;i < moduleData.hintGroups.length;i ++){
if(moduleData.hintGroups[i].id == hintGroupId){
return moduleData.hintGroups[i];
}
}
return e.error("Hint Group with Id = " + hintGroupId + " not found");
}
}
}]);
So, when you have such factory in place, your high-level code, such as in a controller would just log any issues as shown in the example below:
"use strict";
app.controller('standardsCtrl', ['$scope', 'moduleFact', function($scope, moduleFact){
var data = ...//getting data;
var mod = moduleFact.expandAllModules(data);
if(mod.isError){
mod.log(); // log all error details into the console;
}else{
// use the data
}
});
}]);

You can override the $exceptionHandler in order to pass the exceptions to your own central service for exceptions, but the $exceptionHandler seems to only receive the exceptions thrown from your controllers, directives, etc... but not for the exceptions originated from ajax calls. For those exceptions you can implement an interceptor like the one described in this page:
EDITED: Link is dead permanently.
Archive.org link

whats your opinion to create a centralized error handling function for your app
so whenever an error happened with your frontend tear (angular, API calls,...) it executed, so no need to write your error handling every time
so here is my code
(function () {
'use strict';
angular
.module('app')
.factory('$exceptionHandler', ExceptionHandler);
ExceptionHandler.$inject = ['$injector']; //for minification
function ExceptionHandler($injector) {
var $log, sweetAlert, $translate;
return function exceptionHandler(exception, cause) {
// Add DI here to prevent circular dependency
$log = $log || $injector.get('$log');
sweetAlert = sweetAlert || $injector.get('sweetAlert'); //19degrees.ngSweetAlert2
$translate = $translate || $injector.get('$translate');
// $loggerService = $loggerService || $injector.get('$loggerService');
var title, message;
title = $translate.instant('General error title');
message = $translate.instant('General error message', { exceptionMessage: exception.message });
sweetAlert.error(title, message);
$log.error(exception, cause);
// loggerService.logErrorsToBackend(exception, cause);
};
}
})();
I'm not sure if this approach considered to be a best practice but hope it helps you.

Related

Error: "Reference does not exist" when using a custom function

I'm trying to scrape a webpage & to put the value in cache in order to not hit the daily urlFetch limit.
This is the code I'm using, it works without the Cache & Properties service but not when I try to add that element.
function scrapercache(url) {
var url = "https://www.gurufocus.com/term/fscore/nyse:ABBV/Piotroski-F-Score";
var result = [];
var description;
var options = {
'muteHttpExceptions': true,
'followRedirects': false,
};
Logger.log('line 16 OK');
var cache = CacheService.getScriptCache();
var properties = PropertiesService.getScriptProperties();
Logger.log('line 21 OK');
let res = cache.get(url);
// if(res){
// return JSON.parse(res)
//}
Logger.log(res);
Logger.log('line 24 OK');
if (res) {
// trim url to prevent (rare) errors
url.toString().trim();
var r = UrlFetchApp.fetch(url, options);
Logger.log(r);
Logger.log('line 34 OK');
var c = r.getResponseCode();
Logger.log(c);
Logger.log('line 38 OK');
// check for meta refresh if 200 ok
if (c == 200) {
var html = r.getContentText();
cache.put(url, "cached", 21600);
properties.setProperty(url, html);
Logger.log('line 46 OK');
var $ = Cheerio.load(html); // make sure this lib is added to your project!
Logger.log('line 49 OK');
// meta description
if ($('meta[name=description]').attr("content")) {
description = $('meta[name=description]').attr("content").trim();
var trim_des = description.substr(0, 40);
Logger.log('line 55 OK');
}
}
result.push([trim_des]);
Logger.log('line 60 OK');
}
return result;
Logger.log('line 64 OK');
}
I call the function like that:
=scrapercache("https://www.gurufocus.com/term/fscore/nyse:ABBV/Piotroski-F-Score")
& I get the following error message
Error: Reference does not exist
EDIT: I added log lines to see if the script was processing correctly & it looks like it's ok only until like 28
You're not putting the results in the cache, you're putting the string "cached" there. Consider:
let cached = cache.get(url)
if(cached){
return JSON.parse(cached)
}
let results = ["blah","blah"] // stuff we got with cheerio
cache.put(url, JSON.stringify(results), 120)
return results
Error: “Reference does not exist”
This error message is usually returned by calling a custom function in a spreadsheet cell that does not return a value. It is explicitly mentioned by the official docs, but the error message is not provided, so the confusion is understandable.
An empty array is not a valid return value (since there are no elements to return). The error is easily reproducible with the following custom function:
/**
* #customfunction
*/
function testReferenceError() {
const list = [];
return list;
}
Which, when called in a cell, resulting in the "desired" error:
Applied to your case
In your situation, when there is a cached data in cache, the if statement clause evaluates to false (truthy value when negated evaluates to false). When it does, nothing gets pushed to the result, and an empty array is returned in finally (see above for the explanation of consequences). Consider this mock:
const cache = {
get() {
return "cached";
}
};
let res = cache.get("mock_url");
//ternary operator here acts the same as "if...else":
console.log( !res ? "will enter if block" : "will enter else block" );
Note on return in finally: If you put a return statement into a finally block, expect it to override the return statements in try or catch. Consider this example close to how your program is structured:
const final = (add = false) => {
const list = [];
try {
add && list.push(1);
return [1,2]; //this return is skipped
}
catch(error) {
list.push(error);
}
finally {
return list;
}
};
console.log( final() );
Also, the question already has an answer here

JSON array undefined & empty in promise Bluebird

I am using Promise bluebird to process a json array objects from file. The problem arises if I want to store data in a json array (called list) and return this in the final process.
The list is empty/undefined after the return of list or even in the final process. Running the code, I always have 1 value that is not false which trigger the adding/push of the json in the list.
Can you help me with this issue? Below you will find my code.
Thanks in advance !!!
var Promise = require('bluebird');
var join = Promise.join;
var fs = Promise.promisifyAll(require("fs"));
fs.readdirAsync(dir).map(function (filename) {
return fs.readFileAsync(dir + "/" + filename, "utf8");
}).then(function(result){
var list=[];
result.map(function(row, index){
Promise.coroutine(function*() {
update(row, index).then(function(value){
if (value!=false){
var trade_update = new updated_Item(row.ID, row.Quantity, row.Price, row.Remark);
list.push(trade_update);
console.log(JSON.stringify(list)); <-- This works. It gives me data
}
return list;
})
})();
});
console.log('list: ' + JSON.stringify(list)); <-- output: list:[]
return list;
}).finally(function(result){
console.log('Final outcome: '+ ' ' + JSON.stringify(result)); <-- output: Final outcome: undefined
})
With the help of Samuel my code is now:
var Promise = require('bluebird');
var join = Promise.join;
var fs = Promise.promisifyAll(require("fs"));
function updateOrder(done){
fs.readdirAsync(dir).map(function (filename) {
return fs.readFileAsync(dir + "/" + filename, "utf8");
}).then(function(result){
var list=[];
result.map(function(row, index){
Promise.coroutine(function*() {
update(row, index).then(function(value){
if (value!=false){
var trade_update = new updated_Item(row.ID, row.Quantity, row.Price, row.Remark);
list.push(trade_update);
done(list);
}
})
})();
});
//done(list); <--if I put the done callback here, it will give me an empty list. I though once the result.map finished processing all the values give me the end result.
}
}
updateOrder(function(resultList){
console.log('List' + JSON.stringify(resultList));
})
This code give me whole resultList everytime the list has been updated (pushed) now.
I would to receive the resultList at the end once the function updateOrder is finished.
As noted in the comment. Promise.coroutine is asynchronous so this means that a result is not going to get return straight after your code reaches it. And this pretty much explains the phenomenon you are seeing where the latter print statements you got in the code is suggesting that list is undefined.
What you could do is wrap the entire code you got there in a function, then add a callback function as a parameter for the async functions to invoke when it has finished its duty, together returning the populated list back for later processing.
I have written a pseudo code for your case, unfortunately I couldn't test it on my IDE but the concept is there and it should work.
Consider my pseudo code:
var Promise = require('bluebird');
var join = Promise.join;
var fs = Promise.promisifyAll(require("fs"));
// Wrap everything you got into a function with a `done` parameter (callback fn)
function doStuff(done) {
fs.readdirAsync(dir).map(function (filename) {
return fs.readFileAsync(dir + "/" + filename, "utf8");
}).then(function(result){
var list=[];
result.map(function(row, index){
Promise.coroutine(function*() {
update(row, index).then(function(value){
if (value!=false){
var trade_update = new updated_Item(row.ID, row.Quantity, row.Price, row.Remark);
list.push(trade_update);
}
done(list);
})
})();
});
}).finally(function(result){
console.log('File read finish, but this doesnt mean I have finished doing everything!');
})
}
// call your function and provide a callback function for the async method to call
doStuff(function(resultList) {
console.log('list: ' + JSON.stringify(resultList));
// Continue processing the list data.
});

Angular JS correct factory structure

I have a factory in my AngularJS single page application that parses a given date against a JSON file to return season and week-number in season. I am currently calling the JSON file twice in each method $http.get('content/calendar.json').success(function(data) {....
How can i factor out the call to do it once regardless of how many methods?
emmanuel.factory('DayService', function($http, $q){
var obj = {};
obj.season = function(d){
// receives a mm/dd/yyyy string parses against Calendar service for liturgical season
d = new Date(d);
var day = d.getTime();
var promise = $q.defer();
var temp;
$http.get('content/calendar.json').success(function(data) {
for (var i=0; i<data.calendar.seasons.season.length; i++){
var start = new Date(data.calendar.seasons.season[i].start);
var end = new Date(data.calendar.seasons.season[i].end);
end.setHours(23,59);
//sets the time to be the last minute of the season
if (day >= start && day <= end){
//if given time fits within start and end dates in calendar then return season
temp = data.calendar.seasons.season[i].name;
//console.log(temp);
promise.resolve(temp);
break;
}
}
});
return promise.promise;
}
obj.weekInSeason = function(d){
//receives a date format mm/dd/yyyy
var promise = $q.defer();
$http.get('content/calendar.json').success(function(data) {
for (var i=0; i<data.calendar.seasons.season.length; i++){
d = new Date(d);
var day = d.getTime();
var end = new Date(data.calendar.seasons.season[i].end);
end.setHours(23,59);
end = end.getTime();
var diff = end - day;
if (parseFloat(diff) > 0){
var start = new Date(data.calendar.seasons.season[i].start);
start = start.getTime();
var startDiff = day - start;
var week = parseInt(startDiff /(1000*60*60*24*7))+1;
promise.resolve(week);
break;
}
}
});
return promise.promise;
}
obj.getData = function (d) {
console.log('DayService.getData')
console.log(today)
var data = $q.all([
this.season(d),
this.weekInSeason(d)
]);
return data;
};
return obj;
});
This solution assumes that content/calendar.json never changes.
I have answered a question which can help you in this problem one way or another. Basically you must fetch all necessary configurations/settings before the application bootstraps. Manually bootstrap the application, this means that you must remove the ng-app directive in your html.
Steps:
[1] Create bootstrapper.js as instructed in the answered question I have mentioned above. Basically, it should look like this(Note: You can add more configuration urls in urlMap, if you need to add more settings in your application before it bootstraps):
angular.injector(['ng']).invoke(function($http, $q) {
var urlMap = {
$calendar: 'content/calendar.json'
};
var settings = {};
var promises = [];
var appConfig = angular.module('app.settings', []);
angular.forEach(urlMap, function(url, key) {
promises.push($http.get(url).success(function(data) {
settings[key] = data;
}));
});
$q.all(promises).then(function() {
bootstrap(settings);
}).catch(function() {
bootstrap();
});
function bootstrap(settings) {
appConfig.value('Settings', settings);
angular.element(document).ready(function() {
angular.bootstrap(document, ['app', 'app.settings']);
});
}
});
[2] Assuming that the name of your main module is app within app.js:
angular.module('app', [])
.factory('DayService', function(Settings){
var calendar = Settings.$calendar,
season = calendar.seasons.season,
obj = {};
obj.season = function(d){
var day = new Date(d).getTime(),
start, end, value;
for (var i = 0; i < season.length; i++){
start = new Date(season[i].start);
end = new Date(season[i].end);
end.setHours(23,59);
if (day >= start && day <= end){
value = season[i].name;
break;
}
}
return value;
};
obj.weekInSeason = function(d){
var day = new Date(d).getTime(),
end, diff, start, startDiff, week;
for (var i = 0; i < season.length; i++){
end = new Date(season[i].end);
end.setHours(23,59);
end = end.getTime();
diff = end - day;
if (parseFloat(diff) > 0){
start = new Date(season[i].start);
start = start.getTime();
startDiff = day - start;
week = parseInt(startDiff /(1000*60*60*24*7))+1;
break;
}
}
return week;
};
return obj;
});
[3] Controller Usage(Example):
angular.module('app')
.controller('SampleController', function(DayService) {
console.log(DayService.season(3));
console.log(DayService.weekInSeason(3));
});
Another Note: Use .run() to check if Settings === null - if this is true, you can direct to an error page or any page that displays the problem(This means that the application bootstrapped but one of the requested configuration failed).
UPDATE:
I checked the link you have provided, and it seems that the version you are using is AngularJS v1.0.8, which does not have a .catch() method in their $q promise implementation.
You have the following options to consider in solving this problem:
-1 Change the AngularJS version you are using to the latest stable version 1.2.23.
Note that this option may break some of your code that is highly reliant on the version that you are using.
-2 Change this block:
$q.all(promises).then(function() {
bootstrap(settings);
}).catch(function() {
bootstrap();
});
to:
$q.all(promises).then(function() {
bootstrap(settings);
}, function() {
bootstrap();
});
This option is safer if you already have existing code that relies on the current AngularJS version you are using But I would suggest you change to the current stable version as it has more facilities and fixes than the one you are currently using.
Use your scope and closure of the factory to store the value of the response to the http call. I created an object called calData, which happens to already be a promise!
This gives you the ability to kick a few things off when the first call to the factory is made by running an IIFE (this is the function called initService), and everything chains together to resolve after the data is loaded.
.factory('dayService', function dayServiceFactory($http, $q){
var getCalData = $q.defer();
var calData = gettingData.promise; // null/undefined until _loadData is called and resolved
function _loadData(){
var deferred = $q.defer();
$http.get('content/calendar.json').success(function(data) {
calData.seasons = data.calendar.seasons; // your code seems to always use at least calendar.seasons, so easier to assign that
deferred.resolve(data);
});
return deferred.promise;
}
// this function will automatically run and load data the first time the factory is executed
(function initService(){
_loadData().then(){
// here is where you will build all your functions to assign properties to calData.seasons or any other child property of calData;
calData.getSeason = function(){
for (var i=0; i<data.calendar.seasons.season.length; i++){
// code here
}
}// function to get day using calData.seasons
calData.weekInSeason = function(){}
getCalData.resolve(); // this resolves the data in the outer scope
}
}());
return calData; // returns the promise, and will execute the first time called
});
To use this in a controller, make sure to either resolve the service before you instantiate the controller, or withing the controller, use your assignments of the data after it has resolved. (Bound values will auto-update when it's resolved)
dayService.then(function(){
// now you can use this:
var week = dayService.weekInSeason();
})
You can create separate method for getting calendar data and chain promises in getData method:
emmanuel.factory('DayService', ['$q', '$timeout', '$log',
function($q, $timeout, $log) {
return {
season: season,
weekInSeason: weekInSeason,
getData: getData
};
function season(d) {
$log.log('season called');
return getCalendar(d).then(function(calendar) {
return getSeason(d, calendar);
});
}
function weekInSeason(d) {
$log.log('weekInSeason called');
return getCalendar(d).then(function(calendar) {
return getWeekInSeason(d, calendar);
});
}
function getData(d) {
$log.log('getData called');
return getCalendar(d).then(
function(calendar) {
return $q.all({
season: getSeason(d, calendar),
weekInSeason: getWeekInSeason(d, calendar)
});
}
);
}
function getSeason(date, calendar) {
$log.log('getSeason called');
return {
date: date,
calendar: calendar,
method: 'getSeason'
};
}
function getWeekInSeason(date, calendar) {
$log.log('getWeekInSeason called');
return {
date: date,
calendar: calendar,
method: 'getWeekInSeason'
};
}
function getCalendar(d) {
$log.log('getCalendar called');
var deferred = $q.defer();
$timeout(function() {
deferred.resolve(12345);
}, 2000);
return deferred.promise;
}
}
]);
Also, if calendar.json doesn't changed during application lifetime, you can cache calendar.json ajax request result as suggested by #runTarm
Plunker

NodeJS MySQL Dump

I've attempted to write a basic cron script to run and 'dump' a mysql database. For some reason, when it 'successfully saves the file', it does create the file, but it is empty. If instead of saving the file, I perform a console.log, it prints an empty string. Any thoughts on what I may be doing wrong?
Thanks in advance.
var mysql_backup = function(){
this.backup = '';
this.mysql = require('mysql'),
this.init = function(){
this.connection = this.mysql.createConnection({
user: 'root',
password: 'root',
database: 'test'
});
}
this.query = function(sql, callback) {
this.connection.query(sql, function (error, results, fields) {
if (error) {
throw error;
}
if (results.length > 0) {
callback(results);
}
});
}
this.get_tables = function(callback){
var me = this;
me.query('SHOW TABLES',
function(tables) {
for (var table in tables){
me.query(
'SHOW CREATE TABLE ' + tables[table].Tables_in_test,
function(r){
for (var t in r) {
me.backup += "DROP TABLE " + r[t].Table + "\n\n";
me.backup += r[t]["Create Table"] + "\n\n";
}
}
)
}
me.save_backup();
});
}
this.save_backup = function(){
var fs = require('fs');
fs.writeFile("./backup_test.txt", this.backup, function(err) {
if(err) {
console.log(err);
} else {
console.log("The file was saved!");
}
});
}
};
var db = new mysql_backup;
db.init();
db.get_tables();
db.connection.destroy();
The code as written didn't even get to a file saving for me. There seem like a few issues. Not sure if this is the actual code or some things got lost in the copy paste. However, based on what you've got:
A big one is that you never connect to the database in your code with connection.connect().
The code you want to run once connected should be inside the connection.connect() callback. e.g.
connection.connect(function (err, empty) {
if (err)
throw new Error ('Panic');
// if no error, we are off to the races...
}
However, even if you quickly refactor your code to wrap your last lines inside of that get connection callback, you'll still have problems, because you are destroying the connection before the various SQL calls are getting made, so you will want to move the code into some sort of final callback.
Even after you do that, you'll still have an empty file, because you're calling save_backup from your 'SHOW TABLES' callback rather than after you have actually populated it via the inner callback where you get the CREATE TABLE statement and populate the backup property.
This is the minimal rewriting of your code which will do what you are intending. An important thing to note is the "counter" which manages when to write the file and close the connection. I would make other changes if it were mine, including:
Using 'self' instead of 'me'
Using a numeric for loop rather than the for (... in ...) syntax
Having my own callbacks fall the node convention of (err, stuff)
A more substantial changes is that I would rewrite this to use promises, as doing so can spare you some grief with the confusion inherent with deeply nested callbacks. I personally like the Q library, but there are several options here.
Hope this helped.
var mysql_backup = function(){
this.backup = '';
this.mysql = require('mysql');
this.init = function(){
this.connection = this.mysql.createConnection({
user : 'root',
password : 'root',
database : 'test'
});
};
this.query = function(sql, callback) {
this.connection.query(sql, function (error, results, fields) {
if (error) {
throw error;
}
if (results.length > 0) {
callback(results);
}
});
};
this.get_tables = function(callback){
var counter = 0;
var me = this;
this.query('SHOW TABLES',
function(tables) {
for (table in tables){
counter++;
me.query(
'SHOW CREATE TABLE ' + tables[table].Tables_in_mvc,
function(r){
for (t in r) {
me.backup += "DROP TABLE " + r[t].Table + "\n\n";
me.backup += r[t]["Create Table"] + "\n\n";
}
counter--;
if (counter === 0){
me.save_backup();
me.connection.destroy();
}
}
)
}
});
};
this.save_backup = function(){
var fs = require('fs');
fs.writeFile("./backup_test.txt", this.backup, function(err) {
if(err) {
console.log(err);
} else {
console.log("The file was saved!");
}
});
}
};
var db = new mysql_backup;
db.init();
db.connection.connect(function (err){
if (err) console.log(err);
db.get_tables(function(x){;});
});
Update: If you are curious, here is a heavily-commented implementation using promises. Note that without the comments explaining the Q promise library functions, it is somewhat shorter than the original version and also offers more comprehensive error handling.
var MysqlBackup = function(connectionInfo, filename){
var Q = require('q');
var self = this;
this.backup = '';
// my personal preference is to simply require() inline if I am only
// going to use something a single time. I am certain some will find
// this a terrible practice
this.connection = require('mysql').createConnection(connectionInfo);
function getTables(){
// return a promise from invoking the node-style 'query' method
// of self.connection with parameter 'SHOW TABLES'.
return Q.ninvoke(self.connection,'query', 'SHOW TABLES');
};
function doTableEntries(theResults){
// note that because promises only pass a single parameter around,
// if the 'denodeify-ed' callback has more than two parameters (the
// first being the err param), the parameters will be stuffed into
// an array. In this case, the content of the 'fields' param of the
// mysql callback is in theResults[1]
var tables = theResults[0];
// create an array of promises resulting from another Q.ninvoke()
// query call, chained to .then(). Note that then() expects a function,
// so recordEntry() in fact builds and returns a new one-off function
// for actually recording the entry (see recordEntry() impl. below)
var tableDefinitionGetters = [];
for (var i = 0; i < tables.length ; i++){
// I noticed in your original code that your Tables_in_[] did not
// match your connection details ('mvc' vs 'test'), but the below
// should work and is a more generalized solution
var tableName = tables[i]['Tables_in_'+connectionInfo.database];
tableDefinitionGetters.push(Q.ninvoke(self.connection, 'query', 'SHOW CREATE TABLE ' + tableName)
.then(recordEntry(tableName)) );
}
// now that you have an array of promises, you can use Q.allSettled
// to return a promise which will be settled (resolved or rejected)
// when all of the promises in the array are settled. Q.all is similar,
// but its promise will be rejected (immediately) if any promise in the
// array is rejected. I tend to use allSettled() in most cases.
return Q.allSettled(tableDefinitionGetters);
};
function recordEntry (tableName){
return function(createTableQryResult){
self.backup += "DROP TABLE " + tableName + "\n\n";
self.backup += createTableQryResult[0][0]["Create Table"] + "\n\n";
};
};
function saveFile(){
// Q.denodeify return a promise-enabled version of a node-style function
// the below is probably excessively terse with its immediate invocation
return (Q.denodeify(require('fs').writeFile))(filename, self.backup);
}
// with the above all done, now you can actually make the magic happen,
// starting with the promise-return Q.ninvoke to connect to the DB
// note that the successive .then()s will be executed iff (if and only
// if) the preceding item resolves successfully, .catch() will get
// executed in the event of any upstream error, and finally() will
// get executed no matter what.
Q.ninvoke(this.connection, 'connect')
.then(getTables)
.then(doTableEntries)
.then(saveFile)
.then( function() {console.log('Success'); } )
.catch( function(err) {console.log('Something went awry', err); } )
.finally( function() {self.connection.destroy(); } );
};
var myConnection = {
host : '127.0.0.1',
user : 'root',
password : 'root',
database : 'test'
};
// I have left this as constructor-based calling approach, but the
// constructor just does it all so I just ignore the return value
new MysqlBackup(myConnection,'./backup_test.txt');

Parallel form submit and ajax call

I have a web page that invokes long request on the server. The request generates an excel file and stream it back to the client when it is ready.
The request is invoked by creating form element using jQuery and invoking the submit method.
I would like during the request is being processed to display the user with progress of the task.
I thought to do it using jQuery ajax call to service I have on the server that returns status messages.
My problem is that when I am calling this service (using $.ajax) The callback is being called only when the request intiated by the form submit ended.
Any suggestions ?
The code:
<script>
function dummyFunction(){
var notificationContextId = "someid";
var url = $fdbUI.config.baseUrl() + "/Promis/GenerateExcel.aspx";
var $form = $('<form action="' + url + '" method="POST" target="_blank"></form>');
var $hidden = $("<input type='hidden' name='viewModel'/>");
$hidden.val(self.toJSON());
$hidden.appendTo($form);
var $contextId = new $("<input type='hidden' name='notifyContextId'/>").val(notificationContextId);
$contextId.appendTo($form);
$('body').append($form);
self.progressMessages([]);
$fdbUI.notificationHelper.getNotifications(notificationContextId, function (message) {
var messageText = '';
if (message.IsEnded) {
messageText = "Excel is ready to download";
} else if (message.IsError) {
messageText = "An error occured while preparing excel file. Please try again...";
} else {
messageText = message.NotifyData;
}
self.progressMessages.push(messageText);
});
$form.submit();
}
<script>
The code is using utility library that invokes the $.ajax. Its code is:
(function () {
if (!window.flowdbUI) {
throw ("missing reference to flowdb.ui.core.");
}
function NotificationHelper() {
var self = this;
this.intervalId = null;
this.getNotifications = function (contextId, fnCallback) {
if ($.isFunction(fnCallback) == false)
return;
self.intervalId = setInterval(function() {
self._startNotificationPolling(contextId, fnCallback);
}, 500);
};
this._startNotificationPolling = function (contextId, fnCallback) {
if (self._processing)
return;
self._processing = true;
self._notificationPolling(contextId, function (result) {
if (result.success) {
var message = result.retVal;
if (message == null)
return;
if (message.IsEnded || message.IsError) {
clearInterval(self.intervalId);
}
fnCallback(message);
} else {
clearInterval(self.intervalId);
fnCallback({NotifyData:null, IsEnded:false, IsError:true});
}
self._processing = false;
});
};
this._notificationPolling = function (contextId, fnCallback) {
$fdbUI.core.executeAjax("NotificationProvider", { id: contextId }, function(result) {
fnCallback(result);
});
};
return this;
}
window.flowdbUI.notificationHelper = new NotificationHelper();
})();
By default, ASP.NET will only allow a single concurrent request per session, to avoid race conditions. So the server is not responding to your status requests until after the long-polling request is complete.
One possible approach would be to make your form post return immediately, and when the status request shows completion, start up a new request to get the data that it knows is waiting for it on the server.
Or you could try changing the EnableSessionState settings to allow multiple concurrent requests, as described here.