merge from local

This commit is contained in:
SrGooglo 2024-09-25 19:40:53 +00:00
parent e7af64983d
commit 94fd5dd523
35 changed files with 204 additions and 406 deletions

121
README.md
View File

@ -1 +1,120 @@
# linebridge | API Interface library
# linebridge
A multiproposal framework to build fast, scalable, and secure servers.
Currently used on RageStudio's services backends, like [Comty](https://github.com/ragestudio/comty)
## Getting Started
### Installation
```bash
npm install linebridge
```
> Note: Use of Yarn can cause some issues to install the package.
### Example
Create a http server
```js
// index.js
import { Server } from "./linebridge/src"
class MyAPI extends Server {
// set a id for the server
static refName = "my-api"
// define a file based router
static routesPath = `${__dirname}/routes`
// define custom listen port
static listenPort = 3000
// set manual routes
routes = {
// basic route
"/hi": {
method: "get",
fn: async (req, res) => {
return {
message: "Hello world"
}
}
},
// use custom middleware
"/middleware-custom": {
method: "get",
middlewares: [
"custom-middleware"
],
fn: async (req, res) => {
return {
message: "The middleware is working!"
}
}
},
// get from context
"/db": {
method: "get",
useContexts: [
"db"
],
fn: async (req, res, ctx) => {
console.log(ctx)
return ctx.db.data
}
},
// use parameters
"/sum/:value1/:value2": {
method: "get",
fn: async (req, res) => {
return {
result: parseInt(req.params.value1) + parseInt(req.params.value2)
}
}
}
}
// define default middlewares to use on every request
useMiddlewares = [
async (req, res) => {
console.log("Im executed every request")
}
]
// you can also define custom middlewares to use on endpoints
middlewares = {
"custom-middleware": async (req, res) => {
console.log("Im a custom middleware")
}
}
// define custom contexts to use on endpoints
contexts = {
db: {
data: [
{
id: 1,
name: "John Doe"
},
{
id: 2,
name: "Jane Doe"
}
]
}
}
async onInitialize() {
console.log("Server initialized")
}
async onClose() {
console.log("Server closed")
}
}
Boot(MyAPI)
```
Run the server
```bash
linebridge-boot index.js
```
## Documentation
> On the way
<!-- For more information, please visit the [documentation](https://docs.linebridge.com). -->

View File

@ -1,10 +1,13 @@
{
"name": "linebridge",
"version": "0.21.4",
"description": "API Framework for RageStudio backends",
"author": "RageStudio",
"version": "0.22.0",
"description": "Multiproposal framework to build fast, scalable, and secure servers.",
"author": "RageStudio <support@ragestudio.net>",
"bugs": {
"url": "https://github.com/ragestudio/linebridge/issues"
},
"license": "MIT",
"main": "./dist/client/index.js",
"main": "./dist/index.js",
"bin": {
"linebridge-boot": "./bin/boot.js"
},
@ -48,4 +51,4 @@
"@ragestudio/hermes": "^0.1.0",
"mocha": "^9.1.3"
}
}
}

View File

@ -9,12 +9,10 @@ export default class MainEndpoint extends Endpoint {
route = "/"
get = async (req, res) => {
const { params } = this.ctx
return {
name: params.name ?? "unknown",
name: globalThis._linebridge.name ?? "unknown",
version: projectPkg.version ?? "unknown",
engine: params.useEngine ?? "unknown",
engine: globalThis._linebridge.useEngine ?? "unknown",
request_time: new Date().getTime(),
lb_version: defaults.version ?? "unknown",
experimental: defaults.isExperimental.toString() ?? "unknown",

View File

@ -4,7 +4,7 @@ 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 }]) => {
const httpMap = Object.entries(this.server.engine.router.map).reduce((acc, [route, { method, path }]) => {
if (!acc[method]) {
acc[method] = []
}

View File

@ -1,10 +1,15 @@
export default class Endpoint {
constructor(ctx, params = {}) {
this.ctx = ctx
constructor(server, params = {}, ctx = {}) {
this.server = server
this.params = params
this.ctx = ctx
this.route = this.constructor.route ?? this.params.route
this.enabled = this.constructor.enabled ?? this.params.enabled ?? true
if (!server) {
throw new Error("Server is not defined")
}
this.route = this.route ?? this.constructor.route ?? this.params.route
this.enabled = this.enabled ?? this.constructor.enabled ?? this.params.enabled ?? true
this.middlewares = [
...this.middlewares ?? [],
@ -12,7 +17,7 @@ export default class Endpoint {
]
if (this.params.handlers) {
for (const method of this.ctx.valid_http_methods) {
for (const method of globalThis._linebridge.validHttpMethods) {
if (typeof this.params.handlers[method] === "function") {
this[method] = this.params.handlers[method]
}
@ -21,13 +26,23 @@ export default class Endpoint {
this.selfRegister()
if (Array.isArray(this.params.useContexts)) {
for (const contextRef of this.params.useContexts) {
this.endpointContext[contextRef] = this.server.contexts[contextRef]
}
}
return this
}
endpointContext = {}
createHandler(fn) {
fn = fn.bind(this.server)
return async (req, res) => {
try {
const result = await fn(req, res)
const result = await fn(req, res, this.endpointContext)
if (result) {
return res.json(result)
@ -52,15 +67,13 @@ export default class Endpoint {
}
selfRegister = async () => {
const validMethods = this.ctx.valid_http_methods
for await (const method of validMethods) {
for await (const method of globalThis._linebridge.validHttpMethods) {
const methodHandler = this[method]
if (typeof methodHandler !== "undefined") {
const fn = this.createHandler(this[method].fn ?? this[method])
this.ctx.register.http(
this.server.register.http(
{
method,
route: this.route,

View File

@ -1,177 +0,0 @@
const axios = require("axios")
const axiosRetry = require("axios-retry")
const camalize = require("@corenode/utils/dist/camalize").default
const { WSInterface } = require("./classes")
const { generateHTTPRequestDispatcher, generateWSRequestDispatcher } = require("./lib")
const FixedMethods = {
"del": "delete"
}
module.exports = class Bridge {
constructor(params = {}, events = {}) {
this.params = params
this.events = events
this.internalAbortController = new AbortController()
this.origin = this.params.origin
this.wsOrigin = this.origin.replace(/^http/, "ws")
this.wsOrigin = this.wsOrigin.replace(/^https/, "wss")
this.headers = {
...this.params.headers,
}
this.httpInterface = axios.create({
baseURL: this.origin,
headers: this.headers,
signal: this.params.signal ?? this.internalAbortController.signal
})
this.wsInterface = new WSInterface({
origin: this.wsOrigin,
managerOptions: this.params.wsOptions,
mainSocketOptions: this.params.wsMainSocketOptions,
})
this.endpoints = {}
this.wsEndpoints = {}
this.wsInterface.sockets.main.on("disconnect", async (...args) => {
if (typeof this.events.onDisconnect === "function") {
await this.events.onDisconnect(...args)
}
})
this.wsInterface.sockets.main.on("unauthorized", async (...args) => {
if (typeof this.events.onUnauthorized === "function") {
await this.events.onUnauthorized(...args)
}
})
if (this.params.enableRetry) {
axiosRetry(this.httpInterface, {
retries: this.params.onFailRetries ?? 1,
retryDelay: this.params.retryDelay ?? 0,
})
}
return this
}
initialize = async () => {
const instanceManifest = await this.httpInterface.get("/")
.then((res) => res.data)
.catch((err) => {
console.error(err)
throw new Error(`Could not get endpoints map from server. [${err.message}]`)
})
const httpMap = instanceManifest.endpointsMap
const wsMap = instanceManifest.wsEndpointsMap
await this.registerHTTPDispatchers(httpMap)
await this.registerWSDispatchers(wsMap)
this.wsInterface.manager.open((err) => {
if (err) {
console.error(err)
throw new Error(`Could not open socket manager. [${err.message}]`)
}
this.wsInterface.sockets.main.connect()
})
}
handleRequestContext = async (...args) => {
if (typeof this.params.onRequest === "function") {
return await this.params.onRequest(...args)
}
return false
}
handleResponse = async (...args) => {
if (typeof this.params.onResponse === "function") {
return await this.params.onResponse(...args)
}
return false
}
handleBeforeRequest = async (...args) => {
if (typeof this.params.onBeforeRequest === "function") {
return await this.params.onBeforeRequest(...args)
}
return false
}
registerHTTPDispatchers = async (map) => {
if (typeof map !== "object") {
console.error("[Bridge] > createHTTPDispatchers > map is not an object")
return false
}
for await (let HttpMethod of Object.keys(map)) {
HttpMethod = HttpMethod.toLowerCase()
const fixedMethod = FixedMethods[HttpMethod] ?? HttpMethod
if (typeof this.endpoints[fixedMethod] !== "object") {
this.endpoints[fixedMethod] = {}
}
Object.keys(map[HttpMethod]).forEach((route) => {
const tree = route.split("/")
const hasTree = tree.length >= 1
let nameKey = route
// check if has tree
if (hasTree) {
// remove first whitespace item in route index[0]
if (tree[0] == "") {
tree.shift()
}
nameKey = camalize(tree.join("_"))
}
// if is an blank route, set as index
if (nameKey == "") {
nameKey = "index"
}
this.endpoints[fixedMethod][nameKey] = generateHTTPRequestDispatcher({
instance: this.httpInterface,
method: fixedMethod,
route: route,
beforeRequest: this.handleBeforeRequest,
handleRequestContext: this.handleRequestContext,
handleResponse: this.handleResponse,
requestHeaders: this.params.requestHeaders,
})
})
}
return this.endpoints
}
registerWSDispatchers = async (map) => {
if (typeof map !== "object") {
console.error("[Bridge] > createWSDispatchers > map is not an object")
return false
}
for await (let wsChannel of Object.keys(map)) {
const endpoint = map[wsChannel]
endpoint.nsp[0] == "/" ? endpoint.nsp = endpoint.nsp.slice(1) : null
endpoint.method = endpoint.channel[0] == "/" ? endpoint.channel.slice(1) : endpoint.channel
this.wsEndpoints[endpoint.method] = generateWSRequestDispatcher(this.wsInterface.sockets[endpoint.nsp ?? "main"], endpoint.channel)
}
}
}

View File

@ -1,45 +0,0 @@
const io = require("socket.io-client")
class WSInterface {
constructor(params = {}) {
this.params = params
this.manager = new io.Manager(this.params.origin, {
autoConnect: false,
transports: ["websocket"],
...this.params.managerOptions,
})
this.sockets = {}
this.attach("/", "main", this.params.mainSocketOptions)
}
attach = (socket, as, options) => {
if (typeof socket !== "string") {
console.error("socket must be string")
return false
}
socket = this.manager.socket(socket, options)
return this.sockets[as ?? socket] = socket
}
detach = (socketName) => {
if (typeof socketName !== "string") {
console.error("socketName must be string")
return false
}
if (typeof this.sockets[socketName] === "undefined") {
console.error("socket not found")
return false
}
if (this.sockets[socketName].connected) {
this.sockets[socketName].disconnect()
}
delete this.sockets[socketName]
}
}
module.exports = WSInterface

View File

@ -1,3 +0,0 @@
module.exports = {
WSInterface: require("./WSInterface"),
}

View File

@ -1,44 +0,0 @@
// TODO: AutoConnection
module.exports = class Controller {
constructor(params = {}) {
console.warn("[Linebridge] Controller is not finished yet. Please use regular bridges instead.")
this.params = params
this.pool = []
}
async initialize() {
if (typeof this.params.servers !== "undefined" && Array.isArray(this.params.servers)) {
for await (let server of this.params.servers) {
await this.appendServer(server)
}
}
for await (let server of this.pool) {
await this.connect(server)
}
}
async appendServer(server) {
if (typeof server === "string") {
server = new Bridge({
origin: server,
})
} else if (typeof server === "object" && server instanceof Bridge) {
server = new Bridge(...server)
}
this.pool.push(server)
}
// async disconnect() {
// }
async connect(server) {
if (server instanceof Bridge) {
await server.initialize()
} else {
throw new Error("Invalid server. Expected Bridge instance.")
}
}
}

View File

@ -1,8 +0,0 @@
const Controller = require("./controller")
const Bridge = require("./bridge")
module.exports = {
Bridge,
Controller,
version: require("../../package.json").version,
}

View File

@ -1,69 +0,0 @@
export default function generateHTTPRequestDispatcher({
instance,
method,
route,
beforeRequest,
handleRequestContext,
handleResponse,
}) {
return function (body, query, options) {
return new Promise(async (resolve, reject) => {
let requestParams = {
parseData: true,
...options,
method: method,
url: route,
data: body,
params: query,
}
let result = {}
const makeRequest = async () => {
result = {
response: null,
error: null,
}
if (typeof beforeRequest === "function") {
await beforeRequest(requestParams)
}
if (typeof handleRequestContext === "function") {
const context = await handleRequestContext()
requestParams = { ...requestParams, ...context }
}
return await instance(requestParams)
.then((response) => {
result.response = response
return response
})
.catch((error) => {
result.error = error.response.data.error ?? error.response.data
return error
})
}
const request = await makeRequest()
if (typeof handleResponse === "function") {
await handleResponse(request, makeRequest)
}
if (requestParams.parseData) {
if (result.error) {
return reject(result.error)
}
return resolve(result.response.data)
}
return resolve(result)
})
}
}
module.exports = generateHTTPRequestDispatcher

View File

@ -1,17 +0,0 @@
function generateWSRequestDispatcher(instance, channel) {
return function (...payload) {
return new Promise(async (resolve, reject) => {
const req = instance.emit(channel, ...payload)
req.on("response", (...args) => {
return resolve(...args)
})
req.on("responseError", (...args) => {
return reject(...args)
})
})
}
}
module.exports = generateWSRequestDispatcher

View File

@ -1,4 +0,0 @@
module.exports = {
generateHTTPRequestDispatcher: require("./generateHTTPRequestDispatcher"),
generateWSRequestDispatcher: require("./generateWSRequestDispatcher"),
}

View File

@ -1,7 +1,7 @@
const path = require("path")
const fs = require("fs")
const os = require("os")
const packageJSON = require(path.resolve(module.path, "../../package.json"))
const packageJSON = require(path.resolve(module.path, "../package.json"))
function getHostAddress() {
const interfaces = os.networkInterfaces()
@ -22,7 +22,7 @@ function getHostAddress() {
}
export default {
isExperimental: fs.existsSync(path.resolve(module.path, "../../.experimental")),
isExperimental: fs.existsSync(path.resolve(module.path, "../.experimental")),
version: packageJSON.version,
localhost_address: getHostAddress() ?? "localhost",
params: {

View File

@ -11,11 +11,16 @@ export default class Engine {
ws = null
initialize = async (params) => {
this.app = new he.Server({
const serverParams = {
max_body_length: 50 * 1024 * 1024, //50MB in bytes,
key_file_name: params.ssl?.key ?? null,
cert_file_name: params.ssl?.cert ?? null,
})
}
if (params.ssl) {
serverParams.key_file_name = params.ssl?.key ?? null
serverParams.cert_file_name = params.ssl?.cert ?? null
}
this.app = new he.Server(serverParams)
this.router = new he.Router()
@ -52,7 +57,7 @@ export default class Engine {
}
})
if (!params.disableWebSockets) {
if (params.enableWebsockets) {
this.ws = global.websocket = new rtengine({
...params,
handleAuth: params.handleWsAuth,
@ -83,7 +88,7 @@ export default class Engine {
return true
})
if (!params.disableWebSockets) {
if (params.enableWebsockets) {
process.send({
type: "router:ws:register",
id: process.env.lb_service.id,

View File

@ -2,5 +2,5 @@ module.exports = {
Server: require("./server.js"),
Endpoint: require("./classes/endpoint"),
registerBaseAliases: require("./registerAliases"),
version: require("../../package.json").version,
version: require("../package.json").version,
}

View File

@ -1,7 +1,7 @@
import fs from "node:fs"
import path from "node:path"
export default async (ctx) => {
export default async (server) => {
const scanPath = path.join(__dirname, "../../", "baseEndpoints")
const files = fs.readdirSync(scanPath)
@ -12,6 +12,6 @@ export default async (ctx) => {
let endpoint = require(path.join(scanPath, file)).default
new endpoint(ctx)
new endpoint(server)
}
}

View File

@ -5,7 +5,7 @@ import RecursiveRegister from "../../lib/recursiveRegister"
const parametersRegex = /\[([a-zA-Z0-9_]+)\]/g
export default async (startDir, engine, ctx) => {
export default async (startDir, engine, server) => {
if (!fs.existsSync(startDir)) {
return engine
}
@ -52,7 +52,7 @@ export default async (startDir, engine, ctx) => {
let contexts = {}
for (const context of fn.useContext) {
contexts[context] = ctx.contexts[context]
contexts[context] = server.contexts[context]
}
fn.contexts = contexts
@ -62,7 +62,7 @@ export default async (startDir, engine, ctx) => {
}
new Endpoint(
ctx,
server,
{
route: route,
enabled: true,

View File

@ -7,6 +7,7 @@ import { EventEmitter } from "@foxify/events"
import defaults from "./defaults"
import IPCClient from "./classes/IPCClient"
import Endpoint from "./classes/endpoint"
import registerBaseEndpoints from "./initializators/registerBaseEndpoints"
import registerWebsocketsEvents from "./initializators/registerWebsocketsEvents"
@ -51,8 +52,6 @@ class Server {
...headers.default ?? headers,
}
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
@ -61,12 +60,25 @@ class Server {
this.params.listen_port = this.constructor.listenPort ?? 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}://${defaults.localhost_address}:${this.params.listen_port}`
this.params.disableWebSockets = this.constructor.disableWebSockets ?? this.params.disableWebSockets ?? false
this.params.enableWebsockets = this.constructor.enableWebsockets ?? this.params.enableWebsockets ?? false
this.params.ignoreCors = this.constructor.ignoreCors ?? this.params.ignoreCors ?? true
this.params.routesPath = this.constructor.routesPath ?? this.params.routesPath ?? path.resolve(process.cwd(), "routes")
this.params.wsRoutesPath = this.constructor.wsRoutesPath ?? this.params.wsRoutesPath ?? path.resolve(process.cwd(), "routes_ws")
globalThis._linebridge = {
name: this.params.name,
useEngine: this.params.useEngine,
listenIp: this.params.listen_ip,
listenPort: this.params.listen_port,
httpProtocol: this.params.http_protocol,
httpAddress: this.params.http_address,
enableWebsockets: this.params.enableWebsockets,
ignoreCors: this.params.ignoreCors,
routesPath: this.params.routesPath,
validHttpMethods: defaults.valid_http_methods,
}
return this
}
@ -100,7 +112,7 @@ class Server {
handleAuth: this.handleHttpAuth,
requireAuth: this.constructor.requireHttpAuth,
refName: this.constructor.refName ?? this.params.refName,
ssl: this.ssl ?? {},
ssl: this.ssl,
}
// initialize engine
@ -138,6 +150,21 @@ class Server {
this.useDefaultHeaders()
this.useDefaultMiddlewares()
if (this.routes) {
for (const [route, endpoint] of Object.entries(this.routes)) {
this.engine.router.map[route] = new Endpoint(
this,
{
...endpoint,
route: route,
handlers: {
[endpoint.method]: endpoint.fn,
},
}
)
}
}
// register http & ws routes
this.engine = await registerHttpRoutes(this.params.routesPath, this.engine, this)
this.engine = await registerWebsocketsEvents(this.params.wsRoutesPath, this.engine)