UNPKG

@node-server/renderer

Version:

NodeServerJs is a library with standard feature implemented for web and api

448 lines (396 loc) 15.7 kB
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; } }