@node-server/renderer
Version:
NodeServerJs is a library with standard feature implemented for web and api
448 lines (396 loc) • 15.7 kB
text/typescript
import * as chokidar from 'chokidar';
import * as fs from 'fs';
import * as path from 'path';
import { Logger, _JSON, FsFetcher } from "@node-server/utils";
import { Api } from "./api-folder-handlers/api";
import { JSHandler } from "./api-folder-handlers/js.handler";
import { SQLHandler } from "./api-folder-handlers/sql.handler";
import { Renderer } from '@node-server/utils';
class ApiRequest {
name:string;
name_method:string;
method:string;
index:string;
index_method:string;
params:any;
constructor(name, method) {
this.name = (name || "").toLowerCase(),
this.method = (method || "").toLowerCase()
this.name_method = this.name+ "." + this.method;
this.index = this.name + "/"+ "index";
this.index_method = this.index+ "." + this.method;
}
}
export class ApiFolderRenderer extends Renderer {
static handlers = {JSHandler ,SQLHandler}
cache:Api[] = [];
paramsApi:Api[] = [];
extentions:string[] = [];
Watch:any;
full_path:string;
constructor(config) {
super({
default :{
cache : true,
default_allow :true,
prefix: "api",
folder: "api",
handlers : [
new JSHandler(),
new SQLHandler()
]
},
userDefined : config
});
this.extentions = this.pluginConfig.handlers.map(a => a.extention);
}
Init(coreConfig){
super.Init(coreConfig);
this.pluginConfig.handlers.forEach(handler => {
if(handler.Init)
handler.Init(coreConfig);
});
}
DeleteApiParamsCache(file, event){
var l = file.replace(/\\/g, "/").replace(this.full_path , "").split(".")[0].trim().toLowerCase();
if(l[0] === "/")
l = l.substr(1)
Logger.debug("Api event '",event, "'", file)
switch(event)
{
case "unlink":
this.paramsApi = this.paramsApi.splice(l, 1);
break;
case "change":
case "add":
var ext = path.extname(file);
this.Generate(this.GetFileInfoAsApi(file));
break;
}
}
DeleteFromCache(file , event){
if(path.basename(file).indexOf("_") > -1)
{
return this.DeleteApiParamsCache(file, event);
}
var l = file.replace(/\\/g, "/").replace(this.full_path , "").split(".")[0].trim().toLowerCase();
if(l[0] === "/")
l = l.substr(1)
var el = this.cache[l];
if(el)
this.cache = this.cache.splice(l, 1);
return el != undefined;
}
RetrieveApisWithParams(){
var apis = FsFetcher.Fetch(this.full_path, (file) => {
return this.extentions.find(ext => "." + ext === file.ext) && file.basename.indexOf("_") > -1
})
apis.forEach(a => this.Generate(this.GetFileInfoAsApi(a.absolute)));
}
AddContextValues(req, res)
{
res.success = (a) => {
var resp = { success: true, message: a };
if(!res.headersSent)
res.json(JSON.parse(_JSON.stringify(resp)))
};
res.error = (a) => {
if(!res.headersSent)
res.json(JSON.parse(_JSON.stringify({ error: true, message: a }))) };
var ip ;
if (req.IP) {
ip = req.IP;
} else if (req.headers && req.headers['x-forwarded-for']) {
ip = req.headers['x-forwarded-for'].split(",")[0];
} else if (req.connection && req.connection.remoteAddress) {
ip = req.connection.remoteAddress;
} else {
ip = req.ip;
}
var api_params = req.path.split("/")
api_params.splice(0,2);
req.SetContext("ip" , ip);
req.SetContext("get" , req.query || {});
req.SetContext("post" , req.body || {});
req.SetContext("method" , req.method || {});
req.SetContext("status" , (n) => res.status(n));
req.SetContext("contains_errors" , {});
req.SetContext("api" , {
name : api_params.join("/"),
run : (name, data) => {
},
clear: () => {
this.cache = [];
this.paramsApi = [];
this.RetrieveApisWithParams();
}
});
req.SetContext("contains" , (...args: any[]) => {
const post = Object.assign(req.GetContext("params") || {} ,
Object.assign(req.GetContext("get") ,
req.GetContext("post") ));
var contains_error = {};
var good = true;
for (var p in args) {
if (typeof args[p] === "string") {
good = post[args[p]] ? true : false;
if (!good)
contains_error[args[p]] = `NOT_FOUND`;
}
else {
for (var i in args[p]) {
var value = post[i];
good = value !== undefined;
if (!good) {
contains_error[i] = `NOT_FOUND`;
continue;
}
if (args[p][i].constructor && args[p][i].constructor.name === "RegExp") {
good = args[p][i].test(value);
if(!good)
contains_error[i] = 'NOT_VALID';
continue;
}
switch (args[p][i]) {
case "numeric":
good = !isNaN(+value);
if (!good)
contains_error[i] = 'NOT_NUMBER';
break;
case "numeric+":
good = !isNaN(+value) && +value > 0;
if (!good)
contains_error[i] = `NOT_NUMBER_GT_0`;
break;
case "numeric-":
good = !isNaN(+value) && +value < 0;
if (!good)
contains_error[i] = `NOT_NUMBER_LT_0`;
break;
case "email":
good = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(value);
if (!good)
contains_error[i] = `NOT_VALID_EMAIL`;
break;
}
}
}
}
req.SetContext("contains_errors", contains_error);
return Object.keys(contains_error).length == 0;
});
}
GetFileInfoAsApi(file)
{
var l = file.replace(/\\/g, "/").replace(this.full_path , "").split(".")[0].trim().toLowerCase();
if(l[0] === "/")
l = l.substr(1)
return {
ext: path.extname(file).replace(".", ""),
file : file,
name: l
}
}
async Handle(req,res)
{
this.AddContextValues(req,res);
var method = req.GetContext("method");
var request = new ApiRequest(req.GetContext("api").name , method);
var api = this.Get(request);
if(api && (!api.method || api.method === method.toLowerCase()))
{
if(!api.check(req.GetContext()))
{
return res.status(401).error("UNAUTHORIZED")
}
req.SetContext("params" , request.params || {});
var response = await api.handler.Handle(api,req,res);
if(response.result instanceof Error)
{
Logger.error(response.result);
res.status(500).error("An internal error occured");
}
else
response.error ? res.status(response.status).error(response.result) : res.status(response.status).success(response.result);
}
else
res.status(404).error("NOT_FOUND")
}
Condition(req,res)
{
var params = (req.GetContext("_params") || [])[0];
return params.toLowerCase() === this.pluginConfig.prefix;
}
Start(app){
super.Start(app);
this.App.use((req, res , next) => {
var params = req.path.split("/");
params.splice(0,1);
req.SetContext("_params" , params);
next();
})
this.full_path = this.Config.GetFolder(this.pluginConfig.folder);
if(this.Config.env.DEVELOPPMENT && this.pluginConfig.cache)
{
Logger.log("Watching api folder");
this.Watch = chokidar.watch(this.full_path,
{
recursive :true ,
ignoreInitial: true,
ignored : (path) =>{
var ext = path.split(".");
if(ext.length < 2)
return false;
ext = ext[ext.length -1];
return this.extentions.indexOf(ext) == -1;
}
}).on('all', (event, path) => {
this.DeleteFromCache(path , event);
});
}
this.RetrieveApisWithParams();
}
Generate(file): Api {
var handler = this.pluginConfig.handlers.find(x => x.extention === file.ext), api:Api;
if(handler)
{
var data = fs.readFileSync(file.file, { encoding: 'utf-8' });
api = handler.Create(file.file , file.name, data);
api.handler = handler;
api.params = file.name.split(".")[0].split("_");
api.params.splice(0,1);
api.check = this.AllowedApi(api);
api.method = file.name.split(".")[1]
if(api.params.length > 0)
{
var name:any = api.name.split("/");
var last:any = name.pop().split("_")[0];
if(last !== "index")
name.push(last)
name = name.join("/")
api.name_params =name;
this.paramsApi[file.name] = api;
}
else if(this.pluginConfig.cache)
this.cache[file.name] = api;
}
else
Logger.error("No handler found for extention ", file.ext)
return api;
}
Get(request:ApiRequest): Api {
var api;
// Check if api is cached
for(var i in this.paramsApi)
{
var _api = this.paramsApi[i];
if(request.name.indexOf(_api.name_params) > -1)
{
var params = request.name.replace(_api.name_params, "").split("/");
params.splice(0,1)
if(params.length == _api.params.length)
{
var _params = {};
for(var i in _api.params)
_params[_api.params[i]] = params[i]
request.params = _params;
api = _api;
}
}
if(api)
break;
}
if(api)
return api;
if(this.pluginConfig.cache)
{
var api;
var files = [ request.name , request.name_method ,request.index ,request.index_method]
for(var i in files)
{
api = this.cache[files[i]];
if(api)
break;
}
}
if (!api) {
for(var i in this.extentions)
{
var api_files = [
{ file : request.name_method + "." + this.extentions[i] ,
ext : this.extentions[i],
name : request.name_method},
{ file : request.name + "." + this.extentions[i] ,
ext : this.extentions[i],
name : request.name},
{ file : request.index_method + "." + this.extentions[i] ,
ext : this.extentions[i],
name : request.index_method},
{ file : request.index + "." + this.extentions[i] ,
ext : this.extentions[i],
name : request.index}];
for(var i in api_files)
{
var file = api_files[i];
file.file = path.join(this.full_path, file.file);
if (fs.existsSync(file.file))
api = this.Generate(file)
if(api)
break;
}
if(api)
break;
}
}
else
Logger.debug(request.name , "api loaded from cache")
return api;
}
AllowedApi(api:Api):(ctx) => boolean
{
var security = this.pluginConfig.security;
var name = api.name;
if(!security)
return (ctx) => this.pluginConfig.default_allow;
if(name[0] === "/")
name = name.substring(1);
if(Array.isArray(security.connected))
{
var need_connected = security.connected.indexOf(name) > -1;
if(need_connected)
return (ctx) => ctx.session && ctx.session.connected;
}
if(Array.isArray(security.admin))
{
var need_connected = security.connected.indexOf(name) > -1;
if(need_connected)
return (ctx) => ctx.session && ctx.session.role === "admin";
}
var hasarule = false;
if(Array.isArray(security.custom))
for(var sec in security.custom)
{
var el = security.custom[sec];
hasarule = el.api.indexOf(name) > -1;
if(!hasarule)
for(var i in el.folders)
{
hasarule = api.filename.indexOf(path.join(this.full_path, el.folders[i])) == 0;
if(hasarule)
break;
}
if(!hasarule)
continue;
return (ctx) => {
var valid = !el.ips ||el.ips.length == 0 || el.ips.indexOf(ctx.ip) > -1;
if(valid)
for(var i in el.session)
{
valid = valid && ctx.session && ctx.session[i] === el.session[i];
}
return valid;
}
}
return (ctx) => this.pluginConfig.default_allow;
}
}