Comparing version 0.1.0-beta-3 to 0.1.0-beta-30
@@ -10,3 +10,2 @@ "use strict"; | ||
ENV_END_PATTERN = new RegExp('.*\\.json$'), | ||
DEFAULT_SERVER_PORT = 8080, | ||
Bootstrap; | ||
@@ -16,3 +15,8 @@ | ||
Bootstrap = Type.create({ | ||
initalized: Type.BOOLEAN | ||
initalized: Type.BOOLEAN, | ||
listenPort: Type.NUMBER, | ||
listenHost: Type.STRING, | ||
controllersPath: Type.STRING, | ||
modulesPath: Type.STRING, | ||
modelsPath: Type.STRING | ||
}, { | ||
@@ -31,2 +35,7 @@ /** | ||
this.initalized = false; | ||
this.listenPort = 8080; | ||
this.listenHost = "127.0.0.1"; | ||
this.controllersPath = '@{appPath}/controllers/'; | ||
this.modulesPath = '@{appPath}/modules/'; | ||
this.modelsPath = '@{appPath}/models/'; | ||
}, | ||
@@ -82,2 +91,9 @@ /** | ||
} | ||
if (Type.isNumber(env.port)) { | ||
this.setListenPort(env.port); | ||
} | ||
if (Type.isString(env.host)) { | ||
this.setListenHost(env.host); | ||
} | ||
// set aliases | ||
@@ -93,8 +109,20 @@ if (Type.isArray(env.aliases)) { | ||
if (!di.hasAlias('controllersPath')) { | ||
di.setAlias('controllersPath', '@{appPath}/controllers/'); | ||
di.setAlias('controllersPath', this.controllersPath); | ||
} else { | ||
this.controllersPath = di.getAlias('controllersPath'); | ||
} | ||
// if there is no modules path | ||
if (!di.hasAlias('modulesPath')) { | ||
di.setAlias('modulesPath', this.modulesPath); | ||
} else { | ||
this.modulesPath = di.getAlias('modulesPath'); | ||
} | ||
// if there is no models path | ||
if (!di.hasAlias('modelsPath')) { | ||
di.setAlias('modelsPath', '@{appPath}/models/'); | ||
di.setAlias('modelsPath', this.modelsPath); | ||
} else { | ||
this.modelsPath = di.getAlias('modelsPath'); | ||
} | ||
// assets path | ||
@@ -127,6 +155,3 @@ if (Type.isString(env.assetsPath)) { | ||
} | ||
// set favicon path | ||
if (!component.has('core/favicon')) { | ||
component.set('core/favicon', {}); | ||
} | ||
// set view component | ||
@@ -139,3 +164,3 @@ if (!component.has('core/view')) { | ||
try { | ||
di.load(envPath + '/' + env.config)(component, di); | ||
di.load(envPath + '/' + env.config)(component, di, this); | ||
} catch (e) { | ||
@@ -159,6 +184,13 @@ throw new error.Exception('Initialize config: ' + envPath + '/' + env.config, e); | ||
logger.print('Create new request', request.url); | ||
// set paths on each request | ||
di.setAlias('controllersPath', this.controllersPath); | ||
di.setAlias('modulesPath', this.modulesPath); | ||
di.setAlias('modelsPath', this.modelsPath); | ||
// new request | ||
var nRequest = new Request({ | ||
request: request, | ||
response: response | ||
response: response, | ||
encoding: server.getEncoding() | ||
}, request.url); | ||
@@ -168,6 +200,5 @@ /// parse request | ||
// on end destory | ||
request.on('end', function () { | ||
nRequest.destroy(); | ||
}); | ||
}); | ||
nRequest.onEnd(nRequest.destroy.bind(nRequest)); | ||
}.bind(this)); | ||
// server close event | ||
@@ -178,9 +209,4 @@ server.on('close', function () { | ||
}); | ||
// this must be last !! in config order | ||
if (Type.isNumber(env.port)) { | ||
server.listen(env.port); | ||
} else { | ||
server.listen(DEFAULT_SERVER_PORT); | ||
} | ||
// this must be last ! | ||
server.listen(this.listenPort, this.listenHost); | ||
// logger | ||
@@ -194,2 +220,30 @@ logger.print(env); | ||
* @author Igor Ivanovic | ||
* @method Bootstrap#setListenHost | ||
* | ||
* @description | ||
* Set listen host | ||
*/ | ||
setListenHost: function Bootstrap_setListenHost(host) { | ||
if (Type.isString(host)) { | ||
this.listenHost = host; | ||
} | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Bootstrap#setListenPort | ||
* | ||
* @description | ||
* Set listen port | ||
*/ | ||
setListenPort: function Bootstrap_setListenPort(port) { | ||
if (Type.isNumber(port)) { | ||
this.listenPort = port; | ||
} else { | ||
this.listenPort = parseInt(port); | ||
} | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Bootstrap#setBasePath | ||
@@ -196,0 +250,0 @@ * |
@@ -132,3 +132,64 @@ "use strict"; | ||
} | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @function compare | ||
* | ||
* @description | ||
* Compare object a with b | ||
*/ | ||
function compare(a, b) { | ||
if (Type.isString(a)) { | ||
return a === b; | ||
} else if (Type.isNumber(a)) { | ||
if (isNaN(a) || isNaN(b)) { | ||
return isNaN(a) === isNaN(b); | ||
} | ||
return a === b; | ||
} else if (Type.isBoolean(a)) { | ||
return a === b; | ||
} else if (Type.isDate(a) && Type.isDate(b)) { | ||
return a.getTime() === b.getTime(); | ||
} else if (Type.isRegExp(a) && Type.isRegExp(b)) { | ||
return a.source === b.source; | ||
} else if (Type.isArray(a) && Type.isArray(b)) { | ||
// check references first | ||
if (a === b) { | ||
return true; | ||
} | ||
return a.every(function (item, index) { | ||
try { | ||
return compare(item, b[index]); | ||
} catch (e) { | ||
throw e; | ||
} | ||
}); | ||
} else if (Type.isObject(a) && Type.isObject(b)) { | ||
var equal = []; | ||
// check references first | ||
if (a === b) { | ||
return true; | ||
} | ||
try { | ||
for (var key in a) { | ||
equal.push(compare(a[key], b[key])); | ||
} | ||
} catch (e) { | ||
throw e; | ||
} | ||
return equal.every(function (item) { | ||
return item === true; | ||
}); | ||
/// compare undefined and nulls and nans | ||
} else if (a === b) { | ||
return true; | ||
} | ||
return false; | ||
} | ||
/** | ||
* Export functions | ||
@@ -144,3 +205,4 @@ * @type {{isBoolean: isBoolean, isUndefined: isUndefined, isDefined: isDefined, isObject: isObject, isString: isString, isNumber: isNumber, isDate: isDate, isArray: isArray, isFunction: isFunction, isRegExp: isRegExp, isConstructor: isConstructor, copy: copy, trim: trim, throwError: throwError}} | ||
createRegex: createRegex, | ||
toObject: toObject | ||
toObject: toObject, | ||
compare: compare | ||
}; |
@@ -8,2 +8,4 @@ "use strict"; | ||
mime = di.load('mime-types'), | ||
fs = di.load('fs'), | ||
Promise = di.load('promise'), | ||
component = di.load('core/component'), | ||
@@ -50,5 +52,4 @@ logger = component.get('core/logger'), | ||
var maxAge = 60 * 60 * 24 * 30 * 12, // one year | ||
filePath = this.config.path + api.parsedUrl.pathname, | ||
mimeType, | ||
file; | ||
filePath = di.normalizePath(this.config.path + api.parsedUrl.pathname), | ||
mimeType; | ||
@@ -60,23 +61,39 @@ | ||
logger.print('MimeType', mimeType, filePath); | ||
return false; | ||
return this.handleError(function() { | ||
new error.HttpError(500, {path: filePath}, 'Invalid mime type'); | ||
}); | ||
} | ||
file = this.readFile(filePath); | ||
if (api.getMethod() !== 'GET') { | ||
return this.handleError(function() { | ||
new error.HttpError(500, {path: filePath}, 'Assets are accessible only via GET request'); | ||
}); | ||
} | ||
return new Promise(function (resolve, reject) { | ||
api.addHeader('Content-Type', mimeType); | ||
api.addHeader('Cache-Control', 'public, max-age=' + ~~(maxAge)); | ||
api.addHeader('ETag', etag(file)); | ||
this.readFile(filePath, function (err, file) { | ||
if (err) { | ||
try { | ||
new error.HttpError(500, {path: filePath}, 'No file found', err); | ||
} catch (e) { | ||
return reject(e); | ||
} | ||
} | ||
if (api.getMethod() !== 'GET') { | ||
throw new error.HttpError(500, {path: filePath}, 'Assets are accessible only via GET request'); | ||
} else if (api.isHeaderCacheUnModified()) { | ||
api.sendNoChange(); | ||
} | ||
api.addHeader('Content-Type', mimeType); | ||
api.addHeader('Cache-Control', 'public, max-age=' + ~~(maxAge)); | ||
api.addHeader('ETag', etag(file)); | ||
logger.print('MimeType', mimeType, filePath); | ||
if (api.isHeaderCacheUnModified()) { | ||
return resolve(api.sendNoChange()); | ||
} | ||
return file; | ||
logger.print('MimeType', mimeType, filePath); | ||
resolve(file); | ||
}); | ||
}.bind(this)); | ||
}, | ||
@@ -86,2 +103,19 @@ /** | ||
* @author Igor Ivanovic | ||
* @method Favicon#handleError | ||
* | ||
* @description | ||
* Handle error response | ||
*/ | ||
handleError: function Assets_handleError(callback) { | ||
return new Promise(function (resolve, reject) { | ||
try { | ||
callback(); | ||
} catch (e) { | ||
reject(e); | ||
} | ||
}); | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Favicon#mimeType | ||
@@ -103,4 +137,4 @@ * | ||
*/ | ||
readFile: function Assets_readFile(filePath) { | ||
return di.readFileSync(filePath); | ||
readFile: function Assets_readFile() { | ||
return fs.readFile.apply(fs, arguments); | ||
} | ||
@@ -107,0 +141,0 @@ }); |
@@ -23,2 +23,123 @@ "use strict"; | ||
* @author Igor Ivanovic | ||
* @method Controller#setStatusCode | ||
* | ||
* @description | ||
* Set status code | ||
*/ | ||
setStatusCode: function Controller_setStatusCode(code) { | ||
this._request.setStatusCode(code); | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Controller#stopChain | ||
* | ||
* @description | ||
* Stop promise chain | ||
*/ | ||
stopChain: function Controller_stopChain() { | ||
return this._request.stopPromiseChain(); | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Controller#hasHeader | ||
* | ||
* @description | ||
* has response header | ||
*/ | ||
hasHeader: function Controller_hasHeader(key) { | ||
return this._request.hasHeader(key); | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Controller#getRequestBody | ||
* | ||
* @description | ||
* Get request body | ||
*/ | ||
getRequestBody: function Controller_getRequestBody() { | ||
return this._request.getRequestBody(); | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Controller#getRequestHeader | ||
* | ||
* @description | ||
* Get request header | ||
*/ | ||
getRequestHeader: function Controller_getRequestHeader(key) { | ||
return this._request.getRequestHeader(key); | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Controller#getHeaders | ||
* | ||
* @description | ||
* Return response headers | ||
*/ | ||
getHeaders: function Controller_getHeaders() { | ||
return this._request.getHeaders(); | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Controller#getMethod | ||
* | ||
* @description | ||
* Return request method | ||
*/ | ||
getMethod: function Controller_getMethod() { | ||
return this._request.getMethod(); | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Controller#getRequestHeaders | ||
* | ||
* @description | ||
* Return request headers | ||
*/ | ||
getRequestHeaders: function Controller_getRequestHeaders() { | ||
return this._request.getRequestHeaders(); | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Controller#isHeaderCacheUnModified | ||
* | ||
* @description | ||
* Check if cache is unmodified | ||
*/ | ||
isHeaderCacheUnModified: function Controller_isHeaderCacheUnModified() { | ||
return this._request.isHeaderCacheUnModified(); | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Controller#sendNoChange | ||
* | ||
* @description | ||
* Send no change 304 response | ||
*/ | ||
sendNoChange: function Controller_sendNoChange() { | ||
return this._request.sendNoChange(); | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Controller#getParsedUrl | ||
* | ||
* @description | ||
* Return parsed url | ||
*/ | ||
getParsedUrl: function Controller_getParsedUrl() { | ||
return this._request.parsedUrl; | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Controller#onEnd | ||
@@ -76,3 +197,2 @@ * | ||
}, | ||
/** | ||
@@ -103,2 +223,38 @@ * @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Controller#getActionName | ||
* | ||
* @description | ||
* Get action name | ||
* @return {string} | ||
*/ | ||
getActionName: function Controller_getActionName() { | ||
return this._routeInfo.action; | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Controller#getControllerName | ||
* | ||
* @description | ||
* Get controller name | ||
* @return {string} | ||
*/ | ||
getControllerName: function Controller_getControllerName() { | ||
return this._routeInfo.controller; | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Controller#getModuleName | ||
* | ||
* @description | ||
* Get module name | ||
* @return {string} | ||
*/ | ||
getModuleName: function Controller_getModuleName() { | ||
return this._routeInfo.module; | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Controller#hasAction | ||
@@ -105,0 +261,0 @@ * |
@@ -5,2 +5,3 @@ "use strict"; | ||
HttpServiceInterface = di.load('interface/http'), | ||
core = di.load('core'), | ||
http = di.load('http'), | ||
@@ -20,10 +21,27 @@ HttpService; | ||
HttpService = HttpServiceInterface.inherit({ | ||
server: Type.OBJECT | ||
server: Type.OBJECT, | ||
config: Type.OBJECT | ||
}, { | ||
_construct: function HttpService() { | ||
_construct: function HttpService(config) { | ||
this.config = { | ||
encoding: 'utf8' | ||
}; | ||
core.extend(this.config, config); | ||
this.server = http.createServer(); | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method HttpService#getEncoding | ||
* | ||
* @description | ||
* Return encoding | ||
*/ | ||
getEncoding: function HttpService_getEncoding() { | ||
return this.config.encoding; | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method HttpService#on | ||
@@ -30,0 +48,0 @@ * |
@@ -30,2 +30,3 @@ "use strict"; | ||
config: Type.OBJECT, | ||
length: Type.NUMBER, | ||
hooks: Type.ARRAY | ||
@@ -36,4 +37,4 @@ }, | ||
var file; | ||
this.stream = null; | ||
this.server = null; | ||
this.length = 0; | ||
this.stream = this.server = null; | ||
this.hooks = []; | ||
@@ -45,2 +46,3 @@ this.config = core.extend({ | ||
console: false, | ||
readLength: 20000, | ||
port: 9001, | ||
@@ -55,8 +57,36 @@ file: "server.log", | ||
if (this.config.publish) { | ||
this.server = http.createServer(); | ||
this.server.on('request', function (request, response) { | ||
var read = fs.readFileSync(file); | ||
response.writeHead(200, {'Content-type': 'text/plain', 'Content-Length': read.length}); | ||
response.end(read); | ||
}); | ||
var len = this.length, | ||
start = len - this.config.readLength, | ||
blen = 0, | ||
buffer, | ||
blenMessage; | ||
if (start < 0) { | ||
start = 0; | ||
blen = 1000; | ||
} else { | ||
blen = len - start; | ||
} | ||
blenMessage = 'LAST '+ blen + ' BYTES:\n\n'; | ||
buffer = new Buffer(blen + blenMessage.length, 'utf8'); | ||
fs.open(file, 'r', 755, function(status, fd) { | ||
fs.read(fd, buffer, 0, blen, start, function(err) { | ||
if (err) { | ||
var errorMessage = 'Error reading logger buffer'; | ||
response.writeHead(200, {'Content-type': 'text/plain', 'Content-Length': errorMessage.length}); | ||
response.end(errorMessage); | ||
} else { | ||
response.writeHead(200, {'Content-type': 'text/plain', 'Content-Length': buffer.length}); | ||
response.write(blenMessage); | ||
response.end(buffer); | ||
} | ||
}); | ||
}); | ||
}.bind(this)); | ||
this.server.listen(this.config.port); | ||
@@ -146,2 +176,3 @@ this.print('Publishing log write stream on port: ' + this.config.port); | ||
try { | ||
this.length += logs.length; | ||
this.stream.write(logs); | ||
@@ -148,0 +179,0 @@ } catch (e) { |
@@ -5,2 +5,3 @@ "use strict"; | ||
ControllerInterface = di.load('interface/controller'), | ||
ModuleInterface = di.load('interface/module'), | ||
component = di.load('core/component'), | ||
@@ -10,2 +11,3 @@ router = component.get('core/router'), | ||
logger = component.get('core/logger'), | ||
view = component.get('core/view'), | ||
URLParser = di.load('url'), | ||
@@ -41,6 +43,16 @@ Type = di.load('typejs'), | ||
isRendered: Type.BOOLEAN, | ||
isERROR: Type.BOOLEAN | ||
isERROR: Type.BOOLEAN, | ||
isPromiseChainStopped: Type.BOOLEAN, | ||
isForwarded: Type.BOOLEAN, | ||
encoding: Type.STRING, | ||
body: Type.STRING, | ||
id: Type.STRING | ||
}, { | ||
_construct: function Request(config, url) { | ||
this.isForwarded = false; | ||
this.body = ''; | ||
this.isERROR = false; | ||
// body and isForwarded can be overriden | ||
core.extend(this, config); | ||
this.statusCode = 200; | ||
@@ -50,2 +62,6 @@ this.headers = {}; | ||
this.parsedUrl = URLParser.parse(this.url, true); | ||
this.isPromiseChainStopped = false; | ||
this.isRendered = false; | ||
this.id = this._uuid(); | ||
view.setPaths(); | ||
}, | ||
@@ -61,3 +77,3 @@ /** | ||
onEnd: function Request_onEnd(callback) { | ||
this.request.on('end', callback); | ||
this.request.on('destory', callback); | ||
}, | ||
@@ -109,5 +125,31 @@ /** | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Request#setStatusCode | ||
* | ||
* @description | ||
* HTTP status code | ||
*/ | ||
setStatusCode: function Request_setStatusCode(code) { | ||
if (!Type.isNumber(code)) { | ||
throw new error.HttpError(500, {code: code}, "Status code must be number type"); | ||
} | ||
this.statusCode = code; | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Request#stopPromiseChain | ||
* | ||
* @description | ||
* Stop promise chain | ||
*/ | ||
stopPromiseChain: function Request_stopPromiseChain() { | ||
this.isPromiseChainStopped = true; | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Request#getHeaders | ||
@@ -124,2 +166,13 @@ * | ||
* @author Igor Ivanovic | ||
* @method Request#getRequestBody | ||
* | ||
* @description | ||
* Return body data | ||
*/ | ||
getRequestBody: function Request_getRequestBody() { | ||
return this.body; | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Request#getRequestHeaders | ||
@@ -215,5 +268,9 @@ * | ||
this.stopPromiseChain(); | ||
request = new Request({ | ||
request: this.request, | ||
response: this.response | ||
response: this.response, | ||
isForwarded: true, | ||
body: this.body | ||
}, router.createUrl(route, params)); | ||
@@ -237,2 +294,3 @@ | ||
this.addHeader('Location', url); | ||
this.stopPromiseChain(); | ||
if (Type.isBoolean(isTemp) && !!isTemp) { | ||
@@ -254,4 +312,5 @@ this.response.writeHead(302, this.headers); | ||
sendNoChange: function () { | ||
this.response.writeHead(304, this.headers); | ||
this.response.end(); | ||
this.stopPromiseChain(); | ||
this.setStatusCode(304); | ||
this._render(''); | ||
}, | ||
@@ -279,2 +338,44 @@ /** | ||
if (this.isForwarded) { | ||
return this._process().then( | ||
this.request.emit.bind(this.request, 'destory'), | ||
this.request.emit.bind(this.request, 'destory') | ||
); // emit destroy on error and resolve | ||
} | ||
this.request.setEncoding(this.encoding); | ||
this.request.on('data', function (body) { | ||
this.body += body; | ||
}.bind(this)); | ||
return new Promise(this.request.on.bind(this.request, 'end')) | ||
.then(this._process.bind(this)) | ||
.then( | ||
this.request.emit.bind(this.request, 'destory'), | ||
this.request.emit.bind(this.request, 'destory') | ||
); // emit destroy on error and resolve | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Request#_uuid | ||
* | ||
* @description | ||
* Generate uuid | ||
*/ | ||
_uuid: function () { | ||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { | ||
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); | ||
return v.toString(16); | ||
}); | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Request#_process | ||
* | ||
* @description | ||
* Process request | ||
*/ | ||
_process: function () { | ||
return hooks.process(this._getApi()) | ||
@@ -284,10 +385,15 @@ .then(function handleHooks(data) { | ||
return data; | ||
} else if (this.isPromiseChainStopped) { | ||
return false; | ||
} | ||
return router | ||
.process(this.request.method, this.parsedUrl) // find route | ||
.then(this._resolveRoute.bind(this), this._handleError.bind(this)); // resolve route chain | ||
.then(this._resolveRoute.bind(this)); // resolve route chain | ||
}.bind(this), this._handleError.bind(this)) | ||
.then(this._render.bind(this), this._handleError.bind(this)) // render chain | ||
.then(this._render.bind(this), this._handleError.bind(this)); // render error thrown in render function | ||
}.bind(this)) // handle hook chain | ||
.then(this._render.bind(this)) // resolve route chain | ||
.catch(this._handleError.bind(this)) // catch hook error | ||
.then(this._render.bind(this)) // render hook error | ||
.catch(this._handleError.bind(this)) // catch render error | ||
.then(this._render.bind(this)); // resolve render error | ||
}, | ||
@@ -315,51 +421,50 @@ /** | ||
* Handle error | ||
* @return boolean | ||
*/ | ||
_handleError: function Request_handleError(response) { | ||
var Controller, | ||
errorRoute = router.getErrorRoute(), | ||
errorController = '@{controllersPath}/' + errorRoute.shift(), | ||
errorAction = errorRoute.shift(); | ||
var request; | ||
if (this.isRendered) { | ||
// we have multiple recursion in parse for catching | ||
return false; | ||
} | ||
// set status codes | ||
if (response.code) { | ||
this.setStatusCode(response.code); | ||
} else { | ||
this.setStatusCode(500); | ||
} | ||
// stop current chain!!! | ||
this.stopPromiseChain(); | ||
if (response instanceof Error && !this.isERROR && di.exists(di.normalizePath(errorController) + '.js')) { | ||
if (response instanceof Error && !this.isERROR && !!router.getErrorRoute()) { | ||
// return new request | ||
request = new Request({ | ||
request: this.request, | ||
response: this.response, | ||
isForwarded: true, | ||
body: this.body, | ||
isERROR: true | ||
}, router.getErrorRoute()); | ||
// pass exception response over parsed url query as query parameter | ||
request.parsedUrl.query.exception = response; | ||
// set status codes for new request | ||
if (response.code) { | ||
this.statusCode = response.code; | ||
request.setStatusCode(response.code); | ||
} else { | ||
this.statusCode = 500; | ||
request.setStatusCode(500); | ||
} | ||
try { | ||
Controller = di.load(errorController); | ||
errorController = new Controller(); | ||
if (errorController.has('action_' + errorAction)) { | ||
response = errorController.get('action_' + errorAction)(response); | ||
if (response.trace) { | ||
this._render(response.trace); | ||
} else if (response.stack) { | ||
this._render(response.stack); | ||
} else { | ||
this._render(util.inspect(response)); | ||
} | ||
} else { | ||
throw new error.HttpError(500, {errorAction: errorAction}, "No error action defined at error controller"); | ||
} | ||
} catch (e) { | ||
this.isERROR = true; | ||
throw new error.HttpError(500, {}, "Error on executing error action", e); | ||
} | ||
// return parsed request | ||
return request.parse(); | ||
} else if (response.trace) { | ||
this.addHeader('Content-Type', 'text/plain'); | ||
this._render(response.trace); | ||
return this._render(response.trace); | ||
} else if (response.stack) { | ||
this.addHeader('Content-Type', 'text/plain'); | ||
this._render(response.stack); | ||
return this._render(response.stack); | ||
} else if (this.isERROR) { | ||
this.addHeader('Content-Type', 'text/plain'); | ||
this._render(util.inspect(response)); | ||
return this._render(util.inspect(response)); | ||
} else { | ||
this._render(response); | ||
return this._render(response); | ||
} | ||
@@ -374,26 +479,35 @@ }, | ||
* End request | ||
* @return boolean | ||
*/ | ||
_render: function Request__render(response) { | ||
if (!this.isRendered) { | ||
if (this.isRendered) { | ||
return false; | ||
} | ||
logger.print('Request.render', response); | ||
this._checkContentType('text/html'); | ||
this._checkContentType('text/html'); | ||
this.response.writeHead(this.statusCode, this.headers); | ||
this.response.writeHead(this.statusCode, this.headers); | ||
if (Type.isString(response)) { | ||
this.addHeader('Content-Length', response.length); | ||
this.response.end(response); | ||
} else if (response instanceof Buffer) { | ||
this.addHeader('Content-Length', response.length); | ||
this.response.end(response); | ||
} else if (!response) { | ||
throw new error.HttpError(500, {}, 'No data to render'); | ||
} else { | ||
throw new error.HttpError(500, {}, 'Invalid response type, string or buffer is required!'); | ||
} | ||
if (Type.isString(response)) { | ||
this.addHeader('Content-Length', response.length); | ||
this.response.end(response); | ||
} else if (response instanceof Buffer) { | ||
this.addHeader('Content-Length', response.length); | ||
this.response.end(response); | ||
} else if (!response) { | ||
throw new error.HttpError(500, {}, 'No data to render'); | ||
} else { | ||
throw new error.HttpError(500, {}, 'Invalid response type, string or buffer is required!'); | ||
} | ||
if ((response instanceof Error) || this.isERROR || this.statusCode === 500) { | ||
logger.print('Request.error', this.statusCode, this.id, this.getHeader('content-type'), response); | ||
} else { | ||
logger.print('Request.render', this.statusCode, this.id, this.getHeader('content-type')); | ||
} | ||
this.isRendered = true; | ||
return true; | ||
}, | ||
@@ -417,2 +531,3 @@ | ||
getMethod: this.getMethod.bind(this), | ||
getRequestBody: this.getRequestBody.bind(this), | ||
getRequestHeaders: this.getRequestHeaders.bind(this), | ||
@@ -423,2 +538,4 @@ getRequestHeader: this.getRequestHeader.bind(this), | ||
sendNoChange: this.sendNoChange.bind(this), | ||
stopPromiseChain: this.stopPromiseChain.bind(this), | ||
setStatusCode: this.setStatusCode.bind(this), | ||
createUrl: router.createUrl.bind(router), | ||
@@ -438,3 +555,3 @@ parsedUrl: core.copy(this.parsedUrl) | ||
if (!promise) { | ||
return new Promise(function(resolve, reject) { | ||
return new Promise(function (resolve, reject) { | ||
try { | ||
@@ -449,4 +566,7 @@ resolve(next.apply(next, arguments)); | ||
return promise.then(function (data) { | ||
return Promise.resolve(_handler(data)); | ||
}, this._handleError.bind(this)); | ||
if (!!this.isPromiseChainStopped) { | ||
return promise; | ||
} | ||
return _handler(data); | ||
}.bind(this), this._handleError.bind(this)); | ||
@@ -464,3 +584,3 @@ function _handler() { | ||
* @author Igor Ivanovic | ||
* @method Request#_handleRoute | ||
* @method Request#_handleController | ||
* | ||
@@ -470,3 +590,3 @@ * @description | ||
*/ | ||
_handleRoute: function Request_handleRoute() { | ||
_handleController: function Request_handleController() { | ||
var controllerToLoad = '@{controllersPath}/' + this.controller, | ||
@@ -476,5 +596,3 @@ LoadedController, | ||
action, | ||
promise, | ||
afterActionPromise = false, | ||
afterEachPromise = false; | ||
promise; | ||
@@ -487,4 +605,12 @@ try { | ||
controller = new LoadedController(this._getApi()); | ||
if (!Type.assert(Type.FUNCTION, LoadedController)) { | ||
throw new error.HttpError(500, {path: controllerToLoad}, 'Controller must be function type'); | ||
} | ||
controller = new LoadedController(this._getApi(), { | ||
controller: this.controller, | ||
action: this.action, | ||
module: this.module | ||
}); | ||
if (!(controller instanceof ControllerInterface)) { | ||
@@ -530,7 +656,7 @@ throw new error.HttpError(500, controller, 'Controller must be instance of ControllerInterface "core/controller"'); | ||
if (controller.has('after_' + this.action)) { | ||
afterActionPromise = this._chain(promise, controller.get('after_' + this.action).bind(controller, this.params)); | ||
promise = this._chain(promise, controller.get('after_' + this.action).bind(controller, this.params)); | ||
} | ||
if (controller.has("afterEach")) { | ||
afterEachPromise = this._chain(promise, controller.afterEach.bind(controller, this.action, this.params)); | ||
promise = this._chain(promise, controller.afterEach.bind(controller, this.action, this.params)); | ||
} | ||
@@ -540,8 +666,40 @@ | ||
return Promise.all([afterActionPromise, afterEachPromise]).then(function (data) { | ||
logger.print('afterActionPromise, afterEachPromise', data); | ||
return promise; | ||
}); | ||
return promise; | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method Request#_handleModule | ||
* | ||
* @description | ||
* Handle module | ||
* @return {object} Promise | ||
*/ | ||
_handleModule: function Request__handleModule() { | ||
var moduleToLoad = '@{modulesPath}/' + this.module, | ||
LoadedModule, | ||
module; | ||
try { | ||
LoadedModule = di.load(moduleToLoad); | ||
} catch (e) { | ||
throw new error.HttpError(500, {path: moduleToLoad}, 'Missing module', e); | ||
} | ||
if (!Type.assert(Type.FUNCTION, LoadedModule)) { | ||
throw new error.HttpError(500, {path: moduleToLoad}, 'Module must be function type'); | ||
} | ||
module = new LoadedModule(this.module); | ||
if (!(module instanceof ModuleInterface)) { | ||
throw new error.HttpError(500, module, 'Module must be instance of ModuleInterface "core/module"'); | ||
} | ||
module.setControllersPath(); | ||
module.setViewsPath(); | ||
module.setThemesPath(); | ||
return this._handleController(); | ||
}, | ||
/** | ||
@@ -555,7 +713,5 @@ * @since 0.0.1 | ||
* @return {object} Promise | ||
* @todo implement modules | ||
*/ | ||
_resolveRoute: function Request__resolveRoute(routeRule) { | ||
var route; | ||
this.statusCode = 200; | ||
this.route = routeRule.shift(); | ||
@@ -570,3 +726,7 @@ this.params = routeRule.shift(); | ||
return this._handleRoute(); | ||
if (!!this.module) { | ||
return this._handleModule(); | ||
} | ||
return this._handleController(); | ||
} | ||
@@ -573,0 +733,0 @@ |
@@ -30,8 +30,10 @@ "use strict"; | ||
this.config = core.extend({ | ||
errorRoute: "error/index" | ||
errorRoute: false, | ||
errorUrl: '/error' | ||
}, config); | ||
if (!Type.assert(Type.STRING, this.config.errorRoute)) { | ||
throw new error.DataError(this.config, 'Router.construct: errorRoute must be string type'); | ||
} | ||
// add error route so we can resolve it | ||
this.add({ | ||
pattern: this.config.errorUrl, | ||
route: this.config.errorRoute ? this.config.errorRoute : 'core/error' | ||
}); | ||
}, | ||
@@ -48,3 +50,3 @@ /** | ||
getErrorRoute: function Router_getErrorRoute() { | ||
return this.config.errorRoute.split('/').splice(0, 2); | ||
return this.config.errorRoute; | ||
}, | ||
@@ -76,3 +78,3 @@ /** | ||
if (!(rule instanceof RouteRuleInterface)) { | ||
throw new error.HttpError(500, {rule: rule}, 'Router.add: rule must be instance of RouteRuleInterface'); | ||
throw new error.HttpError(500, {rule: rule, route: route}, 'Router.add: rule must be instance of RouteRuleInterface'); | ||
} | ||
@@ -82,3 +84,2 @@ | ||
this.routes.push(rule); | ||
}, | ||
@@ -140,17 +141,25 @@ | ||
parseRequest: function Router_parseRequest(method, parsedUrl) { | ||
var i, len = this.routes.length, routeRule, route = []; | ||
var all = []; | ||
for (i = len - 1; i > -1; --i) { | ||
routeRule = this.routes[i]; | ||
route = routeRule.parseRequest(method, parsedUrl); | ||
if (Type.isArray(route) && route.length) { | ||
break; | ||
this.routes.forEach(function (routeRule) { | ||
all.push( | ||
new Promise(function (resolve, reject) { | ||
try { | ||
resolve(routeRule.parseRequest(method, parsedUrl)) | ||
} catch (e) { | ||
reject(e); | ||
} | ||
}) | ||
); | ||
}); | ||
return Promise.all(all).then(function (data) { | ||
var route; | ||
while (data.length) { | ||
route = data.shift(); | ||
if (Type.isArray(route) && route.length === 2) { | ||
return route; | ||
} | ||
} | ||
} | ||
if (!Type.isArray(route)) { | ||
route = []; | ||
} | ||
logger.print('Router.parseRequest', route); | ||
return route; | ||
return []; | ||
}); | ||
}, | ||
@@ -202,3 +211,3 @@ /** | ||
return new Promise(handlePromise.bind(this)) | ||
return this.parseRequest(method, parsedUrl) | ||
.then(function (routeRule) { | ||
@@ -213,10 +222,2 @@ if (Type.isArray(routeRule) && routeRule.length === 2) { | ||
}); | ||
function handlePromise(resolve, reject) { | ||
try { | ||
resolve(this.parseRequest(method, parsedUrl)); | ||
} catch (e) { | ||
reject(e); | ||
} | ||
} | ||
} | ||
@@ -223,0 +224,0 @@ }); |
@@ -64,4 +64,3 @@ "use strict"; | ||
if (this.config.cache) { | ||
this.preloadTemplates(di.getAlias('viewsPath')); | ||
this.preloadTemplates(di.getAlias('themesPath')); | ||
this.preloadTemplates(di.getAlias('appPath')); | ||
} | ||
@@ -324,2 +323,24 @@ | ||
* @author Igor Ivanovic | ||
* @method View#setPaths | ||
* | ||
* @description | ||
* Set default paths | ||
* @return {string} | ||
*/ | ||
setPaths: function View_setPaths(themesPath, viewsPath) { | ||
if (Type.isString(themesPath) && !!themesPath) { | ||
di.setAlias('themesPath', themesPath); | ||
} else { | ||
di.setAlias('themesPath', this.config.themes); | ||
} | ||
if (Type.isString(viewsPath) && !!viewsPath) { | ||
di.setAlias('viewsPath', viewsPath); | ||
} else { | ||
di.setAlias('viewsPath', this.config.views); | ||
} | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method View#renderFile | ||
@@ -326,0 +347,0 @@ * |
@@ -87,6 +87,10 @@ "use strict"; | ||
normalizePath: function DI_normalizePath(value) { | ||
Object.keys(this.aliases).forEach(function (key) { | ||
value = value.replace('@{' + key + '}', this.aliases[key]); | ||
}.bind(this)); | ||
return path.normalize(value); | ||
if (Type.isString(value)) { | ||
Object.keys(this.aliases).forEach(function (key) { | ||
value = value.replace('@{' + key + '}', this.aliases[key]); | ||
}.bind(this)); | ||
return path.normalize(value); | ||
} else { | ||
throw new error.DataError({value: value}, 'DI.normalizePath: value is not string'); | ||
} | ||
}, | ||
@@ -127,2 +131,18 @@ /** | ||
* @author Igor Ivanovic | ||
* @method DI#refereshNodeModuleCache | ||
* | ||
* @description | ||
* Reset file cache | ||
*/ | ||
refereshNodeModuleCache: function DI_refereshNodeModuleCache(file) { | ||
var path = this.getFilePath(file), | ||
key = require.resolve(path + '.js'); | ||
// because all modules in node are cached while executing tests we want to delete cached version | ||
delete require.cache[key]; | ||
return path; | ||
}, | ||
/** | ||
* @since 0.0.1 | ||
* @author Igor Ivanovic | ||
* @method DI#mockLoad | ||
@@ -144,8 +164,4 @@ * | ||
if (Type.isString(file)) { | ||
// get file | ||
path = this.getFilePath(file); | ||
// because all modules in node are cached while executing tests we want to delete cached version | ||
delete require.cache[require.resolve(path + '.js')]; | ||
// do require | ||
return require(path); | ||
return require(this.refereshNodeModuleCache(file)); | ||
@@ -152,0 +168,0 @@ } else if (Type.isFunction(file)) { |
@@ -16,2 +16,3 @@ { | ||
"core/controller": "@{framework}/core/controller", | ||
"core/module": "@{framework}/core/module", | ||
"core/routeRule": "@{framework}/core/routeRule", | ||
@@ -31,2 +32,3 @@ "core/view": "@{framework}/core/view", | ||
"interface/view": "@{framework}/interface/view", | ||
"interface/module": "@{framework}/interface/module", | ||
@@ -33,0 +35,0 @@ "cache/memory": "@{framework}/cache/memory", |
@@ -19,7 +19,20 @@ "use strict"; | ||
ControllerInterface = Type.create({ | ||
_request: Type.OBJECT | ||
_request: Type.OBJECT, | ||
_routeInfo: Type.OBJECT | ||
}, { | ||
_invoke: function ControllerInterface(request) { | ||
_invoke: function ControllerInterface(request, routeInfo) { | ||
this._request = request; | ||
["has", "get", "redirect", "forward", "addHeader", "onEnd", "createUrl"].forEach(function (method) { | ||
this._routeInfo = routeInfo; | ||
[ | ||
"has", "get", "redirect", | ||
"forward", "addHeader", "onEnd", | ||
"createUrl", "hasHeader", "getRequestHeader", | ||
"getHeaders", "getMethod", "getRequestHeaders", | ||
"isHeaderCacheUnModified", "sendNoChange", "getParsedUrl", | ||
"stopChain", "render", "renderFile", "setStatusCode", | ||
"getRequestBody", | ||
"getActionName", | ||
"getControllerName", | ||
"getModuleName" | ||
].forEach(function (method) { | ||
if (!(method in this)) { | ||
@@ -26,0 +39,0 @@ throw new error.DataError({method: method}, 'ControllerInterface: missing method in Controller object'); |
@@ -22,3 +22,3 @@ "use strict"; | ||
_invoke: function HttpServiceInterface() { | ||
["on", "listen", "close", "setTimeout"].forEach(function (method) { | ||
["on", "listen", "close", "setTimeout", "getEncoding"].forEach(function (method) { | ||
if (!(method in this)) { | ||
@@ -25,0 +25,0 @@ throw new error.DataError({method: method}, 'HttpServiceInterface: missing method in HttpService object'); |
@@ -5,3 +5,3 @@ { | ||
"description": "Powerful lightweight mvc framework for nodejs", | ||
"version": "0.1.0-beta-3", | ||
"version": "0.1.0-beta-30", | ||
"dependencies" : { | ||
@@ -35,2 +35,3 @@ "mongoose": "3.8.x", | ||
"keywords": [ | ||
"node", | ||
"mvcjs", | ||
@@ -37,0 +38,0 @@ "framework", |
124
README.md
@@ -1,2 +0,124 @@ | ||
mvcjs [](https://travis-ci.org/igorzg/node-mvc) | ||
MVC JS [](https://travis-ci.org/igorzg/node-mvc) beta | ||
===== | ||
Powerful lightweight mvc framework for nodejs inspired by [Yii](http://www.yiiframework.com/) | ||
Mvcjs is a first nodejs framework which supports modules as bundles! | ||
Features | ||
==== | ||
1. Fully extensible | ||
2. TDD driven | ||
3. Type checking at runtime | ||
4. Custom DI | ||
5. Component based | ||
6. Twig (swigjs) templating engine | ||
7. Dynamic routing | ||
8. Logger | ||
9. Modules | ||
[Demo application](https://github.com/igorzg/mvcjs-testapp) | ||
[Docs and live app](http://mvcjs.igorivanovic.info) | ||
Getting started | ||
==== | ||
npm install mvcjs | ||
index.js | ||
```javascript | ||
var di = require('mvcjs'); | ||
var framework = di.load('bootstrap'); | ||
framework.setBasePath(__dirname); | ||
framework.init('app/', 'env.json'); | ||
``` | ||
app/env.json | ||
```json | ||
{ | ||
"aliases": [ | ||
{ | ||
"key": "assetsPath", | ||
"value": "@{basePath}/assets" | ||
} | ||
], | ||
"components": [ | ||
{ | ||
"name": "core/logger", | ||
"enabled": true, | ||
"write": true, | ||
"publish": true, | ||
"console": true, | ||
"port": 9001, | ||
"file": "server.log" | ||
}, | ||
{ | ||
"name": "core/router", | ||
"errorRoute": "core/error" | ||
}, | ||
{ | ||
"name": "core/favicon", | ||
"path": "@{basePath}/favicon.ico" | ||
}, | ||
{ | ||
"name": "core/view", | ||
"themes": "@{appPath}/themes/", | ||
"views": "@{appPath}/views/", | ||
"theme": "default", | ||
"cache": true | ||
}, | ||
{ | ||
"name": "core/assets", | ||
"path": "@{basePath}/storage/", | ||
"hook": "^\\/assets" | ||
}, | ||
{ | ||
"name": "db/mongo", | ||
"connection": "mongodb://localhost/testdb" | ||
} | ||
], | ||
"config": "config-test.js", | ||
"assetsPath": "@{assetsPath}", | ||
"port": 9000 | ||
} | ||
``` | ||
app/config-test.js | ||
```javascript | ||
module.exports = function (componet, di) { | ||
"use strict"; | ||
var viewLoader, router, | ||
logger = componet.get('core/logger'), | ||
loggerModel = di.load('@{modelsPath}/logger'); | ||
viewLoader = componet.get('core/view'); | ||
viewLoader.setTheme('home'); | ||
// bind logger hook | ||
logger.addHook(loggerModel.save.bind(loggerModel)); | ||
router = componet.get('core/router'); | ||
router.add([ | ||
{ | ||
pattern: 'home/<action>', | ||
route: 'home/<action>' | ||
}, | ||
{ | ||
pattern: 'posts/<action:(create|update|delete)>', | ||
route: 'posts/<action>', | ||
method: ['GET', 'POST'] | ||
}, | ||
{ | ||
pattern: 'user/<id:(\\d+)>', | ||
route: 'user/view' | ||
} | ||
]); | ||
router.add({ | ||
pattern: '/', | ||
route: 'home/index' | ||
}); | ||
}; | ||
``` | ||
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
125229
34
4025
125