dotweb
Version:
ready to use web server components
1,421 lines (1,397 loc) • 82.9 kB
JavaScript
"use strict";
const fs = require("fs");
const crypto = require("crypto");
const path = require("path");
const mongoose = require("mongoose");
const checksum = require("checksum");
const redis = require("redis");
const jwt = require("jsonwebtoken");
const http = require("http");
const https = require("https");
const express = require("express");
const cookieParser = require("cookie-parser");
const cors = require("cors");
const multer = require("multer");
const socket = require("socket.io");
const admin = require("firebase-admin");
const { populate } = require("mongoose/lib/utils");
const cacheState = {
null: 0,
private: 1,
public: 2,
};
const accessLevel = {
null: 0,
public: 1,
private: 2,
full: 3,
};
const Console = class Console {
colors = {
reset: "\x1b[0m",
bright: "\x1b[1m",
dim: "\x1b[2m",
underscore: "\x1b[4m",
blink: "\x1b[5m",
reverse: "\x1b[7m",
hidden: "\x1b[8m",
black: "\x1b[30m",
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
magenta: "\x1b[35m",
cyan: "\x1b[36m",
white: "\x1b[37m",
fgblack: "\x1b[30m",
fgred: "\x1b[31m",
fggreen: "\x1b[32m",
fgyellow: "\x1b[33m",
fgblue: "\x1b[34m",
fgmagenta: "\x1b[35m",
fgcyan: "\x1b[36m",
fgwhite: "\x1b[37m",
bgblack: "\x1b[40m",
bgred: "\x1b[41m",
bggreen: "\x1b[42m",
bgyellow: "\x1b[43m",
bgblue: "\x1b[44m",
bgmagenta: "\x1b[45m",
bgcyan: "\x1b[46m",
bgwhite: "\x1b[47m",
};
pid_length = 6;
files = [];
constructor(...filenames) {
this.threadId = process.pid.toString();
if (this.threadId.length > this.pid_length) this.threadId = this.threadId.substr(this.threadId.length - this.pid_length, this.pid_length);
else if (this.threadId.length < this.pid_length) this.threadId = "0".repeat(this.pid_length - this.threadId.length) + this.threadId;
this.create = this.create.bind(this);
this.destroy = this.destroy.bind(this);
this.formatConsoleDateTime = this.formatConsoleDateTime.bind(this);
this.formatConsoleTime = this.formatConsoleTime.bind(this);
this.log = this.log.bind(this);
this.create(...filenames);
}
create(...filenames) {
this.files = filenames.map(filename =>
fs.createWriteStream(path.resolve(filename), {
flags: "a",
})
);
this.filenames = filenames;
}
destroy() {
this.files.map((ws, index) => {
ws.end();
});
this.files = [];
}
formatConsoleDateTime(date) {
let year = date.getFullYear();
let month = date.getMonth() + 1;
let day = date.getDate();
let hour = date.getHours();
let minutes = date.getMinutes();
let seconds = date.getSeconds();
return (
"[" +
year +
"-" +
(month < 10 ? "0" + month : month) +
"-" +
(day < 10 ? "0" + day : day) +
" " +
(hour < 10 ? "0" + hour : hour) +
":" +
(minutes < 10 ? "0" + minutes : minutes) +
":" +
(seconds < 10 ? "0" + seconds : seconds) +
"] "
);
}
formatConsoleTime(date) {
let hour = date.getHours();
let minutes = date.getMinutes();
let seconds = date.getSeconds();
let milliseconds = date.getMilliseconds();
return (
"[" +
(hour < 10 ? "0" + hour : hour) +
":" +
(minutes < 10 ? "0" + minutes : minutes) +
":" +
(seconds < 10 ? "0" + seconds : seconds) +
"." +
(milliseconds < 10 ? "00" + milliseconds : milliseconds < 100 ? "0" + milliseconds : milliseconds) +
"] "
);
}
log(text, visual = [], ...more) {
let affect = "",
bgEndSpace = "";
if (typeof visual === "string") visual = [visual];
if (Array.isArray(visual) && visual.length > 0) {
for (let color of visual.map(x => x.toString().toLowerCase())) {
if (typeof this.colors[color] === "string") {
if (!bgEndSpace && color.substr(0, 2) === "bg") bgEndSpace = " ";
affect = this.colors[color] + affect;
}
}
}
let sText = this.threadId + " " + this.formatConsoleTime(new Date()) + text;
let lText = this.threadId + " " + this.formatConsoleDateTime(new Date()) + text;
if (affect) {
sText = affect + sText + bgEndSpace + this.colors.reset;
lText = affect + lText + bgEndSpace + this.colors.reset;
}
console.log(sText);
if (this.files.length > 0) this.files[0].write(lText + "\n");
for (const index in more) {
if (more[index] && typeof this.files[index + 1] !== "undefined") {
this.files[index + 1].write(lText + "\n");
}
}
}
};
const FileWatcher = class FileWatcher {
cache = {};
over = false;
#called = false;
#password = "";
#iv64 = "s7epMK3v";
watcher_interval = 0;
file_has_eof = false;
encoding = "base64";
constructor(application, { watcher_interval, file_has_eof, encoding } = {}) {
this.log = (text, visual, more) => application.log(`require ::: ${text}`, visual, more);
const argv = 3 > process.argv.length ? ["", "", ""] : process.argv.slice(process.argv.length - 1, process.argv.length);
this.#called = argv[0].length === 32;
this.#password = argv[0];
this.create = this.create.bind(this);
this.destroy = this.destroy.bind(this);
this.encode = this.encode.bind(this);
this.decode = this.decode.bind(this);
this.processBuffer = this.processBuffer.bind(this);
this.parser = this.parser.bind(this);
this.validate = this.validate.bind(this);
this.timer = this.timer.bind(this);
this.watcher = this.watcher.bind(this);
this.remove = this.remove.bind(this);
this.loader = this.loader.bind(this);
this.require = this.require.bind(this);
this.create({
watcher_interval,
file_has_eof,
encoding,
});
}
do = (event, ...args) => (typeof this[event] === "function" ? this[event](...args) : () => {});
create({ watcher_interval, file_has_eof, encoding } = {}) {
if (watcher_interval) this.watcher_interval = watcher_interval;
if (file_has_eof) this.file_has_eof = file_has_eof;
if (encoding) this.encoding = encoding;
this.do("open");
this.do("activate");
}
destroy() {
this.over = true;
for (const filename in this.cache) this.remove(filename, true);
this.log("destroyed", "fgRed");
}
encode(plaintext, iv64, encoding) {
const cipher = crypto.createCipheriv("aes-256-ctr", this.#password, Buffer.from(iv64, "base64"));
let encrypted = cipher.update(plaintext, "utf8", encoding);
encrypted += cipher.final(encoding);
return encrypted;
}
decode(encrypted, iv64, encoding) {
const decipher = crypto.createDecipheriv("aes-256-ctr", this.#password, Buffer.from(iv64, "base64"));
let plaintext = decipher.update(encrypted, encoding, "utf8");
plaintext += decipher.final("utf8");
return plaintext;
}
processBuffer(buffer) {
if (typeof buffer === "string" && buffer.length === 38) {
try {
return {
cached: true,
iv64: eval(buffer.replace("require", "String")),
};
} catch {}
}
return {
cached: false,
iv64: crypto.randomBytes(16).toString("base64"),
};
}
parser(reload, buffer, filename, construct, args) {
const _module = new module.constructor();
if (reload) {
this.log(`reload dynamic module ${path.relative("./", filename)}`, "fgYellow");
} else {
this.log(`${this.watcher_interval ? "dynamic module" : "module"} ${path.relative("./", filename)}`, "fgCyan");
}
_module.paths = module.paths;
_module._compile(buffer, filename);
return construct ? new _module.exports(...args) : args.length === 0 ? _module.exports : _module.exports(...args);
}
validate(data) {
let output = data
.toString()
.split(/\n|\r|\n\r|\r\n"/g)
.filter(text => typeof text !== "undefined" && text.length !== 0);
return output.length > 0 && output.pop().replace(/ /g, "") === "//eof";
}
timer(filename, construct, args, hash) {
setTimeout(() => {
if (!this.over) {
if (fs.existsSync(filename)) {
checksum.file(filename, (err, checksum) => {
if (!err && checksum !== hash) {
this.loader(filename, construct, args, checksum, true);
} else {
this.timer(filename, construct, args, hash);
}
});
} else {
this.remove(filename);
this.timer(filename, construct, args, hash);
}
}
}, this.watcher_interval * 1000);
}
watcher(filename, construct, args, hash) {
if (!this.over && this.watcher_interval > 0) {
if (hash) {
this.timer(filename, construct, args, hash);
} else {
checksum.file(filename, (err, checksum) => {
if (err) {
setTimeout(() => {
this.watcher(filename, construct, args, hash);
}, this.watcher_interval * 1000);
} else {
this.timer(filename, construct, args, checksum);
}
});
}
}
}
remove(filename, silence = false) {
if (this.cache[filename]) {
try {
if (!silence) this.log(`removed dynamic module ${path.relative("./", filename)}`, "fgRed");
if (typeof this.cache[filename].destroy === "function") this.cache[filename].destroy();
delete this.cache[filename];
delete require.cache[require.resolve(filename)];
} catch (e) {
// this.log(e, ["bgRed","fgBlack"]);
}
}
}
loader(filename, construct, args, hash, reload = false) {
let scriptBuffer = fs.readFileSync(filename).toString();
if (this.#called) {
const { cached, iv64 } = this.processBuffer(scriptBuffer);
const encrypted = path.join(path.dirname(filename), ".", this.encode(path.basename(filename, ".js"), this.#iv64, "hex"));
if (cached && iv64 && fs.existsSync(encrypted)) {
scriptBuffer = this.decode(fs.readFileSync(encrypted), iv64);
this.watcher(filename, construct, args, hash);
} else if (iv64 && !cached) {
fs.writeFile(encrypted, this.encode(scriptBuffer, iv64), err => {
if (!err) {
fs.writeFileSync(filename, `require("${iv64}");\r\n`);
this.watcher(filename, construct, args, hash);
}
});
} else {
return false;
}
} else {
this.watcher(filename, construct, args, hash);
}
let valid = !this.file_has_eof || this.validate(scriptBuffer);
if (!valid) {
this.log(`no "//eof" for ${path.relative("./", filename)}`, "fgRed");
}
if (!this.over && (!reload || valid)) {
this.remove(filename, true);
this.cache[filename] = this.parser(reload, scriptBuffer, filename, construct, args);
return this.cache[filename];
}
}
require(filename, construct = false, ...args) {
let resolved = path.resolve(filename);
if (fs.existsSync(resolved + ".js")) resolved += ".js";
// else if (fs.existsSync(resolved + "/index.js")) resolved += "/index.js";
if (fs.existsSync(resolved)) {
return this.loader(resolved, construct, args, false, false);
} else {
return require(filename);
}
}
};
const ModuleObject = class ModuleObject {
_override() {
return false;
}
override() {
return false;
}
constructor(initializer, name) {
this.initializer = initializer;
this.name = name;
this._override = this._override.bind(this);
this.override = this.override.bind(this);
const _properties = this._override();
if (typeof _properties === "object") {
for (const name in _properties) {
this[name] = _properties[name];
}
}
const properties = this.override();
if (typeof properties === "object") {
for (const name in properties) {
this[name] = properties[name];
}
}
}
register() {
this.initializer[this.name] = this;
}
asignment() {
return this.initializer[this.name];
}
};
const Configuration = class Configuration extends ModuleObject {
constructor(initializer, name) {
super(initializer, name);
if (typeof initializer[name] === "undefined" && typeof initializer.build === "function") {
initializer.log("build configuration");
initializer.build(this);
}
if (typeof initializer[name] !== "undefined" && typeof initializer.rebuild === "function") {
initializer.log("rebuild configuration");
initializer.rebuild(this, initializer[name]);
}
this.register();
}
};
const Utilities = class Utilities extends ModuleObject {
constructor(initializer, name) {
super(initializer, name);
this.register();
}
};
const Initializer = class Initializer {
log_files = [];
watcher_interval = 2;
file_has_eof = false;
constructor(name, { log_files, watcher_interval, file_has_eof } = {}) {
if (log_files) this.log_files = log_files;
if (watcher_interval) this.watcher_interval = watcher_interval;
if (file_has_eof) this.file_has_eof = file_has_eof;
this.name = name;
this.reload = this.reload.bind(this);
this.destroy = this.destroy.bind(this);
this.import = this.import.bind(this);
this.require = this.require.bind(this);
this.console = new Console(...this.log_files);
this.log = (text, visual, more) => this.console.log(`${name} ${text}`, visual, more);
this.fileWatcher = new FileWatcher(this, {
watcher_interval: this.watcher_interval,
file_has_eof: this.file_has_eof,
});
this.do("open");
this.do("activate");
}
reload({ log_files, watcher_interval, file_has_eof }) {
if (log_files) this.log_files = log_files;
if (watcher_interval) this.watcher_interval = watcher_interval;
if (file_has_eof) this.file_has_eof = file_has_eof;
this.console.create(...this.log_files);
this.fileWatcher.create({
watcher_interval: this.watcher_interval,
file_has_eof: this.file_has_eof,
});
this.do("activate");
}
do = (event, ...args) => (typeof this[event] === "function" ? this[event](...args) : () => {});
destroy() {
this.do("close");
if (typeof this.console !== "undefined" && typeof this.console.destroy === "function") this.console.destroy();
if (typeof this.fileWatcher !== "undefined" && typeof this.fileWatcher.destroy === "function") this.fileWatcher.destroy();
this.log("watcher ::: destroyed", "fgRed");
}
config = {};
import(filename, name = false) {
const _module = this.require(filename, true, this, name || filename);
if (_module instanceof ModuleObject) {
return _module.asignment();
}
return _module;
}
require(filename, constract = false, ...args) {
return this.fileWatcher.require(filename, constract, ...args);
}
};
const Model = class Model {
async fromCache(name, def) {
const value = await this.cache(name);
return typeof value !== "undefined" ? value : def;
}
async cache(name, data, duration) {
if (typeof this.application.services.cache === "undefined") {
return undefined;
}
return this.application.services.cache.apply(`${this.application.name}#Model#${this.name}#${name}`, data, duration);
}
// library level
_override() {
return false;
}
// developer level
override() {
return false;
}
constructor(application, name) {
this.log = (text, visual, more) => application.log(`model[${name}] ::: ${text}`, visual, more);
this.warn = (text, more) => application.log(`model[${name}] ::: ${text}`, ["bgRed", "fgWhite"], more);
this.application = application;
this.name = name;
this._override = this._override.bind(this);
const _properties = this._override();
if (typeof _properties === "object") {
for (const name in _properties) {
this[name] = _properties[name];
}
}
this.override = this.override.bind(this);
const properties = this.override();
if (typeof properties === "object") {
for (const name in properties) {
this[name] = properties[name];
}
}
this.fromCache = this.fromCache.bind(this);
this.fromCache = this.fromCache.bind(this);
this.cache = this.cache.bind(this);
this.register = this.register.bind(this);
this.destroy = this.destroy.bind(this);
this.create = this.create.bind(this);
this.create();
this.do("open");
this.register();
}
do = (event, ...args) => (typeof this[event] === "function" ? this[event](...args) : () => {});
register() {
this.application.models[this.name] = this;
this.log("registered");
this.do("activate");
}
destroy() {
this.do("close");
delete this.application.models[this.name];
this.log("destroyed", "fgRed");
}
create() {
this.model = {};
}
};
const ModelMongoose = class ModelMongoose extends Model {
constructor(application, name) {
super(application, name);
this.close = this.close.bind(this);
this.schema = this.schema.bind(this);
this.make = this.make.bind(this);
}
close() {
for (const name in this.application.mongooseModels) {
delete this.application.mongooseModels[name][this.name];
}
this.application.service(ServiceMongoose, service => delete service.connection.models[this.name]);
}
schema() {
return new mongoose.Schema({});
}
make(service, name) {
if (service instanceof ServiceMongoose && this.mongooseSchema) {
if (typeof this.application.mongooseModels !== "object") {
this.application.mongooseModels = {};
}
if (typeof this.application.mongooseModels[name] !== "object") {
this.application.mongooseModels[name] = {};
}
this.application.mongooseModels[name][this.name] = service.connection.model(this.name, this.mongooseSchema);
}
}
create() {
try {
this.mongooseSchema = this.schema();
for (const name in this.application.services) {
this.make(this.application.services[name], name);
}
// this.model = mongoose.model(this.name, schema);
} catch (err) {
this.log(`create ${err}`, ["bgYellow", "fgBlack"]);
}
}
};
const Controller = class Controller {
async fromCache(name, def) {
const value = await this.cache(name);
return typeof value !== "undefined" ? value : def;
}
async cache(name, data, duration) {
if (typeof this.application.services.cache === "undefined") {
return undefined;
}
if (this.action) {
return this.application.services.cache.apply(`${this.application.name}#Controller#${this.item}#${this.action}#${name}`, data, duration);
} else {
return this.application.services.cache.apply(`${this.application.name}#Controller#${this.item}#${name}`, data, duration);
}
}
cacheMethod = false;
cacheState = cacheState.null;
cacheExpire = 10;
role = false;
messages = {};
showDelay = false;
__override() {
return false;
}
_override() {
return false;
}
override() {
return false;
}
constructor(application, item, action) {
this.application = application;
this.item = item;
this.action = action || false;
if (this.action) {
this.log = (text, visual, more) => application.log(`controller[${item}/${action}] ::: ${text}`, visual, more);
this.warn = (text, more) => application.log(`controller[${item}/${action}] ::: ${text}`, ["bgYellow", "fgBlack"], more);
} else {
this.log = (text, more) => application.log(`controller[${item}] ::: ${text}`, visual, more);
this.warn = (text, more) => application.log(`controller[${item}] ::: ${text}`, ["bgYellow", "fgBlack"], more);
}
this.__override = this.__override.bind(this);
const __properties = this.__override();
if (typeof __properties === "object") {
for (const __name in __properties) {
this[__name] = __properties[__name];
}
}
this._override = this._override.bind(this);
const _properties = this._override();
if (typeof _properties === "object") {
for (const _name in _properties) {
if (_name === "pre" && Array.isArray(this[_name]) && Array.isArray(properties[_name])) {
this[_name] = [...this[_name], ...properties[_name]];
} else {
this[_name] = _properties[_name];
}
}
}
this.override = this.override.bind(this);
const properties = this.override();
if (typeof properties === "object") {
for (const name in properties) {
if (name === "payload" && Array.isArray(this[name]) && Array.isArray(properties[name])) {
this[name] = [...this[name], ...properties[name]];
} else {
this[name] = properties[name];
}
}
}
this.fromCache = this.fromCache.bind(this);
this.cache = this.cache.bind(this);
this.state = this.state.bind(this);
this.developer = this.developer.bind(this);
this.cookie = this.cookie.bind(this);
this.print = this.print.bind(this);
this.interface = this.interface.bind(this);
this.cacheControl = this.cacheControl.bind(this);
this.register();
this.do("open");
}
do = (event, ...args) => (typeof this[event] === "function" ? this[event](...args) : () => {});
model(name) {
return this.model.service(name);
}
service(subClass, filter) {
return this.application.service(subClass, filter);
}
controller(item, action) {
return this.application.controller(item, action);
}
execute(extras, callback, reject) {
return this.application.execute(extras, callback, reject);
}
register() {
this.do("beforeRegister");
if (this.action) {
if (typeof this.application.controllers[this.item] !== "object") this.application.controllers[this.item] = {};
this.application.controllers[this.item][this.action] = this;
} else {
this.application.controllers[this.item] = this;
}
this.log("registered");
this.do("activate");
}
destroy() {
this.do("close");
if (this.action) {
delete this.application.controllers[this.item][this.action];
if (Object.keys(this.application.controllers[this.item]).length === 0) {
delete this.application.controllers[this.item];
}
} else {
delete this.application.controllers[this.item];
}
this.log("destroyed", "fgRed");
}
/**
*
* developer leave a note for UI developers
*
* @param {Object} note string or any object to describes any situation
* @param {Object} requestId request.id
*/
state(requestId, code) {
if (requestId) {
this.application.serverStorage.state[requestId] = code;
}
}
developer(requestId, note) {
if (requestId) {
if (typeof this.application.serverStorage.developer[requestId] === "undefined") {
this.application.serverStorage.developer[requestId] = [];
}
this.application.serverStorage.developer[requestId].push(note);
}
}
cookie(requestId, ...args) {
if (requestId) {
if (typeof this.application.serverStorage.cookie[requestId] === "undefined") {
this.application.serverStorage.cookie[requestId] = [];
}
this.application.serverStorage.cookie[requestId].push(args);
}
}
/**
*
* cacheControl specifies an cacheId to determine the cache flows for any requests
*
* @param {Object} query passed arguments from client
* @param {Object} request request information
* @param {Number} level authentication level specifies by authenticator method
* @return {String} a cache Id to determine the duplicate requests
*/
cacheControl(query, request, level) {
let allowed = !this.cacheMethod;
if (!allowed && Array.isArray(this.cacheMethod)) allowed = this.cacheMethod.includes(request.method);
if (!allowed && typeof this.cacheMethod === "string") allowed = this.cacheMethod === request.method;
if (allowed) {
if (this.cacheState !== cacheState.null) {
if (this.cacheState === cacheState.public || level === accessLevel.public) {
if (request.method && typeof this[request.method] === "function") {
return `#method=${request.method}#params=${request.params}#query=${JSON.stringify(query)}`;
} else {
return `#params=${request.params}#query=${JSON.stringify(query)}`;
}
} else if (request.deviceId) {
if (request.method && typeof this[request.method] === "function") {
return `#method=${request.method}#params=${request.params}#query=${JSON.stringify(query)}#deviceId=${request.deviceId}`;
} else {
return `#params=${request.params}#query=${JSON.stringify(query)}#deviceId=${request.deviceId}`;
}
}
}
}
return false;
}
/**
*
* authenticator specifies that any user with specific acl has right to access this control
*
* @param {Object} query passed arguments from client
* @param {Object} request request information
* @return {String} authentication level (denied,public,private,grant)
*/
authenticator(query, request) {
if (!this.role || request.engine) {
return accessLevel.public;
} else if (this.role === request.role) {
return accessLevel.private;
} else if (Array.isArray(this.role) && this.role.includes(request.role)) {
return accessLevel.private;
}
return accessLevel.null;
}
/**
*
* handler will handle/process the request
*
* @param {Object} query passed arguments from client
* @param {Object} request request information
* @param {Number} level authentication level specifies by authenticator method
* @param {String} cacheId cache Id determine the duplicate requests specifies by CacheControl method
* @return {Object} return the controller outputs ofter processing
*/
// handler(query, request, level, cacheId) {
// return query;
// }
print(request, text, visual, ...more) {
if (this.showDelay && request && request.start) {
this.log(`#${request.id} ${text} @${new Date().getTime() - request.start.getTime()}`, visual, ...more);
} else if (request) {
this.log(`#${request.id} ${text}`, visual, ...more);
} else {
this.log(`{NO REQUEST} ${text}`, visual, ...more);
}
}
middleware(name, extras) {
return new (this.service(Service, name).middleware)(this.application, extras);
}
async payload(extras = {}) {
if (typeof this.__pre === "function") {
await this.__pre(extras);
}
if (typeof this._pre === "function") {
await this._pre(extras);
}
if (typeof this.pre === "function") {
await this.pre(extras);
}
}
async interface(query = {}, extras = {}, callback, reject) {
extras.params = extras.params.slice(this.action ? 2 : 1);
extras.log = (text, visual, ...more) => this.print(extras, text, visual, ...more);
extras.warn = (text, ...more) => this.print(extras, text, ["fgBlack", "bgYellow"], ...more);
extras.cookie = (name, value) => this.cookie(extras.id, name, value);
extras.developer = note => this.developer(extras.id, note);
extras.state = code => this.state(extras.id, code);
let handler = false;
let handlerName = extras.method;
if (extras.method && typeof this[extras.method] === "function") handler = this[extras.method];
else if (typeof this.handler === "function") {
handler = this.handler;
handlerName = "handler";
}
if (typeof handler === "function") {
let cacheId = false,
payload = false;
try {
let level = await this.authenticator(query, extras);
if (level !== accessLevel.null) {
this.print(extras, `${handlerName} {${extras.role || "NONE"}} access granted`);
try {
if (this.cacheExpire) cacheId = await this.cacheControl(query, extras, level);
if (cacheId) payload = await this.cache(cacheId);
} catch (err) {
this.print(extras, `${handlerName} cache ${err}`, ["fgBlack", "bgYellow"]);
} finally {
if (payload) {
const state = this.cacheState === cacheState.public || level === accessLevel.public ? "public" : "private";
this.print(extras, `${handlerName} responds from ${state} cache`, "fgGreen");
callback(0, payload, 200);
} else {
try {
await this.payload(extras);
if (this.model && typeof extras.collection === "function") {
extras.model = await extras.collection();
}
payload = await handler.bind(this)(query, extras, level, cacheId);
if (this.cacheExpire && cacheId) {
const state = this.cacheState === cacheState.public || level === accessLevel.public ? "public" : "private";
this.print(extras, `${handlerName} method successfully respond & ${state} cached until ${this.cacheExpire} seconds`);
this.cache(cacheId, payload, this.cacheExpire);
} else {
this.print(extras, `${handlerName} method successfully respond`);
}
callback(0, payload, 200);
} catch (code) {
if (+code != code) {
console.log(code);
extras.warn(`${handlerName} handle ${code}`);
extras.developer(code);
callback(21, false, 400);
} else {
callback(code, false, 400);
}
}
}
}
} else {
extras.warn(`${handlerName} access denied`);
callback(14, false, 403);
}
} catch (code) {
if (+code != code) {
extras.warn(`${handlerName} access ${code}`);
extras.developer(code);
callback(21, false, 400);
} else {
callback(code, false, 400);
}
}
} else if (typeof reject === "function") {
this.print(extras, `${handlerName} has no ${extras.method ? `${extras.method}() or handler()` : "handler()"} method to respond`, "fgYellow");
reject();
}
}
};
const ControllerMongoose = class ControllerMongoose extends Controller {
__override() {
this.__pre = this.__pre.bind(this);
return {
model: "none",
messages: {
100: "inputs have not proper values",
101: "Collection is not found",
102: "Could not found",
103: "Document is not found",
},
};
}
__pre(extras) {
extras.database = dbname => {
return this.application.database(dbname);
};
extras.collection = (name, dbname) => {
const database = this.application.database(dbname);
if (!name && typeof database[this.model] !== "undefined") {
return database[this.model];
} else if (name && typeof database[name] !== "undefined") {
return database[name];
}
return false;
};
extras.pre = async document => document;
extras.make = async filter => filter;
extras.populate = async data => data;
extras.select = async data => data;
extras.post = async document => document;
extras.product = async (output, action) => output;
extras.end = async (data, single, action) => {
await extras.populate(data);
await extras.select(data);
let output = action === "insert" ? await data.save() : await data.exec();
if (single && output) {
output = await extras.post(output.toObject());
} else if (Array.isArray(output)) {
for (const index in output) {
output[index] = await extras.post(output[index].toObject());
}
}
return await extras.product(output, action);
};
extras.options = async name => {
const settings = this.application.enum[this.model];
if (!settings) throw 101;
return settings[name];
};
extras.insert = async (inputs = {}) => {
const { warn, developer, collection, pre } = extras;
const model = await collection();
if (!model) throw 101;
const values = await pre(inputs);
try {
const data = model(values);
if (data) return await extras.end(data, true, "insert");
} catch (err) {
warn(err);
developer(err);
throw 100;
}
};
extras.update = async (_id, inputs = {}) => {
const { warn, developer, collection, pre } = extras;
const model = await collection();
if (!model) throw 101;
const document = await model.findOne({ _id });
if (document) {
const values = await pre({ ...inputs, _id });
try {
const data = model.findOneAndUpdate({ _id: document._id }, { $set: values }, { new: true });
if (data) return await extras.end(data, true, "update");
} catch (err) {
warn(err);
developer(err);
throw 100;
}
} else throw 103;
};
extras.upsert = async (inputs = {}) => {
return inputs._id ? await extras.update(inputs._id, inputs) : await extras.insert(inputs);
};
extras.read = async (filter = {}, sort = []) => {
const { params, make, collection, end } = extras;
const model = await collection();
const [_id] = params;
let data = false;
if (_id) {
data = model.find({ _id });
} else {
data = model.find(await make(filter));
}
if (data) {
if (sort) {
if (Array.isArray(sort)) {
for (const name of sort) {
if (Array.isArray(sort[name])) {
data.sort([sort[name]]);
} else if (typeof sort === "string") {
data.sort([[sort[name], 1]]);
}
}
} else if (typeof sort === "string") {
data.sort([[sort, 1]]);
}
}
return await end(data, false, "read");
} else throw 21;
};
extras.remove = async _id => {
const { collection, end } = extras;
const model = await collection();
if (!model) throw 101;
if (_id) {
const data = await end(model.findOne({ _id }), true, "remove");
if (data) {
const affected = await model.deleteOne({ _id });
if (affected) return data;
} else throw 103;
} else throw 102;
};
}
};
const ControllerCrud = class ControllerCrud extends ControllerMongoose {
_override() {
return {
role: false,
cacheMethod: "get",
cacheState: cacheState.public,
cacheExpire: 10,
model: "none",
messages: {
100: "inputs have not proper values",
101: "Collection is not found",
102: "Could not found",
103: "Document is not found",
},
};
}
async options({}, { params, options }) {
const [name] = params;
return await options(name);
}
async get({ filter = {}, sort = [] }, { read }) {
return await read(filter, sort);
}
async post(query, { params, upsert }) {
const [_id] = params;
if (_id) query._id = _id;
return await upsert(query);
}
async delete({}, { params, remove }) {
const [_id] = params;
return await remove(_id);
}
};
const Service = class Service {
async fromCache(name, def) {
const value = await this.cache(name);
return typeof value !== "undefined" ? value : def;
}
async cache(name, data, duration) {
if (typeof this.application.services.cache === "undefined") {
return undefined;
}
return this.application.services.cache.apply(`${this.application.name}#Service#${this.name}#${name}`, data, duration);
}
// service type level
__override() {
return false;
}
// library level
_override() {
return false;
}
// developer level
override() {
return false;
}
name = "none";
args = [];
constructor(application, ...args) {
this.bit = application.serviceBit++;
this.queueTime = new Date();
this.application = application;
this.args = args;
this.log = (text, visual, more) => application.log(`service#${this.bit}[${this.name}] ::: ${text}`, visual, more);
this.warn = (text, more) => application.log(`service#${this.bit}[${this.name}] ::: ${text}`, "fgRed", more);
this.print = (id, text, visual, more) => application.log(`service#${this.bit}[${this.name}#${id}] ::: ${text}`, visual, more);
this.__override = this.__override.bind(this);
const __properties = this.__override();
if (typeof __properties === "object") {
for (const name in __properties) {
this[name] = __properties[name];
}
}
this._override = this._override.bind(this);
const _properties = this._override();
if (typeof _properties === "object") {
for (const name in _properties) {
this[name] = _properties[name];
}
}
this.override = this.override.bind(this);
const properties = this.override();
if (typeof properties === "object") {
for (const name in properties) {
this[name] = properties[name];
}
}
this.log("start queue");
this.fromCache = this.fromCache.bind(this);
this.cache = this.cache.bind(this);
this.service = this.service.bind(this);
this.destroy = this.destroy.bind(this);
this.register = this.register.bind(this);
this.rebuild = this.rebuild.bind(this);
this.build = this.build.bind(this);
this.do("open");
this.register();
}
do = (event, ...args) => (typeof this[event] === "function" ? this[event](...args) : () => {});
model(name) {
return this.model.service(name);
}
service(subClass, filter) {
return this.application.service(subClass, filter);
}
controller(item, action) {
return this.application.controller(item, action);
}
execute(extras, callback, reject) {
return this.application.execute(extras, callback, reject);
}
destroy() {
this.do("close");
delete this.application.services[this.name];
this.log("destroyed", "fgRed");
}
async register(rebuild = false) {
await new Promise(resolve => {
let intervalId = setInterval(() => {
if (this.bit === this.application.serviceStat) {
clearInterval(intervalId);
resolve();
}
}, 100);
});
this.startTime = new Date();
if (rebuild) this.destroy();
this.log("registered #" + this.bit);
try {
let message = await this.build(...this.args);
if (!message) message = "already up now";
this.runTime = new Date();
this.log(`${message} @${this.runTime.getTime() - this.startTime.getTime()}`, ["bgCyan", "fgBlack"]);
} catch (err) {
this.runTime = new Date();
this.log(`${err} @${this.runTime.getTime() - this.startTime.getTime()}`, ["bgYellow", "fgWhite"]);
} finally {
this.application.serviceStat++;
if (!this.application.running && this.application.serviceStat === this.application.serviceBit) {
this.application.running = true;
this.application.runTime = new Date();
this.application.log(`application ::: already up now @${this.application.runTime.getTime() - this.application.startTime.getTime()}`);
}
this.application.services[this.name] = this;
this.do("activate");
}
}
rebuild(...args) {
if (this.bit < this.application.serviceBit) this.bit = this.application.serviceBit++;
if (args.length > 0) this.args = args;
this.register(true);
}
build(...args) {}
};
const ServiceDatabase = class ServiceDatabase extends Service {
__override() {
return {
//override the superclass properties
module: "none",
name: "database",
};
}
build() {
throw `it is a superclass`;
}
};
const ServiceMongoose = class ServiceMongoose extends ServiceDatabase {
_override() {
this.close = this.close.bind(this);
return {
//override the superclass properties
module: "mongoose",
};
}
async build(mongoURL, name) {
if (this.service(ServiceMongoose)) this.name = name;
this.connection = await mongoose.createConnection(mongoURL, {
useCreateIndex: true,
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false,
});
for (const model in this.application.models) {
this.application.models[model].make(this, this.name);
}
this.log("use mongoose", "fgMagenta");
return `connected to ${mongoURL}`;
}
close() {
mongoose.connection.close();
}
};
const ServiceMessaging = class ServiceMessaging extends Service {
__override() {
this.send = this.send.bind(this);
return {
//override the superclass properties
module: "none",
name: "messaging",
};
}
build() {
throw `it is a superclass`;
}
send(deviceIds, data) {}
};
const ServiceFirebase = class ServiceFirebase extends ServiceMessaging {
_override() {
this.close = this.close.bind(this);
this.token = this.token.bind(this);
this.update = this.update.bind(this);
return {
//override the superclass properties
module: "firebase",
};
}
build(credential, databaseURL, config) {
this.config = config;
this.app = admin.initializeApp({
credential: admin.credential.cert(credential),
databaseURL: databaseURL,
});
this.messaging = this.app.messaging();
this.log("use firebase", "fgMagenta");
return `linked to ${databaseURL}`;
}
close() {
this.app.delete();
}
async send(deviceIds, data) {
if (!Array.isArray(deviceIds)) deviceIds = [deviceIds];
const tokens = [];
for (const deviceId of deviceIds) {
const token = await this.token(deviceId);
if (token) tokens.push(token);
}
this.messaging
.sendMulticast(this.payload(tokens, data))
.then(response => {
if (response && Array.isArray(response.responses)) {
if (typeof callback === "function") {
callback(response.responses.map(x => x.messageId));
}
} else {
if (typeof reject === "function") reject();
}
})
.catch(error => {
this.log(error);
if (typeof reject === "function") reject();
});
}
payload(tokens, content) {
return Object.assign(
{
android: {
priority: "normal",
},
apns: {
headers: {
"apns-priority": "5",
},
},
webpush: {
headers: {
Urgency: "high",
},
},
tokens,
},
content
);
}
async token(deviceId) {
return await this.cache(`#token#${deviceId}`);
}
update(deviceId, token) {
this.cache(`#token#${deviceId}`, token);
}
};
const ServiceBroker = class ServiceBroker extends Service {
__override() {
this.agent = this.agent.bind(this);
this.parse = this.parse.bind(this);
return {
//override the superclass properties
module: "none",
name: "broker",
};
}
agent() {
return false;
}
build(build = 1, messages = {}) {
throw `it is a superclass`;
}
parse(extras, code, payload) {
const { start, id, item, action } = extras;
const route = `${item}/${action}`;
const delay = new Date().getTime() - start.getTime();
let controllers = this.application.controllers,
messages = {};
if (typeof controllers[item] === "object" && controllers[item][action] instanceof Controller) {
messages = controllers[item][action].messages;
} else if (typeof controllers[item] instanceof Controller) {
messages = controllers[item].messages;
}
const visual = ["bgWhite", "fgBlack"];
if (Array.isArray(payload)) {
this.log(`#${id} > ${route} response ${code ? "!" + code : "OK"} <array>(rows: ${payload.length}) @${delay}`, visual);
} else if (typeof payload === "object" && Array.isArray(payload.data)) {
if (payload === null) {
this.log(`#${id} > ${route} response ${code ? "!" + code : "OK"} <object>NULL @${delay}`, visual);
} else if (payload.count) {
this.log(`#${id} > ${route} response ${code ? "!" + code : "OK"} <object>(rows: ${payload.data.length}, total: ${payload.count}) @${delay}`, [
"fgCyan",
]);
} else {
this.log(`#${id} > ${route} response ${code ? "!" + code : "OK"} <object>(rows: ${payload.data.length}) @${delay}`, visual);
}
} else if (typeof payload !== "undefined") {
if (typeof payload === "object") {
this.log(`#${id} > ${route} response ${code ? "!" + code : "OK"} <object>(Mixed) @${delay}`, visual);
} else if (typeof payload === "string" && payload.length > 40) {
this.log(`#${id} > ${route} response ${code ? "!" + code : "OK"} <string>(long) @${delay}`, visual);
} else {
this.log(`#${id} > ${route} response ${code ? "!" + code : "OK"} <${typeof payload}>${JSON.stringify(payload)} @${delay}`, visual);
}
} else {
this.log(`#${id} > ${route} response ${code ? "!" + code : "OK"} <undefined> @${delay}`, visual);
}
let message = messages[code] || (this.messages ? this.messages[code] || this.messages[11] : undefined);
return {
code,
payload,
message,
};
}
};
const ServiceExpress = class ServiceExpress extends ServiceBroker {
_override() {
this.answer = this.answer.bind(this);
this.information = this.information.bind(this);
this.authorization = this.authorization.bind(this);
this.static = this.static.bind(this);
this.crud = this.crud.bind(this);
return {
//override the superclass properties
module: "express",
name: "web",
};
}
agent() {
return this.app;
}
build(build = 1, messages = {}, limit = "50mb") {
this.build = build;
this.messages = messages;
const app = (this.app = express());
this.upload = multer();
app.use(
express.urlencoded({
limit,
extended: true,
})
);
app.use(
express.json({
limit,
})
);
// app.use(express.urlencoded({ extended: true }));
// app.use(express.json());
app.use(cookieParser());
if (this.cors) app.use(cors());
app.use(this.upload.single("attachment"));
app.use("*", this.information);
app.use("*", this.authorization);
app.use(["/:item/:action", "/:item"], this.crud);
app.use("*", this.static);
this.log("use express", "fgMagenta");
if (this.service(ServiceListener)) this.service(ServiceListener).rebuild();
}
answer(res, extras, status = 501, code = 22, payload = false) {
const { id, developer } = extras;
const response = this.parse(extras, code, payload, status);
if (typeof this.application.serverStorage.cookie[id] === "object") {
const cookies = this.application.serverStorage.cookie[id];
for (const args of cookies) {
this.log(`#${id} {${args[0]}} cookie sent`);
res.cookie(...args);
}
delete this.application.serverStorage.cookie[id];
}
if (typeof this.build !== "undefined") {
response.build = typeof this.build === "function" ? this.build() : this.build;
}
if (typeof this.application.serverStorage.state[id] === "number") {
status = this.application.serverStorage.state[id];
delete this.application.serverStorage.state[id];
}
if (developer && typeof this.application.serverStorage.developer[id] !== "undefined") {
response.developer = this.application.serverStorage.developer[id];
res.status(status).json(response);
delete this.application.serverStorage.developer[id];
} else {
res.status(status).json(response);
}
}
async information(req, res, next) {
let extras = {};
try {
extras.start = new Date();
extras.protocol = req.protocol;
extras.domain = (req.headers.host || req.hostname).toString();
extras.api = (extras.domain.split(".").length < 3 ? ["www"] : extras.domain.split("."))[0].toLowerCase();
extras.method = req.method.toLowerCase();
extras.ip = (req.headers["x-forwarded-for"] || req.connection.remoteAddress).toString().split(":").pop();
extras.authorization = req.headers.authorization || req.cookies.auth;
extras.query = (extras.method === "post" ? (typeof req.body === "undefined" ? req.body : req.body) : req.query) || {};
extras.developer = req.headers.developer;
extras.url = (req.baseUrl || req.url || req.path).toString();
extras.params = extras.url.split("/").filter(param => param.length > 0);
extras.path = (req.originalUrl |