push from local

This commit is contained in:
SrGooglo 2024-03-10 00:25:35 +00:00
parent c011f2353f
commit 6d553830ab
55 changed files with 899 additions and 676 deletions

0
.DS_Store vendored Normal file → Executable file
View File

0
.experimental Normal file → Executable file
View File

0
.npmignore Normal file → Executable file
View File

2
bootstrap.js vendored Normal file → Executable file
View File

@ -67,7 +67,7 @@ global.toBoolean = (value) => {
}
async function injectEnvFromInfisical() {
const envMode = global.FORCE_ENV ?? global.isProduction ? "prod" : "dev"
const envMode = "dev"
console.log(`🔑 Injecting env variables from INFISICAL in [${envMode}] mode...`)

View File

@ -1,9 +0,0 @@
{
"name": "example_server",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "hermes-node ./src/index.js"
}
}

View File

@ -1,17 +0,0 @@
export default {
method: "get",
route: "/fail",
fn: async (req, res) => {
throw new Error("Testing catch handler")
return res.json({
message: "This is not supposed to be here!"
})
},
onCatch: async (err, req, res) => {
return res.json({
message: "Catch handler works!",
error: err.message,
})
}
}

View File

@ -1,9 +0,0 @@
export default {
method: "get",
route: "/",
fn: async (req, res) => {
return res.json({
message: "Hello world!"
})
}
}

View File

@ -1,13 +0,0 @@
export default {
method: "get",
route: "/middlewaresTest",
middlewares: ["test", (req, res, next) => {
console.log("Hello from inline middleware 2")
next()
}],
fn: async (req, res) => {
return res.json({
message: "Hello world! Look at the console for the middlewares!"
})
}
}

View File

@ -1,9 +0,0 @@
export default {
method: "get",
route: "/withoutClass",
fn: async (req, res) => {
return res.json({
message: "Im an object endpoint",
})
}
}

View File

@ -1,13 +0,0 @@
import { Endpoint } from "../../../../../src/server"
export default class TestOneEndpoint extends Endpoint {
static method = "get"
static route = "/one"
fn = async (req, res) => {
return res.json({
message: "Hello world! Im using Endpoint class!"
})
}
}

View File

@ -1,8 +0,0 @@
import { Controller } from "../../../../src/server"
import generateEndpointsFromDir from "../../../../src/server/lib/generateEndpointsFromDir"
export default class TestController extends Controller {
static useRoute = "/test"
httpEndpoints = generateEndpointsFromDir(__dirname + "/endpoints")
}

View File

@ -1 +0,0 @@
export { default as TestController } from "./TestController"

View File

@ -1,13 +0,0 @@
import { registerBaseAliases, Server } from "../../src/server"
registerBaseAliases()
const server = new Server({
name: "example_server",
listen_port: 3011,
},
require("@controllers"),
require("@middlewares"),
)
server.initialize()

View File

@ -1,6 +0,0 @@
export default {
"test": (req, res, next) => {
console.log("Hello loaded middleware 1")
next()
}
}

View File

@ -1,6 +1,6 @@
{
"name": "linebridge",
"version": "0.16.0",
"version": "0.18.0",
"description": "A simple, fast, and powerful REST API interface library",
"author": "RageStudio",
"main": "./dist/client/index.js",
@ -13,12 +13,14 @@
"access": "public"
},
"files": [
"src/**/**",
"dist/**/**",
"./package.json"
],
"license": "MIT",
"dependencies": {
"@foxify/events": "^2.1.0",
"@gullerya/object-observer": "^6.1.3",
"@socket.io/cluster-adapter": "^0.2.2",
"@socket.io/redis-adapter": "^8.2.1",
"@socket.io/redis-emitter": "^5.1.0",

0
src/client/bridge.js Normal file → Executable file
View File

0
src/client/classes/WSInterface/index.js Normal file → Executable file
View File

0
src/client/classes/index.js Normal file → Executable file
View File

0
src/client/controller.js Normal file → Executable file
View File

0
src/client/lib/generateHTTPRequestDispatcher/index.js Normal file → Executable file
View File

0
src/client/lib/generateWSRequestDispatcher/index.js Normal file → Executable file
View File

0
src/client/lib/index.js Normal file → Executable file
View File

0
src/lib/event_emitter/index.js Normal file → Executable file
View File

View File

@ -0,0 +1,23 @@
import path from "node:path"
import Endpoint from "../classes/endpoint"
import defaults from "../defaults"
const projectPkg = require(path.resolve(process.cwd(), "package.json"))
export default class MainEndpoint extends Endpoint {
route = "/"
get = async (req, res) => {
const { params } = this.ctx
return {
name: params.name ?? "unknown",
version: projectPkg.version ?? "unknown",
engine: params.useEngine ?? "unknown",
request_time: new Date().getTime(),
lb_version: defaults.version ?? "unknown",
experimental: defaults.isExperimental.toString() ?? "unknown",
}
}
}

View File

@ -0,0 +1,24 @@
import Endpoint from "../classes/endpoint"
export default class MainEndpoint extends Endpoint {
route = "/_map"
get = async (req, res) => {
const httpMap = Object.entries(this.ctx.engine.router.map).reduce((acc, [route, { method, path }]) => {
if (!acc[method]) {
acc[method] = []
}
acc[method].push({
route: path
})
return acc
}, {})
return res.json({
http: httpMap,
websocket: []
})
}
}

View File

@ -0,0 +1,155 @@
import { EventEmitter } from "@foxify/events"
export default class IPCClient {
constructor(self, _process) {
this.self = self
this.process = _process
this.process.on("message", (msg) => {
if (typeof msg !== "object") {
// not an IPC message, ignore
return false
}
const { event, payload } = msg
if (!event || !event.startsWith("ipc:")) {
return false
}
if (event.startsWith("ipc:exec")) {
return this.handleExecution(payload)
}
if (event.startsWith("ipc:akn")) {
return this.handleAcknowledgement(payload)
}
})
}
eventBus = new EventEmitter()
handleExecution = async (payload) => {
let { id, command, args, from } = payload
let fn = this.self.ipcEvents[command]
if (!fn) {
this.process.send({
event: `ipc:akn:${id}`,
payload: {
target: from,
from: this.self.constructor.refName,
id: id,
error: `IPC: Command [${command}] not found`,
}
})
return false
}
try {
let result = await fn(this.self.contexts, ...args)
this.process.send({
event: `ipc:akn:${id}`,
payload: {
target: from,
from: this.self.constructor.refName,
id: id,
result: result,
}
})
} catch (error) {
this.process.send({
event: `ipc:akn:${id}`,
payload: {
target: from,
from: this.self.constructor.refName,
id: id,
error: error.message,
}
})
}
}
handleAcknowledgement = async (payload) => {
let { id, result, error } = payload
this.eventBus.emit(`ipc:akn:${id}`, {
id: id,
result: result,
error: error,
})
}
// call a command on a remote service, and waits to get a response from akn (async)
call = async (to_service_id, command, ...args) => {
const remote_call_id = Date.now()
const response = await new Promise((resolve, reject) => {
try {
this.process.send({
event: "ipc:exec",
payload: {
target: to_service_id,
from: this.self.constructor.refName,
id: remote_call_id,
command,
args,
}
})
this.eventBus.once(`ipc:akn:${remote_call_id}`, resolve)
} catch (error) {
console.error(error)
reject(error)
}
}).catch((error) => {
return {
error: error
}
})
if (response.error) {
throw new OperationError(500, response.error)
}
return response.result
}
// call a command on a remote service, but return it immediately
invoke = async (to_service_id, command, ...args) => {
const remote_call_id = Date.now()
try {
this.process.send({
event: "ipc:exec",
payload: {
target: to_service_id,
from: this.self.constructor.refName,
id: remote_call_id,
command,
args,
}
})
return {
id: remote_call_id
}
} catch (error) {
console.error(error)
return {
error: error
}
}
}
}

View File

@ -0,0 +1,54 @@
export default class IPCRouter {
processes = []
register = (service) => {
service.instance.on("message", (msg) => {
if (typeof msg !== "object") {
// not an IPC message, ignore
return false
}
const { event, payload } = msg
if (!event || !event.startsWith("ipc:")) {
// not an IPC message, ignore
return false
}
const { target } = payload
if (!target) {
return false
}
if (event.startsWith("ipc:")) {
return this.route(event, payload)
}
})
this.processes.push(service)
}
unregister = (service) => {
this.processes = this.processes.filter((_process) => _process.id !== service.id)
}
route = (event, payload) => {
const { target, from } = payload
// first search service
let targetService = this.processes.find((_process) => _process.id === target)
if (!targetService) {
// TODO: respond with error
console.error(`[IPC:ROUTER] Service [${destinationId}] not found`)
return false
}
targetService.instance.send({
event: event,
payload: payload
})
}
}

0
src/server/classes/controller/index.js Normal file → Executable file
View File

73
src/server/classes/endpoint/index.js Normal file → Executable file
View File

@ -1,3 +1,74 @@
module.exports = class Endpoint {
export default class Endpoint {
constructor(ctx, params = {}) {
this.ctx = ctx
this.params = params
this.route = this.constructor.route ?? this.params.route
this.enabled = this.constructor.enabled ?? this.params.enabled ?? true
this.middlewares = [
...this.middlewares ?? [],
...this.params.middlewares ?? [],
]
if (this.params.handlers) {
for (const method of this.ctx.valid_http_methods) {
if (typeof this.params.handlers[method] === "function") {
this[method] = this.params.handlers[method]
}
}
}
this.selfRegister()
return this
}
createHandler(fn) {
return async (req, res) => {
try {
const result = await fn(req, res)
if (result) {
return res.json(result)
}
} catch (error) {
if (error instanceof OperationError) {
return res.status(error.code).json({
"error": error.message
})
}
console.error({
message: "Unhandled route error:",
description: error.stack,
})
return res.status(500).json({
"error": error.message
})
}
}
}
selfRegister = async () => {
const validMethods = this.ctx.valid_http_methods
for await (const method of validMethods) {
const methodHandler = this[method]
if (typeof methodHandler !== "undefined") {
const fn = this.createHandler(this[method].fn ?? this[method])
this.ctx.register.http(
{
method,
route: this.route,
middlewares: this.middlewares,
fn: fn,
},
)
}
}
}
}

0
src/server/classes/index.js Normal file → Executable file
View File

View File

@ -0,0 +1,6 @@
export default class OperationError {
constructor(code, message) {
this.code = code ?? 500
this.message = message
}
}

0
src/server/classes/rtengine/index.js Normal file → Executable file
View File

65
src/server/defaults.js Normal file
View File

@ -0,0 +1,65 @@
const path = require("path")
const fs = require("fs")
const os = require("os")
const packageJSON = require(path.resolve(module.path, "../../package.json"))
function getHostAddress() {
const interfaces = os.networkInterfaces()
for (const key in interfaces) {
const iface = interfaces[key]
for (let index = 0; index < iface.length; index++) {
const alias = iface[index]
if (alias.family === "IPv4" && alias.address !== "127.0.0.1" && !alias.internal) {
return alias.address
}
}
}
return "0.0.0.0"
}
export default {
isExperimental: fs.existsSync(path.resolve(module.path, "../../.experimental")),
version: packageJSON.version,
localhost_address: getHostAddress() ?? "localhost",
params: {
urlencoded: true,
engine: "express",
http_protocol: "http",
ws_protocol: "ws",
},
headers: {
"Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS, PUT, PATCH, DELETE, DEL",
"Access-Control-Allow-Credentials": "true",
},
middlewares: {
cors: require("./middlewares/cors").default,
logs: require("./middlewares/logger").default,
},
useMiddlewares: [
"cors",
"logs",
],
controllers: [],
fixed_http_methods: {
"del": "delete",
},
valid_http_methods: [
"get",
"post",
"put",
"patch",
"del",
"delete",
"trace",
"head",
"any",
"options",
"ws",
],
}

View File

@ -0,0 +1,35 @@
import { createServer } from "node:http"
import express from "express"
import socketio from "socket.io"
import rtengine from "../../classes/rtengine"
export default class Engine {
constructor(params) {
this.params = params
}
http = null
app = null
io = null
ws = null
router = express.Router()
init = async (params) => {
this.app = express()
this.http = createServer(this.app)
this.io = new socketio.Server(this.http)
this.ws = new rtengine({
...params,
io: this.io,
http: false,
})
this.app.use(express.json())
this.app.use(express.urlencoded({ extended: true }))
}
listen = async () => {
await this.http.listen(this.params.listen_port)
}
}

View File

@ -0,0 +1,30 @@
import he from "hyper-express"
export default class Engine {
constructor(params) {
this.params = params
}
app = new he.Server()
router = new he.Router()
init = async (params) => {
// register 404
await this.router.any("*", (req, res) => {
return res.status(404).json({
code: 404,
message: "Not found"
})
})
// register body parser
await this.app.use(async (req, res, next) => {
req.body = await req.urlencoded()
})
}
listen = async () => {
await this.app.listen(this.params.listen_port)
}
}

View File

@ -0,0 +1,126 @@
import { EventEmitter } from "@foxify/events"
class WorkerEngineRouter {
routes = []
get = (path, ...execs) => {
}
post = (path, ...execs) => {
}
delete = (path, ...execs) => {
}
put = (path, ...execs) => {
}
patch = (path, ...execs) => {
}
head = (path, ...execs) => {
}
options = (path, ...execs) => {
}
any = (path, ...execs) => {
}
use = (path, ...execs) => {
}
}
class WorkerEngine {
static ipcPrefix = "rail:"
selfId = process.env.lb_service.id
router = new WorkerEngineRouter()
eventBus = new EventEmitter()
perExecTail = []
initialize = async () => {
console.error(`[WorkerEngine] Worker engine its not implemented yet...`)
process.on("message", this.handleIPCMessage)
}
listen = async () => {
console.log(`Sending Rail Register`)
process.send({
type: "rail:register",
data: {
id: process.env.lb_service.id,
pid: process.pid,
routes: this.router.routes,
}
})
}
handleIPCMessage = async (msg) => {
if (typeof msg !== "object") {
// ignore, its not for us
return false
}
if (!msg.event || !msg.event.startsWith(WorkerEngine.ipcPrefix)) {
return false
}
const { event, payload } = msg
switch (event) {
case "rail:request": {
const { req } = payload
break
}
case "rail:response": {
}
}
}
use = (fn) => {
if (fn instanceof WorkerEngineRouter) {
this.router = fn
return
}
if (fn instanceof Function) {
this.perExecTail.push(fn)
return
}
}
}
export default class Engine {
constructor(params) {
this.params = params
}
app = new WorkerEngine()
router = new WorkerEngineRouter()
init = async () => {
await this.app.initialize()
}
listen = async () => {
await this.app.listen()
}
}

View File

@ -1,115 +1,7 @@
const path = require("path")
const fs = require("fs")
const net = require("corenode/net")
const experimentalFlag = path.resolve(module.path, "../../.experimental")
const packageJSON = require(path.resolve(module.path, "../../package.json"))
const moduleAlias = require("module-alias")
// set globals variables
global.LINEBRIDGE_SERVER_EXPERIMENTAL = fs.existsSync(experimentalFlag)
global.LINEBRIDGE_SERVER_VERSION = packageJSON.version
global.LOCALHOST_ADDRESS = net.ip.getHostAddress() ?? "localhost"
global.FIXED_HTTP_METHODS = {
"del": "delete"
}
global.VALID_HTTP_METHODS = [
"get",
"post",
"put",
"patch",
"del",
"delete",
"trace",
"head",
"any",
"options",
"ws"
]
global.DEFAULT_HEADERS = {
"Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept, Authorization",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS, PUT, PATCH, DELETE, DEL",
"Access-Control-Allow-Credentials": "true",
}
global.DEFAULT_SERVER_PARAMS = {
urlencoded: true,
engine: "express",
http_protocol: "http",
ws_protocol: "ws",
}
global.DEFAULT_MIDDLEWARES = [
require("cors")({
"origin": "*",
"methods": DEFAULT_HEADERS["Access-Control-Allow-Methods"],
"preflightContinue": false,
"optionsSuccessStatus": 204
}),
require("morgan")(process.env.MORGAN_FORMAT ?? ":method :status :url - :response-time ms")
]
// patches
const { Buffer } = require("buffer")
global.b64Decode = (data) => {
return Buffer.from(data, "base64").toString("utf-8")
}
global.b64Encode = (data) => {
return Buffer.from(data, "utf-8").toString("base64")
}
Array.prototype.updateFromObjectKeys = function (obj) {
this.forEach((value, index) => {
if (obj[value] !== undefined) {
this[index] = obj[value]
}
})
return this
}
global.toBoolean = (value) => {
if (typeof value === "boolean") {
return value
}
if (typeof value === "string") {
return value.toLowerCase() === "true"
}
return false
}
function registerBaseAliases(fromPath, customAliases = {}) {
if (typeof fromPath === "undefined") {
if (module.parent.filename.includes("dist")) {
fromPath = path.resolve(process.cwd(), "dist")
} else {
fromPath = path.resolve(process.cwd(), "src")
}
}
moduleAlias.addAliases({
...customAliases,
"@controllers": path.resolve(fromPath, "controllers"),
"@middlewares": path.resolve(fromPath, "middlewares"),
"@models": path.resolve(fromPath, "models"),
"@classes": path.resolve(fromPath, "classes"),
"@lib": path.resolve(fromPath, "lib"),
"@utils": path.resolve(fromPath, "utils"),
})
}
module.exports = {
registerBaseAliases: registerBaseAliases,
Server: require("./server.js"),
Controller: require("./classes/controller"),
Endpoint: require("./classes/endpoint"),
registerBaseAliases: require("./registerAliases"),
version: require("../../package.json").version,
}

0
src/server/lib/generateController/index.js Normal file → Executable file
View File

0
src/server/lib/generateEndpointsFromDir/index.js Normal file → Executable file
View File

0
src/server/lib/internalConsole/index.js Normal file → Executable file
View File

0
src/server/lib/loadEndpointsFromDir/index.js Normal file → Executable file
View File

0
src/server/lib/redis_map/index.js Normal file → Executable file
View File

View File

@ -0,0 +1,8 @@
import cors from "cors"
export default cors({
origin: "*",
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "CONNECT", "TRACE"],
preflightContinue: false,
optionsSuccessStatus: 204,
})

View File

@ -0,0 +1,19 @@
export default (req, res, next) => {
const startHrTime = process.hrtime()
res.on("finish", () => {
const elapsedHrTime = process.hrtime(startHrTime)
const elapsedTimeInMs = elapsedHrTime[0] * 1000 + elapsedHrTime[1] / 1e6
res._responseTimeMs = elapsedTimeInMs
// cut req.url if is too long
if (req.url.length > 100) {
req.url = req.url.substring(0, 100) + "..."
}
console.log(`${req.method} ${res._status_code ?? res.statusCode ?? 200} ${req.url} ${elapsedTimeInMs}ms`)
})
next()
}

3
src/server/patches.js Normal file
View File

@ -0,0 +1,3 @@
import OperationError from "./classes/operation_error"
global.OperationError = OperationError

View File

@ -0,0 +1,21 @@
const moduleAlias = require("module-alias")
export default (fromPath, customAliases = {}) => {
if (typeof fromPath === "undefined") {
if (module.parent.filename.includes("dist")) {
fromPath = path.resolve(process.cwd(), "dist")
} else {
fromPath = path.resolve(process.cwd(), "src")
}
}
moduleAlias.addAliases({
...customAliases,
"@controllers": path.resolve(fromPath, "controllers"),
"@middlewares": path.resolve(fromPath, "middlewares"),
"@models": path.resolve(fromPath, "models"),
"@classes": path.resolve(fromPath, "classes"),
"@lib": path.resolve(fromPath, "lib"),
"@utils": path.resolve(fromPath, "utils"),
})
}

486
src/server/server.js Normal file → Executable file
View File

@ -1,105 +1,82 @@
const fs = require("fs")
const path = require("path")
const rtengine = require("./classes/rtengine").default
import("./patches")
const { EventEmitter } = require("@foxify/events")
import fs from "node:fs"
import path from "node:path"
import { EventEmitter } from "@foxify/events"
const tokenizer = require("corenode/libs/tokenizer")
const { serverManifest, internalConsole } = require("./lib")
import Endpoint from "./classes/endpoint"
const pkgjson = require(path.resolve(process.cwd(), "package.json"))
import defaults from "./defaults"
const Engines = {
"hyper-express": () => {
console.warn("HyperExpress is not fully supported yet!")
import IPCClient from "./classes/IPCClient"
const engine = require("hyper-express")
async function loadEngine(engine) {
const enginesPath = path.resolve(__dirname, "engines")
return new engine.Server()
},
"express": (params) => {
const { createServer } = require("node:http")
const express = require("express")
const socketio = require("socket.io")
const selectedEnginePath = path.resolve(enginesPath, engine)
const app = express()
const http = createServer(app)
if (!fs.existsSync(selectedEnginePath)) {
throw new Error(`Engine ${engine} not found!`)
}
const io = new socketio.Server(http)
const ws = new rtengine({
...params,
io: io,
http: false,
})
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
return {
ws,
http,
app,
}
},
return require(selectedEnginePath).default
}
class Server {
eventBus = new EventEmitter()
constructor(params = {}, controllers = {}, middlewares = {}, headers = {}) {
if (global.LINEBRIDGE_SERVER_EXPERIMENTAL) {
console.warn("🚧🚧🚧 This version of Linebridge is experimental!")
this.isExperimental = defaults.isExperimental ?? false
if (this.isExperimental) {
console.warn("🚧 This version of Linebridge is experimental! 🚧")
}
// register aliases
this.params = {
...global.DEFAULT_SERVER_PARAMS,
...params,
...defaults.params,
...params.default ?? params,
}
this.controllers = {
...controllers
...controllers.default ?? controllers,
}
this.middlewares = {
...middlewares.default ?? middlewares,
}
this.headers = {
...global.DEFAULT_SERVER_HEADERS,
...headers
...defaults.headers,
...headers.default ?? headers,
}
this.endpoints_map = {}
this.valid_http_methods = defaults.valid_http_methods
// fix and fulfill params
this.params.useMiddlewares = this.params.useMiddlewares ?? []
this.params.name = this.constructor.refName ?? this.params.refName
this.params.useEngine = this.constructor.useEngine ?? this.params.useEngine ?? "express"
this.params.listen_ip = this.params.listen_ip ?? "0.0.0.0"
this.params.listen_port = this.constructor.listen_port ?? this.params.listen_port ?? 3000
this.params.http_protocol = this.params.http_protocol ?? "http"
this.params.http_address = `${this.params.http_protocol}://${global.LOCALHOST_ADDRESS}:${this.params.listen_port}`
this.engine = null
this.InternalConsole = new internalConsole({
server_name: this.params.name
})
this.initializeManifest()
// handle silent mode
global.consoleSilent = this.params.silent
if (global.consoleSilent) {
// find morgan middleware and remove it
const morganMiddleware = global.DEFAULT_MIDDLEWARES.find(middleware => middleware.name === "logger")
if (morganMiddleware) {
global.DEFAULT_MIDDLEWARES.splice(global.DEFAULT_MIDDLEWARES.indexOf(morganMiddleware), 1)
}
}
this.params.http_address = `${this.params.http_protocol}://${defaults.localhost_address}:${this.params.listen_port}`
this.params.routesPath = this.constructor.routesPath ?? this.params.routesPath
return this
}
engine = null
events = null
ipc = null
ipcEvents = null
eventBus = new EventEmitter()
initialize = async () => {
const startHrTime = process.hrtime()
// register events
if (this.events) {
if (this.events.default) {
this.events = this.events.default
@ -111,73 +88,80 @@ class Server {
}
// initialize engine
this.engine = global.engine = Engines[this.params.engine]({
this.engine = await loadEngine(this.params.useEngine)
this.engine = new this.engine({
...this.params,
handleAuth: this.handleWsAuth,
requireAuth: this.constructor.requireWSAuth,
handleAuth: this.handleHttpAuth,
requireAuth: this.constructor.requireHttpAuth,
})
// before initialize headers, middlewares and controllers, try to execute onInitialize hook.
if (typeof this.engine.init === "function") {
await this.engine.init(this.params)
}
// create a router map
if (typeof this.engine.router.map !== "object") {
this.engine.router.map = []
}
// try to execute onInitialize hook
if (typeof this.onInitialize === "function") {
await this.onInitialize()
}
// set server defined headers
this.initializeHeaders()
this.useDefaultHeaders()
// set server defined middlewares
this.initializeRequiredMiddlewares()
this.useDefaultMiddlewares()
// register controllers
await this.initializeControllers()
// register routes
await this.initializeRoutes()
// register main index endpoint `/`
await this.registerBaseEndpoints()
if (typeof this.engine.ws?.initialize !== "function") {
this.InternalConsole.warn("❌ WebSocket is not supported!")
} else {
// use main router
await this.engine.app.use(this.engine.router)
// initialize websocket init hook if needed
if (typeof this.engine.ws?.initialize == "function") {
await this.engine.ws.initialize({
redisInstance: this.redis
})
}
if (typeof this.beforeInitialize === "function") {
await this.beforeInitialize()
// if is a linebridge service then initialize IPC Channels
if (process.env.lb_service) {
await this.initializeIpc()
}
await this.engine.http.listen(this.params.listen_port)
// try to execute beforeInitialize hook.
if (typeof this.afterInitialize === "function") {
await this.afterInitialize()
}
this.InternalConsole.info(`✅ Server ready on => ${this.params.listen_ip}:${this.params.listen_port}`)
// listen
await this.engine.listen()
// calculate elapsed time on ms, to fixed 2
const elapsedHrTime = process.hrtime(startHrTime)
const elapsedTimeInMs = elapsedHrTime[0] * 1e3 + elapsedHrTime[1] / 1e6
console.info(`🛰 Server ready!\n\t - ${this.params.http_protocol}://${this.params.listen_ip}:${this.params.listen_port} \n\t - Tooks ${elapsedTimeInMs.toFixed(2)}ms`)
}
initializeManifest = () => {
// check if origin.server exists
if (!fs.existsSync(serverManifest.filepath)) {
serverManifest.create()
}
initializeIpc = async () => {
console.info("🚄 Starting IPC client")
// check origin.server integrity
const MANIFEST_DATA = global.MANIFEST_DATA = serverManifest.get()
const MANIFEST_STAT = global.MANIFEST_STAT = serverManifest.stat()
if (typeof MANIFEST_DATA.created === "undefined") {
this.InternalConsole.warn("Server generation file not contains an creation date")
serverManifest.write({ created: Date.parse(MANIFEST_STAT.birthtime) })
}
if (typeof MANIFEST_DATA.server_token === "undefined") {
this.InternalConsole.warn("Missing server token!")
serverManifest.create()
}
this.usid = tokenizer.generateUSID()
this.server_token = serverManifest.get("server_token")
serverManifest.write({ last_start: Date.now() })
this.ipc = global.ipc = new IPCClient(this, process)
}
initializeHeaders = () => {
useDefaultHeaders = () => {
this.engine.app.use((req, res, next) => {
Object.keys(this.headers).forEach((key) => {
res.setHeader(key, this.headers[key])
@ -187,13 +171,14 @@ class Server {
})
}
initializeRequiredMiddlewares = () => {
const useMiddlewares = [...this.params.useMiddlewares ?? [], ...global.DEFAULT_MIDDLEWARES]
useDefaultMiddlewares = async () => {
const middlewares = await this.resolveMiddlewares([
...this.params.useMiddlewares,
...defaults.useMiddlewares,
])
useMiddlewares.forEach((middleware) => {
if (typeof middleware === "function") {
this.engine.app.use(middleware)
}
middlewares.forEach((middleware) => {
this.engine.app.use(middleware)
})
}
@ -206,7 +191,7 @@ class Server {
}
if (controller.disabled) {
this.InternalConsole.warn(`⏩ Controller [${controller.name}] is disabled! Initialization skipped...`)
console.warn(`⏩ Controller [${controller.name}] is disabled! Initialization skipped...`)
continue
}
@ -218,115 +203,190 @@ class Server {
const WSEndpoints = ControllerInstance.__get_ws_endpoints()
HTTPEndpoints.forEach((endpoint) => {
this.registerHTTPEndpoint(endpoint, ...this.resolveMiddlewares(controller.useMiddlewares))
this.register.http(endpoint, ...this.resolveMiddlewares(controller.useMiddlewares))
})
// WSEndpoints.forEach((endpoint) => {
// this.registerWSEndpoint(endpoint)
// })
} catch (error) {
if (!global.silentOutputServerErrors) {
this.InternalConsole.error(`\n\x1b[41m\x1b[37m🆘 [${controller.refName ?? controller.name}] Controller initialization failed:\x1b[0m ${error.stack} \n`)
}
console.error(`\n\x1b[41m\x1b[37m🆘 [${controller.refName ?? controller.name}] Controller initialization failed:\x1b[0m ${error.stack} \n`)
}
}
}
registerHTTPEndpoint = (endpoint, ...execs) => {
// check and fix method
endpoint.method = endpoint.method?.toLowerCase() ?? "get"
if (global.FIXED_HTTP_METHODS[endpoint.method]) {
endpoint.method = global.FIXED_HTTP_METHODS[endpoint.method]
}
// check if method is supported
if (typeof this.engine.app[endpoint.method] !== "function") {
throw new Error(`Method [${endpoint.method}] is not supported!`)
}
// grab the middlewares
let middlewares = [...execs]
if (endpoint.middlewares) {
middlewares = [...middlewares, ...this.resolveMiddlewares(endpoint.middlewares)]
}
// make sure method has root object on endpointsMap
if (typeof this.endpoints_map[endpoint.method] !== "object") {
this.endpoints_map[endpoint.method] = {}
}
// create model for http interface router
const routeModel = [endpoint.route, ...middlewares, this.createHTTPRequestHandler(endpoint)]
// register endpoint to http interface router
this.engine.app[endpoint.method](...routeModel)
// extend to map
this.endpoints_map[endpoint.method] = {
...this.endpoints_map[endpoint.method],
[endpoint.route]: {
route: endpoint.route,
enabled: endpoint.enabled ?? true,
},
}
}
registerWSEndpoint = (endpoint, ...execs) => {
endpoint.nsp = endpoint.nsp ?? "/main"
this.websocket_instance.eventsChannels.push([endpoint.nsp, endpoint.on, endpoint.dispatch])
this.websocket_instance.map[endpoint.on] = {
nsp: endpoint.nsp,
channel: endpoint.on,
}
}
registerBaseEndpoints() {
if (this.params.disableBaseEndpoint) {
this.InternalConsole.warn("‼️ [disableBaseEndpoint] Base endpoint is disabled! Endpoints mapping will not be available, so linebridge client bridges will not work! ‼️")
initializeRoutes = async (filePath) => {
if (!this.params.routesPath) {
return false
}
this.registerHTTPEndpoint({
method: "get",
route: "/",
fn: (req, res) => {
return res.json({
LINEBRIDGE_SERVER_VERSION: LINEBRIDGE_SERVER_VERSION,
version: pkgjson.version ?? "unknown",
usid: this.usid,
requestTime: new Date().getTime(),
})
}
})
const scanPath = filePath ?? this.params.routesPath
this.registerHTTPEndpoint({
method: "GET",
route: "/__http_map",
fn: (req, res) => {
return res.json({
endpointsMap: this.endpoints_map,
const files = fs.readdirSync(scanPath)
for await (const file of files) {
const filePath = `${scanPath}/${file}`
const stat = fs.statSync(filePath)
if (stat.isDirectory()) {
await this.initializeRoutes(filePath)
continue
} else if (file.endsWith(".js") || file.endsWith(".jsx") || file.endsWith(".ts") || file.endsWith(".tsx")) {
let splitedFilePath = filePath.split("/")
splitedFilePath = splitedFilePath.slice(splitedFilePath.indexOf("routes") + 1)
const method = splitedFilePath[splitedFilePath.length - 1].split(".")[0].toLocaleLowerCase()
splitedFilePath = splitedFilePath.slice(0, splitedFilePath.length - 1)
// parse parametrized routes
const parametersRegex = /\[([a-zA-Z0-9_]+)\]/g
splitedFilePath = splitedFilePath.map((route) => {
if (route.match(parametersRegex)) {
route = route.replace(parametersRegex, ":$1")
}
route = route.replace("[$]", "*")
return route
})
let route = splitedFilePath.join("/")
route = route.replace(".jsx", "")
route = route.replace(".js", "")
route = route.replace(".ts", "")
route = route.replace(".tsx", "")
if (route.endsWith("/index")) {
route = route.replace("/index", "")
}
route = `/${route}`
// import route
let routeFile = require(filePath)
routeFile = routeFile.default ?? routeFile
if (typeof routeFile !== "function") {
if (!routeFile.fn) {
console.warn(`Missing fn handler in [${method}][${route}]`)
continue
}
if (Array.isArray(routeFile.useContext)) {
let contexts = {}
for (const context of routeFile.useContext) {
contexts[context] = this.contexts[context]
}
routeFile.contexts = contexts
routeFile.fn.bind({ contexts })
}
}
new Endpoint(
this,
{
route: route,
enabled: true,
middlewares: routeFile.middlewares,
handlers: {
[method]: routeFile.fn ?? routeFile,
}
}
)
continue
}
})
}
}
register = {
http: (endpoint, ..._middlewares) => {
// check and fix method
endpoint.method = endpoint.method?.toLowerCase() ?? "get"
if (defaults.fixed_http_methods[endpoint.method]) {
endpoint.method = defaults.fixed_http_methods[endpoint.method]
}
// check if method is supported
if (typeof this.engine.router[endpoint.method] !== "function") {
throw new Error(`Method [${endpoint.method}] is not supported!`)
}
// grab the middlewares
let middlewares = [..._middlewares]
if (endpoint.middlewares) {
middlewares = [...middlewares, ...this.resolveMiddlewares(endpoint.middlewares)]
}
this.engine.router.map[endpoint.route] = {
method: endpoint.method,
path: endpoint.route,
}
// register endpoint to http interface router
this.engine.router[endpoint.method](endpoint.route, ...middlewares, endpoint.fn)
},
ws: (endpoint, ...execs) => {
endpoint.nsp = endpoint.nsp ?? "/main"
this.websocket_instance.eventsChannels.push([endpoint.nsp, endpoint.on, endpoint.dispatch])
this.websocket_instance.map[endpoint.on] = {
nsp: endpoint.nsp,
channel: endpoint.on,
}
},
}
async registerBaseEndpoints() {
if (this.params.disableBaseEndpoint) {
console.warn("‼️ [disableBaseEndpoint] Base endpoint is disabled! Endpoints mapping will not be available, so linebridge client bridges will not work! ‼️")
return false
}
const scanPath = path.join(__dirname, "baseEndpoints")
const files = fs.readdirSync(scanPath)
for await (const file of files) {
if (file === "index.js") {
continue
}
let endpoint = require(path.join(scanPath, file)).default
new endpoint(this)
}
}
//* resolvers
resolveMiddlewares = (requestedMiddlewares) => {
const middlewares = {
...this.middlewares,
...defaults.middlewares,
}
requestedMiddlewares = Array.isArray(requestedMiddlewares) ? requestedMiddlewares : [requestedMiddlewares]
const execs = []
requestedMiddlewares.forEach((middlewareKey) => {
if (typeof middlewareKey === "string") {
if (typeof this.middlewares[middlewareKey] !== "function") {
if (typeof middlewares[middlewareKey] !== "function") {
throw new Error(`Middleware ${middlewareKey} not found!`)
}
execs.push(this.middlewares[middlewareKey])
execs.push(middlewares[middlewareKey])
}
if (typeof middlewareKey === "function") {
@ -337,47 +397,7 @@ class Server {
return execs
}
cleanupProcess = () => {
this.InternalConsole.log("🛑 Stopping server...")
if (typeof this.engine.app.close === "function") {
this.engine.app.close()
}
this.engine.io.close()
}
// handlers
createHTTPRequestHandler = (endpoint) => {
return async (req, res) => {
try {
// check if endpoint is disabled
if (!this.endpoints_map[endpoint.method][endpoint.route].enabled) {
throw new Error("Endpoint is disabled!")
}
// return the returning call of the endpoint function
return await endpoint.fn(req, res)
} catch (error) {
if (typeof this.params.onRouteError === "function") {
return this.params.onRouteError(req, res, error)
} else {
if (!global.silentOutputServerErrors) {
this.InternalConsole.error({
message: "Unhandled route error:",
description: error.stack,
ref: [endpoint.method, endpoint.route].join("|"),
})
}
return res.status(500).json({
"error": error.message
})
}
}
}
}
// Utilities
toogleEndpointReachability = (method, route, enabled) => {
if (typeof this.endpoints_map[method] !== "object") {
throw new Error(`Cannot toogle endpoint, method [${method}] not set!`)

0
src/utils/index.js Normal file → Executable file
View File

0
src/utils/linebridge_ascii.js Normal file → Executable file
View File

0
src/utils/nanoid/index.js Normal file → Executable file
View File

0
src/utils/schematized/index.js Normal file → Executable file
View File

View File

@ -1,30 +0,0 @@
const assert = require("assert")
const linebridgeClient = require("../dist/client/index.js")
const exampleTestHTTPPort = 3010
let exampleBridge = null
describe("[Client]", async function () {
it("exports fine", function () {
assert.equal(typeof linebridgeClient, "object")
console.log(linebridgeClient)
})
it("create test bridge", async () => {
exampleBridge = new linebridgeClient.Bridge({
origin: `http://0.0.0.0:${exampleTestHTTPPort}`,
})
await exampleBridge.initialize()
})
it("bridge.endpoints is an object", async () => {
assert.equal(typeof exampleBridge.endpoints, "object")
})
it("test endpoint should correctly respond", async () => {
let response = await exampleBridge.endpoints.get.test()
assert.equal(response.test, "testing")
})
})

View File

@ -1,60 +0,0 @@
const assert = require("assert")
const linebridgeServer = require("../dist/server/index.js")
const exampleTestHTTPPort = 3010
const testEndpoints = [
{
route: "/test",
method: "GET",
fn: function (req, res) {
return res.send({ test: "testing" })
}
},
{
route: "/disabledByDefault",
method: "GET",
fn: function (req, res) {
return res.send("This must not be sended")
},
enabled: false,
},
{
route: "/shouldBeDisabled",
method: "GET",
fn: function (req, res) {
return res.send("This must not be sended after use `toogleEndpointReachability`")
}
}
]
let exampleServer = null
describe("[Server]", async function () {
it("should export", function () {
assert.equal(typeof linebridgeServer, "function")
})
it("create server", async function () {
exampleServer = new linebridgeServer({
port: exampleTestHTTPPort,
})
})
it("register test controllers", async function () {
testEndpoints.forEach((endpoint) => {
exampleServer.registerHTTPEndpoint(endpoint)
})
})
it("initialize server", async function () {
await exampleServer.initialize()
})
it("toogleEndpointReachability", async function () {
exampleServer.toogleEndpointReachability("get", "/shouldBeDisabled", false)
// check if endpoint is disabled
assert.equal(exampleServer.endpointsMap["get"]["/shouldBeDisabled"].enabled, false)
})
})

View File

@ -1,32 +0,0 @@
import { Server, Controller } from "../src/server"
const TestControllers = [
class AController extends Controller {
get = {
"/test": (req, res) => {
return res.send("Hello World!")
}
}
},
class BController extends Controller {
static useRoute = "/bcontroller"
get = {
"/test": (req, res) => {
return res.send("Hello World!")
}
}
}
]
async function _main() {
const server = new Server({
httpEngine: "express"
}, TestControllers)
await server.initialize()
}
_main().catch((error) => {
console.error(`[MAIN_ERROR] ${error}`)
})

View File

@ -1,111 +0,0 @@
import { Bridge } from "../src/client"
import { Server, Controller } from "../src/server"
const Middlewares = {
"test": (req, res, next) => {
console.log("test middleware, it should run on every endpoint of a controller")
return next()
},
"test2": (req, res, next) => {
console.log("test2 middleware, it should run on request of a endpoint")
return next()
}
}
const Controllers = [
class DisabledController extends Controller {
static disabled = true
get = {
"/unreachable": (req, res) => {
return res.send("this must not be reachable")
}
}
},
class TestController extends Controller {
static useMiddlewares = ["test"]
channels = {
"epicEvent": (socket, ...args) => {
console.log(`[SERVER WS EVENT] > ${socket.id} > `, ...args)
return socket.res("elo")
}
}
get = {
"/test/:name": {
fn: (req, res) => {
const name = req.params.name
return res.json({
message: name ? `Hello ${name}!` : "Hello World!"
})
},
},
"/crashTest": (req, res) => {
throw new Error("Boom!")
},
"/test": (req, res) => {
return res.send("Hello World!")
}
}
delete = {
"/test": (req, res) => {
return res.send(`Deleting ${req.body.a}`)
}
}
},
]
async function _main() {
const server = new Server({
onWSClientConnection: (socket) => {
const authToken = socket.handshake.auth?.token
console.log(`AUTH TOKEN: ${authToken}`)
if (!authToken) {
socket.emit("unauthorized", "No auth token provided!")
return socket.disconnect()
}
if (authToken !== "123") {
socket.emit("unauthorized", "invalid auth token!")
return socket.disconnect()
}
}
}, Controllers, Middlewares)
const clientBridge = new Bridge({
origin: server.HTTPAddress,
wsOrigin: server.WSAddress,
wsMainSocketOptions: {
auth: {
token: "123"
}
},
}, {
onUnauthorized: (reason) => {
console.log(reason)
}
})
await server.initialize()
await clientBridge.initialize()
const test = await clientBridge.endpoints.get.test()
const crashTest = await clientBridge.endpoints.get.crashtest().catch(error => {
console.log(error)
return "Crash test passed!"
})
const wsEpicEvent = await clientBridge.wsEndpoints.epicEvent("Hello", "World")
console.log(`[get.test] > ${test}`)
console.log(`[get.crashtest] > ${crashTest}`)
console.log(`[ws.epicEvent] > ${wsEpicEvent}`)
}
_main().catch((error) => {
console.error(`[MAIN_ERROR] ${error}`)
})